Skip to content

Commit 9e1eeaa

Browse files
committed
fix(cdk/dialog): use inert to block content outside of dialog
Currently we're setting `aria-hidden` on all elements outside of a dialog in order to prevent assistive technology from interacting with it. These changes switch to using the `inert` attribute when supported which resolves some long-standing issues like tabbing directly into the dialog from the address bar.
1 parent 04e5e0f commit 9e1eeaa

File tree

1 file changed

+25
-9
lines changed

1 file changed

+25
-9
lines changed

src/cdk/dialog/dialog.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export class Dialog implements OnDestroy {
4646
private _openDialogsAtThisLevel: DialogRef<any, any>[] = [];
4747
private readonly _afterAllClosedAtThisLevel = new Subject<void>();
4848
private readonly _afterOpenedAtThisLevel = new Subject<DialogRef>();
49-
private _ariaHiddenElements = new Map<Element, string | null>();
49+
private _inertElements = new Map<Element, [ariaHidden: string | null, inert: string | null]>();
5050
private _scrollStrategy = inject(DIALOG_SCROLL_STRATEGY);
5151

5252
/** Keeps track of the currently-open dialogs. */
@@ -157,7 +157,7 @@ export class Dialog implements OnDestroy {
157157
ngOnDestroy() {
158158
// Make one pass over all the dialogs that need to be untracked, but should not be closed. We
159159
// want to stop tracking the open dialog even if it hasn't been closed, because the tracking
160-
// determines when `aria-hidden` is removed from elements outside the dialog.
160+
// determines when `inert` is removed from elements outside the dialog.
161161
reverseForEach(this._openDialogsAtThisLevel, dialog => {
162162
// Check for `false` specifically since we want `undefined` to be interpreted as `true`.
163163
if (dialog.config.closeOnDestroy === false) {
@@ -340,18 +340,27 @@ export class Dialog implements OnDestroy {
340340
if (index > -1) {
341341
(this.openDialogs as DialogRef<R, C>[]).splice(index, 1);
342342

343-
// If all the dialogs were closed, remove/restore the `aria-hidden`
343+
// If all the dialogs were closed, remove/restore the inert attribute
344344
// to a the siblings and emit to the `afterAllClosed` stream.
345345
if (!this.openDialogs.length) {
346-
this._ariaHiddenElements.forEach((previousValue, element) => {
347-
if (previousValue) {
348-
element.setAttribute('aria-hidden', previousValue);
346+
this._inertElements.forEach((previousValue, element) => {
347+
const [ariaHidden, inert] = previousValue;
348+
349+
// Note: this code is somewhat repetitive, but we want to use static strings inside
350+
// the `setAttribute` calls so that we don't trip up some internal XSS checks.
351+
if (ariaHidden) {
352+
element.setAttribute('aria-hidden', ariaHidden);
349353
} else {
350354
element.removeAttribute('aria-hidden');
351355
}
352-
});
353356

354-
this._ariaHiddenElements.clear();
357+
if (inert) {
358+
element.setAttribute('inert', inert);
359+
} else {
360+
element.removeAttribute('inert');
361+
}
362+
});
363+
this._inertElements.clear();
355364

356365
if (emitEvent) {
357366
this._getAfterAllClosed().next();
@@ -377,8 +386,15 @@ export class Dialog implements OnDestroy {
377386
sibling.nodeName !== 'STYLE' &&
378387
!sibling.hasAttribute('aria-live')
379388
) {
380-
this._ariaHiddenElements.set(sibling, sibling.getAttribute('aria-hidden'));
389+
const ariaHidden = sibling.getAttribute('aria-hidden');
390+
const inert = sibling.getAttribute('inert');
391+
392+
// TODO(crisbeto): ideally we'd set only either `aria-hidden` or `inert` here, but
393+
// at the moment of writing, some internal checks don't consider `inert` elements as
394+
// removed from the a11y tree which reveals a bunch of pre-existing breakages.
395+
this._inertElements.set(sibling, [ariaHidden, inert]);
381396
sibling.setAttribute('aria-hidden', 'true');
397+
sibling.setAttribute('inert', 'true');
382398
}
383399
}
384400
}

0 commit comments

Comments
 (0)