From 4e065eca7f928f017b0b987300be0cd152c442e7 Mon Sep 17 00:00:00 2001 From: Iben Van de Veire Date: Fri, 18 Oct 2024 08:34:28 +0200 Subject: [PATCH] feat(ngx-layout): Fix issue with displayContent and add WCAG/aria compliancy --- libs/layout/README.md | 14 ++++ libs/layout/package.json | 5 +- .../display-content.directive.ts | 67 +++++++++++++++++++ .../src/lib/types/display-content.types.ts | 3 + 4 files changed, 88 insertions(+), 1 deletion(-) diff --git a/libs/layout/README.md b/libs/layout/README.md index 5c472439..84a4f960 100644 --- a/libs/layout/README.md +++ b/libs/layout/README.md @@ -311,6 +311,20 @@ Sometimes, we do not wish to use the default component at all, and want to provi ``` +#### Accessibility + +In order to provide a WCAG/ARIA compliant implementation, the `*displayContent` directive automatically sets the `aria-live` and `aria-busy` labels when needed. + +By default, the `aria-live` label gets set to `polite`. You can overwrite this setting using the `ariaLive` property in the override configuration. + +```html +
+ Hello world! +
+``` + +If multiple items in a parent have this directive or if the parent already has an `aria-live` label set, the label with the highest importance gets used. The ranking is `assertive`, `polite` and `off` respectively. + ### Services #### NgxOnlineService diff --git a/libs/layout/package.json b/libs/layout/package.json index c5e80418..ed0dcb59 100644 --- a/libs/layout/package.json +++ b/libs/layout/package.json @@ -13,7 +13,10 @@ "visual", "accordion", "wcag", - "aria" + "aria", + "display-content", + "loading", + "error" ], "homepage": "https://github.com/studiohyperdrive/ngx-tools/tree/master/libs/layout", "license": "MIT", diff --git a/libs/layout/src/lib/directives/display-content/display-content.directive.ts b/libs/layout/src/lib/directives/display-content/display-content.directive.ts index 4f61190b..3d945132 100644 --- a/libs/layout/src/lib/directives/display-content/display-content.directive.ts +++ b/libs/layout/src/lib/directives/display-content/display-content.directive.ts @@ -2,6 +2,7 @@ import { AfterViewInit, ChangeDetectorRef, Directive, + ElementRef, Inject, Input, OnDestroy, @@ -11,6 +12,7 @@ import { } from '@angular/core'; import { Subject, distinctUntilChanged, takeUntil, tap } from 'rxjs'; import { + NgxDisplayContentAriaLive, NgxDisplayContentConditions, NgxDisplayContentConfiguration, NgxDisplayContentOverrideConfiguration, @@ -72,7 +74,15 @@ export class NgxDisplayContentDirective implements AfterViewInit, OnDestroy { this.updateViewSubject.next(); } + /** + * The aria-live label we wish to provide to the parent element. By default, this is 'polite'. + * + * https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions + */ + @Input() displayContentAriaLive: NgxDisplayContentAriaLive = 'polite'; + constructor( + private readonly elementRef: ElementRef, private readonly templateRef: TemplateRef, private readonly cdRef: ChangeDetectorRef, private readonly viewContainer: ViewContainerRef, @@ -99,6 +109,9 @@ export class NgxDisplayContentDirective implements AfterViewInit, OnDestroy { } public ngAfterViewInit(): void { + // Iben: Set the aria-live and aria-busy tag of the parent + this.setAriaLiveTag(this.displayContentAriaLive); + // Iben: Listen to whenever we need to update the view and act accordingly this.updateViewSubject .asObservable() @@ -107,6 +120,9 @@ export class NgxDisplayContentDirective implements AfterViewInit, OnDestroy { // Iben: Clear the current view container this.viewContainer.clear(); + // Iben: Update the busy tag + this.setAriaBusyTag(this.conditions.loading); + // Iben: If we're offline, we render the offline component or template if (this.conditions.offline) { this.renderTemplate('offline', this.configuration.components.offline); @@ -137,6 +153,9 @@ export class NgxDisplayContentDirective implements AfterViewInit, OnDestroy { takeUntil(this.onDestroySubject.asObservable()) ) .subscribe(); + + // Iben: Run the initial content check + this.updateViewSubject.next(); } public ngOnDestroy(): void { @@ -203,4 +222,52 @@ export class NgxDisplayContentDirective implements AfterViewInit, OnDestroy { }; } } + + /** + * Sets the aria-live tag of the item + * @param value - The value we wish to set + */ + private setAriaLiveTag(value: 'polite' | 'assertive' | 'off'): void { + // Iben: Get the parent element and early exit if it isn't found + const parentElement: HTMLElement = this.elementRef.nativeElement.parentElement; + + if (!parentElement) { + // Iben: + console.error( + 'NgxLayout: No parent element was found for NgxDisplayContentDirective. Because of that, the correct aria-live label could not be set.' + ); + + return; + } + + // Iben: If the value is assertive then we always set it, as it has the highest priority + if (value === 'assertive') { + parentElement.setAttribute('aria-live', value); + + return; + } + + // Iben: Fetch the current aria-live label. If none were found, set it automatically + const currentValue = parentElement.getAttribute('aria-live'); + + if (!currentValue) { + parentElement.setAttribute('aria-live', value); + } + + // Iben: If the current value is assertive or if the values are the same, we early exit + if (currentValue === 'assertive' || currentValue === value) { + return; + } + + // Iben: Set the value + parentElement.setAttribute('aria-live', value); + } + + /** + * Sets the aria-busy tag of the item + * @param isLoading - The loading state of the item + */ + private setAriaBusyTag(isLoading: boolean): void { + this.elementRef.nativeElement.parentElement?.setAttribute('aria-busy', `${isLoading}`); + } } diff --git a/libs/layout/src/lib/types/display-content.types.ts b/libs/layout/src/lib/types/display-content.types.ts index c8812da2..7038f31c 100644 --- a/libs/layout/src/lib/types/display-content.types.ts +++ b/libs/layout/src/lib/types/display-content.types.ts @@ -2,6 +2,9 @@ import { TemplateRef, Type } from '@angular/core'; import { NgxDisplayContentComponent } from '../abstracts'; export type NgxDisplayContentStatus = 'loading' | 'error' | 'offline'; + +export type NgxDisplayContentAriaLive = 'polite' | 'assertive' | 'off'; + type NgxDisplayContentRecord = Partial>; export type NgxDisplayContentConditions = NgxDisplayContentRecord;