Skip to content

Commit 4d9caff

Browse files
committed
feat(sort): fix animation; show on hover
1 parent d6fec35 commit 4d9caff

File tree

4 files changed

+388
-60
lines changed

4 files changed

+388
-60
lines changed

src/lib/sort/sort-header.html

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
<div class="mat-sort-header-container"
2-
[class.mat-sort-header-position-before]="arrowPosition == 'before'">
2+
[class.mat-sort-header-sorted]="_isSorted()"
3+
[class.mat-sort-header-position-before]="viewState == 'before'">
34
<button class="mat-sort-header-button" type="button"
5+
(focus)="showIndicatorHint = true"
6+
(blur)="showIndicatorHint = false"
47
[attr.aria-label]="_intl.sortButtonLabel(id)">
58
<ng-content></ng-content>
69
</button>
710

8-
<div *ngIf="_isSorted()" class="mat-sort-header-arrow" [@indicatorToggle]="_sort.direction">
11+
<!-- Disable animations while a current animation is running -->
12+
<div class="mat-sort-header-arrow"
13+
[@arrowOpacity]="_getArrowViewState()"
14+
[@arrowPosition]="_getArrowViewState()"
15+
(@arrowPosition.start)="_disableStateAnimation = true"
16+
(@arrowPosition.done)="_disableStateAnimation = false">
917
<div class="mat-sort-header-stem"></div>
10-
<div class="mat-sort-header-indicator" [@indicator]="_sort.direction" >
11-
<div class="mat-sort-header-pointer-left" [@leftPointer]="_sort.direction"></div>
12-
<div class="mat-sort-header-pointer-right" [@rightPointer]="_sort.direction"></div>
18+
<div class="mat-sort-header-indicator" [@indicator]="_getArrowDirectionState()" >
19+
<div class="mat-sort-header-pointer-left" [@leftPointer]="_getArrowDirectionState()"></div>
20+
<div class="mat-sort-header-pointer-right" [@rightPointer]="_getArrowDirectionState()"></div>
1321
<div class="mat-sort-header-pointer-middle"></div>
1422
</div>
1523
</div>

src/lib/sort/sort-header.scss

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ $mat-sort-header-arrow-container-size: 12px;
33
$mat-sort-header-arrow-stem-size: 10px;
44
$mat-sort-header-arrow-pointer-length: 6px;
55
$mat-sort-header-arrow-thickness: 2px;
6-
$mat-sort-header-arrow-transition: 225ms cubic-bezier(0.4, 0, 0.2, 1);
6+
$mat-sort-header-arrow-hint-opacity: 0.38;
77

88
.mat-sort-header-container {
99
display: flex;
@@ -56,7 +56,6 @@ $mat-sort-header-arrow-transition: 225ms cubic-bezier(0.4, 0, 0.2, 1);
5656
position: absolute;
5757
top: 0;
5858
left: 0;
59-
transition: $mat-sort-header-arrow-transition;
6059
}
6160

6261
.mat-sort-header-pointer-middle {
@@ -72,7 +71,6 @@ $mat-sort-header-arrow-transition: 225ms cubic-bezier(0.4, 0, 0.2, 1);
7271
background: currentColor;
7372
width: $mat-sort-header-arrow-pointer-length;
7473
height: $mat-sort-header-arrow-thickness;
75-
transition: $mat-sort-header-arrow-transition;
7674
position: absolute;
7775
top: 0;
7876
}

src/lib/sort/sort-header.ts

Lines changed: 179 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,35 @@ import {
1515
ViewEncapsulation
1616
} from '@angular/core';
1717
import {coerceBooleanProperty} from '@angular/cdk/coercion';
18-
import {
19-
trigger,
20-
state,
21-
style,
22-
animate,
23-
transition,
24-
keyframes,
25-
} from '@angular/animations';
18+
import {animate, keyframes, state, style, transition, trigger} from '@angular/animations';
2619
import {CdkColumnDef} from '@angular/cdk/table';
2720
import {Subscription} from 'rxjs/Subscription';
2821
import {merge} from 'rxjs/observable/merge';
2922
import {MatSort, MatSortable} from './sort';
23+
import {SortDirection} from './sort-direction';
3024
import {MatSortHeaderIntl} from './sort-header-intl';
3125
import {getSortHeaderNotContainedWithinSortError} from './sort-errors';
3226
import {AnimationCurves, AnimationDurations} from '@angular/material/core';
3327

