Skip to content

Commit 2d2eb16

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 619abba commit 2d2eb16

File tree

4 files changed

+34
-9
lines changed

4 files changed

+34
-9
lines changed

src/cdk/dialog/dialog.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {of as observableOf, Observable, Subject, defer} from 'rxjs';
2323
import {DialogRef} from './dialog-ref';
2424
import {DialogConfig} from './dialog-config';
2525
import {Directionality} from '@angular/cdk/bidi';
26+
import {_supportsInert} from '@angular/cdk/platform';
2627
import {
2728
ComponentType,
2829
Overlay,
@@ -44,8 +45,9 @@ export class Dialog implements OnDestroy {
4445
private _openDialogsAtThisLevel: DialogRef<any, any>[] = [];
4546
private readonly _afterAllClosedAtThisLevel = new Subject<void>();
4647
private readonly _afterOpenedAtThisLevel = new Subject<DialogRef>();
47-
private _ariaHiddenElements = new Map<Element, string | null>();
48+
private _inertElements = new Map<Element, string | null>();
4849
private _scrollStrategy: () => ScrollStrategy;
50+
private _inertAttribute = _supportsInert() ? 'inert' : 'aria-hidden';
4951

5052
/** Keeps track of the currently-open dialogs. */
5153
get openDialogs(): readonly DialogRef<any, any>[] {
@@ -162,7 +164,7 @@ export class Dialog implements OnDestroy {
162164
ngOnDestroy() {
163165
// Make one pass over all the dialogs that need to be untracked, but should not be closed. We
164166
// want to stop tracking the open dialog even if it hasn't been closed, because the tracking
165-
// determines when `aria-hidden` is removed from elements outside the dialog.
167+
// determines when `inert` is removed from elements outside the dialog.
166168
reverseForEach(this._openDialogsAtThisLevel, dialog => {
167169
// Check for `false` specifically since we want `undefined` to be interpreted as `true`.
168170
if (dialog.config.closeOnDestroy === false) {
@@ -350,18 +352,18 @@ export class Dialog implements OnDestroy {
350352
if (index > -1) {
351353
(this.openDialogs as DialogRef<R, C>[]).splice(index, 1);
352354

353-
// If all the dialogs were closed, remove/restore the `aria-hidden`
355+
// If all the dialogs were closed, remove/restore the inert attribute
354356
// to a the siblings and emit to the `afterAllClosed` stream.
355357
if (!this.openDialogs.length) {
356-
this._ariaHiddenElements.forEach((previousValue, element) => {
358+
this._inertElements.forEach((previousValue, element) => {
357359
if (previousValue) {
358-
element.setAttribute('aria-hidden', previousValue);
360+
element.setAttribute(this._inertAttribute, previousValue);
359361
} else {
360-
element.removeAttribute('aria-hidden');
362+
element.removeAttribute(this._inertAttribute);
361363
}
362364
});
363365

364-
this._ariaHiddenElements.clear();
366+
this._inertElements.clear();
365367

366368
if (emitEvent) {
367369
this._getAfterAllClosed().next();
@@ -387,8 +389,8 @@ export class Dialog implements OnDestroy {
387389
sibling.nodeName !== 'STYLE' &&
388390
!sibling.hasAttribute('aria-live')
389391
) {
390-
this._ariaHiddenElements.set(sibling, sibling.getAttribute('aria-hidden'));
391-
sibling.setAttribute('aria-hidden', 'true');
392+
this._inertElements.set(sibling, sibling.getAttribute(this._inertAttribute));
393+
sibling.setAttribute(this._inertAttribute, 'true');
392394
}
393395
}
394396
}

src/cdk/platform/features/inert.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
let supportsInert: boolean | undefined;
10+
11+
/** Returns whether the browser supports the `inert` attribute. */
12+
export function _supportsInert(): boolean {
13+
if (typeof supportsInert === 'boolean') {
14+
return supportsInert;
15+
}
16+
17+
supportsInert = typeof document !== 'undefined' && !!document.body && 'inert' in document.body;
18+
return supportsInert;
19+
}

src/cdk/platform/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ export * from './features/passive-listeners';
1313
export * from './features/scrolling';
1414
export * from './features/shadow-dom';
1515
export * from './features/test-environment';
16+
export * from './features/inert';

tools/public_api_guard/cdk/platform.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ export const enum RtlScrollAxisType {
6262
NORMAL = 0
6363
}
6464

65+
// @public
66+
export function _supportsInert(): boolean;
67+
6568
// @public
6669
export function supportsPassiveEventListeners(): boolean;
6770

0 commit comments

Comments
 (0)