Skip to content

Commit

Permalink
fix(material/dialog): macOS dialog title not read in chrome and firefox
Browse files Browse the repository at this point in the history
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
  • Loading branch information
essjay05 committed Jun 21, 2024
1 parent 182aa2a commit 85ba74d
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 2 deletions.
34 changes: 33 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,17 @@ export class CdkDialogContainer<C extends DialogConfig = DialogConfig>

this._document = _document;

console.log('inside constructor this._config');
console.log(this._config);
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 +159,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
37 changes: 36 additions & 1 deletion src/material/dialog/dialog-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {FocusMonitor, FocusTrapFactory, InteractivityChecker} from '@angular/cdk
import {OverlayRef} from '@angular/cdk/overlay';
import {DOCUMENT} from '@angular/common';
import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
ComponentRef,
Expand All @@ -25,7 +26,9 @@ import {
import {MatDialogConfig} from './dialog-config';
import {CdkDialogContainer} from '@angular/cdk/dialog';
import {coerceNumberProperty} from '@angular/cdk/coercion';
import {PlatformModule} from '@angular/cdk/platform';
import {CdkPortalOutlet, ComponentPortal} from '@angular/cdk/portal';
import {Observable, Subject, Subscription, defer, fromEvent, merge, of as observableOf} from 'rxjs';

/** Event that captures the state of dialog container animations. */
interface LegacyDialogAnimationEvent {
Expand Down Expand Up @@ -71,7 +74,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 AfterViewInit, OnDestroy
{
/** Emits when an animation state changes. */
_animationStateChanged = new EventEmitter<LegacyDialogAnimationEvent>();

Expand All @@ -93,6 +99,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 +128,30 @@ export class MatDialogContainer extends CdkDialogContainer<MatDialogConfig> impl
);
}

/** Get Dialog name from aria attributes */
private _getDialogName = async (): Promise<void> => {
const configData = this._config;
/**_ariaLabelledByQueue and _ariaDescribedByQueue are created if ariaLabelledBy
or ariaDescribedBy values are applied to the dialog config
*/
const ariaLabelledByRefId = await this._ariaLabelledByQueue[0];
const ariaDescribedByRefId = await 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;
return;
};
ngAfterViewInit() {
const window = this._getWindow();
this._getDialogName();
}

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

0 comments on commit 85ba74d

Please sign in to comment.