Skip to content

Commit 6fad88d

Browse files
committed
fix(material/sort): simplify animations
For a long time the sort header's animation was set up by rendering out 4 `div` elements and then arranging them to look like an arrow. This is somewhat complicated to maintain, difficult to customize, in some cases it leads to weird visual bugs and ends up triggering excessive change detections. On top of that, because it depends on `@angular/animations`, it is prone to memory leaks (see angular/angular#54149). These changes aim to simplify the component and make it more robust by using an `svg` icon and dealing with the animations. Fixes #9758. Fixes #9844. Fixes #10088. Fixes #15451. Fixes #19441.
1 parent 00959ec commit 6fad88d

File tree

6 files changed

+111
-438
lines changed

6 files changed

+111
-438
lines changed

src/material/sort/sort-animations.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ const SORT_ANIMATION_TRANSITION =
2424
/**
2525
* Animations used by MatSort.
2626
* @docs-private
27+
* @deprecated No longer being used, to be removed.
28+
* @breaking-change 21.0.0
2729
*/
2830
export const matSortAnimations: {
2931
readonly indicator: AnimationTriggerMetadata;

src/material/sort/sort-header.html

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111
<div class="mat-sort-header-container mat-focus-indicator"
1212
[class.mat-sort-header-sorted]="_isSorted()"
1313
[class.mat-sort-header-position-before]="arrowPosition === 'before'"
14+
[class.mat-sort-header-descending]="this._sort.direction === 'desc'"
15+
[class.mat-sort-header-ascending]="this._sort.direction === 'asc'"
16+
[class.mat-sort-header-recently-cleared-ascending]="_recentlyCleared() === 'asc'"
17+
[class.mat-sort-header-recently-cleared-descending]="_recentlyCleared() === 'desc'"
18+
[class.mat-sort-header-animations-disabled]="_animationModule === 'NoopAnimations'"
1419
[attr.tabindex]="_isDisabled() ? null : 0"
1520
[attr.role]="_isDisabled() ? null : 'button'">
1621

@@ -26,18 +31,10 @@
2631

2732
<!-- Disable animations while a current animation is running -->
2833
@if (_renderArrow()) {
29-
<div class="mat-sort-header-arrow"
30-
[@arrowOpacity]="_getArrowViewState()"
31-
[@arrowPosition]="_getArrowViewState()"
32-
[@allowChildren]="_getArrowDirectionState()"
33-
(@arrowPosition.start)="_disableViewStateAnimation = true"
34-
(@arrowPosition.done)="_disableViewStateAnimation = false">
35-
<div class="mat-sort-header-stem"></div>
36-
<div class="mat-sort-header-indicator" [@indicator]="_getArrowDirectionState()">
37-
<div class="mat-sort-header-pointer-left" [@leftPointer]="_getArrowDirectionState()"></div>
38-
<div class="mat-sort-header-pointer-right" [@rightPointer]="_getArrowDirectionState()"></div>
39-
<div class="mat-sort-header-pointer-middle"></div>
40-
</div>
34+
<div class="mat-sort-header-arrow">
35+
<svg viewBox="0 -960 960 960" focusable="false" aria-hidden="true">
36+
<path d="M440-240v-368L296-464l-56-56 240-240 240 240-56 56-144-144v368h-80Z"/>
37+
</svg>
4138
</div>
4239
}
4340
</div>

src/material/sort/sort-header.scss

Lines changed: 64 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
1-
@use '@angular/cdk';
2-
31
@use '../core/tokens/m2/mat/sort' as tokens-mat-sort;
42
@use '../core/tokens/token-utils';
53
@use '../core/focus-indicators/private';
64

7-
$header-arrow-margin: 6px;
8-
$header-arrow-container-size: 12px;
9-
$header-arrow-stem-size: 10px;
10-
$header-arrow-pointer-length: 6px;
11-
$header-arrow-thickness: 2px;
12-
$header-arrow-hint-opacity: 0.38;
5+
$header-arrow-margin: 4px;
6+
$header-arrow-container-size: 24px;
137

148
.mat-sort-header-container {
159
display: flex;
@@ -51,93 +45,90 @@ $header-arrow-hint-opacity: 0.38;
5145
flex-direction: row-reverse;
5246
}
5347

48+
@keyframes _mat-sort-header-recently-cleared-ascending {
49+
from {
50+
transform: translateY(0);
51+
opacity: 1;
52+
}
53+
54+
to {
55+
transform: translateY(-25%);
56+
opacity: 0;
57+
}
58+
}
59+
60+
@keyframes _mat-sort-header-recently-cleared-descending {
61+
from {
62+
transform: translateY(0) rotate(180deg);
63+
opacity: 1;
64+
}
65+
66+
to {
67+
transform: translateY(25%) rotate(180deg);
68+
opacity: 0;
69+
}
70+
}
71+
5472
.mat-sort-header-arrow {
73+
$timing: 225ms cubic-bezier(0.4, 0, 0.2, 1);
5574
height: $header-arrow-container-size;
5675
width: $header-arrow-container-size;
57-
min-width: $header-arrow-container-size;
5876
position: relative;
59-
display: flex;
77+
transition: transform $timing, opacity $timing;
78+
opacity: 0;
6079

6180
@include token-utils.use-tokens(tokens-mat-sort.$prefix, tokens-mat-sort.get-token-slots()) {
6281
@include token-utils.create-token-slot(color, arrow-color);
6382
}
6483

65-
// Start off at 0 since the arrow may become visible while parent are animating.
66-
// This will be overwritten when the arrow animations kick in. See #11819.
67-
opacity: 0;
68-
69-
&,
70-
[dir='rtl'] .mat-sort-header-position-before & {
71-
margin: 0 0 0 $header-arrow-margin;
84+
.mat-sort-header:hover & {
85+
opacity: 0.54;
7286
}
7387

74-
.mat-sort-header-position-before &,
75-
[dir='rtl'] & {
76-
margin: 0 $header-arrow-margin 0 0;
88+
.mat-sort-header .mat-sort-header-sorted & {
89+
opacity: 1;
7790
}
78-
}
7991

80-
.mat-sort-header-stem {
81-
background: currentColor;
82-
height: $header-arrow-stem-size;
83-
width: $header-arrow-thickness;
84-
margin: auto;
85-
display: flex;
86-
align-items: center;
92+
.mat-sort-header-descending & {
93+
transform: rotate(180deg);
94+
}
8795

88-
@include cdk.high-contrast {
89-
width: 0;
90-
border-left: solid $header-arrow-thickness;
96+
.mat-sort-header-recently-cleared-ascending & {
97+
transform: translateY(-25%);
9198
}
92-
}
9399

94-
.mat-sort-header-indicator {
95-
width: 100%;
96-
height: $header-arrow-thickness;
97-
display: flex;
98-
align-items: center;
99-
position: absolute;
100-
top: 0;
101-
left: 0;
102-
}
100+
.mat-sort-header-recently-cleared-ascending & {
101+
transition: none; // Without this the animation looks glitchy on Safari.
102+
animation: _mat-sort-header-recently-cleared-ascending $timing forwards;
103+
}
103104

104-
.mat-sort-header-pointer-middle {
105-
margin: auto;
106-
height: $header-arrow-thickness;
107-
width: $header-arrow-thickness;
108-
background: currentColor;
109-
transform: rotate(45deg);
105+
.mat-sort-header-recently-cleared-descending & {
106+
transition: none; // Without this the animation looks glitchy on Safari.
107+
animation: _mat-sort-header-recently-cleared-descending $timing forwards;
108+
}
110109

111-
@include cdk.high-contrast {
112-
width: 0;
113-
height: 0;
114-
border-top: solid $header-arrow-thickness;
115-
border-left: solid $header-arrow-thickness;
110+
// Set the durations to 0, but keep the actual animation, since we still want it to play.
111+
.mat-sort-header-animations-disabled & {
112+
transition-duration: 0ms;
113+
animation-duration: 0ms;
116114
}
117-
}
118115

119-
.mat-sort-header-pointer-left,
120-
.mat-sort-header-pointer-right {
121-
background: currentColor;
122-
width: $header-arrow-pointer-length;
123-
height: $header-arrow-thickness;
124-
position: absolute;
125-
top: 0;
116+
svg {
117+
width: 100%;
118+
height: 100%;
119+
fill: currentColor;
126120

127-
@include cdk.high-contrast {
128-
width: 0;
129-
height: 0;
130-
border-left: solid $header-arrow-pointer-length;
131-
border-top: solid $header-arrow-thickness;
121+
// Without this transform the element twitches at the end of the transition on Safari.
122+
transform: translateZ(0);
132123
}
133-
}
134124

135-
.mat-sort-header-pointer-left {
136-
transform-origin: right;
137-
left: 0;
138-
}
125+
&,
126+
[dir='rtl'] .mat-sort-header-position-before & {
127+
margin: 0 0 0 $header-arrow-margin;
128+
}
139129

140-
.mat-sort-header-pointer-right {
141-
transform-origin: left;
142-
right: 0;
130+
.mat-sort-header-position-before &,
131+
[dir='rtl'] & {
132+
margin: 0 $header-arrow-margin 0 0;
133+
}
143134
}

0 commit comments

Comments
 (0)