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 { 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.

View File

@ -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>

View File

@ -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';
}
}

View File

@ -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);
}
}

View File

@ -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();

View File

@ -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--;
}
}

View File

@ -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.
*/