From 505891fa1170e1d9c645c88204e6a9f37b1dd2d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 21 Mar 2022 10:52:45 +0100 Subject: [PATCH] MOBILE-3814 auto-focus: Improve focus handling --- .../services/handlers/mediaplugin.ts | 14 +-- .../show-password/core-show-password.html | 6 +- .../components/show-password/show-password.ts | 97 +++++++++++-------- src/core/directives/auto-focus.ts | 50 +++------- .../rich-text-editor/rich-text-editor.ts | 2 +- src/core/services/utils/dom.ts | 29 ++++-- src/core/singletons/form.ts | 4 +- 7 files changed, 103 insertions(+), 99 deletions(-) diff --git a/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts b/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts index ad08e3186..509451927 100644 --- a/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts +++ b/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts @@ -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 { 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. diff --git a/src/core/components/show-password/core-show-password.html b/src/core/components/show-password/core-show-password.html index b33aefbdd..f6f283d40 100644 --- a/src/core/components/show-password/core-show-password.html +++ b/src/core/components/show-password/core-show-password.html @@ -1,4 +1,6 @@ - - + + diff --git a/src/core/components/show-password/show-password.ts b/src/core/components/show-password/show-password.ts index 56faad272..61d6bbc08 100644 --- a/src/core/components/show-password/show-password.ts +++ b/src/core/components/show-password/show-password.ts @@ -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 { + async ngAfterViewInit(): Promise { 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 = this.element.querySelector(this.selector); + this.input = this.element.querySelector('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(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(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'; + } + } diff --git a/src/core/directives/auto-focus.ts b/src/core/directives/auto-focus.ts index 6aeb4d829..152d3a5e4 100644 --- a/src/core/directives/auto-focus.ts +++ b/src/core/directives/auto-focus.ts @@ -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 { 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); } } diff --git a/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts b/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts index e13862358..370816d78 100644 --- a/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts +++ b/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts @@ -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(); diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index 5e1aed45e..a0f25b3c0 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -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 { + 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--; } } diff --git a/src/core/singletons/form.ts b/src/core/singletons/form.ts index f3f3d8775..f4beefa7c 100644 --- a/src/core/singletons/form.ts +++ b/src/core/singletons/form.ts @@ -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. */