Skip to content

Commit

Permalink
feat(dialog): add is-alert-dialog option
Browse files Browse the repository at this point in the history
  • Loading branch information
gerjanvangeest committed Jan 16, 2025
1 parent 45f0666 commit f388b28
Show file tree
Hide file tree
Showing 13 changed files with 435 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .changeset/cuddly-bottles-camp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lion/ui': minor
---

[dialog] add an option to set role="alertdialog" instead of the default role="dialog"
68 changes: 67 additions & 1 deletion docs/components/dialog/use-cases.md
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -23,6 +24,71 @@ import './src/external-dialog.js';
</lion-dialog>
```

## 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`
<style>
${demoStyle} .button__group {
display: flex;
align-items: center;
}
.button-submit {
margin-top: 4px;
margin-bottom: 4px;
}
.dialog {
margin-bottom: 4px;
}
</style>
<lion-form @submit="${submitHandler}" @form-reset="${formResetHandler}">
<form>
<lion-input name="firstName" label="First Name"></lion-input>
<lion-input name="lastName" label="Last Name"></lion-input>
<div class="button__group">
<button class="button-submit">Submit</button>
<lion-dialog is-alert-dialog class="dialog">
<button type="button" slot="invoker">Reset</button>
<div slot="content" class="demo-box">
Are you sure you want to clear the input field?
<button orange type="button" @click="${resetHandler}">Yes</button>
<button
grey
type="button"
@click="${ev =>
ev.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}"
>
No
</button>
</div>
</lion-dialog>
</div>
</form>
</lion-form>
`;
};
```
## External trigger
```js preview-story
Expand Down
9 changes: 9 additions & 0 deletions docs/components/input-range/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ import '@lion/ui/define/lion-input-range.js';
<lion-input-range min="200" max="500" .modelValue="${300}" label="Input range"></lion-input-range>
```

```html preview-story
<lion-input-range-group
min="10"
max="300"
.modelValue="${{ low: 40, high: 200 }}"
label="Input range multi-thumb"
></lion-input-range-group>
```

## Features

- Based on our [input](../input/overview.md).
Expand Down
24 changes: 24 additions & 0 deletions docs/fundamentals/systems/overlays/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
<demo-el-using-overlaymixin .config="${placementModeGlobalConfig}">
<button slot="invoker">Click me to open the alert dialog!</button>
<div slot="content" class="demo-overlay">
Hello! You can close this notification here:
<button
class="close-button"
@click="${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}"
>
</button>
</div>
</demo-el-using-overlaymixin>
`;
};
```

## 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:
Expand Down
13 changes: 13 additions & 0 deletions packages/ui/components/dialog/src/LionDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,26 @@ 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
*/
// eslint-disable-next-line class-methods-use-this
_defineOverlayConfig() {
return {
...withModalDialogConfig(),
isAlertDialog: this.isAlertDialog,
};
}

Expand Down
56 changes: 56 additions & 0 deletions packages/ui/components/dialog/test/lion-dialog.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,27 @@ describe('lion-dialog', () => {
});

describe('Accessibility', () => {
it('passes a11y audit', async () => {
const el = await fixture(html`
<lion-dialog>
<button slot="invoker">Invoker</button>
<div slot="content" class="dialog" aria-label="Dialog">Hey there</div>
</lion-dialog>
`);
await expect(el).to.be.accessible();
});

it('passes a11y audit when opened', async () => {
const el = await fixture(html`
<lion-dialog opened>
<button slot="invoker">Invoker</button>
<div slot="content" class="dialog" aria-label="Dialog">Hey there</div>
</lion-dialog>
`);
// 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'] });
});

it('does not add [aria-expanded] to invoker button', async () => {
const el = await fixture(
html` <lion-dialog>
Expand All @@ -187,6 +208,41 @@ describe('lion-dialog', () => {
await aTimeout(0);
expect(invokerButton.getAttribute('aria-expanded')).to.equal(null);
});

it('has role="dialog" by default', async () => {
const el = await fixture(
html` <lion-dialog>
<div slot="content" class="dialog">Hey there</div>
<button slot="invoker">Popup button</button>
</lion-dialog>`,
);
const contentNode = /** @type {HTMLElement} */ (el.querySelector('[slot="content"]'));

expect(contentNode.getAttribute('role')).to.equal('dialog');
});

it('has role="alertdialog" by when "is-alert-dialog" is set', async () => {
const el = await fixture(
html` <lion-dialog is-alert-dialog>
<div slot="content" class="dialog">Hey there</div>
<button slot="invoker">Popup button</button>
</lion-dialog>`,
);
const contentNode = /** @type {HTMLElement} */ (el.querySelector('[slot="content"]'));

expect(contentNode.getAttribute('role')).to.equal('alertdialog');
});

it('passes a11y audit when opened and role="alertdialog"', async () => {
const el = await fixture(html`
<lion-dialog opened is-alert-dialog>
<button slot="invoker">Invoker</button>
<div slot="content" class="dialog" aria-label="Dialog">Hey there</div>
</lion-dialog>
`);
// 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', () => {
Expand Down
Loading

0 comments on commit f388b28

Please sign in to comment.