3428
const SORT_ANIMATION_TRANSITION =
3529
AnimationDurations.ENTERING + ' ' + AnimationCurves.STANDARD_CURVE;
3630

31+
/**
32+
* Valid positions for the arrow to be in for its opacity and translation.
33+
* @docs-private
34+
*/
35+
export type ArrowViewState = SortDirection | 'peek' | 'active';
36+
37+
/**
38+
* States describing the arrow's animated position (animating fromState -> toState).
39+
* If the fromState is not defined, there will be no animated transition to the toState.
40+
* @docs-private
41+
*/
42+
export interface ArrowViewStateTransition {
43+
fromState?: ArrowViewState;
44+
toState: ArrowViewState;
45+
}
46+
3747
/**
3848
* Applies sorting behavior (click to change sort) and styles to an element, including an
3949
* arrow to display the current sort direction.
@@ -50,52 +60,135 @@ const SORT_ANIMATION_TRANSITION =
5060
templateUrl: 'sort-header.html',
5161
styleUrls: ['sort-header.css'],
5262
host: {
53-
'(click)': '_sort.sort(this)',
54-
'[class.mat-sort-header-sorted]': '_isSorted()',
63+
'(click)': '_handleClick()',
64+
'(mouseenter)': 'showIndicatorHint = true',
65+
'(longpress)': 'showIndicatorHint = true',
66+
'(mouseleave)': 'showIndicatorHint = false',
5567
},
5668
encapsulation: ViewEncapsulation.None,
5769
preserveWhitespaces: false,
5870
changeDetection: ChangeDetectionStrategy.OnPush,
5971
animations: [
72+
// Individual arrow parts that show direction
6073
trigger('indicator', [
61-
state('asc', style({transform: 'translateY(0px)'})),
74+
state('active-asc, asc', style({transform: 'translateY(0px)'})),
6275
// 10px is the height of the sort indicator, minus the width of the pointers
63-
state('desc', style({transform: 'translateY(10px)'})),
64-
transition('asc <=> desc', animate(SORT_ANIMATION_TRANSITION))
76+
state('active-desc, desc', style({transform: 'translateY(10px)'})),
77+
transition('active-asc <=> active-desc', animate(SORT_ANIMATION_TRANSITION))
6578
]),
6679
trigger('leftPointer', [
67-
state('asc', style({transform: 'rotate(-45deg)'})),
68-
state('desc', style({transform: 'rotate(45deg)'})),
69-
transition('asc <=> desc', animate(SORT_ANIMATION_TRANSITION))
80+
state('active-asc, asc', style({transform: 'rotate(-45deg)'})),
81+
state('active-desc, desc', style({transform: 'rotate(45deg)'})),
82+
transition('active-asc <=> active-desc', animate(SORT_ANIMATION_TRANSITION))
7083
]),
7184
trigger('rightPointer', [
72-
state('asc', style({transform: 'rotate(45deg)'})),
73-
state('desc', style({transform: 'rotate(-45deg)'})),
74-
transition('asc <=> desc', animate(SORT_ANIMATION_TRANSITION))
85+
state('active-asc, asc', style({transform: 'rotate(45deg)'})),
86+
state('active-desc, desc', style({transform: 'rotate(-45deg)'})),
87+
transition('active-asc <=> active-desc', animate(SORT_ANIMATION_TRANSITION))
88+
]),
89+
90+
// Arrow opacity
91+
trigger('arrowOpacity', [
92+
state('desc-to-active, asc-to-active, active',
93+
style({opacity: 1})),
94+
state('desc-to-peek, asc-to-peek, peek',
95+
style({opacity: .54})),
96+
state('peek-to-desc, active-to-desc, desc, peek-to-asc, active-to-asc, asc',
97+
style({opacity: 0})),
98+
// Transition between all states except for immediate transitions
99+
transition('* => asc, * => desc, * => active, * => peek', animate('0ms')),
100+
transition('* <=> *', animate(SORT_ANIMATION_TRANSITION))
75101
]),
76-
trigger('indicatorToggle', [
77-
transition('void => asc', animate(SORT_ANIMATION_TRANSITION, keyframes([
78-
style({transform: 'translateY(25%)', opacity: 0}),
79-
style({transform: 'none', opacity: 1})
102+
103+
// Translation of the arrow as a whole. States are separated into two groups: ones with
104+
// animations and others that are immediate. Immediate states are asc, desc, peek, and active.
105+
// The other states define a specific animation (source-to-destination) and are determined as
106+
// a function of their prev user-perceived state and what the next state should be.
107+
trigger('arrowPosition', [
108+
// Hidden Above => Peek Center
109+
transition('* => desc-to-peek, * => desc-to-active',
110+
animate(SORT_ANIMATION_TRANSITION, keyframes([
111+
style({transform: 'translateY(-25%)'}),
112+
style({transform: 'translateY(0)'})
80113
]))),
81-
transition('asc => void', animate(SORT_ANIMATION_TRANSITION, keyframes([
82-
style({transform: 'none', opacity: 1}),
83-
style({transform: 'translateY(-25%)', opacity: 0})
114+
// Peek Center => Hidden Below
115+
transition('* => peek-to-desc, * => active-to-desc',
116+
animate(SORT_ANIMATION_TRANSITION, keyframes([
117+
style({transform: 'translateY(0)'}),
118+
style({transform: 'translateY(25%)'})
84119
]))),
85-
transition('void => desc', animate(SORT_ANIMATION_TRANSITION, keyframes([
86-
style({transform: 'translateY(-25%)', opacity: 0}),
87-
style({transform: 'none', opacity: 1})
120+
// Hidden Below => Peek Center
121+
transition('* => asc-to-peek, * => asc-to-active',
122+
animate(SORT_ANIMATION_TRANSITION, keyframes([
123+
style({transform: 'translateY(25%)'}),
124+
style({transform: 'translateY(0)'})
88125
]))),
89-
transition('desc => void', animate(SORT_ANIMATION_TRANSITION, keyframes([
90-
style({transform: 'none', opacity: 1}),
91-
style({transform: 'translateY(25%)', opacity: 0})
126+
// Peek Center => Hidden Above
127+
transition('* => peek-to-asc, * => active-to-asc',
128+
animate(SORT_ANIMATION_TRANSITION, keyframes([
129+
style({transform: 'translateY(0)'}),
130+
style({transform: 'translateY(-25%)'})
92131
]))),
132+
state('desc-to-peek, asc-to-peek, peek, desc-to-active, asc-to-active, active',
133+
style({transform: 'translateY(0)'})),
134+
state('peek-to-desc, active-to-desc, desc',
135+
style({transform: 'translateY(-25%)'})),
136+
state('peek-to-asc, active-to-asc, asc',
137+
style({transform: 'translateY(25%)'})),
93138
])
94139
]
95140
})
96141
export class MatSortHeader implements MatSortable {
97142
private _rerenderSubscription: Subscription;
98143

144+
/**
145+
* Flag set to true when the indicator should be displayed while the sort is not active. Used to
146+
* provide an affordance that the header is sortable by showing on focus and hover.
147+
* @docs-private
148+
*/
149+
set showIndicatorHint(showIndicatorHint: boolean) {
150+
this._showIndicatorHint = showIndicatorHint;
151+
152+
if (!this._isSorted()) {
153+
this._updateArrowDirection();
154+
if (this.showIndicatorHint) {
155+
this.viewState = {fromState: this._arrowDirection, toState: 'peek'};
156+
} else {
157+
this.viewState = {fromState: 'peek', toState: this._arrowDirection};
158+
}
159+
}
160+
}
161+
get showIndicatorHint(): boolean { return this._showIndicatorHint; }
162+
_showIndicatorHint: boolean = false;
163+
164+
/**
165+
* The view transition state of the arrow (translation/ opacity) - indicates its `from` and `to`
166+
* position through the animation. If animations are currently disabled, the fromState is removed
167+
* so that there is no animation displayed.
168+
* @docs-private
169+
*/
170+
set viewState(arrowPosition: ArrowViewStateTransition) {
171+
this._viewState = arrowPosition;
172+
173+
// If the animation for arrow position state (opacity/translation) should be disabled,
174+
// remove the fromState so that it jumps right to the toState.
175+
if (this._disableViewStateAnimation) {
176+
this._viewState = {toState: arrowPosition.toState};
177+
}
178+
}
179+
get viewState(): ArrowViewStateTransition {
180+
return this._viewState;
181+
}
182+
_viewState: ArrowViewStateTransition;
183+
184+
/** The direction the arrow should be facing according to the current state. */
185+
_arrowDirection: SortDirection = '';
186+
187+
/**
188+
* Whether the view state animation should show the transition between the `from` and `to` states.
189+
*/
190+
_disableViewStateAnimation = false;
191+
99192
/**
100193
* ID of this sort header. If used within the context of a CdkColumnDef, this will default to
101194
* the column's name.
@@ -123,6 +216,16 @@ export class MatSortHeader implements MatSortable {
123216
}
124217

125218
this._rerenderSubscription = merge(_sort.sortChange, _intl.changes).subscribe(() => {
219+
if (this._isSorted()) {
220+
this._updateArrowDirection();
221+
}
222+
223+
// If this header was recently active and now no longer sorted, animate away the arrow.
224+
if (!this._isSorted() && this.viewState.toState === 'active') {
225+
this._disableViewStateAnimation = false;
226+
this.viewState = {fromState: 'active', toState: this._arrowDirection};
227+
}
228+
126229
changeDetectorRef.markForCheck();
127230
});
128231
}
@@ -132,6 +235,10 @@ export class MatSortHeader implements MatSortable {
132235
this.id = this._cdkColumnDef.name;
133236
}
134237

238+
// Initialize the direction of the arrow and set the view state to be immediately that state.
239+
this._updateArrowDirection();
240+
this.viewState = {toState: this._arrowDirection};
241+
135242
this._sort.register(this);
136243
}
137244

@@ -140,9 +247,49 @@ export class MatSortHeader implements MatSortable {
140247
this._rerenderSubscription.unsubscribe();
141248
}
142249

250+
/** Triggers the sort on this sort header and removes the indicator hint. */
251+
_handleClick() {
252+
this._sort.sort(this);
253+
254+
// Do not show the animation if the header was already shown in the right position.
255+
if (this.viewState.toState === 'peek' || this.viewState.toState === 'active') {
256+
this._disableViewStateAnimation = true;
257+
}
258+
259+
// If the arrow is now sorted, animate the arrow into place. Otherwise, animate it away into
260+
// the direction it is facing.
261+
this.viewState = this._isSorted() ?
262+
{fromState: this._arrowDirection, toState: 'active'} :
263+
{fromState: 'active', toState: this._arrowDirection};
264+
265+
this._showIndicatorHint = false;
266+
}
267+
143268
/** Whether this MatSortHeader is currently sorted in either ascending or descending order. */
144269
_isSorted() {
145270
return this._sort.active == this.id &&
146271
(this._sort.direction === 'asc' || this._sort.direction === 'desc');
147272
}
273+
274+
/** Returns the animation state for the arrow direction (indicator and pointers). */
275+
_getArrowDirectionState() {
276+
return `${this._isSorted() ? 'active-' : ''}${this._arrowDirection}`;
277+
}
278+
279+
/** Returns the arrow position state (opacity, translation). */
280+
_getArrowViewState() {
281+
const fromState = this.viewState.fromState;
282+
return (fromState ? `${fromState}-to-` : '') + this.viewState.toState;
283+
}
284+
285+
/**
286+
* Updates the direction the arrow should be pointing. If it is not sorted, the arrow should be
287+
* facing the start direction. Otherwise if it is sorted, the arrow should point in the currently
288+
* active sorted direction.
289+
*/
290+
_updateArrowDirection() {
291+
this._arrowDirection = this._isSorted() ?
292+
this._sort.direction :
293+
(this.start || this._sort.start);
294+
}
148295
}

0 commit comments

Comments
 (0)