- Route aria-label/alt/title strings (rating, discount, carousel arrows, hero slides, dialog close, toast dismiss, QR code, bank payment iframe, guest checkout fallback) through the translate pipe/service instead of literal English. - Drop the "Oops!"/"Упс!" apology framing from category/subcategory empty states (en/ru/hy) - zero results is not an error.
110 lines
2.7 KiB
TypeScript
110 lines
2.7 KiB
TypeScript
import {
|
|
AfterViewInit,
|
|
ChangeDetectionStrategy,
|
|
Component,
|
|
ElementRef,
|
|
HostListener,
|
|
input,
|
|
OnChanges,
|
|
output,
|
|
SimpleChanges,
|
|
ViewChild
|
|
} from '@angular/core';
|
|
import { TranslatePipe } from '../../../i18n/translate.pipe';
|
|
|
|
export type DialogSize = 'sm' | 'md' | 'lg';
|
|
|
|
const FOCUSABLE_SELECTOR =
|
|
'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
|
|
@Component({
|
|
selector: 'app-dialog',
|
|
standalone: true,
|
|
imports: [TranslatePipe],
|
|
templateUrl: './dialog.component.html',
|
|
styleUrl: './dialog.component.scss',
|
|
changeDetection: ChangeDetectionStrategy.OnPush
|
|
})
|
|
export class DialogComponent implements OnChanges, AfterViewInit {
|
|
readonly open = input(false);
|
|
readonly titleText = input<string | null>(null);
|
|
readonly size = input<DialogSize>('md');
|
|
|
|
readonly closed = output<void>();
|
|
|
|
@ViewChild('panel') private panelRef?: ElementRef<HTMLElement>;
|
|
|
|
private previouslyFocused: HTMLElement | null = null;
|
|
|
|
ngOnChanges(changes: SimpleChanges): void {
|
|
if (changes['open']) {
|
|
if (this.open()) {
|
|
this.previouslyFocused = document.activeElement as HTMLElement | null;
|
|
queueMicrotask(() => this.focusPanel());
|
|
} else {
|
|
this.previouslyFocused?.focus();
|
|
this.previouslyFocused = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
ngAfterViewInit(): void {
|
|
if (this.open()) {
|
|
this.focusPanel();
|
|
}
|
|
}
|
|
|
|
@HostListener('document:keydown', ['$event'])
|
|
protected handleKeydown(event: KeyboardEvent): void {
|
|
if (!this.open()) {
|
|
return;
|
|
}
|
|
if (event.key === 'Escape') {
|
|
this.requestClose();
|
|
return;
|
|
}
|
|
if (event.key === 'Tab') {
|
|
this.trapFocus(event);
|
|
}
|
|
}
|
|
|
|
protected requestClose(): void {
|
|
this.closed.emit();
|
|
}
|
|
|
|
protected handleBackdropClick(): void {
|
|
this.requestClose();
|
|
}
|
|
|
|
private focusPanel(): void {
|
|
const panel = this.panelRef?.nativeElement;
|
|
if (!panel) {
|
|
return;
|
|
}
|
|
const focusable = panel.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR);
|
|
(focusable[0] ?? panel).focus();
|
|
}
|
|
|
|
private trapFocus(event: KeyboardEvent): void {
|
|
const panel = this.panelRef?.nativeElement;
|
|
if (!panel) {
|
|
return;
|
|
}
|
|
const focusable = Array.from(panel.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR));
|
|
if (focusable.length === 0) {
|
|
return;
|
|
}
|
|
const first = focusable[0];
|
|
const last = focusable[focusable.length - 1];
|
|
const active = document.activeElement;
|
|
|
|
if (event.shiftKey && active === first) {
|
|
event.preventDefault();
|
|
last.focus();
|
|
} else if (!event.shiftKey && active === last) {
|
|
event.preventDefault();
|
|
first.focus();
|
|
}
|
|
}
|
|
}
|