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
4 changes: 3 additions & 1 deletion apps/layout-test/src/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { TourItemComponent } from '../tour/tour.component';
import { routes } from '../routes';
import { TooltipComponent } from '../tooltip/tooltip.component';
import { ConfirmModalComponent } from '../modal/confirm.component';
import { provideNgxDisplayContentConfiguration } from '@ngx/layout';
import { DragAndDropService } from '../services/drag-and-drop.service';
import { provideNgxDisplayContentConfiguration, provideNgxDragAndDropService } from '@ngx/layout';
import { provideNgxTourConfiguration } from '@ngx/tour';
import { provideNgxModalConfiguration, provideNgxTooltipConfiguration } from '@ngx/inform';

Expand Down Expand Up @@ -37,5 +38,6 @@ export const appConfig: ApplicationConfig = {
panelClass: 'modal-panelelelelele',
}),
provideRouter(routes),
provideNgxDragAndDropService(DragAndDropService),
],
};
4 changes: 3 additions & 1 deletion apps/layout-test/src/pages/main/main.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ <h2>Editable</h2>
[dropPredicate]="drop"
rowGap="10px"
columnGap="10px"
itemLabel="Blok"
rowLabel="rij"
>
<ngx-configurable-layout-item key="a">
<div class="layout-items large">Form key a</div>
Expand All @@ -125,7 +127,7 @@ <h2>Editable</h2>
<div class="layout-items">Form key b</div>
</ngx-configurable-layout-item>

<ngx-configurable-layout-item key="1">
<ngx-configurable-layout-item key="1" label="Custom blok">
<div class="layout-items">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Voluptatum veritatis laudantium
ut officia omnis, impedit eligendi, ea molestiae magnam odit et animi quod illo eius.
Expand Down
10 changes: 10 additions & 0 deletions apps/layout-test/src/services/drag-and-drop.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { NgxAccessibleDragAndDropAbstractService } from '@ngx/layout';

@Injectable({ providedIn: 'root' })
export class DragAndDropService extends NgxAccessibleDragAndDropAbstractService {
get currentLanguage(): string | Observable<string> {
return 'nl';
}
}
47 changes: 45 additions & 2 deletions libs/layout/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,43 @@ For more information about the build process, authors, contributions and issues,

`ngx-layout` is a package to help facilitate common layout use-cases.

Currently the package provides a `configurable layout` component which can be used to render components in a grid based on provided templates. This approach is ideal for use-cases such as a custom configurable dashboard.

Currently the package provides a `configurable layout` component which can be used to render components in a grid based on provided templates. This approach is ideal for use-cases such as a custom configurable dashboard. We also provide a fully accessible `accordion` component and an accessible `displayContent` approach to handle loading, error and offline flows.


## Implementation

### Accessibility

With all the packages of Studio Hyperdrive we aim to provide components and implementations that are WCAG/WAI-ARIA compliant. This means that rather than making this optional to the implementation, we enforce it throughout the packages.

Where custom input is needed to make the implementation accessible, a `Accessibility` chapter can be found for each implementation.

#### Drag and drop
Drag and drop is a common and well known pattern for end users, but often ends up being inaccessible for users that prefer or need to use a keyboard for interacting with the interface. On top of that, for visually impaired users, it becomes difficult to understand how to use this pattern.

Within this package we use `Angular CDK Drag and Drop`, but further enhance to make it accessible for keyboard users and users using screen readers. We already provide several measures for this functionality, but further input from the developer is required.

##### Concept

To make the drag and drop pattern accessible for keyboard users, we allow the items in the drag and drop container to be moved using keyboard interactions. By tabbing to the item and pressing `Enter` or `Space`, we can select an element and then move it using the `Arrow` keys. Once the item is in the correct place, we can deselect the element by pressing the `Enter` or `Space` key again.

For users with assistive technologies, such as screenreaders, we provide a a live region that will announce each change in the drag and drop container. This will announce select events, deselect events and move events. `ngx-layout` provides a set of default messages for a select amount of languages, but offers the ability to overwrite these with your own messages when needed.

##### Implementation

In order to make the drag and drop accessible for every user, you need to provide an implementation of the `NgxAccessibleDragAndDropAbstractService`. This service requires you to provide the current language of your application by implementing the `currentLanguage` method. This can be either a string or an Observable string.

If you wish to overwrite the default message record with your own, you can do that by providing the `customMessages` property. This is however optional, if not provided, the default language options will be provided.

You can provide your service in the following manner:

```ts
providers: [
provideNgxDragAndDropService(DragAndDropService),
]
```
##### Setup

### Components

