Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(material/dialog): dialog name on mac only using aria-label #29264

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
32 changes: 31 additions & 1 deletion src/cdk/dialog/dialog-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export function throwDialogContentAlreadyAttachedError() {
'[attr.aria-modal]': '_config.ariaModal',
'[attr.aria-labelledby]': '_config.ariaLabel ? null : _ariaLabelledByQueue[0]',
'[attr.aria-label]': '_config.ariaLabel',
'[attr.aria-describedby]': '_config.ariaDescribedBy || null',
'[attr.aria-describedby]': '_config.ariaLabel ? null : _ariaDescribedByQueue[0]',
},
})
export class CdkDialogContainer<C extends DialogConfig = DialogConfig>
Expand All @@ -94,6 +94,8 @@ export class CdkDialogContainer<C extends DialogConfig = DialogConfig>
*/
_closeInteractionType: FocusOrigin | null = null;

_ariaLabel: string;

/**
* Queue of the IDs of the dialog's label element, based on their definition order. The first
* ID will be used as the `aria-labelledby` value. We use a queue here to handle the case
Expand All @@ -102,6 +104,14 @@ export class CdkDialogContainer<C extends DialogConfig = DialogConfig>
*/
_ariaLabelledByQueue: string[] = [];

/**
* Queue of the IDs of the dialog's label element, based on their definition order. The first
* ID will be used as the `aria-describedby` value. We use a queue here to handle the case
* where there are two or more titles in the DOM at a time and the first one is destroyed while
* the rest are present.
*/
_ariaDescribedByQueue: string[] = [];

protected readonly _changeDetectorRef = inject(ChangeDetectorRef);

private _injector = inject(Injector);
Expand All @@ -122,9 +132,15 @@ export class CdkDialogContainer<C extends DialogConfig = DialogConfig>

this._document = _document;

if (this._config.ariaLabel) {
this._ariaLabel = this._config.ariaLabel;
}
if (this._config.ariaLabelledBy) {
this._ariaLabelledByQueue.push(this._config.ariaLabelledBy);
}
if (this._config.ariaDescribedBy) {
this._ariaDescribedByQueue.push(this._config.ariaDescribedBy);
}
}

_addAriaLabelledBy(id: string) {
Expand All @@ -141,6 +157,20 @@ export class CdkDialogContainer<C extends DialogConfig = DialogConfig>
}
}

_addAriaDescribedBy(id: string) {
this._ariaDescribedByQueue.push(id);
this._changeDetectorRef.markForCheck();
}

_removeAriaDescribedBy(id: string) {
const index = this._ariaDescribedByQueue.indexOf(id);

if (index > -1) {
this._ariaDescribedByQueue.splice(index, 1);
this._changeDetectorRef.markForCheck();
}
}

