MOBILE-3814 auto-focus: Improve focus handling
parent
faa43fbece
commit
505891fa11
|
@ -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.
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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';
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in New Issue