Skip to content

Commit 2674a57

Browse files
authored
refactor: set aria-modal and tabindex on the dialog (#10024)
1 parent ca63306 commit 2674a57

File tree

8 files changed

+85
-83
lines changed

8 files changed

+85
-83
lines changed

packages/crud/src/vaadin-crud-dialog.js

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,30 @@ class CrudDialogOverlay extends OverlayMixin(DirMixin(ThemableMixin(PolylitMixin
4848
return this.owner;
4949
}
5050

51+
/**
52+
* Override method from OverlayFocusMixin to use dialog as focus trap root
53+
* @protected
54+
* @override
55+
*/
56+
get _focusTrapRoot() {
57+
// Do not use `owner` since that points to `vaadin-crud`
58+
return this.getRootNode().host;
59+
}
60+
61+
/**
62+
* Override method from OverlayFocusMixin to not set `aria-hidden`
63+
* @protected
64+
* @override
65+
*/
66+
get _useAriaHidden() {
67+
return false;
68+
}
69+
5170
/** @protected */
5271
render() {
5372
return html`
5473
<div part="backdrop" id="backdrop" ?hidden="${!this.withBackdrop}"></div>
55-
<div part="overlay" id="overlay" tabindex="0">
74+
<div part="overlay" id="overlay">
5675
<section id="resizerContainer" class="resizer-container">
5776
<header part="header">
5877
<slot name="header"></slot>
@@ -99,13 +118,18 @@ class CrudDialog extends DialogBaseMixin(ThemePropertyMixin(PolylitMixin(LitElem
99118
:host([opened]),
100119
:host([opening]),
101120
:host([closing]) {
102-
display: contents !important;
121+
display: block !important;
122+
position: absolute;
103123
}
104124
105125
:host,
106126
:host([hidden]) {
107127
display: none !important;
108128
}
129+
130+
:host(:focus) ::part(overlay) {
131+
outline: var(--vaadin-focus-ring-width) solid var(--vaadin-focus-ring-color);
132+
}
109133
`;
110134
}
111135

packages/crud/test/a11y.test.js

Lines changed: 3 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ describe('a11y', () => {
1515

1616
function focusRestorationTests(testId, createFixture) {
1717
describe(`focus restoration - ${testId}`, () => {
18-
let grid, form, overlay, newButton, saveButton, cancelButton, editButtons;
18+
let grid, form, dialog, newButton, saveButton, cancelButton, editButtons;
1919

2020
describe('create item', () => {
2121
beforeEach(async () => {
2222
crud = createFixture();
2323
crud.items = [{ title: 'Item 1' }];
2424
await nextRender();
25-
overlay = getDialogEditor(crud).$.overlay;
25+
dialog = getDialogEditor(crud);
2626
form = crud.querySelector('vaadin-crud-form');
2727
newButton = crud.querySelector('[slot=new-button]');
2828
saveButton = crud.querySelector('[slot=save-button]');
@@ -34,7 +34,7 @@ describe('a11y', () => {
3434
newButton.focus();
3535
newButton.click();
3636
await nextRender();
37-
expect(getDeepActiveElement()).to.equal(overlay.$.overlay);
37+
expect(getDeepActiveElement()).to.equal(dialog);
3838
});
3939

4040
it('should restore focus to previous element on new dialog close', async () => {
@@ -327,73 +327,4 @@ describe('a11y', () => {
327327
expect(dialog.getAttribute('aria-label')).to.equal('Edit item');
328328
});
329329
});
330-
331-
describe('modal dialog', () => {
332-
let dialog, grid, header, form, newButton, saveButton, cancelButton, deleteButton, sibling;
333-
334-
beforeEach(async () => {
335-
crud = fixtureSync('<vaadin-crud></vaadin-crud>');
336-
crud.items = [{ title: 'Item 1' }];
337-
338-
dialog = getDialogEditor(crud);
339-
grid = crud.querySelector('[slot="grid"]');
340-
header = crud.querySelector('[slot="header"]');
341-
form = crud.querySelector('[slot="form"]');
342-
newButton = crud.querySelector('[slot="new-button"]');
343-
saveButton = crud.querySelector('[slot="save-button"]');
344-
cancelButton = crud.querySelector('[slot="cancel-button"]');
345-
deleteButton = crud.querySelector('[slot="delete-button"]');
346-
347-
sibling = fixtureSync('<button></button>');
348-
await nextRender();
349-
});
350-
351-
it('should hide all elements outside of the dialog when opened', async () => {
352-
crud._newButton.click();
353-
await nextRender();
354-
355-
// Sibling elements of CRUD must be hidden
356-
expect(sibling.parentElement.getAttribute('aria-hidden')).to.equal('true');
357-
358-
// Hierarchy to dialog must not be hidden
359-
[crud.parentElement, crud, dialog, dialog.$.overlay].forEach((el) => {
360-
expect(el.hasAttribute('aria-hidden')).to.be.false;
361-
});
362-
363-
// Elements slotted into the dialog must not be hidden
364-
[header, form, saveButton, cancelButton, deleteButton].forEach((el) => {
365-
expect(el.hasAttribute('aria-hidden')).to.be.false;
366-
});
367-
368-
// Elements not slotted into the dialog must be hidden
369-
[grid, newButton].forEach((el) => {
370-
expect(el.getAttribute('aria-hidden')).to.equal('true');
371-
});
372-
});
373-
374-
it('should restore visibility of elements outside of the dialog when closed', async () => {
375-
crud._newButton.click();
376-
await nextRender();
377-
378-
// Close the dialog
379-
await sendKeys({ press: 'Escape' });
380-
381-
[
382-
sibling.parentElement,
383-
crud.parentElement,
384-
crud,
385-
dialog,
386-
dialog.$.overlay,
387-
header,
388-
form,
389-
saveButton,
390-
cancelButton,
391-
deleteButton,
392-
grid,
393-
newButton,
394-
].forEach((el) => {
395-
expect(el.hasAttribute('aria-hidden')).to.be.false;
396-
});
397-
});
398-
});
399330
});

packages/crud/test/dom/__snapshots__/crud.test.snap.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,9 +281,11 @@ snapshots["vaadin-crud shadow default"] =
281281
</div>
282282
</div>
283283
<vaadin-crud-dialog
284+
aria-modal="true"
284285
exportparts="backdrop, overlay, header, content, footer"
285286
id="dialog"
286287
role="dialog"
288+
tabindex="0"
287289
>
288290
<slot
289291
name="header"

packages/dialog/src/vaadin-dialog-base-mixin.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ export const DialogBaseMixin = (superClass) =>
105105
if (!this.hasAttribute('role')) {
106106
this.role = 'dialog';
107107
}
108+
109+
this.setAttribute('tabindex', '0');
108110
}
109111

110112
/** @protected */
@@ -114,6 +116,14 @@ export const DialogBaseMixin = (superClass) =>
114116
if (props.has('overlayRole')) {
115117
this.role = this.overlayRole || 'dialog';
116118
}
119+
120+
if (props.has('modeless')) {
121+
if (!this.modeless) {
122+
this.setAttribute('aria-modal', 'true');
123+
} else {
124+
this.removeAttribute('aria-modal');
125+
}
126+
}
117127
}
118128

119129
/** @private */

packages/dialog/src/vaadin-dialog-overlay.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,29 @@ export class DialogOverlay extends DialogOverlayMixin(
3333
return dialogOverlayStyles;
3434
}
3535

36+
/**
37+
* Override method from OverlayFocusMixin to use owner as focus trap root
38+
* @protected
39+
* @override
40+
*/
41+
get _focusTrapRoot() {
42+
return this.owner;
43+
}
44+
45+
/**
46+
* Override method from OverlayFocusMixin to not set `aria-hidden`
47+
* @protected
48+
* @override
49+
*/
50+
get _useAriaHidden() {
51+
return false;
52+
}
53+
3654
/** @protected */
3755
render() {
3856
return html`
3957
<div id="backdrop" part="backdrop" ?hidden="${!this.withBackdrop}"></div>
40-
<div part="overlay" id="overlay" tabindex="0">
58+
<div part="overlay" id="overlay">
4159
<section id="resizerContainer" class="resizer-container">
4260
<header part="header">
4361
<div part="title"><slot name="title"></slot></div>

packages/dialog/src/vaadin-dialog.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,19 @@ class Dialog extends DialogSizeMixin(
101101
:host([opened]),
102102
:host([opening]),
103103
:host([closing]) {
104-
display: contents !important;
104+
display: block !important;
105+
position: absolute;
106+
outline: none;
105107
}
106108
107109
:host,
108110
:host([hidden]) {
109111
display: none !important;
110112
}
113+
114+
:host(:focus) ::part(overlay) {
115+
outline: var(--vaadin-focus-ring-width) solid var(--vaadin-focus-ring-color);
116+
}
111117
`;
112118
}
113119

packages/dialog/test/dialog.test.js

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
import { expect } from '@vaadin/chai-plugins';
2-
import { aTimeout, click, esc, fixtureSync, listenOnce, nextRender, nextUpdate } from '@vaadin/testing-helpers';
2+
import {
3+
aTimeout,
4+
click,
5+
esc,
6+
fixtureSync,
7+
listenOnce,
8+
nextRender,
9+
nextUpdate,
10+
oneEvent,
11+
} from '@vaadin/testing-helpers';
312
import sinon from 'sinon';
413
import '../src/vaadin-dialog.js';
514
import { getDeepActiveElement } from '@vaadin/a11y-base/src/focus-utils.js';
@@ -36,14 +45,15 @@ describe('vaadin-dialog', () => {
3645
});
3746

3847
['opened', 'opening', 'closing'].forEach((state) => {
39-
it(`should use display: contents when ${state} attribute is set`, () => {
48+
it(`should use display: block when ${state} attribute is set`, () => {
4049
dialog.setAttribute(state, '');
41-
expect(getComputedStyle(dialog).display).to.equal('contents');
50+
expect(getComputedStyle(dialog).display).to.equal('block');
4251
});
4352
});
4453

4554
it('should use display: none when hidden while opened', async () => {
4655
dialog.opened = true;
56+
await oneEvent(dialog.$.overlay, 'vaadin-overlay-open');
4757
dialog.hidden = true;
4858
await nextRender();
4959
expect(getComputedStyle(dialog).display).to.equal('none');
@@ -235,13 +245,13 @@ describe('vaadin-dialog', () => {
235245

236246
it('should move focus to the dialog on open', async () => {
237247
dialog.opened = true;
238-
await nextRender();
239-
expect(getDeepActiveElement()).to.equal(overlay.$.overlay);
248+
await oneEvent(overlay, 'vaadin-overlay-open');
249+
expect(getDeepActiveElement()).to.equal(dialog);
240250
});
241251

242252
it('should restore focus on dialog close', async () => {
243253
dialog.opened = true;
244-
await nextRender();
254+
await oneEvent(overlay, 'vaadin-overlay-open');
245255
dialog.opened = false;
246256
await nextRender();
247257
expect(getDeepActiveElement()).to.equal(button);

packages/dialog/test/dom/__snapshots__/dialog.test.snap.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ export const snapshots = {};
33

44
snapshots["vaadin-dialog host"] =
55
`<vaadin-dialog
6+
aria-modal="true"
67
opened=""
78
role="dialog"
9+
tabindex="0"
810
with-backdrop=""
911
>
1012
content
@@ -115,7 +117,6 @@ snapshots["vaadin-dialog overlay"] =
115117
<div
116118
id="overlay"
117119
part="overlay"
118-
tabindex="0"
119120
>
120121
<section
121122
class="resizer-container"

0 commit comments

Comments
 (0)