MOBILE-4470 rte: Fix focus and blur

main
Pau Ferrer Ocaña 2024-05-10 13:36:29 +02:00
parent dc698724ac
commit 0fedfee2e4
4 changed files with 59 additions and 59 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -1,12 +1,12 @@
<div class="core-rte-editor-container" (click)="focusRTE()" [class.toolbar-hidden]="toolbarHidden"> <div class="core-rte-editor-container" (click)="focusRTE($event)" [class.toolbar-hidden]="toolbarHidden">
<div [hidden]="!rteEnabled" #editor class="core-rte-editor" role="textbox" contenteditable="true" [class.empty]="isEmpty" <div [hidden]="!rteEnabled" #editor class="core-rte-editor" role="textbox" contenteditable="true" [class.empty]="isEmpty"
[attr.aria-labelledby]="ariaLabelledBy" [attr.data-placeholder-text]="placeholder" (focus)="showToolbar($event)" [attr.aria-labelledby]="ariaLabelledBy" [attr.data-placeholder-text]="placeholder" (focus)="focusRTE($event)"
(blur)="hideToolbar($event)" (keydown)="onKeyDown($event)"> (blur)="blurRTE($event)" (keydown)="onKeyDown($event)">
</div> </div>
<ion-textarea [hidden]="rteEnabled" #textarea class="core-textarea" role="textbox" [attr.name]="name" ngControl="control" <ion-textarea [hidden]="rteEnabled" #textarea class="core-textarea" role="textbox" [attr.name]="name" ngControl="control"
[placeholder]="placeholder" [attr.aria-labelledby]="ariaLabelledBy" (ionChange)="onChange()" (ionFocus)="showToolbar($event)" [placeholder]="placeholder" [attr.aria-labelledby]="ariaLabelledBy" (ionChange)="onChange()" (ionFocus)="focusRTE($event)"
(ionBlur)="hideToolbar($event)" /> (ionBlur)="blurRTE($event)" />
<div class="core-rte-info-message" *ngIf="infoMessage"> <div class="core-rte-info-message" *ngIf="infoMessage">
<ion-icon name="fas-circle-info" aria-hidden="true" /> <ion-icon name="fas-circle-info" aria-hidden="true" />
@ -110,12 +110,6 @@
<ion-icon name="fas-code" aria-hidden="true" /> <ion-icon name="fas-code" aria-hidden="true" />
</button> </button>
</swiper-slide> </swiper-slide>
<swiper-slide *ngIf="isPhone">
<button [title]="'core.editor.hidetoolbar' | translate" (click)="hideToolbar($event, true)" (keyup)="hideToolbar($event, true)"
(mousedown)="downAction($event)" (keydown)="downAction($event)" tabindex="0">
<ion-icon name="fas-xmark" aria-hidden="true" />
</button>
</swiper-slide>
</swiper-container> </swiper-container>
<button *ngIf="toolbarArrows" class="toolbar-arrow" [attr.disabled]="toolbarNextHidden ? 'true' : null" <button *ngIf="toolbarArrows" class="toolbar-arrow" [attr.disabled]="toolbarNextHidden ? 'true' : null"
[attr.aria-label]="'core.next' | translate" (click)="toolbarNext($event)" (keyup)="toolbarNext($event)" [attr.aria-label]="'core.next' | translate" (click)="toolbarNext($event)" (keyup)="toolbarNext($event)"

View File

