MOBILE-3814 auto-focus: Improve focus handling
This commit is contained in:
		
							parent
							
								
									faa43fbece
								
							
						
					
					
						commit
						505891fa11
					
				@ -15,7 +15,6 @@
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
import { CoreFilterDefaultHandler } from '@features/filter/services/handlers/default-filter';
 | 
			
		||||
import { CoreFilterFilter, CoreFilterFormatTextOptions } from '@features/filter/services/filter';
 | 
			
		||||
import { CoreTextUtils } from '@services/utils/text';
 | 
			
		||||
import { CoreUrlUtils } from '@services/utils/url';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
@ -32,19 +31,10 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl
 | 
			
		||||
    protected template = document.createElement('template'); // A template element to convert HTML to element.
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Filter some text.
 | 
			
		||||
     *
 | 
			
		||||
     * @param text The text to filter.
 | 
			
		||||
     * @param filter The filter.
 | 
			
		||||
     * @param options Options passed to the filters.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Filtered text (or promise resolved with the filtered text).
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    filter(
 | 
			
		||||
        text: string,
 | 
			
		||||
        filter: CoreFilterFilter, // eslint-disable-line @typescript-eslint/no-unused-vars
 | 
			
		||||
        options: CoreFilterFormatTextOptions, // eslint-disable-line @typescript-eslint/no-unused-vars
 | 
			
		||||
        siteId?: string, // eslint-disable-line @typescript-eslint/no-unused-vars
 | 
			
		||||
    ): string | Promise<string> {
 | 
			
		||||
        this.template.innerHTML = text;
 | 
			
		||||
 | 
			
		||||
@ -60,7 +50,7 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl
 | 
			
		||||
    /**
 | 
			
		||||
     * Treat video filters. Currently only treating youtube video using video JS.
 | 
			
		||||
     *
 | 
			
		||||
     * @param el Video element.
 | 
			
		||||
     * @param video Video element.
 | 
			
		||||
     */
 | 
			
		||||
    protected treatVideoFilters(video: HTMLElement): void {
 | 
			
		||||
        // Treat Video JS Youtube video links and translate them to iframes.
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,6 @@
 | 
			
		||||
<ng-content></ng-content>
 | 
			
		||||
<ion-button fill="clear" [attr.aria-label]="label | translate" core-suppress-events (onClick)="toggle($event)">
 | 
			
		||||
    <ion-icon [name]="iconName" slot="icon-only" aria-hidden="true"></ion-icon>
 | 
			
		||||
<ion-button fill="clear" [attr.aria-label]="label | translate" core-suppress-events (onClick)="toggle($event)"
 | 
			
		||||
    (mousedown)="doNotBlur($event)" (keydown)="doNotBlur($event)" (keyup)="toggle($event)">
 | 
			
		||||
    <ion-icon [name]=" iconName" slot="icon-only" aria-hidden="true">
 | 
			
		||||
    </ion-icon>
 | 
			
		||||
</ion-button>
 | 
			
		||||
 | 
			
		||||
@ -44,12 +44,11 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
 | 
			
		||||
    @Input() initialShown?: boolean | string; // Whether the password should be shown at start.
 | 
			
		||||
    @ContentChild(IonInput) ionInput?: IonInput;
 | 
			
		||||
 | 
			
		||||
    shown!: boolean; // Whether the password is shown.
 | 
			
		||||
    label!: string; // Label for the button to show/hide.
 | 
			
		||||
    iconName!: string; // Name of the icon of the button to show/hide.
 | 
			
		||||
    selector = ''; // Selector to identify the input.
 | 
			
		||||
    shown = false; // Whether the password is shown.
 | 
			
		||||
    label = ''; // Label for the button to show/hide.
 | 
			
		||||
    iconName = ''; // Name of the icon of the button to show/hide.
 | 
			
		||||
 | 
			
		||||
    protected input?: HTMLInputElement | null; // Input affected.
 | 
			
		||||
    protected input?: HTMLInputElement; // Input affected.
 | 
			
		||||
    protected element: HTMLElement; // Current element.
 | 
			
		||||
 | 
			
		||||
    constructor(element: ElementRef) {
 | 
			
		||||
@ -57,58 +56,51 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component being initialized.
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngOnInit(): void {
 | 
			
		||||
        this.shown = CoreUtils.isTrueOrOne(this.initialShown);
 | 
			
		||||
        this.selector = 'input[name="' + this.name + '"]';
 | 
			
		||||
        this.setData();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * View has been initialized.
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngAfterViewInit(): void {
 | 
			
		||||
        this.searchInput();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Search the input to show/hide.
 | 
			
		||||
     */
 | 
			
		||||
    protected async searchInput(): Promise<void> {
 | 
			
		||||
    async ngAfterViewInit(): Promise<void> {
 | 
			
		||||
        if (this.ionInput) {
 | 
			
		||||
            // It's an ion-input, use it to get the native element.
 | 
			
		||||
            this.input = await this.ionInput.getInputElement();
 | 
			
		||||
            this.setData(this.input);
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Search the input.
 | 
			
		||||
        this.input = <HTMLInputElement> this.element.querySelector(this.selector);
 | 
			
		||||
        this.input = this.element.querySelector<HTMLInputElement>('input[name="' + this.name + '"]') ?? undefined;
 | 
			
		||||
 | 
			
		||||
        if (this.input) {
 | 
			
		||||
            // Input found. Set the right type.
 | 
			
		||||
            this.input.type = this.shown ? 'text' : 'password';
 | 
			
		||||
        if (!this.input) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
            // By default, don't autocapitalize and autocorrect.
 | 
			
		||||
            if (!this.input.getAttribute('autocorrect')) {
 | 
			
		||||
                this.input.setAttribute('autocorrect', 'off');
 | 
			
		||||
            }
 | 
			
		||||
            if (!this.input.getAttribute('autocapitalize')) {
 | 
			
		||||
                this.input.setAttribute('autocapitalize', 'none');
 | 
			
		||||
            }
 | 
			
		||||
        this.setData(this.input);
 | 
			
		||||
 | 
			
		||||
        // By default, don't autocapitalize and autocorrect.
 | 
			
		||||
        if (!this.input.getAttribute('autocorrect')) {
 | 
			
		||||
            this.input.setAttribute('autocorrect', 'off');
 | 
			
		||||
        }
 | 
			
		||||
        if (!this.input.getAttribute('autocapitalize')) {
 | 
			
		||||
            this.input.setAttribute('autocapitalize', 'none');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set label, icon name and input type.
 | 
			
		||||
     *
 | 
			
		||||
     * @param input The input element.
 | 
			
		||||
     */
 | 
			
		||||
    protected setData(): void {
 | 
			
		||||
    protected setData(input: HTMLInputElement): void {
 | 
			
		||||
        this.label = this.shown ? 'core.hide' : 'core.show';
 | 
			
		||||
        this.iconName = this.shown ? 'fas-eye-slash' : 'fas-eye';
 | 
			
		||||
        if (this.input) {
 | 
			
		||||
            this.input.type = this.shown ? 'text' : 'password';
 | 
			
		||||
        }
 | 
			
		||||
        input.type = this.shown ? 'text' : 'password';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -117,20 +109,49 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
 | 
			
		||||
     * @param event The mouse event.
 | 
			
		||||
     */
 | 
			
		||||
    toggle(event: Event): void {
 | 
			
		||||
        if (event.type == 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        event.preventDefault();
 | 
			
		||||
        event.stopPropagation();
 | 
			
		||||
 | 
			
		||||
        const isFocused = document.activeElement === this.input;
 | 
			
		||||
 | 
			
		||||
        this.shown = !this.shown;
 | 
			
		||||
        this.setData();
 | 
			
		||||
 | 
			
		||||
        if (!this.input) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.setData(this.input);
 | 
			
		||||
        // In Android, the keyboard is closed when the input type changes. Focus it again.
 | 
			
		||||
        if (isFocused && CoreApp.isAndroid()) {
 | 
			
		||||
            // In Android, the keyboard is closed when the input type changes. Focus it again.
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                CoreDomUtils.focusElement(this.input!);
 | 
			
		||||
            }, 400);
 | 
			
		||||
            CoreDomUtils.focusElement(this.input);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Do not loose focus.
 | 
			
		||||
     *
 | 
			
		||||
     * @param event The mouse event.
 | 
			
		||||
     */
 | 
			
		||||
    doNotBlur(event: Event): void {
 | 
			
		||||
        if (event.type == 'keydown' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        event.preventDefault();
 | 
			
		||||
        event.stopPropagation();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Checks if Space or Enter have been pressed.
 | 
			
		||||
     *
 | 
			
		||||
     * @param event Keyboard Event.
 | 
			
		||||
     * @returns Wether space or enter have been pressed.
 | 
			
		||||
     */
 | 
			
		||||
    protected isValidKeyboardKey(event: KeyboardEvent): boolean {
 | 
			
		||||
        return event.key == ' ' || event.key == 'Enter';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -32,7 +32,7 @@ export class CoreAutoFocusDirective implements AfterViewInit {
 | 
			
		||||
 | 
			
		||||
    @Input('core-auto-focus') autoFocus: boolean | string = true;
 | 
			
		||||
 | 
			
		||||
    protected element: HTMLElement;
 | 
			
		||||
    protected element: HTMLIonInputElement | HTMLIonTextareaElement | HTMLIonSearchbarElement | HTMLElement;
 | 
			
		||||
 | 
			
		||||
    constructor(element: ElementRef) {
 | 
			
		||||
        this.element = element.nativeElement;
 | 
			
		||||
@ -41,51 +41,27 @@ export class CoreAutoFocusDirective implements AfterViewInit {
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngAfterViewInit(): void {
 | 
			
		||||
    async ngAfterViewInit(): Promise<void> {
 | 
			
		||||
        if (CoreUtils.isFalseOrZero(this.autoFocus)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.setFocus();
 | 
			
		||||
    }
 | 
			
		||||
        await CoreDomUtils.waitToBeInDOM(this.element);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Function to focus the element.
 | 
			
		||||
     *
 | 
			
		||||
     * @param retries Internal param to stop retrying on 0.
 | 
			
		||||
     */
 | 
			
		||||
    protected setFocus(retries = 10): void {
 | 
			
		||||
        if (retries == 0) {
 | 
			
		||||
        let focusElement = this.element;
 | 
			
		||||
 | 
			
		||||
        if ('getInputElement' in focusElement) {
 | 
			
		||||
            // If it's an Ionic element get the right input to use.
 | 
			
		||||
            focusElement.componentOnReady && await focusElement.componentOnReady();
 | 
			
		||||
            focusElement = await focusElement.getInputElement();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!focusElement) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Wait a bit to make sure the view is loaded.
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
            // If it's a ion-input or ion-textarea, search the right input to use.
 | 
			
		||||
            let element: HTMLElement | null = null;
 | 
			
		||||
        CoreDomUtils.focusElement(focusElement);
 | 
			
		||||
 | 
			
		||||
            if (this.element.tagName == 'ION-INPUT') {
 | 
			
		||||
                element = this.element.querySelector('input');
 | 
			
		||||
            } else if (this.element.tagName == 'ION-TEXTAREA') {
 | 
			
		||||
                element = this.element.querySelector('textarea');
 | 
			
		||||
            } else {
 | 
			
		||||
                element = this.element;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!element) {
 | 
			
		||||
                this.setFocus(retries - 1);
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            CoreDomUtils.focusElement(element);
 | 
			
		||||
 | 
			
		||||
            if (element != document.activeElement) {
 | 
			
		||||
                this.setFocus(retries - 1);
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        }, 200);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -773,7 +773,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
 | 
			
		||||
     * @param event Event.
 | 
			
		||||
     */
 | 
			
		||||
    stopBubble(event: Event): void {
 | 
			
		||||
        if (event.type != 'touchend' &&event.type != 'mouseup' && event.type != 'keyup') {
 | 
			
		||||
        if (event.type != 'touchend' && event.type != 'mouseup' && event.type != 'keyup') {
 | 
			
		||||
            event.preventDefault();
 | 
			
		||||
        }
 | 
			
		||||
        event.stopPropagation();
 | 
			
		||||
 | 
			
		||||
@ -510,15 +510,30 @@ export class CoreDomUtilsProvider {
 | 
			
		||||
    /**
 | 
			
		||||
     * Focus an element and open keyboard.
 | 
			
		||||
     *
 | 
			
		||||
     * @param el HTML element to focus.
 | 
			
		||||
     * @param focusElement HTML element to focus.
 | 
			
		||||
     */
 | 
			
		||||
    focusElement(el: HTMLElement): void {
 | 
			
		||||
        if (el?.focus) {
 | 
			
		||||
            el.focus();
 | 
			
		||||
            if (CoreApp.isAndroid() && this.supportsInputKeyboard(el)) {
 | 
			
		||||
                // On some Android versions the keyboard doesn't open automatically.
 | 
			
		||||
                CoreApp.openKeyboard();
 | 
			
		||||
    async focusElement(focusElement: HTMLElement): Promise<void> {
 | 
			
		||||
        let retries = 10;
 | 
			
		||||
 | 
			
		||||
        if (!focusElement.focus) {
 | 
			
		||||
            throw new CoreError('Element to focus cannot be focused');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        while (retries > 0 && focusElement !== document.activeElement) {
 | 
			
		||||
            focusElement.focus();
 | 
			
		||||
 | 
			
		||||
            if (focusElement === document.activeElement) {
 | 
			
		||||
                await CoreUtils.nextTick();
 | 
			
		||||
                if (CoreApp.isAndroid() && this.supportsInputKeyboard(focusElement)) {
 | 
			
		||||
                    // On some Android versions the keyboard doesn't open automatically.
 | 
			
		||||
                    CoreApp.openKeyboard();
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // @TODO Probably a Mutation Observer would get this working.
 | 
			
		||||
            await CoreUtils.wait(50);
 | 
			
		||||
            retries--;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -60,7 +60,7 @@ export class CoreForms {
 | 
			
		||||
    /**
 | 
			
		||||
     * Trigger form cancelled event.
 | 
			
		||||
     *
 | 
			
		||||
     * @param form Form element.
 | 
			
		||||
     * @param formRef Form element.
 | 
			
		||||
     * @param siteId The site affected. If not provided, no site affected.
 | 
			
		||||
     */
 | 
			
		||||
    static triggerFormCancelledEvent(formRef?: ElementRef | HTMLFormElement | undefined, siteId?: string): void {
 | 
			
		||||
@ -77,7 +77,7 @@ export class CoreForms {
 | 
			
		||||
    /**
 | 
			
		||||
     * Trigger form submitted event.
 | 
			
		||||
     *
 | 
			
		||||
     * @param form Form element.
 | 
			
		||||
     * @param formRef Form element.
 | 
			
		||||
     * @param online Whether the action was done in offline or not.
 | 
			
		||||
     * @param siteId The site affected. If not provided, no site affected.
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user