Skip to content

Commit 440cb16

Browse files
committed
fix(material/autocomplete): remove modal workaround
Now that we're rendering the autocomplete popover inline, we no longer need the `aria-owns` workaround.
1 parent a49508b commit 440cb16

File tree

2 files changed

+1
-165
lines changed

2 files changed

+1
-165
lines changed

src/material/autocomplete/autocomplete-trigger.ts

Lines changed: 0 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {addAriaReferencedId, removeAriaReferencedId} from '@angular/cdk/a11y';
109
import {Directionality} from '@angular/cdk/bidi';
1110
import {DOWN_ARROW, ENTER, ESCAPE, TAB, UP_ARROW, hasModifierKey} from '@angular/cdk/keycodes';
1211
import {BreakpointObserver, Breakpoints} from '@angular/cdk/layout';
@@ -269,7 +268,6 @@ export class MatAutocompleteTrigger
269268
this._componentDestroyed = true;
270269
this._destroyPanel();
271270
this._closeKeyEventStream.complete();
272-
this._clearFromModal();
273271
}
274272

275273
/** Whether or not the autocomplete panel is open. */
@@ -327,11 +325,6 @@ export class MatAutocompleteTrigger
327325
// user clicks outside.
328326
this._changeDetectorRef.detectChanges();
329327
}
330-
331-
// Remove aria-owns attribute when the autocomplete is no longer visible.
332-
if (this._trackedModal) {
333-
removeAriaReferencedId(this._trackedModal, 'aria-owns', this.autocomplete.id);
334-
}
335328
}
336329

337330
/**
@@ -756,11 +749,6 @@ export class MatAutocompleteTrigger
756749
private _openPanelInternal(valueOnAttach = this._element.nativeElement.value) {
757750
this._attachOverlay(valueOnAttach);
758751
this._floatLabel();
759-
// Add aria-owns attribute when the autocomplete becomes visible.
760-
if (this._trackedModal) {
761-
const panelId = this.autocomplete.id;
762-
addAriaReferencedId(this._trackedModal, 'aria-owns', panelId);
763-
}
764752
}
765753

766754
private _attachOverlay(valueOnAttach: string): void {
@@ -827,7 +815,6 @@ export class MatAutocompleteTrigger
827815
this.autocomplete._latestOpeningTrigger = this;
828816
this.autocomplete._setColor(this._formField?.color);
829817
this._updatePanelState();
830-
this._applyModalPanelOwnership();
831818

832819
// We need to do an extra `panelOpen` check in here, because the
833820
// autocomplete won't be shown if there are no options.
@@ -1036,66 +1023,4 @@ export class MatAutocompleteTrigger
10361023
}
10371024
}
10381025
}
1039-
1040-
/**
1041-
* Track which modal we have modified the `aria-owns` attribute of. When the combobox trigger is
1042-
* inside an aria-modal, we apply aria-owns to the parent modal with the `id` of the options
1043-
* panel. Track the modal we have changed so we can undo the changes on destroy.
1044-
*/
1045-
private _trackedModal: Element | null = null;
1046-
1047-
/**
1048-
* If the autocomplete trigger is inside of an `aria-modal` element, connect
1049-
* that modal to the options panel with `aria-owns`.
1050-
*
1051-
* For some browser + screen reader combinations, when navigation is inside
1052-
* of an `aria-modal` element, the screen reader treats everything outside
1053-
* of that modal as hidden or invisible.
1054-
*
1055-
* This causes a problem when the combobox trigger is _inside_ of a modal, because the
1056-
* options panel is rendered _outside_ of that modal, preventing screen reader navigation
1057-
* from reaching the panel.
1058-
*
1059-
* We can work around this issue by applying `aria-owns` to the modal with the `id` of
1060-
* the options panel. This effectively communicates to assistive technology that the
1061-
* options panel is part of the same interaction as the modal.
1062-
*
1063-
* At time of this writing, this issue is present in VoiceOver.
1064-
* See https://github.com/angular/components/issues/20694
1065-
*/
1066-
private _applyModalPanelOwnership() {
1067-
// TODO(http://github.com/angular/components/issues/26853): consider de-duplicating this with
1068-
// the `LiveAnnouncer` and any other usages.
1069-
//
1070-
// Note that the selector here is limited to CDK overlays at the moment in order to reduce the
1071-
// section of the DOM we need to look through. This should cover all the cases we support, but
1072-
// the selector can be expanded if it turns out to be too narrow.
1073-
const modal = this._element.nativeElement.closest(
1074-
'body > .cdk-overlay-container [aria-modal="true"]',
1075-
);
1076-
1077-
if (!modal) {
1078-
// Most commonly, the autocomplete trigger is not inside a modal.
1079-
return;
1080-
}
1081-
1082-
const panelId = this.autocomplete.id;
1083-
1084-
if (this._trackedModal) {
1085-
removeAriaReferencedId(this._trackedModal, 'aria-owns', panelId);
1086-
}
1087-
1088-
addAriaReferencedId(modal, 'aria-owns', panelId);
1089-
this._trackedModal = modal;
1090-
}
1091-
1092-
/** Clears the references to the listbox overlay element from the modal it was added to. */
1093-
private _clearFromModal() {
1094-
if (this._trackedModal) {
1095-
const panelId = this.autocomplete.id;
1096-
1097-
removeAriaReferencedId(this._trackedModal, 'aria-owns', panelId);
1098-
this._trackedModal = null;
1099-
}
1100-
}
11011026
}

