Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions libs/layout/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,20 @@ Sometimes, we do not wish to use the default component at all, and want to provi
</div>
```

#### 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
<div *displayContent="{loading: true}; ariaLive:'assertive' ">
Hello world!
</div>
```

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
Expand Down
5 changes: 4 additions & 1 deletion libs/layout/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
AfterViewInit,
ChangeDetectorRef,
Directive,
ElementRef,
Inject,
Input,
OnDestroy,
Expand All @@ -11,6 +12,7 @@ import {
} from '@angular/core';
import { Subject, distinctUntilChanged, takeUntil, tap } from 'rxjs';
import {
NgxDisplayContentAriaLive,
NgxDisplayContentConditions,
NgxDisplayContentConfiguration,
NgxDisplayContentOverrideConfiguration,
Expand Down Expand Up @@ -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<any>,
private readonly cdRef: ChangeDetectorRef,
private readonly viewContainer: ViewContainerRef,
Expand All @@ -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()
Expand All @@ -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);
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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}`);
}
}
3 changes: 3 additions & 0 deletions libs/layout/src/lib/types/display-content.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<DataType> = Partial<Record<NgxDisplayContentStatus, DataType>>;

export type NgxDisplayContentConditions = NgxDisplayContentRecord<boolean>;
Expand Down