Skip to content

Commit

Permalink
fix(material/sort): simplify animations (#30057)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
crisbeto authored Nov 27, 2024
1 parent 2c4358e commit a08eeeb
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 442 deletions.
2 changes: 2 additions & 0 deletions src/material/sort/sort-animations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const SORT_ANIMATION_TRANSITION =
/**
* Animations used by MatSort.
* @docs-private
* @deprecated No longer being used, to be removed.
* @breaking-change 21.0.0
*/
export const matSortAnimations: {
readonly indicator: AnimationTriggerMetadata;
Expand Down
21 changes: 9 additions & 12 deletions src/material/sort/sort-header.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
<div class="mat-sort-header-container mat-focus-indicator"
[class.mat-sort-header-sorted]="_isSorted()"
[class.mat-sort-header-position-before]="arrowPosition === 'before'"
[class.mat-sort-header-descending]="this._sort.direction === 'desc'"
[class.mat-sort-header-ascending]="this._sort.direction === 'asc'"
[class.mat-sort-header-recently-cleared-ascending]="_recentlyCleared() === 'asc'"
[class.mat-sort-header-recently-cleared-descending]="_recentlyCleared() === 'desc'"
[class.mat-sort-header-animations-disabled]="_animationModule === 'NoopAnimations'"
[attr.tabindex]="_isDisabled() ? null : 0"
[attr.role]="_isDisabled() ? null : 'button'">

Expand All @@ -26,18 +31,10 @@

<!-- Disable animations while a current animation is running -->
@if (_renderArrow()) {
<div class="mat-sort-header-arrow"
[@arrowOpacity]="_getArrowViewState()"
[@arrowPosition]="_getArrowViewState()"
[@allowChildren]="_getArrowDirectionState()"
(@arrowPosition.start)="_disableViewStateAnimation = true"
(@arrowPosition.done)="_disableViewStateAnimation = false">
<div class="mat-sort-header-stem"></div>
<div class="mat-sort-header-indicator" [@indicator]="_getArrowDirectionState()">
<div class="mat-sort-header-pointer-left" [@leftPointer]="_getArrowDirectionState()"></div>
<div class="mat-sort-header-pointer-right" [@rightPointer]="_getArrowDirectionState()"></div>
<div class="mat-sort-header-pointer-middle"></div>
</div>
<div class="mat-sort-header-arrow">
<svg viewBox="0 -960 960 960" focusable="false" aria-hidden="true">
<path d="M440-240v-368L296-464l-56-56 240-240 240 240-56 56-144-144v368h-80Z"/>
</svg>
</div>
}
</div>
148 changes: 71 additions & 77 deletions src/material/sort/sort-header.scss
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
@use '@angular/cdk';

@use '../core/tokens/m2/mat/sort' as tokens-mat-sort;
@use '../core/tokens/token-utils';
@use '../core/focus-indicators/private';

$header-arrow-margin: 6px;
$header-arrow-container-size: 12px;
$header-arrow-stem-size: 10px;
$header-arrow-pointer-length: 6px;
$header-arrow-thickness: 2px;
$header-arrow-hint-opacity: 0.38;

.mat-sort-header-container {
display: flex;
cursor: pointer;
Expand Down Expand Up @@ -51,93 +42,96 @@ $header-arrow-hint-opacity: 0.38;
flex-direction: row-reverse;
}

@keyframes _mat-sort-header-recently-cleared-ascending {
from {
transform: translateY(0);
opacity: 1;
}

to {
transform: translateY(-25%);
opacity: 0;
}
}

@keyframes _mat-sort-header-recently-cleared-descending {
from {
transform: translateY(0) rotate(180deg);
opacity: 1;
}

to {
transform: translateY(25%) rotate(180deg);
opacity: 0;
}
}

.mat-sort-header-arrow {
height: $header-arrow-container-size;
width: $header-arrow-container-size;
min-width: $header-arrow-container-size;
$timing: 225ms cubic-bezier(0.4, 0, 0.2, 1);
height: 12px;
width: 12px;
position: relative;
display: flex;
transition: transform $timing, opacity $timing;
opacity: 0;
overflow: visible;

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

// Start off at 0 since the arrow may become visible while parent are animating.
// This will be overwritten when the arrow animations kick in. See #11819.
opacity: 0;

&,
[dir='rtl'] .mat-sort-header-position-before & {
margin: 0 0 0 $header-arrow-margin;
.mat-sort-header:hover & {
opacity: 0.54;
}

.mat-sort-header-position-before &,
[dir='rtl'] & {
margin: 0 $header-arrow-margin 0 0;
.mat-sort-header .mat-sort-header-sorted & {
opacity: 1;
}
}

.mat-sort-header-stem {
background: currentColor;
height: $header-arrow-stem-size;
width: $header-arrow-thickness;
margin: auto;
display: flex;
align-items: center;

@include cdk.high-contrast {
width: 0;
border-left: solid $header-arrow-thickness;
.mat-sort-header-descending & {
transform: rotate(180deg);
}
}

.mat-sort-header-indicator {
width: 100%;
height: $header-arrow-thickness;
display: flex;
align-items: center;
position: absolute;
top: 0;
left: 0;
}
.mat-sort-header-recently-cleared-ascending & {
transform: translateY(-25%);
}

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

@include cdk.high-contrast {
width: 0;
height: 0;
border-top: solid $header-arrow-thickness;
border-left: solid $header-arrow-thickness;
.mat-sort-header-recently-cleared-descending & {
transition: none; // Without this the animation looks glitchy on Safari.
animation: _mat-sort-header-recently-cleared-descending $timing forwards;
}
}

.mat-sort-header-pointer-left,
.mat-sort-header-pointer-right {
background: currentColor;
width: $header-arrow-pointer-length;
height: $header-arrow-thickness;
position: absolute;
top: 0;
// Set the durations to 0, but keep the actual animation, since we still want it to play.
.mat-sort-header-animations-disabled & {
transition-duration: 0ms;
animation-duration: 0ms;
}

@include cdk.high-contrast {
width: 0;
height: 0;
border-left: solid $header-arrow-pointer-length;
border-top: solid $header-arrow-thickness;
svg {
// Even though this is 24x24, the actual `path` inside ends up being 12x12.
width: 24px;
height: 24px;
fill: currentColor;
position: absolute;
top: 50%;
left: 50%;
margin: -12px 0 0 -12px;

// Without this transform the element twitches at the end of the transition on Safari.
transform: translateZ(0);
}
}

.mat-sort-header-pointer-left {
transform-origin: right;
left: 0;
}
&,
[dir='rtl'] .mat-sort-header-position-before & {
margin: 0 0 0 6px;
}

.mat-sort-header-pointer-right {
transform-origin: left;
right: 0;
.mat-sort-header-position-before &,
[dir='rtl'] & {
margin: 0 6px 0 0;
}
}
Loading

0 comments on commit a08eeeb

Please sign in to comment.