diff --git a/.changeset/cuddly-bottles-camp.md b/.changeset/cuddly-bottles-camp.md
new file mode 100644
index 0000000000..6fa18fa227
--- /dev/null
+++ b/.changeset/cuddly-bottles-camp.md
@@ -0,0 +1,5 @@
+---
+'@lion/ui': minor
+---
+
+[dialog] add an option to set role="alertdialog" instead of the default role="dialog"
diff --git a/docs/components/dialog/use-cases.md b/docs/components/dialog/use-cases.md
index 0ea5f21419..58662eb119 100644
--- a/docs/components/dialog/use-cases.md
+++ b/docs/components/dialog/use-cases.md
@@ -6,7 +6,8 @@ Its purpose is to make it easy to use our Overlay System declaratively.
```js script
import { html } from '@mdjs/mdjs-preview';
import '@lion/ui/define/lion-dialog.js';
-
+import '@lion/ui/define/lion-form.js';
+import '@lion/ui/define/lion-input.js';
import { demoStyle } from './src/demoStyle.js';
import './src/styled-dialog-content.js';
import './src/slots-dialog-content.js';
@@ -23,6 +24,71 @@ import './src/external-dialog.js';
```
+## Alert dialog
+
+In some cases the dialog should act like an [alertdialog](https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/examples/alertdialog/), which is a combination of an alert and dialog. If that is the case, you can add `is-alert-dialog` attribute, which sets the correct role on the dialog.
+
+```js preview-story
+export const alertDialog = () => {
+ const submitHandler = ev => {
+ const formData = ev.target.serializedValue;
+ console.log('formData', formData);
+ if (!ev.target.hasFeedbackFor?.includes('error')) {
+ fetch('/api/foo/', {
+ method: 'POST',
+ body: JSON.stringify(formData),
+ });
+ }
+ };
+ const resetHandler = ev => {
+ ev.target.dispatchEvent(new Event('close-overlay', { bubbles: true }));
+ ev.target.dispatchEvent(new Event('form-reset', { bubbles: true }));
+ };
+ const formResetHandler = ev => {
+ ev.currentTarget.resetGroup();
+ };
+ return html`
+
+
+
+
+ `;
+};
+```
+
## External trigger
```js preview-story
diff --git a/docs/fundamentals/systems/overlays/configuration.md b/docs/fundamentals/systems/overlays/configuration.md
index ec7b91e4af..43be0fc2bd 100644
--- a/docs/fundamentals/systems/overlays/configuration.md
+++ b/docs/fundamentals/systems/overlays/configuration.md
@@ -67,6 +67,30 @@ export const placementGlobal = () => {
};
```
+## isAlertDialog
+
+In some cases the dialog should act like an [alertdialog](https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/examples/alertdialog/), which is a combination of an alert and dialog. If that is the case, you can add `is-alert-dialog` attribute, which sets the correct role on the dialog.
+
+```js preview-story
+export const alertDialog = () => {
+ const placementModeGlobalConfig = { placementMode: 'global', isAlertDialog: true };
+ return html`
+
+
+
+ Hello! You can close this notification here:
+
+
+
+ `;
+};
+```
+
## isTooltip (placementMode: 'local')
As specified in the [overlay rationale](./rationale.md) there are only two official types of overlays: dialogs and tooltips. And their main differences are:
diff --git a/packages/ui/components/dialog/src/LionDialog.js b/packages/ui/components/dialog/src/LionDialog.js
index 906f3bb403..55069f95d9 100644
--- a/packages/ui/components/dialog/src/LionDialog.js
+++ b/packages/ui/components/dialog/src/LionDialog.js
@@ -2,6 +2,18 @@ import { html, LitElement } from 'lit';
import { OverlayMixin, withModalDialogConfig } from '@lion/ui/overlays.js';
export class LionDialog extends OverlayMixin(LitElement) {
+ /** @type {any} */
+ static get properties() {
+ return {
+ isAlertDialog: { type: Boolean, attribute: 'is-alert-dialog' },
+ };
+ }
+
+ constructor() {
+ super();
+ this.isAlertDialog = false;
+ }
+
/**
* @protected
*/
@@ -9,6 +21,7 @@ export class LionDialog extends OverlayMixin(LitElement) {
_defineOverlayConfig() {
return {
...withModalDialogConfig(),
+ isAlertDialog: this.isAlertDialog,
};
}
diff --git a/packages/ui/components/dialog/test/lion-dialog.test.js b/packages/ui/components/dialog/test/lion-dialog.test.js
index 35074be09c..2902ea7a4b 100644
--- a/packages/ui/components/dialog/test/lion-dialog.test.js
+++ b/packages/ui/components/dialog/test/lion-dialog.test.js
@@ -170,6 +170,27 @@ describe('lion-dialog', () => {
});
describe('Accessibility', () => {
+ it('passes a11y audit', async () => {
+ const el = await fixture(html`
+
+
+
+
+ `);
+ // error expected since we put role="none" on the dialog itself, which is valid but not recognized by Axe
+ await expect(el).to.be.accessible({ ignoredRules: ['aria-allowed-role'] });
+ });
});
describe('Edge cases', () => {
diff --git a/packages/ui/components/overlays/src/OverlayController.js b/packages/ui/components/overlays/src/OverlayController.js
index 17f15a20a7..563b78f233 100644
--- a/packages/ui/components/overlays/src/OverlayController.js
+++ b/packages/ui/components/overlays/src/OverlayController.js
@@ -151,6 +151,7 @@ export class OverlayController extends EventTarget {
hidesOnOutsideEsc: false,
hidesOnOutsideClick: false,
isTooltip: false,
+ isAlertDialog: false,
invokerRelation: 'description',
visibilityTriggerFunction: undefined,
handlesAccessibility: false,
@@ -381,6 +382,14 @@ export class OverlayController extends EventTarget {
return /** @type {boolean} */ (this.config?.isTooltip);
}
+ /**
+ * The alertdialog role is to be used on modal alert dialogs that interrupt a user's workflow
+ * to communicate an important message and require a response.
+ */
+ get isAlertDialog() {
+ return /** @type {boolean} */ (this.config?.isAlertDialog);
+ }
+
/**
* By default, the tooltip content is a 'description' for the invoker (uses aria-describedby).
* Setting this property to 'label' makes the content function as a label (via aria-labelledby)
@@ -672,7 +681,9 @@ export class OverlayController extends EventTarget {
if (this.invokerNode && !isModal) {
this.invokerNode.setAttribute('aria-expanded', `${this.isShown}`);
}
- if (!this.contentNode.getAttribute('role')) {
+ if (this.isAlertDialog) {
+ this.contentNode.setAttribute('role', 'alertdialog');
+ } else if (!this.contentNode.getAttribute('role')) {
this.contentNode.setAttribute('role', 'dialog');
}
}
diff --git a/packages/ui/components/overlays/test/OverlayController.test.js b/packages/ui/components/overlays/test/OverlayController.test.js
index 5eca2c44c7..5ebd4bed77 100644
--- a/packages/ui/components/overlays/test/OverlayController.test.js
+++ b/packages/ui/components/overlays/test/OverlayController.test.js
@@ -1876,6 +1876,22 @@ describe('OverlayController', () => {
it.skip('adds attributes inert and aria-hidden="true" on all siblings of rootNode if an overlay is shown', async () => {});
it.skip('disables pointer events and selection on inert elements', async () => {});
+ describe('Alert dialog', () => {
+ it('sets role="alertdialog" when isAlertDialog is set', async () => {
+ const invokerNode = /** @type {HTMLElement} */ (
+ await fixture('
invoker
')
+ );
+ const ctrl = new OverlayController({
+ ...withLocalTestConfig(),
+ handlesAccessibility: true,
+ isAlertDialog: true,
+ invokerNode,
+ });
+
+ expect(ctrl.contentNode?.getAttribute('role')).to.equal('alertdialog');
+ });
+ });
+
describe('Tooltip', () => {
it('adds [aria-describedby] on invoker', async () => {
const invokerNode = /** @type {HTMLElement} */ (
diff --git a/packages/ui/components/overlays/types/OverlayConfig.ts b/packages/ui/components/overlays/types/OverlayConfig.ts
index 4d1bf63611..77c36e4e3a 100644
--- a/packages/ui/components/overlays/types/OverlayConfig.ts
+++ b/packages/ui/components/overlays/types/OverlayConfig.ts
@@ -41,7 +41,7 @@ export interface OverlayConfig {
contentNode?: HTMLElement;
/** The wrapper element of contentNode, used to supply inline positioning styles. When a Popper arrow is needed, it acts as parent of the arrow node. Will be automatically created for global and non projected contentNodes. Required when used in shadow dom mode or when Popper arrow is supplied. Essential for allowing webcomponents to style their projected contentNodes */
contentWrapperNode?: HTMLElement;
- /** The element that is placed behin the contentNode. When not provided and `hasBackdrop` is true, a backdropNode will be automatically created */
+ /** The element that is placed behind the contentNode. When not provided and `hasBackdrop` is true, a backdropNode will be automatically created */
backdropNode?: HTMLElement;
/** The element that should be called `.focus()` on after dialog closes */
elementToFocusAfterHide?: HTMLElement;
@@ -59,7 +59,7 @@ export interface OverlayConfig {
trapsKeyboardFocus?: boolean;
/** Hides the overlay when pressing [ esc ] */
hidesOnEsc?: boolean;
- /** Hides the overlay when clicking next to it, exluding invoker */
+ /** Hides the overlay when clicking next to it, excluding invoker */
hidesOnOutsideClick?: boolean;
/** Hides the overlay when pressing esc, even when contentNode has no focus */
hidesOnOutsideEsc?: boolean;
@@ -82,6 +82,8 @@ export interface OverlayConfig {
/** Has a totally different interaction- and accessibility pattern from all other overlays. Will behave as role="tooltip" element instead of a role="dialog" element */
isTooltip?: boolean;
+ /** The alertdialog role is to be used on modal alert dialogs that interrupt a user's workflow to communicate an important message and require a response. */
+ isAlertDialog?: boolean;
/** By default, the tooltip content is a 'description' for the invoker (uses aria-describedby) Setting this property to 'label' makes the content function as a label (via aria-labelledby) */
invokerRelation?: 'label' | 'description';