Skip to content

Commit 348c3c8

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

File tree

2 files changed

+2
-129
lines changed

2 files changed

+2
-129
lines changed

src/material/select/select.spec.ts

Lines changed: 1 addition & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
TAB,
1616
UP_ARROW,
1717
} from '@angular/cdk/keycodes';
18-
import {OverlayModule, createCloseScrollStrategy} from '@angular/cdk/overlay';
18+
import {createCloseScrollStrategy} from '@angular/cdk/overlay';
1919
import {ScrollDispatcher} from '@angular/cdk/scrolling';
2020
import {
2121
createKeyboardEvent,
@@ -30,7 +30,6 @@ import {
3030
ChangeDetectorRef,
3131
Component,
3232
DebugElement,
33-
ElementRef,
3433
Injector,
3534
OnInit,
3635
QueryList,
@@ -1097,27 +1096,6 @@ describe('MatSelect', () => {
10971096
});
10981097
});
10991098

1100-
describe('for select inside a modal', () => {
1101-
let fixture: ComponentFixture<SelectInsideAModal>;
1102-
1103-
beforeEach(() => {
1104-
fixture = TestBed.createComponent(SelectInsideAModal);
1105-
fixture.detectChanges();
1106-
});
1107-
1108-
it('should add the id of the select panel to the aria-owns of the modal', () => {
1109-
fixture.componentInstance.select.open();
1110-
fixture.detectChanges();
1111-
1112-
const panelId = `${fixture.componentInstance.select.id}-panel`;
1113-
const modalElement = fixture.componentInstance.modal.nativeElement;
1114-
1115-
expect(modalElement.getAttribute('aria-owns')?.split(' '))
1116-
.withContext('expecting modal to own the select panel')
1117-
.toContain(panelId);
1118-
});
1119-
});
1120-
11211099
describe('for options', () => {
11221100
let fixture: ComponentFixture<BasicSelect>;
11231101
let trigger: HTMLElement;
@@ -5482,35 +5460,3 @@ class BasicSelectWithFirstAndLastOptionDisabled {
54825460
@ViewChild(MatSelect, {static: true}) select!: MatSelect;
54835461
@ViewChildren(MatOption) options!: QueryList<MatOption>;
54845462
}
5485-
5486-
@Component({
5487-
template: `
5488-
<button cdkOverlayOrigin #trigger="cdkOverlayOrigin">open dialog</button>
5489-
<ng-template cdkConnectedOverlay [cdkConnectedOverlayOpen]="true"
5490-
[cdkConnectedOverlayOrigin]="trigger">
5491-
<div role="dialog" [attr.aria-modal]="'true'" #modal>
5492-
<mat-form-field>
5493-
<mat-label>Select a food</mat-label>
5494-
<mat-select placeholder="Food" ngModel>
5495-
@for (food of foods; track food) {
5496-
<mat-option [value]="food.value">{{ food.viewValue }}</mat-option>
5497-
}
5498-
</mat-select>
5499-
</mat-form-field>
5500-
</div>
5501-
</ng-template>
5502-
`,
5503-
imports: [MatSelect, MatOption, MatFormFieldModule, FormsModule, OverlayModule],
5504-
changeDetection: ChangeDetectionStrategy.Eager,
5505-
})
5506-
class SelectInsideAModal {
5507-
foods = [
5508-
{value: 'steak-0', viewValue: 'Steak'},
5509-
{value: 'pizza-1', viewValue: 'Pizza'},
5510-
{value: 'tacos-2', viewValue: 'Tacos'},
5511-
];
5512-
5513-
@ViewChild(MatSelect) select!: MatSelect;
5514-
@ViewChildren(MatOption) options!: QueryList<MatOption>;
5515-
@ViewChild('modal') modal!: ElementRef;
5516-
}

src/material/select/select.ts

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

9-
import {
10-
_IdGenerator,
11-
ActiveDescendantKeyManager,
12-
addAriaReferencedId,
13-
LiveAnnouncer,
14-
removeAriaReferencedId,
15-
} from '@angular/cdk/a11y';
9+
import {_IdGenerator, ActiveDescendantKeyManager, LiveAnnouncer} from '@angular/cdk/a11y';
1610
import {Directionality} from '@angular/cdk/bidi';
1711
import {SelectionModel} from '@angular/cdk/collections';
1812
import {
@@ -720,7 +714,6 @@ export class MatSelect
720714
this._destroy.next();
721715
this._destroy.complete();
722716
this.stateChanges.complete();
723-
this._clearFromModal();
724717
}
725718

726719
/** Toggles the overlay panel open or closed. */
@@ -743,7 +736,6 @@ export class MatSelect
743736

744737
this._cleanupDetach?.();
745738
this._overlayWidth = this._getOverlayWidth(this._preferredOverlayOrigin);
746-
this._applyModalPanelOwnership();
747739
this._panelOpen = true;
748740
this._overlayDir.positionChange.pipe(take(1)).subscribe(() => {
749741
this._changeDetectorRef.detectChanges();
@@ -761,71 +753,6 @@ export class MatSelect
761753
Promise.resolve().then(() => this.openedChange.emit(true));
762754
}
763755

764-
/**
765-
* Track which modal we have modified the `aria-owns` attribute of. When the combobox trigger is
766-
* inside an aria-modal, we apply aria-owns to the parent modal with the `id` of the options
767-
* panel. Track the modal we have changed so we can undo the changes on destroy.
768-
*/
769-
private _trackedModal: Element | null = null;
770-
771-
/**
772-
* If the autocomplete trigger is inside of an `aria-modal` element, connect
773-
* that modal to the options panel with `aria-owns`.
774-
*
775-
* For some browser + screen reader combinations, when navigation is inside
776-
* of an `aria-modal` element, the screen reader treats everything outside
777-
* of that modal as hidden or invisible.
778-
*
779-
* This causes a problem when the combobox trigger is _inside_ of a modal, because the
780-
* options panel is rendered _outside_ of that modal, preventing screen reader navigation
781-
* from reaching the panel.
782-
*
783-
* We can work around this issue by applying `aria-owns` to the modal with the `id` of
784-
* the options panel. This effectively communicates to assistive technology that the
785-
* options panel is part of the same interaction as the modal.
786-
*
787-
* At time of this writing, this issue is present in VoiceOver.
788-
* See https://github.com/angular/components/issues/20694
789-
*/
790-
private _applyModalPanelOwnership() {
791-
// TODO(http://github.com/angular/components/issues/26853): consider de-duplicating this with
792-
// the `LiveAnnouncer` and any other usages.
793-
//
794-
// Note that the selector here is limited to CDK overlays at the moment in order to reduce the
795-
// section of the DOM we need to look through. This should cover all the cases we support, but
796-
// the selector can be expanded if it turns out to be too narrow.
797-
const modal = this._elementRef.nativeElement.closest(
798-
'body > .cdk-overlay-container [aria-modal="true"]',
799-
);
800-
801-
if (!modal) {
802-
// Most commonly, the autocomplete trigger is not inside a modal.
803-
return;
804-
}
805-
806-
const panelId = `${this.id}-panel`;
807-
808-
if (this._trackedModal) {
809-
removeAriaReferencedId(this._trackedModal, 'aria-owns', panelId);
810-
}
811-
812-
addAriaReferencedId(modal, 'aria-owns', panelId);
813-
this._trackedModal = modal;
814-
}
815-
816-
/** Clears the reference to the listbox overlay element from the modal it was added to. */
817-
private _clearFromModal() {
818-
if (!this._trackedModal) {
819-
// Most commonly, the autocomplete trigger is not used inside a modal.
820-
return;
821-
}
822-
823-
const panelId = `${this.id}-panel`;
824-
825-
removeAriaReferencedId(this._trackedModal, 'aria-owns', panelId);
826-
this._trackedModal = null;
827-
}
828-
829756
/** Closes the overlay panel and focuses the host element. */
830757
close(): void {
831758
if (this._panelOpen) {

0 commit comments

Comments
 (0)