-
Notifications
You must be signed in to change notification settings - Fork 6.8k
fix(material/sort): add description input for sort-header #23633
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,23 +6,23 @@ | |
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import {AriaDescriber, FocusMonitor} from '@angular/cdk/a11y'; | ||
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; | ||
import {ENTER, SPACE} from '@angular/cdk/keycodes'; | ||
import { | ||
AfterViewInit, | ||
ChangeDetectionStrategy, | ||
ChangeDetectorRef, | ||
Component, | ||
ElementRef, | ||
Inject, | ||
Input, | ||
OnDestroy, | ||
OnInit, | ||
Optional, | ||
ViewEncapsulation, | ||
Inject, | ||
ElementRef, | ||
AfterViewInit, | ||
} from '@angular/core'; | ||
import {CanDisable, mixinDisabled} from '@angular/material/core'; | ||
import {FocusMonitor} from '@angular/cdk/a11y'; | ||
import {ENTER, SPACE} from '@angular/cdk/keycodes'; | ||
import {merge, Subscription} from 'rxjs'; | ||
import {MatSort, MatSortable} from './sort'; | ||
import {matSortAnimations} from './sort-animations'; | ||
|
@@ -99,6 +99,12 @@ export class MatSortHeader extends _MatSortHeaderBase | |
implements CanDisable, MatSortable, OnDestroy, OnInit, AfterViewInit { | ||
private _rerenderSubscription: Subscription; | ||
|
||
/** | ||
* The element with role="button" inside this component's view. We need this | ||
* in order to apply a description with AriaDescriber. | ||
*/ | ||
private _sortButton: HTMLElement; | ||
|
||
/** | ||
* Flag set to true when the indicator should be displayed while the sort is not active. Used to | ||
* provide an affordance that the header is sortable by showing on focus and hover. | ||
|
@@ -132,6 +138,22 @@ export class MatSortHeader extends _MatSortHeaderBase | |
/** Overrides the sort start value of the containing MatSort for this MatSortable. */ | ||
@Input() start: 'asc' | 'desc'; | ||
|
||
/** | ||
* Description applied to MatSortHeader's button element with aria-describedby. This text should | ||
* describe the action that will occur when the user clicks the sort header. | ||
*/ | ||
@Input() | ||
get sortActionDescription(): string { | ||
return this._sortActionDescription; | ||
} | ||
set sortActionDescription(value: string) { | ||
this._updateSortActionDescription(value); | ||
} | ||
// Default the action description to "Sort" because it's better than nothing. | ||
// Without a description, the button's label comes from the sort header text content, | ||
// which doesn't give any indication that it performs a sorting operation. | ||
private _sortActionDescription: string = 'Sort'; | ||
|
||
/** Overrides the disable clear value of the containing MatSort for this MatSortable. */ | ||
@Input() | ||
get disableClear(): boolean { return this._disableClear; } | ||
|
@@ -151,7 +173,9 @@ export class MatSortHeader extends _MatSortHeaderBase | |
@Inject('MAT_SORT_HEADER_COLUMN_DEF') @Optional() | ||
public _columnDef: MatSortHeaderColumnDef, | ||
private _focusMonitor: FocusMonitor, | ||
private _elementRef: ElementRef<HTMLElement>) { | ||
private _elementRef: ElementRef<HTMLElement>, | ||
/** @breaking-change 14.0.0 _ariaDescriber will be required. */ | ||
@Optional() private _ariaDescriber?: AriaDescriber | null) { | ||
// Note that we use a string token for the `_columnDef`, because the value is provided both by | ||
// `material/table` and `cdk/table` and we can't have the CDK depending on Material, | ||
// and we want to avoid having the sort header depending on the CDK table because | ||
|
@@ -176,6 +200,9 @@ export class MatSortHeader extends _MatSortHeaderBase | |
{toState: this._isSorted() ? 'active' : this._arrowDirection}); | ||
|
||
this._sort.register(this); | ||
|
||
this._sortButton = this._elementRef.nativeElement.querySelector('[role="button"]')!; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we use a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO I don't see the point of using |
||
this._updateSortActionDescription(this._sortActionDescription); | ||
} | ||
|
||
ngAfterViewInit() { | ||
|
@@ -310,6 +337,23 @@ export class MatSortHeader extends _MatSortHeaderBase | |
return !this._isDisabled() || this._isSorted(); | ||
} | ||
|
||
private _updateSortActionDescription(newDescription: string) { | ||
// We use AriaDescriber for the sort button instead of setting an `aria-label` because some | ||
// screen readers (notably VoiceOver) will read both the column header *and* the button's label | ||
// for every *cell* in the table, creating a lot of unnecessary noise. | ||
|
||
// If _sortButton is undefined, the component hasn't been initialized yet so there's | ||
// nothing to update in the DOM. | ||
if (this._sortButton) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we also need to check that the description is defined/isn't an empty string here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
// removeDescription will no-op if there is no existing message. | ||
// TODO(jelbourn): remove optional chaining when AriaDescriber is required. | ||
this._ariaDescriber?.removeDescription(this._sortButton, this._sortActionDescription); | ||
this._ariaDescriber?.describe(this._sortButton, newDescription); | ||
} | ||
|
||
this._sortActionDescription = newDescription; | ||
} | ||
|
||
/** Handles changes in the sorting state. */ | ||
private _handleStateChanges() { | ||
this._rerenderSubscription = | ||
|
Uh oh!
There was an error while loading. Please reload this page.