@ -25,7 +25,7 @@
border: 1px solid var(--stroke); border: 1px solid var(--stroke);
border-radius: var(--mdl-shape-borderRadius-md); border-radius: var(--mdl-shape-borderRadius-md);
&:focus-within { &.has-focus {
border-color: var(--a11y-focus-color); border-color: var(--a11y-focus-color);
border-width: 2px; border-width: 2px;
outline: none !important; outline: none !important;

View File

@ -78,9 +78,13 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
@Input() draftExtraParams?: Record<string, unknown>; // Extra params to identify the draft. @Input() draftExtraParams?: Record<string, unknown>; // Extra params to identify the draft.
@Output() contentChanged: EventEmitter<string | undefined | null>; @Output() contentChanged: EventEmitter<string | undefined | null>;
@ViewChild('editor') editor?: ElementRef; // WYSIWYG editor. protected editorElement?: HTMLDivElement; // WYSIWYG editor.
@ViewChild('editor') editor?: ElementRef<HTMLDivElement>;
@ViewChild('toolbar') toolbar?: ElementRef<HTMLDivElement>;
protected textareaElement?: HTMLTextAreaElement;
@ViewChild('textarea') textarea?: IonTextarea; // Textarea editor. @ViewChild('textarea') textarea?: IonTextarea; // Textarea editor.
@ViewChild('toolbar') toolbar?: ElementRef;
protected toolbarSlides?: Swiper; protected toolbarSlides?: Swiper;
@ViewChild('swiperRef') set swiperRef(swiperRef: ElementRef) { @ViewChild('swiperRef') set swiperRef(swiperRef: ElementRef) {
@ -88,7 +92,9 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
* This setTimeout waits for Ionic's async initialization to complete. * This setTimeout waits for Ionic's async initialization to complete.
* Otherwise, an outdated swiper reference will be used. * Otherwise, an outdated swiper reference will be used.
*/ */
setTimeout(() => { setTimeout(async () => {
await this.waitLoadingsDone();
const swiper = CoreSwiper.initSwiperIfAvailable(this.toolbarSlides, swiperRef, this.swiperOpts); const swiper = CoreSwiper.initSwiperIfAvailable(this.toolbarSlides, swiperRef, this.swiperOpts);
if (!swiper) { if (!swiper) {
return; return;
@ -102,8 +108,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
protected readonly RESTORE_MESSAGE_CLEAR_TIME = 6000; protected readonly RESTORE_MESSAGE_CLEAR_TIME = 6000;
protected readonly SAVE_MESSAGE_CLEAR_TIME = 2000; protected readonly SAVE_MESSAGE_CLEAR_TIME = 2000;
protected element: HTMLDivElement; protected element: HTMLElement;
protected editorElement?: HTMLDivElement;
protected minHeight = 200; // Minimum height of the editor. protected minHeight = 200; // Minimum height of the editor.
protected valueChangeSubscription?: Subscription; protected valueChangeSubscription?: Subscription;
@ -127,6 +132,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
protected domPromise?: CoreCancellablePromise<void>; protected domPromise?: CoreCancellablePromise<void>;
protected buttonsDomPromise?: CoreCancellablePromise<void>; protected buttonsDomPromise?: CoreCancellablePromise<void>;
protected shortcutCommands?: Record<string, EditorCommand>; protected shortcutCommands?: Record<string, EditorCommand>;
protected blurTimeout?: number;
rteEnabled = false; rteEnabled = false;
isPhone = false; isPhone = false;
@ -165,7 +171,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
elementRef: ElementRef, elementRef: ElementRef,
) { ) {
this.contentChanged = new EventEmitter<string>(); this.contentChanged = new EventEmitter<string>();
this.element = elementRef.nativeElement as HTMLDivElement; this.element = elementRef.nativeElement;
this.pageInstance = 'app_' + Date.now(); // Generate a "unique" ID based on timestamp. this.pageInstance = 'app_' + Date.now(); // Generate a "unique" ID based on timestamp.
} }
@ -188,6 +194,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
// Setup the editor. // Setup the editor.
this.editorElement = this.editor?.nativeElement as HTMLDivElement; this.editorElement = this.editor?.nativeElement as HTMLDivElement;
this.textareaElement = await this.textarea?.getInputElement();
this.setContent(this.control?.value); this.setContent(this.control?.value);
this.originalContent = this.control?.value ?? undefined; this.originalContent = this.control?.value ?? undefined;
this.lastDraft = this.control?.value ?? ''; this.lastDraft = this.control?.value ?? '';
@ -508,17 +515,18 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
// Set focus and cursor at the end. // Set focus and cursor at the end.
// Modify the DOM directly so the keyboard stays open. // Modify the DOM directly so the keyboard stays open.
if (this.rteEnabled) { if (this.rteEnabled) {
this.editorElement?.removeAttribute('hidden'); this.editorElement?.removeAttribute('hidden');
const textareaInputElement = await this.textarea?.getInputElement(); this.textareaElement?.setAttribute('hidden', '');
textareaInputElement?.setAttribute('hidden', '');
this.editorElement?.focus();
} else { } else {
this.editorElement?.setAttribute('hidden', ''); this.editorElement?.setAttribute('hidden', '');
const textareaInputElement = await this.textarea?.getInputElement(); this.textareaElement?.removeAttribute('hidden');
textareaInputElement?.removeAttribute('hidden');
this.textarea?.setFocus();
} }
await CoreUtils.nextTick();
this.focusRTE(event);
} }
/** /**
@ -707,40 +715,28 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
} }
/** /**
* Focus editor when click the area. * Blur and hide the toolbar in phone mode.
*/
focusRTE(): void {
if (this.rteEnabled) {
this.editorElement?.focus();
} else {
this.textarea?.setFocus();
}
}
/**
* Hide the toolbar in phone mode.
* *
* @param event Event. * @param event Event.
* @param force If true it will not check the target of the event.
*/ */
hideToolbar(event: FocusEvent | KeyboardEvent | MouseEvent, force = false): void { blurRTE(event: FocusEvent): void {
if (!force && event.target && this.element.contains(event.target as HTMLElement)) { const doBlur = (event: FocusEvent) => {
// Do not hide if clicked inside the editor area, except forced. if (this.element.contains(document.activeElement)) {
// Do not hide if clicked inside the editor area, except hideButton.
return; return;
} }
if (event.type === 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) { this.element.classList.remove('has-focus');
return;
}
this.element.classList.remove('has-focus'); this.stopBubble(event);
this.stopBubble(event); if (this.isPhone) {
this.toolbarHidden = true;
}
};
if (this.isPhone) { this.blurTimeout = window.setTimeout(() => doBlur(event),300);
this.toolbarHidden = true;
}
} }
/** /**
@ -754,19 +750,28 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
} }
/** /**
* Show the toolbar. * Focus editor when click the area and show toolbar.
*
* @param event Event.
*/ */
showToolbar(event: FocusEvent): void { focusRTE(event: Event): void {
this.updateToolbarButtons(); clearTimeout(this.blurTimeout);
if (this.rteEnabled) {
this.editorElement?.focus();
} else {
this.textarea?.setFocus();
}
this.element.classList.add('ion-touched'); this.element.classList.add('ion-touched');
this.element.classList.remove('ion-untouched'); this.element.classList.remove('ion-untouched');
this.element.classList.add('has-focus'); this.element.classList.add('has-focus');
this.stopBubble(event); event && this.stopBubble(event);
this.editorElement?.focus();
this.toolbarHidden = false; this.toolbarHidden = false;
this.updateToolbarButtons();
} }
/** /**
@ -839,7 +844,8 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
* Update the number of toolbar buttons displayed. * Update the number of toolbar buttons displayed.
*/ */
async updateToolbarButtons(): Promise<void> { async updateToolbarButtons(): Promise<void> {
if (!this.isCurrentView || !this.toolbar || !this.toolbarSlides || this.element.offsetParent === null) { if (!this.isCurrentView || !this.toolbar || !this.toolbarSlides ||
this.toolbarHidden || this.element.offsetParent === null) {
// Don't calculate if component isn't in current view, the calculations are wrong. // Don't calculate if component isn't in current view, the calculations are wrong.
return; return;
} }
@ -848,10 +854,10 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
// Cancel previous one, if any. // Cancel previous one, if any.
this.buttonsDomPromise?.cancel(); this.buttonsDomPromise?.cancel();
this.buttonsDomPromise = CoreDom.waitToBeInDOM(this.toolbar.nativeElement); this.buttonsDomPromise = CoreDom.waitToBeInDOM(this.toolbar?.nativeElement);
await this.buttonsDomPromise; await this.buttonsDomPromise;
const width = this.toolbar.nativeElement.getBoundingClientRect().width; const width = this.toolbar?.nativeElement.getBoundingClientRect().width;
if (length > 0 && width > length * this.toolbarButtonWidth) { if (length > 0 && width > length * this.toolbarButtonWidth) {
this.swiperOpts.slidesPerView = length; this.swiperOpts.slidesPerView = length;
@ -1069,7 +1075,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit,
const text = await CoreUtils.scanQR(); const text = await CoreUtils.scanQR();
if (text) { if (text) {
this.editorElement?.focus(); // Make sure the editor is focused. this.focusRTE(event); // Make sure the editor is focused.
// eslint-disable-next-line deprecation/deprecation // eslint-disable-next-line deprecation/deprecation
document.execCommand('insertText', false, text); document.execCommand('insertText', false, text);
} }