MOBILE-3814 auto-focus: Improve focus handling

main
Pau Ferrer Ocaña 2022-03-21 10:52:45 +01:00
parent faa43fbece
commit 505891fa11
7 changed files with 103 additions and 99 deletions

View File

@ -15,7 +15,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { CoreFilterDefaultHandler } from '@features/filter/services/handlers/default-filter'; import { CoreFilterDefaultHandler } from '@features/filter/services/handlers/default-filter';
import { CoreFilterFilter, CoreFilterFormatTextOptions } from '@features/filter/services/filter';
import { CoreTextUtils } from '@services/utils/text'; import { CoreTextUtils } from '@services/utils/text';
import { CoreUrlUtils } from '@services/utils/url'; import { CoreUrlUtils } from '@services/utils/url';
import { makeSingleton } from '@singletons'; 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. protected template = document.createElement('template'); // A template element to convert HTML to element.
/** /**
* Filter some text. * @inheritdoc
*
* @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).
*/ */
filter( filter(
text: string, 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> { ): string | Promise<string> {
this.template.innerHTML = text; this.template.innerHTML = text;
@ -60,7 +50,7 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl
/** /**
* Treat video filters. Currently only treating youtube video using video JS. * Treat video filters. Currently only treating youtube video using video JS.
* *
* @param el Video element. * @param video Video element.
*/ */
protected treatVideoFilters(video: HTMLElement): void { protected treatVideoFilters(video: HTMLElement): void {
// Treat Video JS Youtube video links and translate them to iframes. // Treat Video JS Youtube video links and translate them to iframes.

View File

@ -1,4 +1,6 @@
<ng-content></ng-content> <ng-content></ng-content>
<ion-button fill="clear" [attr.aria-label]="label | translate" core-suppress-events (onClick)="toggle($event)"> <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> (mousedown)="doNotBlur($event)" (keydown)="doNotBlur($event)" (keyup)="toggle($event)">
<ion-icon [name]=" iconName" slot="icon-only" aria-hidden="true">
</ion-icon>
</ion-button> </ion-button>

View File

@ -44,12 +44,11 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
@Input() initialShown?: boolean | string; // Whether the password should be shown at start. @Input() initialShown?: boolean | string; // Whether the password should be shown at start.
@ContentChild(IonInput) ionInput?: IonInput; @ContentChild(IonInput) ionInput?: IonInput;
shown!: boolean; // Whether the password is shown. shown = false; // Whether the password is shown.
label!: string; // Label for the button to show/hide. label = ''; // Label for the button to show/hide.
iconName!: string; // Name of the icon of the button to show/hide. iconName = ''; // Name of the icon of the button to show/hide.
selector = ''; // Selector to identify the input.
protected input?: HTMLInputElement | null; // Input affected. protected input?: HTMLInputElement; // Input affected.
protected element: HTMLElement; // Current element. protected element: HTMLElement; // Current element.
constructor(element: ElementRef) { constructor(element: ElementRef) {
@ -57,58 +56,51 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
} }
/** /**
* Component being initialized. * @inheritdoc
*/ */
ngOnInit(): void { ngOnInit(): void {
this.shown = CoreUtils.isTrueOrOne(this.initialShown); this.shown = CoreUtils.isTrueOrOne(this.initialShown);
this.selector = 'input[name="' + this.name + '"]';
this.setData();
} }
/** /**
* View has been initialized. * @inheritdoc
*/ */
ngAfterViewInit(): void { async ngAfterViewInit(): Promise<void> {
this.searchInput();
}
/**
* Search the input to show/hide.
*/
protected async searchInput(): Promise<void> {
if (this.ionInput) { if (this.ionInput) {
// It's an ion-input, use it to get the native element. // It's an ion-input, use it to get the native element.
this.input = await this.ionInput.getInputElement(); this.input = await this.ionInput.getInputElement();
this.setData(this.input);
return; return;
} }
// Search the input. // 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) { if (!this.input) {
// Input found. Set the right type. return;
this.input.type = this.shown ? 'text' : 'password'; }
// By default, don't autocapitalize and autocorrect. this.setData(this.input);
if (!this.input.getAttribute('autocorrect')) {
this.input.setAttribute('autocorrect', 'off'); // By default, don't autocapitalize and autocorrect.
} if (!this.input.getAttribute('autocorrect')) {
if (!this.input.getAttribute('autocapitalize')) { this.input.setAttribute('autocorrect', 'off');
this.input.setAttribute('autocapitalize', 'none'); }
} if (!this.input.getAttribute('autocapitalize')) {
this.input.setAttribute('autocapitalize', 'none');
} }
} }
/** /**
* Set label, icon name and input type. * 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.label = this.shown ? 'core.hide' : 'core.show';
this.iconName = this.shown ? 'fas-eye-slash' : 'fas-eye'; this.iconName = this.shown ? 'fas-eye-slash' : 'fas-eye';
if (this.input) { input.type = this.shown ? 'text' : 'password';
this.input.type = this.shown ? 'text' : 'password';
}
} }
/** /**
@ -117,20 +109,49 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
* @param event The mouse event. * @param event The mouse event.
*/ */
toggle(event: Event): void { toggle(event: Event): void {
if (event.type == 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) {
return;
}
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
const isFocused = document.activeElement === this.input; const isFocused = document.activeElement === this.input;
this.shown = !this.shown; 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()) { if (isFocused && CoreApp.isAndroid()) {
// In Android, the keyboard is closed when the input type changes. Focus it again. CoreDomUtils.focusElement(this.input);
setTimeout(() => {
CoreDomUtils.focusElement(this.input!);
}, 400);
} }
} }
/**
* 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';
}
} }

View File

@ -32,7 +32,7 @@ export class CoreAutoFocusDirective implements AfterViewInit {
@Input('core-auto-focus') autoFocus: boolean | string = true; @Input('core-auto-focus') autoFocus: boolean | string = true;
protected element: HTMLElement; protected element: HTMLIonInputElement | HTMLIonTextareaElement | HTMLIonSearchbarElement | HTMLElement;
constructor(element: ElementRef) { constructor(element: ElementRef) {
this.element = element.nativeElement; this.element = element.nativeElement;
@ -41,51 +41,27 @@ export class CoreAutoFocusDirective implements AfterViewInit {
/** /**
* @inheritdoc * @inheritdoc
*/ */
ngAfterViewInit(): void { async ngAfterViewInit(): Promise<void> {
if (CoreUtils.isFalseOrZero(this.autoFocus)) { if (CoreUtils.isFalseOrZero(this.autoFocus)) {
return; return;
} }
this.setFocus(); await CoreDomUtils.waitToBeInDOM(this.element);
}
/** let focusElement = this.element;
* Function to focus the element.
* if ('getInputElement' in focusElement) {
* @param retries Internal param to stop retrying on 0. // If it's an Ionic element get the right input to use.
*/ focusElement.componentOnReady && await focusElement.componentOnReady();
protected setFocus(retries = 10): void { focusElement = await focusElement.getInputElement();
if (retries == 0) { }
if (!focusElement) {
return; return;
} }
// Wait a bit to make sure the view is loaded. CoreDomUtils.focusElement(focusElement);
setTimeout(() => {
// If it's a ion-input or ion-textarea, search the right input to use.
let element: HTMLElement | null = null;
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);
} }
} }

