Skip to content

Commit 775c7ce

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. Fixes #10242.
1 parent 00959ec commit 775c7ce

File tree

6 files changed

+118
-442
lines changed

6 files changed

+118
-442
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: 71 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,7 @@
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;
13-
145
.mat-sort-header-container {
156
display: flex;
167
cursor: pointer;
@@ -51,93 +42,96 @@ $header-arrow-hint-opacity: 0.38;
5142
flex-direction: row-reverse;
5243
}
5344

45+
@keyframes _mat-sort-header-recently-cleared-ascending {
46+
from {
47+
transform: translateY(0);
48+
opacity: 1;
49+
}
50+
51+
to {
52+
transform: translateY(-25%);
53+
opacity: 0;
54+
}
55+
}
56+
57+
@keyframes _mat-sort-header-recently-cleared-descending {
58+
from {
59+
transform: translateY(0) rotate(180deg);
60+
opacity: 1;
61+
}
62+
63+
to {
64+
transform: translateY(25%) rotate(180deg);
65+
opacity: 0;
66+
}
67+
}
68+
5469
.mat-sort-header-arrow {
55-
height: $header-arrow-container-size;
56-
width: $header-arrow-container-size;
57-
min-width: $header-arrow-container-size;
70+
$timing: 225ms cubic-bezier(0.4, 0, 0.2, 1);
71+
height: 12px;
72+
width: 12px;
5873
position: relative;
59-
display: flex;
74+
transition: transform $timing, opacity $timing;
75+
opacity: 0;
76+
overflow: visible;
6077

6178
@include token-utils.use-tokens(tokens-mat-sort.$prefix, tokens-mat-sort.get-token-slots()) {
6279
@include token-utils.create-token-slot(color, arrow-color);
6380
}
6481

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;
82+
.mat-sort-header:hover & {
83+
opacity: 0.54;
7284
}
7385

74-
.mat-sort-header-position-before &,
75-
[dir='rtl'] & {
76-
margin: 0 $header-arrow-margin 0 0;
86+
.mat-sort-header .mat-sort-header-sorted & {
87+
opacity: 1;
7788
}
78-
}
7989

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;
87-
88-
@include cdk.high-contrast {
89-
width: 0;
90-
border-left: solid $header-arrow-thickness;
90+
.mat-sort-header-descending & {
91+
transform: rotate(180deg);
9192
}
92-
}
9393

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-
}
94+
.mat-sort-header-recently-cleared-ascending & {
95+
transform: translateY(-25%);
96+
}
10397

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);
98+
.mat-sort-header-recently-cleared-ascending & {
99+
transition: none; // Without this the animation looks glitchy on Safari.
100+
animation: _mat-sort-header-recently-cleared-ascending $timing forwards;
101+
}
110102

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;
103+
.mat-sort-header-recently-cleared-descending & {
104+
transition: none; // Without this the animation looks glitchy on Safari.
105+
animation: _mat-sort-header-recently-cleared-descending $timing forwards;
116106
}
117-
}
118107

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;
108+
// Set the durations to 0, but keep the actual animation, since we still want it to play.
109+
.mat-sort-header-animations-disabled & {
110+
transition-duration: 0ms;
111+
animation-duration: 0ms;
112+
}
126113

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;
114+
svg {
115+
// Even though this is 24x24, the actual `path` inside ends up being 12x12.
116+
width: 24px;
117+
height: 24px;
118+
fill: currentColor;
119+
position: absolute;
120+
top: 50%;
121+
left: 50%;
122+
margin: -12px 0 0 -12px;
123+
124+
// Without this transform the element twitches at the end of the transition on Safari.
125+
transform: translateZ(0);
132126
}
133-
}
134127

135-
.mat-sort-header-pointer-left {
136-
transform-origin: right;
137-
left: 0;
138-
}
128+
&,
129+
[dir='rtl'] .mat-sort-header-position-before & {
130+
margin: 0 0 0 6px;
131+
}
139132

140-
.mat-sort-header-pointer-right {
141-
transform-origin: left;
142-
right: 0;
133+
.mat-sort-header-position-before &,
134+
[dir='rtl'] & {
135+
margin: 0 6px 0 0;
136+
}
143137
}

0 commit comments

Comments
 (0)