forked from angular/components
-
Notifications
You must be signed in to change notification settings - Fork 0
/
sort-header.ts
399 lines (349 loc) · 14.1 KB
/
sort-header.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {AriaDescriber, FocusMonitor} from '@angular/cdk/a11y';
import {ENTER, SPACE} from '@angular/cdk/keycodes';
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
Inject,
Input,
OnDestroy,
OnInit,
Optional,
ViewEncapsulation,
booleanAttribute,
} from '@angular/core';
import {merge, Subscription} from 'rxjs';
import {
MAT_SORT_DEFAULT_OPTIONS,
MatSort,
MatSortable,
MatSortDefaultOptions,
SortHeaderArrowPosition,
} from './sort';
import {matSortAnimations} from './sort-animations';
import {SortDirection} from './sort-direction';
import {getSortHeaderNotContainedWithinSortError} from './sort-errors';
import {MatSortHeaderIntl} from './sort-header-intl';
/**
* Valid positions for the arrow to be in for its opacity and translation. If the state is a
* sort direction, the position of the arrow will be above/below and opacity 0. If the state is
* hint, the arrow will be in the center with a slight opacity. Active state means the arrow will
* be fully opaque in the center.
*
* @docs-private
*/
export type ArrowViewState = SortDirection | 'hint' | 'active';
/**
* States describing the arrow's animated position (animating fromState to toState).
* If the fromState is not defined, there will be no animated transition to the toState.
* @docs-private
*/
export interface ArrowViewStateTransition {
fromState?: ArrowViewState;
toState?: ArrowViewState;
}
/** Column definition associated with a `MatSortHeader`. */
interface MatSortHeaderColumnDef {
name: string;
}
/**
* Applies sorting behavior (click to change sort) and styles to an element, including an
* arrow to display the current sort direction.
*
* Must be provided with an id and contained within a parent MatSort directive.
*
* If used on header cells in a CdkTable, it will automatically default its id from its containing
* column definition.
*/
@Component({
selector: '[mat-sort-header]',
exportAs: 'matSortHeader',
templateUrl: 'sort-header.html',
styleUrls: ['sort-header.css'],
host: {
'class': 'mat-sort-header',
'(click)': '_handleClick()',
'(keydown)': '_handleKeydown($event)',
'(mouseenter)': '_setIndicatorHintVisible(true)',
'(mouseleave)': '_setIndicatorHintVisible(false)',
'[attr.aria-sort]': '_getAriaSortAttribute()',
'[class.mat-sort-header-disabled]': '_isDisabled()',
},
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [
matSortAnimations.indicator,
matSortAnimations.leftPointer,
matSortAnimations.rightPointer,
matSortAnimations.arrowOpacity,
matSortAnimations.arrowPosition,
matSortAnimations.allowChildren,
],
standalone: true,
})
export class MatSortHeader implements 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.
*/
_showIndicatorHint: boolean = false;
/**
* The view transition state of the arrow (translation/ opacity) - indicates its `from` and `to`
* position through the animation. If animations are currently disabled, the fromState is removed
* so that there is no animation displayed.
*/
_viewState: ArrowViewStateTransition = {};
/** The direction the arrow should be facing according to the current state. */
_arrowDirection: SortDirection = '';
/**
* Whether the view state animation should show the transition between the `from` and `to` states.
*/
_disableViewStateAnimation = false;
/**
* ID of this sort header. If used within the context of a CdkColumnDef, this will default to
* the column's name.
*/
@Input('mat-sort-header') id: string;
/** Sets the position of the arrow that displays when sorted. */
@Input() arrowPosition: SortHeaderArrowPosition = 'after';
/** Overrides the sort start value of the containing MatSort for this MatSortable. */
@Input() start: SortDirection;
/** whether the sort header is disabled. */
@Input({transform: booleanAttribute})
disabled: boolean = false;
/**
* 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({transform: booleanAttribute})
disableClear: boolean;
constructor(
/**
* @deprecated `_intl` parameter isn't being used anymore and it'll be removed.
* @breaking-change 13.0.0
*/
public _intl: MatSortHeaderIntl,
private _changeDetectorRef: ChangeDetectorRef,
// `MatSort` is not optionally injected, but just asserted manually w/ better error.
// tslint:disable-next-line: lightweight-tokens
@Optional() public _sort: MatSort,
@Inject('MAT_SORT_HEADER_COLUMN_DEF')
@Optional()
public _columnDef: MatSortHeaderColumnDef,
private _focusMonitor: FocusMonitor,
private _elementRef: ElementRef<HTMLElement>,
/** @breaking-change 14.0.0 _ariaDescriber will be required. */
@Optional() private _ariaDescriber?: AriaDescriber | null,
@Optional()
@Inject(MAT_SORT_DEFAULT_OPTIONS)
defaultOptions?: MatSortDefaultOptions,
) {
// 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
// of this single reference.
if (!_sort && (typeof ngDevMode === 'undefined' || ngDevMode)) {
throw getSortHeaderNotContainedWithinSortError();
}
if (defaultOptions?.arrowPosition) {
this.arrowPosition = defaultOptions?.arrowPosition;
}
this._handleStateChanges();
}
ngOnInit() {
if (!this.id && this._columnDef) {
this.id = this._columnDef.name;
}
// Initialize the direction of the arrow and set the view state to be immediately that state.
this._updateArrowDirection();
this._setAnimationTransitionState({
toState: this._isSorted() ? 'active' : this._arrowDirection,
});
this._sort.register(this);
this._sortButton = this._elementRef.nativeElement.querySelector('.mat-sort-header-container')!;
this._updateSortActionDescription(this._sortActionDescription);
}
ngAfterViewInit() {
// We use the focus monitor because we also want to style
// things differently based on the focus origin.
this._focusMonitor.monitor(this._elementRef, true).subscribe(origin => {
const newState = !!origin;
if (newState !== this._showIndicatorHint) {
this._setIndicatorHintVisible(newState);
this._changeDetectorRef.markForCheck();
}
});
}
ngOnDestroy() {
this._focusMonitor.stopMonitoring(this._elementRef);
this._sort.deregister(this);
this._rerenderSubscription.unsubscribe();
}
/**
* Sets the "hint" state such that the arrow will be semi-transparently displayed as a hint to the
* user showing what the active sort will become. If set to false, the arrow will fade away.
*/
_setIndicatorHintVisible(visible: boolean) {
// No-op if the sort header is disabled - should not make the hint visible.
if (this._isDisabled() && visible) {
return;
}
this._showIndicatorHint = visible;
if (!this._isSorted()) {
this._updateArrowDirection();
if (this._showIndicatorHint) {
this._setAnimationTransitionState({fromState: this._arrowDirection, toState: 'hint'});
} else {
this._setAnimationTransitionState({fromState: 'hint', toState: this._arrowDirection});
}
}
}
/**
* Sets the animation transition view state for the arrow's position and opacity. If the
* `disableViewStateAnimation` flag is set to true, the `fromState` will be ignored so that
* no animation appears.
*/
_setAnimationTransitionState(viewState: ArrowViewStateTransition) {
this._viewState = viewState || {};
// If the animation for arrow position state (opacity/translation) should be disabled,
// remove the fromState so that it jumps right to the toState.
if (this._disableViewStateAnimation) {
this._viewState = {toState: viewState.toState};
}
}
/** Triggers the sort on this sort header and removes the indicator hint. */
_toggleOnInteraction() {
this._sort.sort(this);
// Do not show the animation if the header was already shown in the right position.
if (this._viewState.toState === 'hint' || this._viewState.toState === 'active') {
this._disableViewStateAnimation = true;
}
}
_handleClick() {
if (!this._isDisabled()) {
this._sort.sort(this);
}
}
_handleKeydown(event: KeyboardEvent) {
if (!this._isDisabled() && (event.keyCode === SPACE || event.keyCode === ENTER)) {
event.preventDefault();
this._toggleOnInteraction();
}
}
/** Whether this MatSortHeader is currently sorted in either ascending or descending order. */
_isSorted() {
const currentSortDirection = this._sort.getCurrentSortDirection(this.id);
return (
this._sort.isActive(this.id) &&
(currentSortDirection === 'asc' || currentSortDirection === 'desc')
);
}
/** Returns the animation state for the arrow direction (indicator and pointers). */
_getArrowDirectionState() {
return `${this._isSorted() ? 'active-' : ''}${this._arrowDirection}`;
}
/** Returns the arrow position state (opacity, translation). */
_getArrowViewState() {
const fromState = this._viewState.fromState;
return (fromState ? `${fromState}-to-` : '') + this._viewState.toState;
}
/**
* Updates the direction the arrow should be pointing. If it is not sorted, the arrow should be
* facing the start direction. Otherwise if it is sorted, the arrow should point in the currently
* active sorted direction. The reason this is updated through a function is because the direction
* should only be changed at specific times - when deactivated but the hint is displayed and when
* the sort is active and the direction changes. Otherwise the arrow's direction should linger
* in cases such as the sort becoming deactivated but we want to animate the arrow away while
* preserving its direction, even though the next sort direction is actually different and should
* only be changed once the arrow displays again (hint or activation).
*/
_updateArrowDirection() {
this._arrowDirection = this._isSorted()
? this._sort.getCurrentSortDirection(this.id)
: this.start || this._sort.start;
}
_isDisabled() {
return this._sort.disabled || this.disabled;
}
/**
* Gets the aria-sort attribute that should be applied to this sort header. If this header
* is not sorted, returns null so that the attribute is removed from the host element. Aria spec
* says that the aria-sort property should only be present on one header at a time, so removing
* ensures this is true.
*/
_getAriaSortAttribute() {
if (!this._isSorted()) {
return 'none';
}
return this._sort.getCurrentSortDirection(this.id) == 'asc' ? 'ascending' : 'descending';
}
/** Whether the arrow inside the sort header should be rendered. */
_renderArrow() {
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) {
// 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 = merge(
this._sort.sortChange,
this._sort._stateChanges,
this._intl.changes,
).subscribe(() => {
if (this._isSorted()) {
this._updateArrowDirection();
// Do not show the animation if the header was already shown in the right position.
if (this._viewState.toState === 'hint' || this._viewState.toState === 'active') {
this._disableViewStateAnimation = true;
}
this._setAnimationTransitionState({fromState: this._arrowDirection, toState: 'active'});
this._showIndicatorHint = false;
}
// If this header was recently active and now no longer sorted, animate away the arrow.
if (!this._isSorted() && this._viewState && this._viewState.toState === 'active') {
this._disableViewStateAnimation = false;
this._setAnimationTransitionState({fromState: 'active', toState: this._arrowDirection});
}
this._changeDetectorRef.markForCheck();
});
}
}