View File

@ -773,7 +773,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
* @param event Event. * @param event Event.
*/ */
stopBubble(event: Event): void { 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.preventDefault();
} }
event.stopPropagation(); event.stopPropagation();

View File

@ -510,15 +510,30 @@ export class CoreDomUtilsProvider {
/** /**
* Focus an element and open keyboard. * Focus an element and open keyboard.
* *
* @param el HTML element to focus. * @param focusElement HTML element to focus.
*/ */
focusElement(el: HTMLElement): void { async focusElement(focusElement: HTMLElement): Promise<void> {
if (el?.focus) { let retries = 10;
el.focus();
if (CoreApp.isAndroid() && this.supportsInputKeyboard(el)) { if (!focusElement.focus) {
// On some Android versions the keyboard doesn't open automatically. throw new CoreError('Element to focus cannot be focused');
CoreApp.openKeyboard(); }
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--;
} }
} }

View File

@ -60,7 +60,7 @@ export class CoreForms {
/** /**
* Trigger form cancelled event. * Trigger form cancelled event.
* *
* @param form Form element. * @param formRef Form element.
* @param siteId The site affected. If not provided, no site affected. * @param siteId The site affected. If not provided, no site affected.
*/ */
static triggerFormCancelledEvent(formRef?: ElementRef | HTMLFormElement | undefined, siteId?: string): void { static triggerFormCancelledEvent(formRef?: ElementRef | HTMLFormElement | undefined, siteId?: string): void {
@ -77,7 +77,7 @@ export class CoreForms {
/** /**
* Trigger form submitted event. * Trigger form submitted event.
* *
* @param form Form element. * @param formRef Form element.
* @param online Whether the action was done in offline or not. * @param online Whether the action was done in offline or not.
* @param siteId The site affected. If not provided, no site affected. * @param siteId The site affected. If not provided, no site affected.
*/ */