From ffed73fce761b88bd613982810060157caa14ac7 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Mon, 26 Oct 2020 17:29:28 -0700 Subject: [PATCH 1/2] fix(material/autocomplete): add missing aria-label for autocomplete panel --- .../mdc-autocomplete/autocomplete.html | 2 + .../mdc-autocomplete/autocomplete.spec.ts | 52 ++++++++++++++++++- .../autocomplete/autocomplete-trigger.ts | 1 + src/material/autocomplete/autocomplete.html | 7 ++- .../autocomplete/autocomplete.spec.ts | 47 ++++++++++++++++- src/material/autocomplete/autocomplete.ts | 20 +++++++ .../material/autocomplete.d.ts | 6 ++- 7 files changed, 131 insertions(+), 4 deletions(-) diff --git a/src/material-experimental/mdc-autocomplete/autocomplete.html b/src/material-experimental/mdc-autocomplete/autocomplete.html index ddf5bae4c074..8a38b4f810e0 100644 --- a/src/material-experimental/mdc-autocomplete/autocomplete.html +++ b/src/material-experimental/mdc-autocomplete/autocomplete.html @@ -4,6 +4,8 @@ role="listbox" [id]="id" [ngClass]="_classList" + [attr.aria-label]="ariaLabel || null" + [attr.aria-labelledby]="_getPanelAriaLabelledby()" #panel> 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 +260,17 @@ export abstract class _MatAutocompleteBase extends _MatAutocompleteMixinBase imp this.optionSelected.emit(event); } + /** Gets the aria-labelledby for the autocomplete panel. */ + _getPanelAriaLabelledby(): string | null { + if (this.ariaLabel) { + return null; + } + + const labelId = this._formFieldLabelId ?? ''; + 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..4cca3e94b89d 100644 --- a/tools/public_api_guard/material/autocomplete.d.ts +++ b/tools/public_api_guard/material/autocomplete.d.ts @@ -2,10 +2,13 @@ export declare abstract class _MatAutocompleteBase extends _MatAutocompleteMixin _classList: { [key: string]: boolean; }; + _formFieldLabelId: string; protected abstract _hiddenClass: string; _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 +28,7 @@ export declare abstract class _MatAutocompleteBase extends _MatAutocompleteMixin template: TemplateRef; constructor(_changeDetectorRef: ChangeDetectorRef, _elementRef: ElementRef, defaults: MatAutocompleteDefaultOptions, platform?: Platform); _emitSelectEvent(option: _MatOptionBase): void; + _getPanelAriaLabelledby(): string | null; _getScrollTop(): number; _setScrollTop(scrollTop: number): void; _setVisibility(): void; @@ -32,7 +36,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>; } From 1371e316e84b462b6da238ae7da36a14a71e6d39 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Mon, 23 Nov 2020 11:48:59 -0800 Subject: [PATCH 2/2] fix(material/autocomplete): pass in form field id in ng-template --- .../mdc-autocomplete/autocomplete.html | 4 ++-- src/material/autocomplete/autocomplete-trigger.ts | 5 +++-- src/material/autocomplete/autocomplete.html | 7 ++++--- src/material/autocomplete/autocomplete.ts | 6 +----- tools/public_api_guard/material/autocomplete.d.ts | 3 +-- 5 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/material-experimental/mdc-autocomplete/autocomplete.html b/src/material-experimental/mdc-autocomplete/autocomplete.html index 8a38b4f810e0..c931d3bc5d96 100644 --- a/src/material-experimental/mdc-autocomplete/autocomplete.html +++ b/src/material-experimental/mdc-autocomplete/autocomplete.html @@ -1,11 +1,11 @@ - +
diff --git a/src/material/autocomplete/autocomplete-trigger.ts b/src/material/autocomplete/autocomplete-trigger.ts index a1c87f57f32c..03327273b4e4 100644 --- a/src/material/autocomplete/autocomplete-trigger.ts +++ b/src/material/autocomplete/autocomplete-trigger.ts @@ -591,7 +591,9 @@ export abstract class _MatAutocompleteTriggerBase implements ControlValueAccesso let overlayRef = this._overlayRef; if (!overlayRef) { - this._portal = new TemplatePortal(this.autocomplete.template, this._viewContainerRef); + this._portal = new TemplatePortal(this.autocomplete.template, + this._viewContainerRef, + {id: this._formField?._labelId}); overlayRef = this._overlay.create(this._getOverlayConfig()); this._overlayRef = overlayRef; @@ -632,7 +634,6 @@ export abstract class _MatAutocompleteTriggerBase implements ControlValueAccesso this.autocomplete._setVisibility(); this.autocomplete._isOpen = this._overlayAttached = true; - this.autocomplete._formFieldLabelId = this._formField?._labelId; // We need to do an extra `panelOpen` check in here, because the // autocomplete won't be shown if there are no options. diff --git a/src/material/autocomplete/autocomplete.html b/src/material/autocomplete/autocomplete.html index d930090ca9eb..78118ff9f205 100644 --- a/src/material/autocomplete/autocomplete.html +++ b/src/material/autocomplete/autocomplete.html @@ -1,8 +1,9 @@ - +
diff --git a/src/material/autocomplete/autocomplete.ts b/src/material/autocomplete/autocomplete.ts index fed55dcf3d8a..c71cdb4fc4ee 100644 --- a/src/material/autocomplete/autocomplete.ts +++ b/src/material/autocomplete/autocomplete.ts @@ -115,9 +115,6 @@ export abstract class _MatAutocompleteBase extends _MatAutocompleteMixinBase imp get isOpen(): boolean { return this._isOpen && this.showPanel; } _isOpen: boolean = false; - /** Label id of form field the autocomplete is associated with. */ - _formFieldLabelId: string; - // The @ViewChild query for TemplateRef here needs to be static because some code paths // lead to the overlay being created before change detection has finished for this component. // Notably, another component may trigger `focus` on the autocomplete-trigger. @@ -261,12 +258,11 @@ export abstract class _MatAutocompleteBase extends _MatAutocompleteMixinBase imp } /** Gets the aria-labelledby for the autocomplete panel. */ - _getPanelAriaLabelledby(): string | null { + _getPanelAriaLabelledby(labelId: string): string | null { if (this.ariaLabel) { return null; } - const labelId = this._formFieldLabelId ?? ''; return this.ariaLabelledby ? labelId + ' ' + this.ariaLabelledby : labelId; } diff --git a/tools/public_api_guard/material/autocomplete.d.ts b/tools/public_api_guard/material/autocomplete.d.ts index 4cca3e94b89d..1c20f1ac2a1c 100644 --- a/tools/public_api_guard/material/autocomplete.d.ts +++ b/tools/public_api_guard/material/autocomplete.d.ts @@ -2,7 +2,6 @@ export declare abstract class _MatAutocompleteBase extends _MatAutocompleteMixin _classList: { [key: string]: boolean; }; - _formFieldLabelId: string; protected abstract _hiddenClass: string; _isOpen: boolean; _keyManager: ActiveDescendantKeyManager<_MatOptionBase>; @@ -28,7 +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(): string | null; + _getPanelAriaLabelledby(labelId: string): string | null; _getScrollTop(): number; _setScrollTop(scrollTop: number): void; _setVisibility(): void;