Skip to content

Commit 0dc67a3

Browse files
committed
fix(material/dialog): macOS dialog title not read in chrome and firefox
Fixes issue with Angular Components Dialog component where VoiceOver does not read the dialog name if the dialog is supposed to be read by aria-labelledby or aria-describedby attributes. Updates dialog-container.ts so that the aria-labelledby or aria-described by value (if any) is used as an aria-label value. Fixes b/274674581
1 parent 6bd5a0d commit 0dc67a3

File tree

2 files changed

+69
-2
lines changed

2 files changed

+69
-2
lines changed

src/cdk/dialog/dialog-container.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export function throwDialogContentAlreadyAttachedError() {
6868
'[attr.aria-modal]': '_config.ariaModal',
6969
'[attr.aria-labelledby]': '_config.ariaLabel ? null : _ariaLabelledByQueue[0]',
7070
'[attr.aria-label]': '_config.ariaLabel',
71-
'[attr.aria-describedby]': '_config.ariaDescribedBy || null',
71+
'[attr.aria-describedby]': '_config.ariaLabel ? null : _ariaDescribedByQueue[0]',
7272
},
7373
})
7474
export class CdkDialogContainer<C extends DialogConfig = DialogConfig>
@@ -94,6 +94,8 @@ export class CdkDialogContainer<C extends DialogConfig = DialogConfig>
9494
*/
9595
_closeInteractionType: FocusOrigin | null = null;
9696

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

107+
/**
108+
* Queue of the IDs of the dialog's label element, based on their definition order. The first
109+
* ID will be used as the `aria-describedby` value. We use a queue here to handle the case
110+
* where there are two or more titles in the DOM at a time and the first one is destroyed while
111+
* the rest are present.
112+
*/
113+
_ariaDescribedByQueue: string[] = [];
114+
105115
protected readonly _changeDetectorRef = inject(ChangeDetectorRef);
106116

107117
private _injector = inject(Injector);
@@ -122,9 +132,17 @@ export class CdkDialogContainer<C extends DialogConfig = DialogConfig>
122132

123133
this._document = _document;
124134

135+
console.log('inside constructor this._config');
136+
console.log(this._config);
137+
if (this._config.ariaLabel) {
138+
this._ariaLabel = this._config.ariaLabel;
139+
}
125140
if (this._config.ariaLabelledBy) {
126141
this._ariaLabelledByQueue.push(this._config.ariaLabelledBy);
127142
}
143+
if (this._config.ariaDescribedBy) {
144+
this._ariaDescribedByQueue.push(this._config.ariaDescribedBy);
145+
}
128146
}
129147

130148
_addAriaLabelledBy(id: string) {
@@ -141,6 +159,20 @@ export class CdkDialogContainer<C extends DialogConfig = DialogConfig>
141159
}
142160
}
143161

162+
_addAriaDescribedBy(id: string) {
163+
this._ariaDescribedByQueue.push(id);
164+
this._changeDetectorRef.markForCheck();
165+
}
166+
167+
_removeAriaDescribedBy(id: string) {
168+
const index = this._ariaDescribedByQueue.indexOf(id);
169+
170+
if (index > -1) {
171+
this._ariaDescribedByQueue.splice(index, 1);
172+
this._changeDetectorRef.markForCheck();
173+
}
174+
}
175+
144176
protected _contentAttached() {
145177
this._initializeFocusTrap();
146178
this._handleBackdropClicks();

src/material/dialog/dialog-container.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {FocusMonitor, FocusTrapFactory, InteractivityChecker} from '@angular/cdk
1010
import {OverlayRef} from '@angular/cdk/overlay';
1111
import {DOCUMENT} from '@angular/common';
1212
import {
13+
AfterViewInit,
1314
ChangeDetectionStrategy,
1415
Component,
1516
ComponentRef,
@@ -25,7 +26,9 @@ import {
2526
import {MatDialogConfig} from './dialog-config';
2627
import {CdkDialogContainer} from '@angular/cdk/dialog';
2728
import {coerceNumberProperty} from '@angular/cdk/coercion';
29+
import {PlatformModule} from '@angular/cdk/platform';
2830
import {CdkPortalOutlet, ComponentPortal} from '@angular/cdk/portal';
31+
import {Observable, Subject, Subscription, defer, fromEvent, merge, of as observableOf} from 'rxjs';
2932

3033
/** Event that captures the state of dialog container animations. */
3134
interface LegacyDialogAnimationEvent {
@@ -71,7 +74,10 @@ export const CLOSE_ANIMATION_DURATION = 75;
7174
'[class.mat-mdc-dialog-container-with-actions]': '_actionSectionCount > 0',
7275
},
7376
})
74-
export class MatDialogContainer extends CdkDialogContainer<MatDialogConfig> implements OnDestroy {
77+
export class MatDialogContainer
78+
extends CdkDialogContainer<MatDialogConfig>
79+
implements AfterViewInit, OnDestroy
80+
{
7581
/** Emits when an animation state changes. */
7682
_animationStateChanged = new EventEmitter<LegacyDialogAnimationEvent>();
7783

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

97108
constructor(
98109
elementRef: ElementRef,
@@ -117,6 +128,30 @@ export class MatDialogContainer extends CdkDialogContainer<MatDialogConfig> impl
117128
);
118129
}
119130

131+
/** Get Dialog name from aria attributes */
132+
private _getDialogName = async (): Promise<void> => {
133+
const configData = this._config;
134+
/**_ariaLabelledByQueue and _ariaDescribedByQueue are created if ariaLabelledBy
135+
or ariaDescribedBy values are applied to the dialog config
136+
*/
137+
const ariaLabelledByRefId = await this._ariaLabelledByQueue[0];
138+
const ariaDescribedByRefId = await this._ariaDescribedByQueue[0];
139+
/** Get Element to get name/title from if ariaLabelledBy or ariaDescribedBy */
140+
const dialogNameElement =
141+
document.getElementById(ariaLabelledByRefId) || document.getElementById(ariaDescribedByRefId);
142+
const dialogNameInnerText =
143+
/** If no ariaLabelledBy, ariaDescribedBy, or ariaLabel, create default aria label */
144+
!dialogNameElement || !this._config.ariaLabel
145+
? 'Dialog Modal'
146+
: /** Otherwise prioritize use of ariaLabel */
147+
this._config.ariaLabel || dialogNameElement?.innerText || dialogNameElement?.ariaLabel;
148+
return;
149+
};
150+
ngAfterViewInit() {
151+
const window = this._getWindow();
152+
this._getDialogName();
153+
}
154+
120155
protected override _contentAttached(): void {
121156
// Delegate to the original dialog-container initialization (i.e. saving the
122157
// previous element, setting up the focus trap and moving focus to the container).

0 commit comments

Comments
 (0)