src/material/autocomplete/autocomplete.spec.ts

Lines changed: 1 addition & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {Direction} from '@angular/cdk/bidi';
22
import {DOWN_ARROW, ENTER, ESCAPE, SPACE, TAB, UP_ARROW} from '@angular/cdk/keycodes';
3-
import {OverlayModule, createCloseScrollStrategy} from '@angular/cdk/overlay';
3+
import {createCloseScrollStrategy} from '@angular/cdk/overlay';
44
import {_supportsShadowDom} from '@angular/cdk/platform';
55
import {ScrollDispatcher} from '@angular/cdk/scrolling';
66
import {
@@ -16,7 +16,6 @@ import {
1616
import {
1717
ChangeDetectionStrategy,
1818
Component,
19-
ElementRef,
2019
Injector,
2120
OnDestroy,
2221
OnInit,
@@ -3953,51 +3952,6 @@ describe('MatAutocomplete', () => {
39533952
expect(document.querySelectorAll('.mat-pseudo-checkbox').length).toBe(0);
39543953
});
39553954
});
3956-
3957-
describe('when used inside a modal', () => {
3958-
let fixture: ComponentFixture<AutocompleteInsideAModal>;
3959-
3960-
beforeEach(() => {
3961-
fixture = createComponent(AutocompleteInsideAModal);
3962-
fixture.detectChanges();
3963-
});
3964-
3965-
it('should add the id of the autocomplete panel to the aria-owns of the modal', () => {
3966-
fixture.componentInstance.trigger.openPanel();
3967-
fixture.detectChanges();
3968-
3969-
const panelId = fixture.componentInstance.autocomplete.id;
3970-
const modalElement = fixture.componentInstance.modal.nativeElement;
3971-
3972-
expect(modalElement.getAttribute('aria-owns')?.split(' '))
3973-
.withContext('expecting modal to own the autocommplete panel')
3974-
.toContain(panelId);
3975-
});
3976-
3977-
it('should remove the aria-owns attribute of the modal when the autocomplete panel closes', () => {
3978-
fixture.componentInstance.trigger.openPanel();
3979-
fixture.componentInstance.trigger.closePanel();
3980-
fixture.detectChanges();
3981-
3982-
const modalElement = fixture.componentInstance.modal.nativeElement;
3983-
3984-
expect(modalElement.getAttribute('aria-owns')).toBeFalsy();
3985-
});
3986-
3987-
it('should readd the aria-owns attribute of the modal when the autocomplete panel opens again', () => {
3988-
fixture.componentInstance.trigger.openPanel();
3989-
fixture.componentInstance.trigger.closePanel();
3990-
fixture.componentInstance.trigger.openPanel();
3991-
fixture.detectChanges();
3992-
3993-
const panelId = fixture.componentInstance.autocomplete.id;
3994-
const modalElement = fixture.componentInstance.modal.nativeElement;
3995-
3996-
expect(modalElement.getAttribute('aria-owns')?.split(' '))
3997-
.withContext('expecting modal to own the autocommplete panel')
3998-
.toContain(panelId);
3999-
});
4000-
});
40013955
});
40023956

40033957
const SIMPLE_AUTOCOMPLETE_TEMPLATE = `
@@ -4547,49 +4501,6 @@ class AutocompleteWithActivatedEvent {
45474501
@ViewChildren(MatOption) options!: QueryList<MatOption>;
45484502
}
45494503

4550-
@Component({
4551-
template: `
4552-
<button cdkOverlayOrigin #trigger="cdkOverlayOrigin">open dialog</button>
4553-
<ng-template cdkConnectedOverlay [cdkConnectedOverlayOpen]="true"
4554-
[cdkConnectedOverlayOrigin]="trigger">
4555-
<div role="dialog" [attr.aria-modal]="'true'" #modal>
4556-
<mat-form-field>
4557-
<mat-label>Food</mat-label>
4558-
<input matInput [matAutocomplete]="reactiveAuto" [formControl]="formControl">
4559-
</mat-form-field>
4560-
<mat-autocomplete #reactiveAuto="matAutocomplete">
4561-
@for (food of foods; track food; let index = $index) {
4562-
<mat-option [value]="food">{{food.viewValue}}</mat-option>
4563-
}
4564-
</mat-autocomplete>
4565-
</div>
4566-
</ng-template>
4567-
`,
4568-
imports: [
4569-
MatAutocomplete,
4570-
MatAutocompleteTrigger,
4571-
MatOption,
4572-
MatInputModule,
4573-
ReactiveFormsModule,
4574-
OverlayModule,
4575-
],
4576-
changeDetection: ChangeDetectionStrategy.Eager,
4577-
})
4578-
class AutocompleteInsideAModal {
4579-
foods = [
4580-
{value: 'steak-0', viewValue: 'Steak'},
4581-
{value: 'pizza-1', viewValue: 'Pizza'},
4582-
{value: 'tacos-2', viewValue: 'Tacos'},
4583-
];
4584-
4585-
formControl = new FormControl();
4586-
4587-
@ViewChild(MatAutocomplete) autocomplete!: MatAutocomplete;
4588-
@ViewChild(MatAutocompleteTrigger) trigger!: MatAutocompleteTrigger;
4589-
@ViewChildren(MatOption) options!: QueryList<MatOption>;
4590-
@ViewChild('modal') modal!: ElementRef;
4591-
}
4592-
45934504
@Component({
45944505
template: `
45954506
<mat-form-field>

0 commit comments

Comments
 (0)