diff --git a/src/material-experimental/mdc-autocomplete/autocomplete.html b/src/material-experimental/mdc-autocomplete/autocomplete.html index ddf5bae4c074..c931d3bc5d96 100644 --- a/src/material-experimental/mdc-autocomplete/autocomplete.html +++ b/src/material-experimental/mdc-autocomplete/autocomplete.html @@ -1,9 +1,11 @@ - +
diff --git a/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts b/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts index 51f15af34dcc..3fd62bd7630a 100644 --- a/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts +++ b/src/material-experimental/mdc-autocomplete/autocomplete.spec.ts @@ -1471,6 +1471,52 @@ describe('MDC-based MatAutocomplete', () => { .toEqual('listbox', 'Expected role of the panel to be listbox.'); }); + it('should point the aria-labelledby of the panel to the field label', () => { + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + + const panel = + fixture.debugElement.query(By.css('.mat-mdc-autocomplete-panel'))!.nativeElement; + const labelId = fixture.nativeElement.querySelector('label').id; + expect(panel.getAttribute('aria-labelledby')).toBe(labelId); + expect(panel.hasAttribute('aria-label')).toBe(false); + }); + + it('should add a custom aria-labelledby to the panel', () => { + fixture.componentInstance.ariaLabelledby = 'myLabelId'; + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + + const panel = + fixture.debugElement.query(By.css('.mat-mdc-autocomplete-panel'))!.nativeElement; + const labelId = fixture.nativeElement.querySelector('label').id; + expect(panel.getAttribute('aria-labelledby')).toBe(`${labelId} myLabelId`); + expect(panel.hasAttribute('aria-label')).toBe(false); + }); + + it('should clear aria-labelledby from the panel if an aria-label is set', () => { + fixture.componentInstance.ariaLabel = 'My label'; + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + + const panel = + fixture.debugElement.query(By.css('.mat-mdc-autocomplete-panel'))!.nativeElement; + expect(panel.getAttribute('aria-label')).toBe('My label'); + expect(panel.hasAttribute('aria-labelledby')).toBe(false); + }); + + it('should support setting a custom aria-label', () => { + fixture.componentInstance.ariaLabel = 'Custom Label'; + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + + const panel = + fixture.debugElement.query(By.css('.mat-mdc-autocomplete-panel'))!.nativeElement; + + expect(panel.getAttribute('aria-label')).toEqual('Custom Label'); + expect(panel.hasAttribute('aria-labelledby')).toBe(false); + }); + it('should set aria-autocomplete to list', () => { expect(input.getAttribute('aria-autocomplete')) .toEqual('list', 'Expected aria-autocomplete attribute to equal list.'); @@ -2682,6 +2728,7 @@ describe('MDC-based MatAutocomplete', () => { const SIMPLE_AUTOCOMPLETE_TEMPLATE = ` + State + [disableRipple]="disableRipple" [aria-label]="ariaLabel" [aria-labelledby]="ariaLabelledby" + (opened)="openedSpy()" (closed)="closedSpy()"> -
+ +
diff --git a/src/material/autocomplete/autocomplete.spec.ts b/src/material/autocomplete/autocomplete.spec.ts index 76d4791115c1..fa09e0ddbc51 100644 --- a/src/material/autocomplete/autocomplete.spec.ts +++ b/src/material/autocomplete/autocomplete.spec.ts @@ -1467,6 +1467,48 @@ describe('MatAutocomplete', () => { .toEqual('listbox', 'Expected role of the panel to be listbox.'); }); + it('should point the aria-labelledby of the panel to the field label', () => { + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + + const panel = fixture.debugElement.query(By.css('.mat-autocomplete-panel'))!.nativeElement; + const labelId = fixture.nativeElement.querySelector('.mat-form-field-label').id; + expect(panel.getAttribute('aria-labelledby')).toBe(labelId); + expect(panel.hasAttribute('aria-label')).toBe(false); + }); + + it('should add a custom aria-labelledby to the panel', () => { + fixture.componentInstance.ariaLabelledby = 'myLabelId'; + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + + const panel = fixture.debugElement.query(By.css('.mat-autocomplete-panel'))!.nativeElement; + const labelId = fixture.nativeElement.querySelector('.mat-form-field-label').id; + expect(panel.getAttribute('aria-labelledby')).toBe(`${labelId} myLabelId`); + expect(panel.hasAttribute('aria-label')).toBe(false); + }); + + it('should clear aria-labelledby from the panel if an aria-label is set', () => { + fixture.componentInstance.ariaLabel = 'My label'; + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + + const panel = fixture.debugElement.query(By.css('.mat-autocomplete-panel'))!.nativeElement; + expect(panel.getAttribute('aria-label')).toBe('My label'); + expect(panel.hasAttribute('aria-labelledby')).toBe(false); + }); + + it('should support setting a custom aria-label', () => { + fixture.componentInstance.ariaLabel = 'Custom Label'; + fixture.componentInstance.trigger.openPanel(); + fixture.detectChanges(); + + const panel = fixture.debugElement.query(By.css('.mat-autocomplete-panel'))!.nativeElement; + + expect(panel.getAttribute('aria-label')).toEqual('Custom Label'); + expect(panel.hasAttribute('aria-labelledby')).toBe(false); + }); + it('should set aria-autocomplete to list', () => { expect(input.getAttribute('aria-autocomplete')) .toEqual('list', 'Expected aria-autocomplete attribute to equal list.'); @@ -2701,7 +2743,8 @@ const SIMPLE_AUTOCOMPLETE_TEMPLATE = ` + [disableRipple]="disableRipple" [aria-label]="ariaLabel" [aria-labelledby]="ariaLabelledby" + (opened)="openedSpy()" (closed)="closedSpy()"> ; + /** Aria label of the select. If not specified, the placeholder will be used as label. */ + @Input('aria-label') ariaLabel: string; + + /** Input that can be used to specify the `aria-labelledby` attribute. */ + @Input('aria-labelledby') ariaLabelledby: string; + /** Function that maps an option's control value to its display value in the trigger. */ @Input() displayWith: ((value: any) => string) | null = null; @@ -251,6 +257,16 @@ export abstract class _MatAutocompleteBase extends _MatAutocompleteMixinBase imp this.optionSelected.emit(event); } + /** Gets the aria-labelledby for the autocomplete panel. */ + _getPanelAriaLabelledby(labelId: string): string | null { + if (this.ariaLabel) { + return null; + } + + return this.ariaLabelledby ? labelId + ' ' + this.ariaLabelledby : labelId; + } + + /** Sets the autocomplete visibility classes on a classlist based on the panel is visible. */ private _setVisibilityClasses(classList: {[key: string]: boolean}) { classList[this._visibleClass] = this.showPanel; diff --git a/tools/public_api_guard/material/autocomplete.d.ts b/tools/public_api_guard/material/autocomplete.d.ts index 64b3ffecc87c..1c20f1ac2a1c 100644 --- a/tools/public_api_guard/material/autocomplete.d.ts +++ b/tools/public_api_guard/material/autocomplete.d.ts @@ -6,6 +6,8 @@ export declare abstract class _MatAutocompleteBase extends _MatAutocompleteMixin _isOpen: boolean; _keyManager: ActiveDescendantKeyManager<_MatOptionBase>; protected abstract _visibleClass: string; + ariaLabel: string; + ariaLabelledby: string; get autoActiveFirstOption(): boolean; set autoActiveFirstOption(value: boolean); set classList(value: string | string[]); @@ -25,6 +27,7 @@ export declare abstract class _MatAutocompleteBase extends _MatAutocompleteMixin template: TemplateRef; constructor(_changeDetectorRef: ChangeDetectorRef, _elementRef: ElementRef, defaults: MatAutocompleteDefaultOptions, platform?: Platform); _emitSelectEvent(option: _MatOptionBase): void; + _getPanelAriaLabelledby(labelId: string): string | null; _getScrollTop(): number; _setScrollTop(scrollTop: number): void; _setVisibility(): void; @@ -32,7 +35,7 @@ export declare abstract class _MatAutocompleteBase extends _MatAutocompleteMixin ngOnDestroy(): void; static ngAcceptInputType_autoActiveFirstOption: BooleanInput; static ngAcceptInputType_disableRipple: BooleanInput; - static ɵdir: i0.ɵɵDirectiveDefWithMeta<_MatAutocompleteBase, never, never, { "displayWith": "displayWith"; "autoActiveFirstOption": "autoActiveFirstOption"; "panelWidth": "panelWidth"; "classList": "class"; }, { "optionSelected": "optionSelected"; "opened": "opened"; "closed": "closed"; "optionActivated": "optionActivated"; }, never>; + static ɵdir: i0.ɵɵDirectiveDefWithMeta<_MatAutocompleteBase, never, never, { "ariaLabel": "aria-label"; "ariaLabelledby": "aria-labelledby"; "displayWith": "displayWith"; "autoActiveFirstOption": "autoActiveFirstOption"; "panelWidth": "panelWidth"; "classList": "class"; }, { "optionSelected": "optionSelected"; "opened": "opened"; "closed": "closed"; "optionActivated": "optionActivated"; }, never>; static ɵfac: i0.ɵɵFactoryDef<_MatAutocompleteBase, never>; }