Skip to content
Snippets Groups Projects
Commit 4578e628 authored by OZGCloud's avatar OZGCloud
Browse files

OZG-6101 Use cdk for key navigation

parent 6b3a1f3f
Branches
Tags
No related merge requests found
......@@ -6,7 +6,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
standalone: true,
imports: [CommonModule],
template: `<button
class="flex min-h-12 w-full items-center gap-4 border-2 border-transparent px-4 py-3 text-start outline-none hover:border-primary focus:border-focus"
class="flex min-h-12 w-full items-center gap-4 border-2 border-transparent bg-whitetext px-4 py-3 text-start outline-none hover:border-primary focus-visible:border-focus"
role="listitem"
(click)="itemClicked.emit()"
>
......
import { CdkTrapFocus } from '@angular/cdk/a11y';
import { CommonModule } from '@angular/common';
import { Component, ElementRef, HostListener, Input, ViewChild } from '@angular/core';
import { first, last } from 'lodash-es';
import { twMerge } from 'tailwind-merge';
@Component({
selector: 'ods-popup',
standalone: true,
imports: [CommonModule],
imports: [CommonModule, CdkTrapFocus],
template: `<div class="relative w-fit">
<button
class="w-fit outline-2 outline-offset-2 outline-focus"
[ngClass]="[twMerge('w-fit outline-2 outline-offset-2 outline-focus', buttonClass)]"
(click)="toggleClicked()"
[attr.aria-expanded]="showPopupList"
(click)="togglePopup()"
[attr.aria-expanded]="isPopupOpen"
aria-haspopup="true"
[attr.aria-label]="label"
data-test-id="popup-button"
#popupButton
#button
>
<ng-content select="[button]" />
</button>
<ul
*ngIf="showPopupList"
class="absolute max-h-120 min-w-44 max-w-80 animate-fadeIn overflow-y-auto rounded shadow-lg shadow-grayborder focus:outline-none"
*ngIf="isPopupOpen"
class="max-h-120 animate-fadeIn absolute min-w-44 max-w-80 overflow-y-auto rounded shadow-lg shadow-grayborder focus:outline-none"
[ngClass]="alignTo === 'left' ? 'right-0' : 'left-0'"
role="dialog"
aria-modal="true"
tabindex="0"
#popupList
cdkTrapFocus
[cdkTrapFocusAutoCapture]="true"
>
<ng-content />
</ul>
......@@ -38,102 +38,39 @@ export class PopupComponent {
@Input() label: string = '';
@Input() buttonClass: string = '';
constructor(public ref: ElementRef) {}
showPopupList: boolean = false;
isPopupOpen: boolean = false;
twMerge = twMerge;
@ViewChild('popupList') popupListRef: ElementRef<HTMLUListElement>;
@ViewChild('popupButton') popupButtonRef: ElementRef<HTMLButtonElement>;
@ViewChild('button') buttonRef: ElementRef<HTMLButtonElement>;
@HostListener('document:keydown', ['$event'])
onKeydownHandler(e: KeyboardEvent): void {
if (this.isPopupListHidden()) return;
if (this.shouldFocusFirstItem(e)) this.focusFirstItem(this.popupListRef);
if (this.shouldFocusLastItem(e)) this.focusLastItem(this.popupListRef);
if (this.isEscapeKey(e)) this.hidePopupListAndFocusButton();
if (this.isPopupClosed()) return;
if (this.isEscapeKey(e)) this.closePopupAndFocusButton();
}
@HostListener('document:click', ['$event'])
onClickHandler(e: MouseEvent): void {
if (this.isPopupListHidden()) return;
if (!this.popupButtonRef.nativeElement.contains(e.target as HTMLElement)) {
this.hidePopupListAndFocusButton();
}
}
toggleShowPopupList(): void {
this.showPopupList = !this.showPopupList;
}
hidePopupListAndFocusButton(): void {
this.showPopupList = false;
this.popupButtonRef.nativeElement.focus();
}
toggleClicked(): void {
this.toggleShowPopupList();
if (this.showPopupList) {
setTimeout(() => this.popupListRef.nativeElement.focus());
if (this.isPopupClosed()) return;
if (!this.buttonRef.nativeElement.contains(e.target as HTMLElement)) {
this.closePopupAndFocusButton();
}
}
isTabKey(e: KeyboardEvent): boolean {
return e.key === 'Tab' && !e.shiftKey;
togglePopup(): void {
this.isPopupOpen = !this.isPopupOpen;
}
isShiftTabKey(e: KeyboardEvent): boolean {
return e.key === 'Tab' && Boolean(e.shiftKey);
closePopupAndFocusButton(): void {
this.isPopupOpen = false;
this.buttonRef.nativeElement.focus();
}
isEscapeKey(e: KeyboardEvent): boolean {
return e.key === 'Escape';
}
isFirstItemFocused(popupList: ElementRef<HTMLElement>, document: Document): boolean {
if (!popupList) return false;
return popupList.nativeElement.firstChild.contains(document.activeElement);
}
isPopupListFocused(popupList: ElementRef<HTMLElement>, document: Document): boolean {
if (!popupList) return false;
return popupList.nativeElement.isSameNode(document.activeElement);
}
isLastItemFocused(popupList: ElementRef<HTMLElement>, document: Document): boolean {
if (!popupList) return false;
return popupList.nativeElement.lastChild.contains(document.activeElement);
}
isPopupListHidden(): boolean {
return !this.showPopupList;
}
focusFirstItem(popupList: ElementRef<HTMLElement>): void {
const firstItem: HTMLElement = this.getFocusableElement(
first(popupList.nativeElement.children),
);
setTimeout(() => firstItem.focus());
}
focusLastItem(popupList: ElementRef<HTMLElement>): void {
const lastItem: HTMLElement = this.getFocusableElement(last(popupList.nativeElement.children));
setTimeout(() => lastItem.focus());
}
shouldFocusFirstItem(e: KeyboardEvent): boolean {
return this.isTabKey(e) && this.isLastItemFocused(this.popupListRef, document);
}
shouldFocusLastItem(e: KeyboardEvent): boolean {
return (
this.isShiftTabKey(e) &&
(this.isFirstItemFocused(this.popupListRef, document) ||
this.isPopupListFocused(this.popupListRef, document))
);
}
getFocusableElement(element: Element): HTMLElement {
return first(element.querySelectorAll('a[href], button, [tabindex]')) as HTMLElement;
isPopupClosed(): boolean {
return !this.isPopupOpen;
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment