Browse Source

fix(admin-ui): Make dropdowns keyboard-accessible

Michael Bromley 2 years ago
parent
commit
d9c6cdddb5

+ 4 - 0
packages/admin-ui/src/lib/core/src/shared/components/dropdown/dropdown-menu.component.scss

@@ -15,6 +15,10 @@
         clr-icon {
             margin-right: 3px;
         }
+        &:focus {
+            outline: var(--color-dropdown-item-focus-outline) solid 1px;
+            outline-offset: 1px 0;
+        }
     }
 }
 

+ 42 - 2
packages/admin-ui/src/lib/core/src/shared/components/dropdown/dropdown-menu.component.ts

@@ -13,6 +13,7 @@ import {
     Component,
     ContentChild,
     ElementRef,
+    HostListener,
     Input,
     OnDestroy,
     OnInit,
@@ -42,7 +43,11 @@ export type DropdownPosition = 'top-left' | 'top-right' | 'bottom-left' | 'botto
         <ng-template #menu>
             <div class="dropdown open">
                 <div class="dropdown-menu" [ngClass]="customClasses">
-                    <div class="dropdown-content-wrapper">
+                    <div
+                        class="dropdown-content-wrapper"
+                        [cdkTrapFocus]="true"
+                        [cdkTrapFocusAutoCapture]="true"
+                    >
                         <ng-content></ng-content>
                     </div>
                 </div>
@@ -56,10 +61,45 @@ export class DropdownMenuComponent implements AfterViewInit, OnInit, OnDestroy {
     @Input('vdrPosition') private position: DropdownPosition = 'bottom-left';
     @Input() customClasses: string;
     @ViewChild('menu', { static: true }) private menuTemplate: TemplateRef<any>;
-    private menuPortal: TemplatePortal<any>;
+    private menuPortal: TemplatePortal;
     private overlayRef: OverlayRef;
     private backdropClickSub: Subscription;
 
+    @HostListener('window:keydown.escape', ['$event'])
+    onEscapeKeydown(event: KeyboardEvent) {
+        if (this.dropdown.isOpen) {
+            if (this.overlayRef.overlayElement.contains(document.activeElement)) {
+                this.dropdown.toggleOpen();
+            }
+        }
+    }
+
+    @HostListener('window:keydown', ['$event'])
+    onArrowKey(event: KeyboardEvent) {
+        if (
+            this.dropdown.isOpen &&
+            document.activeElement instanceof HTMLElement &&
+            (event.key === 'ArrowDown' || event.key === 'ArrowUp')
+        ) {
+            const dropdownItems = Array.from(
+                this.overlayRef.overlayElement.querySelectorAll<HTMLElement>('.dropdown-item'),
+            );
+            const currentIndex = dropdownItems.indexOf(document.activeElement);
+            if (currentIndex === -1) {
+                return;
+            }
+            if (event.key === 'ArrowDown') {
+                const nextItem = dropdownItems[(currentIndex + 1) % dropdownItems.length];
+                nextItem.focus();
+            }
+            if (event.key === 'ArrowUp') {
+                const previousItem =
+                    dropdownItems[(currentIndex - 1 + dropdownItems.length) % dropdownItems.length];
+                previousItem.focus();
+            }
+        }
+    }
+
     constructor(
         private overlay: Overlay,
         private viewContainerRef: ViewContainerRef,

+ 1 - 1
packages/admin-ui/src/lib/core/src/shared/components/dropdown/dropdown.component.ts

@@ -32,7 +32,7 @@ import { ChangeDetectionStrategy, Component, ElementRef, Input } from '@angular/
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class DropdownComponent {
-    private isOpen = false;
+    isOpen = false;
     private onOpenChangeCallbacks: Array<(isOpen: boolean) => void> = [];
     public trigger: ElementRef;
     @Input() manualToggle = false;

+ 2 - 0
packages/admin-ui/src/lib/core/src/shared/shared.module.ts

@@ -1,5 +1,6 @@
 import { DragDropModule } from '@angular/cdk/drag-drop';
 import { OverlayModule } from '@angular/cdk/overlay';
+import { A11yModule } from '@angular/cdk/a11y';
 import { CommonModule } from '@angular/common';
 import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
 import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@@ -182,6 +183,7 @@ const IMPORTS = [
     TranslateModule,
     OverlayModule,
     DragDropModule,
+    A11yModule,
 ];
 
 const DECLARATIONS = [

+ 2 - 0
packages/admin-ui/src/lib/static/styles/theme/default.scss

@@ -185,6 +185,8 @@
 
     --color-login-page-bg: var(--color-weight-100);
 
+    --color-dropdown-item-focus-outline: rgba(77, 207, 255, 0.53);
+
     // Layout
     --layout-content-max-width: 1400px;
     --left-nav-width: 0px;