#### Accordion
Expand Down Expand Up @@ -95,6 +126,18 @@ By using content projection, we render our components inside of a `ngx-configura

This means that the order of rendering is now no longer depended on how you provide the components in the template, but by the two dimensional array provided to the `ngx-configurable-layout` component. This significantly streamlines the process and allows you to easily refactor existing flows. In the chapters below we'll explain how to provide the two dimensional array to the component.

In order to provide an accessible experience for end-users, the earlier mentioned `NgxDragAndDropService` needs to be provided.

##### Accessibility

Currently, default texts have been provided for the following languages: Dutch (`nl`), English (`en`), French (`fr`), German (`de`), Spanish (`es`),Portuguese (`pt`), Turkish (`tr`), and Kurdish (`ku`).

In order to further customize the messages for end users with assistive technologies, we can pass several configuration items to the `ngx-configurable-layout` and `ngx-configurable-layout-item`.

By passing an `itemLabel` and a `rowLabel` we can define specific names for the rows and the items within the rows of the `ngx-configurable-layout`. By default, these are `item` and `list`; but you can change these to your own preference.

The `ngx-configurable-layout-item` also has an optional `label` property which can be used to overwrite both the default and the layout defined label for the item.

##### Static

Earlier we mentioned that the layout is build up using a provided two dimensional array. Depending on whether you want this layout to be `static` or `editable`, we provide the array in a different fashion.
Expand Down
6 changes: 5 additions & 1 deletion libs/layout/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@
"aria",
"display-content",
"loading",
"error"
"error",
"drag and drop",
"accessible drag and drop",
"drag-and-drop",
"accessible drag-and-drop"
],
"homepage": "https://github.com/studiohyperdrive/ngx-tools/tree/master/libs/layout",
"license": "MIT",
Expand Down
165 changes: 165 additions & 0 deletions libs/layout/src/lib/abstracts/drag-and-drop/drag-and-drop.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { filter, map, Observable, of, take, tap } from 'rxjs';
import { inject } from '@angular/core';

import { UUID } from 'angular2-uuid';

import { NgxLiveRegionService } from '../../services';
import {
NgxAccessibleDragAndDropMessage,
NgxAccessibleDragAndDropMessageRecord,
} from '../../types';
import { NgxAccessibleDragAndDropMessageRecords } from '../../const';
import { hideElement } from '../../utils';

/**
* An abstract service that is used to make drag and drop components accessible for assistive technologies
*/
export abstract class NgxAccessibleDragAndDropAbstractService {
/**
* The live region service
*/
private readonly liveRegionService: NgxLiveRegionService = inject(NgxLiveRegionService);

/**
* A method that passes the current language, can either be a string or an Observable
*/
abstract get currentLanguage(): string | Observable<string>;

/**
* A custom set of messages used for the drag and drop events.
*
* Please check the readme for more information on what is necessary to make these messages accessible.
*/
public customMessages: Record<string, NgxAccessibleDragAndDropMessageRecord>;

/**
* Sets a message to the live region
*
* @param message - The provided message
*/
public setMessage(message: NgxAccessibleDragAndDropMessage): Observable<void> {
// Iben: If no language was set, we early exit
if (!this.currentLanguage) {
console.error(
'NgxAccessibleDragAndDropAbstractService: No language was provided, so no message could be set.'
);

return of();
}

// Iben: Take the current language to fetch the message
return (
typeof this.currentLanguage === 'string'
? of(this.currentLanguage)
: this.currentLanguage
).pipe(
filter(Boolean),
take(1),
tap((currentLanguage) => {
// Iben: Fetch the necessary data
const { type, data } = message;

let result: string = this.messageRecord[currentLanguage][type];

// Iben: If no message was found, we early exit and throw an error
if (!result) {
console.error(
'NgxAccessibleDragAndDropAbstractService: No message for the corresponding drag and drop event was found.'
);

return;
}

// Iben: Replace the necessary substrings
if (type === 'selected' || type === 'deselected' || type === 'cancelled') {
result = result.replace(
'{{#item}}',
data.itemLabel || `${this.messageRecord[currentLanguage].item} ${data.item}`
);
} else if (type === 'moved') {
result = result
.replace(
'{{#item}}',
data.itemLabel ||
`${this.messageRecord[currentLanguage].item} ${data.item}`
)
.replace(
`{{#from}}`,
data.fromLabel ||
`${this.messageRecord[currentLanguage].container} ${data.from}`
)
.replace(
`{{#to}}`,
data.toLabel ||
`${this.messageRecord[currentLanguage].container} ${data.to}`
);
} else if (type === 'reordered') {
result = result
.replace(
'{{#item}}',
data.itemLabel ||
`${this.messageRecord[currentLanguage].item} ${data.item}`
)
.replace(`{{#from}}`, data.from)
.replace(`{{#to}}`, data.to);
}

// Iben: Update the message in the live region
this.liveRegionService.setMessage(result);
}),
map(() => null)
);
}

/**
* Adds a description to the drag and drop host explaining how the drag and drop functions
*
* @param parent - The drag and drop host
* @param description - An optional description used to overwrite the default description
*/
public setDragAndDropDescription(parent: HTMLElement, description?: string): Observable<void> {
// Iben: Create the description element and its id
const element: HTMLParagraphElement = document.createElement('p');
const id: string = UUID.UUID();

// Iben: Take the current language to fetch the message
return (
typeof this.currentLanguage === 'string'
? of(this.currentLanguage)
: this.currentLanguage
).pipe(
tap((language: string) => {
// Iben: Get the description text
const text = description || this.messageRecord[language].description;

// Iben: If no description was found, we early exit and throw an error
if (!text) {
console.error(
'NgxAccessibleDragAndDropAbstractService: No description for the drag and drop container was found.'
);

return;
}

// Iben: Set the description and id of the element
element.innerText = text;
element.setAttribute('id', id);

// Iben: Attach the element to the parent and update the aria id
parent.appendChild(element);
parent.setAttribute('aria-describedby', id);

// Iben: Hide element
hideElement(element);
}),
map(() => null)
);
}

/**
* Returns the custom message record or the default when no custom record was provided
*/
private get messageRecord(): Record<string, NgxAccessibleDragAndDropMessageRecord> {
return this.customMessages || NgxAccessibleDragAndDropMessageRecords;
}
}
1 change: 1 addition & 0 deletions libs/layout/src/lib/abstracts/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './display-content/display-content.component';
export * from './drag-and-drop/drag-and-drop.service';
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export class NgxConfigurableLayoutItemComponent {
*/
@Input({ required: true }) public key: string;

/**
* An optional label for the layout item used for WCAG purposes.
*/
@Input() public label: string;

/**
* The template reference of the;
*/
Expand Down
Loading