MOBILE-3814 auto-focus: Improve focus handling
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…
Reference in New Issue