protected _contentAttached() {
this._initializeFocusTrap();
this._handleBackdropClicks();
Expand Down
7 changes: 4 additions & 3 deletions src/dev-app/dialog/dialog-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export class DialogDemo {
minHeight: '',
maxWidth: '',
maxHeight: '',
ariaLabelledBy: 'jazz-title',
position: {
top: '',
bottom: '',
Expand Down Expand Up @@ -145,7 +146,7 @@ export class DialogDemo {
selector: 'demo-jazz-dialog',
template: `
<div cdkDrag cdkDragRootElement=".cdk-overlay-pane">
<p>Order printer ink refills.</p>
<p id="jazz-title">Order printer ink refills.</p>

<mat-form-field>
<mat-label>How many?</mat-label>
Expand Down Expand Up @@ -214,7 +215,7 @@ export class JazzDialog {
}
`,
template: `
<h2 mat-dialog-title>Neptune</h2>
<h2 id="jazz-title" mat-dialog-title>Neptune</h2>

<mat-dialog-content>
<p>
Expand Down Expand Up @@ -277,7 +278,7 @@ export class ContentElementDialog {
}
`,
template: `
<h2 mat-dialog-title>Neptune</h2>
<h2 id="jazz-title" mat-dialog-title>Neptune</h2>

<mat-dialog-content>
<iframe style="border: 0" src="https://en.wikipedia.org/wiki/Neptune"></iframe>
Expand Down
64 changes: 63 additions & 1 deletion src/material/dialog/dialog-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
Inject,
NgZone,
OnDestroy,
OnInit,
Optional,
ViewEncapsulation,
ANIMATION_MODULE_TYPE,
Expand Down Expand Up @@ -71,7 +72,10 @@ export const CLOSE_ANIMATION_DURATION = 75;
'[class.mat-mdc-dialog-container-with-actions]': '_actionSectionCount > 0',
},
})
export class MatDialogContainer extends CdkDialogContainer<MatDialogConfig> implements OnDestroy {
export class MatDialogContainer
extends CdkDialogContainer<MatDialogConfig>
implements OnInit, OnDestroy
{
/** Emits when an animation state changes. */
_animationStateChanged = new EventEmitter<LegacyDialogAnimationEvent>();

Expand All @@ -93,6 +97,11 @@ export class MatDialogContainer extends CdkDialogContainer<MatDialogConfig> impl
: 0;
/** Current timer for dialog animations. */
private _animationTimer: ReturnType<typeof setTimeout> | null = null;
/** Platform Observer */
// private _userAgentSubscription = Subscription.EMPTY;
private _getWindow(): Window {
return this._document?.defaultView || window;
}

constructor(
elementRef: ElementRef,
Expand All @@ -117,6 +126,59 @@ export class MatDialogContainer extends CdkDialogContainer<MatDialogConfig> impl
);
}

/** Get userAgent to check for useragent operating system */
private _getUserPlatform = (): string => {
const window = this._getWindow();
let userAgent = window.navigator.userAgent.toLowerCase(),
macosPlatforms = /(macintosh|macintel|macppc|mac68k|macos)/i,
windowsPlatforms = /(win32|win64|windows|wince)/i,
iosPlatforms = /(iphone|ipad|ipod)/i,
os = '';
if (macosPlatforms.test(userAgent)) {
os = 'macos';
} else if (iosPlatforms.test(userAgent)) {
os = 'ios';
} else if (windowsPlatforms.test(userAgent)) {
os = 'windows';
} else if (/android/.test(userAgent)) {
os = 'android';
} else if (!os && /linux/.test(userAgent)) {
os = 'linux';
}
return os;
};

/** Get Dialog name from aria attributes */
private _getDialogName = (): string => {
// _ariaLabelledByQueue and _ariaDescribedByQueue are created if ariaLabelledBy
// or ariaDescribedBy values are applied to the dialog config
const ariaLabelledByRefId = this._ariaLabelledByQueue[0];
const ariaDescribedByRefId = this._ariaDescribedByQueue[0];
// Get Element to get name/title from if ariaLabelledBy or ariaDescribedBy
const dialogNameElement =
document.getElementById(ariaLabelledByRefId) || document.getElementById(ariaDescribedByRefId);
const dialogNameInnerText =
// If no ariaLabelledBy, ariaDescribedBy, or ariaLabel, create default aria label
!dialogNameElement && !this._config.ariaLabel
? 'Dialog Modal'
: // : Otherwise prioritize use of ariaLabel
this._config.ariaLabel || dialogNameElement?.innerText || dialogNameElement?.ariaLabel;
this._config.ariaLabel = dialogNameInnerText || 'Dialog Modal';
return this._config.ariaLabel;
};

private _setAriaLabel = (): void => {
const os = this._getUserPlatform();
if (os === 'macos') {
this._getDialogName();
}
return;
};

ngOnInit() {
this._setAriaLabel();
}

protected override _contentAttached(): void {
// Delegate to the original dialog-container initialization (i.e. saving the
// previous element, setting up the focus trap and moving focus to the container).
Expand Down
34 changes: 19 additions & 15 deletions src/material/dialog/dialog.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export class YourDialog {
```

### Specifying global configuration defaults

Default dialog options can be specified by providing an instance of `MatDialogConfig` for
MAT_DIALOG_DEFAULT_OPTIONS in your application's root module.

Expand All @@ -52,6 +53,7 @@ MAT_DIALOG_DEFAULT_OPTIONS in your application's root module.
```

### Sharing data with the Dialog component.

If you want to share data with your dialog, you can use the `data`
option to pass information to the dialog component.

Expand Down Expand Up @@ -88,16 +90,18 @@ will be available implicitly in the template:
<!-- example(dialog-data) -->

### Dialog content

Several directives are available to make it easier to structure your dialog content:

| Name | Description |
|------------------------|---------------------------------------------------------------------------------------------------------------|
| `mat-dialog-title` | \[Attr] Dialog title, applied to a heading element (e.g., `<h1>`, `<h2>`) |
| `<mat-dialog-content>` | Primary scrollable content of the dialog. |
| `<mat-dialog-actions>` | Container for action buttons at the bottom of the dialog. Button alignment can be controlled via the `align` attribute which can be set to `end` and `center`. |
| `mat-dialog-close` | \[Attr] Added to a `<button>`, makes the button close the dialog with an optional result from the bound value.|
| Name | Description |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mat-dialog-title` | \[Attr] Dialog title, applied to a heading element (e.g., `<h1>`, `<h2>`) |
| `<mat-dialog-content>` | Primary scrollable content of the dialog. |
| `<mat-dialog-actions>` | Container for action buttons at the bottom of the dialog. Button alignment can be controlled via the `align` attribute which can be set to `end` and `center`. |
| `mat-dialog-close` | \[Attr] Added to a `<button>`, makes the button close the dialog with an optional result from the bound value. |

For example:

```html
<h2 mat-dialog-title>Delete all elements?</h2>
<mat-dialog-content>This will delete all elements that are currently on this page and cannot be undone.</mat-dialog-content>
Expand All @@ -119,6 +123,7 @@ You can control which elements are tab stops with the `tabindex` attribute
<!-- example(dialog-content) -->

### Controlling the dialog animation

You can control the duration of the dialog's enter and exit animations using the
`enterAnimationDuration` and `exitAnimationDuration` options. If you want to disable the dialog's
animation completely, you can do so by setting the properties to `0ms`.
Expand All @@ -130,11 +135,10 @@ animation completely, you can do so by setting the properties to `0ms`.
`MatDialog` creates modal dialogs that implements the ARIA `role="dialog"` pattern by default.
You can change the dialog's role to `alertdialog` via `MatDialogConfig`.

You should provide an accessible label to this root dialog element by setting the `ariaLabel` or
`ariaLabelledBy` properties of `MatDialogConfig`. You can additionally specify a description element
ID via the `ariaDescribedBy` property of `MatDialogConfig`.
In order to make your dialog title/name known and read by all screenreaders regardless of OS or browser, you should provide an accessible label to this root dialog element. You can do so either by setting the dialog name/title as a value to the `ariaLabel` property of `MatDialogConfig` or providing the id of the respective element with the dialog name as `ariaLabelledBy` property of `MatDialogConfig`. You can additionally specify a description element ID via the `ariaDescribedBy` property of `MatDialogConfig`. If none of these properties (`ariaLabel`,`ariaLabelledBy`, or `ariaDescribedBy`) are applied to `MatDialogConfig` the default aria-label value will be "Dialog Modal".

#### Keyboard interaction

By default, the escape key closes `MatDialog`. While you can disable this behavior via
the `disableClose` property of `MatDialogConfig`, doing this breaks the expected interaction
pattern for the ARIA `role="dialog"` pattern.
Expand All @@ -146,12 +150,12 @@ When opened, `MatDialog` traps browser focus such that it cannot escape the root
You can customize which element receives focus with the `autoFocus` property of
`MatDialogConfig`, which supports the following values.

| Value | Behavior |
|------------------|--------------------------------------------------------------------------|
| `first-tabbable` | Focus the first tabbable element. This is the default setting. |
| `first-header` | Focus the first header element (`role="heading"`, `h1` through `h6`) |
| `dialog` | Focus the root `role="dialog"` element. |
| Any CSS selector | Focus the first element matching the given selector. |
| Value | Behavior |
| ---------------- | -------------------------------------------------------------------- |
| `first-tabbable` | Focus the first tabbable element. This is the default setting. |
| `first-header` | Focus the first header element (`role="heading"`, `h1` through `h6`) |
| `dialog` | Focus the root `role="dialog"` element. |
| Any CSS selector | Focus the first element matching the given selector. |

While the default setting applies the best behavior for most applications, special cases may benefit
from these alternatives. Always test your application to verify the behavior that works best for
Expand Down
Loading