Skip to content

Commit bcb9d88

Browse files
fix(components/indicators)!: replace @angular/animations in tokens component with CSS transitions (#4303)
BREAKING CHANGE `SkyTokensComponent` has replaced `@angular/animations` with CSS transitions. Tests that interact with tokens may need to add `provideNoopSkyAnimations()` from `@skyux/core` to their `TestBed` providers to disable SKY UX CSS transitions during tests. ``` import { provideNoopSkyAnimations } from '@skyux/core'; TestBed.configureTestingModule({ providers: [provideNoopSkyAnimations()], }); ```
1 parent 90b183b commit bcb9d88

File tree

10 files changed

+107
-49
lines changed

10 files changed

+107
-49
lines changed

libs/components/indicators/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
},
1717
"homepage": "https://github.com/blackbaud/skyux#readme",
1818
"peerDependencies": {
19-
"@angular/animations": "^21.2.0",
2019
"@angular/cdk": "^21.2.0",
2120
"@angular/common": "^21.2.0",
2221
"@angular/core": "^21.2.0",
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { NgModule } from '@angular/core';
2-
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
32

43
import { SkyTokensModule } from '../tokens.module';
54

@@ -8,7 +7,7 @@ import { SkyTokensTestComponent } from './tokens.component.fixture';
87

98
@NgModule({
109
declarations: [SkyTokenTestComponent, SkyTokensTestComponent],
11-
imports: [NoopAnimationsModule, SkyTokensModule],
10+
imports: [SkyTokensModule],
1211
exports: [SkyTokenTestComponent, SkyTokensTestComponent],
1312
})
1413
export class SkyTokensFixturesModule {}

libs/components/indicators/src/lib/modules/tokens/token.component.scss

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22
@use 'libs/components/theme/src/lib/styles/variables' as *;
33
@use 'libs/components/theme/src/lib/styles/compat-tokens-mixins' as compatMixins;
44

5+
:host {
6+
interpolate-size: allow-keywords;
7+
flex: 0 0 auto;
8+
display: inline-flex;
9+
padding: var(--sky-token-gutter, 0);
10+
}
11+
512
@include compatMixins.sky-default-overrides('.sky-token') {
613
--sky-override-token-active-background-color: #{darken(
714
$sky-background-color-info-light,

libs/components/indicators/src/lib/modules/tokens/token.component.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ComponentFixture, TestBed } from '@angular/core/testing';
22
import { SkyAppTestUtility, expectAsync } from '@skyux-sdk/testing';
3-
import { SkyIdService } from '@skyux/core';
3+
import { SkyIdService, provideNoopSkyAnimations } from '@skyux/core';
44

55
import { SkyTokenComponent } from '../tokens/token.component';
66
import { SkyTokensModule } from '../tokens/tokens.module';
@@ -11,6 +11,7 @@ describe('Token component', () => {
1111
beforeEach(() => {
1212
TestBed.configureTestingModule({
1313
imports: [SkyTokensModule],
14+
providers: [provideNoopSkyAnimations()],
1415
});
1516

1617
// Mock the ID service.

libs/components/indicators/src/lib/modules/tokens/tokens.component.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<div
22
class="sky-tokens"
3-
[@blockAnimationOnLoad]
3+
[class.sky-tokens-animation-ready]="animationReady()"
4+
[class.sky-tokens-animation-enabled]="trackWith"
45
[attr.role]="tokens.length ? 'grid' : null"
56
>
67
<!--
@@ -9,13 +10,12 @@
910
-->
1011
@for (token of tokens; track trackTokenFn(i, token); let i = $index) {
1112
<sky-token
12-
[@.disabled]="!trackWith"
13-
[@dismiss]
13+
animate.enter="sky-token-enter"
14+
animate.leave="sky-token-leave"
1415
[disabled]="disabled"
1516
[dismissible]="dismissible"
1617
[focusable]="focusable"
1718
[role]="'row'"
18-
(@dismiss.done)="animationDone()"
1919
(dismiss)="removeToken(token)"
2020
(click)="onTokenClick(token)"
2121
(keydown)="onTokenKeyDown($event)"

libs/components/indicators/src/lib/modules/tokens/tokens.component.scss

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,57 @@
22

33
$sky-tokens-gutter-size: 2px;
44

5+
@keyframes sky-token-enter-animation {
6+
from {
7+
opacity: 0;
8+
width: 0;
9+
padding: 0;
10+
}
11+
12+
to {
13+
opacity: 1;
14+
width: auto;
15+
}
16+
}
17+
18+
@keyframes sky-token-leave-animation {
19+
from {
20+
opacity: 1;
21+
width: auto;
22+
}
23+
24+
to {
25+
opacity: 0;
26+
width: 0;
27+
padding: 0;
28+
}
29+
}
30+
531
.sky-tokens {
32+
--sky-token-gutter: #{$sky-tokens-gutter-size};
33+
634
display: flex;
735
flex-wrap: wrap;
836
align-items: baseline;
937
margin: -1 * $sky-tokens-gutter-size;
1038

11-
.sky-tokens-content,
12-
::ng-deep sky-token {
13-
flex: 0 0 auto;
14-
display: inline-flex;
15-
padding: $sky-tokens-gutter-size;
39+
&.sky-tokens-animation-enabled sky-token.sky-token-leave {
40+
animation: sky-token-leave-animation var(--sky-transition-time-short)
41+
ease-in forwards;
42+
overflow: hidden;
43+
}
44+
45+
&.sky-tokens-animation-ready.sky-tokens-animation-enabled
46+
sky-token.sky-token-enter {
47+
animation: sky-token-enter-animation var(--sky-transition-time-short)
48+
ease-in;
49+
overflow: hidden;
1650
}
1751

1852
.sky-tokens-content {
53+
flex: 0 0 auto;
54+
display: inline-flex;
55+
padding: $sky-tokens-gutter-size;
1956
flex-grow: 1;
2057
flex-basis: 0px;
2158

libs/components/indicators/src/lib/modules/tokens/tokens.component.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
tick,
66
} from '@angular/core/testing';
77
import { SkyAppTestUtility, expect, expectAsync } from '@skyux-sdk/testing';
8-
import { SkyLiveAnnouncerService } from '@skyux/core';
8+
import { SkyLiveAnnouncerService, provideNoopSkyAnimations } from '@skyux/core';
99

1010
import { Subject } from 'rxjs';
1111

@@ -84,6 +84,7 @@ describe('Tokens component', () => {
8484
beforeEach(() => {
8585
TestBed.configureTestingModule({
8686
imports: [SkyTokensFixturesModule],
87+
providers: [provideNoopSkyAnimations()],
8788
});
8889

8990
fixture = TestBed.createComponent(SkyTokensTestComponent);

libs/components/indicators/src/lib/modules/tokens/tokens.component.ts

Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1-
import { animate, style, transition, trigger } from '@angular/animations';
21
import {
32
ChangeDetectionStrategy,
43
ChangeDetectorRef,
54
Component,
65
EventEmitter,
6+
Injector,
77
Input,
88
OnDestroy,
99
Output,
1010
QueryList,
1111
TrackByFunction,
1212
ViewChildren,
13+
afterNextRender,
1314
inject,
15+
signal,
1416
} from '@angular/core';
1517

1618
import { Subject, Subscription } from 'rxjs';
@@ -34,33 +36,6 @@ const DISPLAY_WITH_DEFAULT = 'name';
3436
templateUrl: './tokens.component.html',
3537
styleUrls: ['./tokens.component.scss'],
3638
changeDetection: ChangeDetectionStrategy.OnPush,
37-
animations: [
38-
trigger('blockAnimationOnLoad', [transition(':enter', [])]),
39-
trigger('dismiss', [
40-
transition(':enter', [
41-
style({
42-
opacity: 0,
43-
width: 0,
44-
}),
45-
animate(
46-
'150ms ease-in',
47-
style({
48-
opacity: 1,
49-
width: '*',
50-
}),
51-
),
52-
]),
53-
transition(':leave', [
54-
animate(
55-
'150ms ease-in',
56-
style({
57-
opacity: 0,
58-
width: 0,
59-
}),
60-
),
61-
]),
62-
]),
63-
],
6439
standalone: false,
6540
})
6641
export class SkyTokensComponent implements OnDestroy {
@@ -138,6 +113,8 @@ export class SkyTokensComponent implements OnDestroy {
138113
// get accessor when set to `undefined`. Emitting `value` instead of `this.#_tokensOrDefault`
139114
// preserves that behavior.
140115
this.tokensChange.emit(value);
116+
117+
this.#queueTokensRenderedEmit();
141118
}
142119

143120
public get tokens(): SkyToken[] {
@@ -223,13 +200,31 @@ export class SkyTokensComponent implements OnDestroy {
223200

224201
#messageStreamSub: Subscription | undefined;
225202
#ngUnsubscribe = new Subject<void>();
203+
#tokensRenderedQueued = false;
226204

227-
#changeDetector = inject(ChangeDetectorRef);
205+
readonly #changeDetector = inject(ChangeDetectorRef);
206+
readonly #injector = inject(Injector);
228207

229208
#_activeIndex = 0;
230209
#_messageStream = new Subject<SkyTokensMessage>();
231210

211+
/**
212+
* Tracks whether the component has completed its initial render.
213+
* Used to suppress enter animations on first load.
214+
*/
215+
protected readonly animationReady = signal(false);
216+
232217
constructor() {
218+
// Wait for Angular's animation scheduler to remove initial enter classes
219+
// before enabling enter animations for future token changes.
220+
afterNextRender({
221+
read: () => {
222+
requestAnimationFrame(() => {
223+
this.animationReady.set(true);
224+
});
225+
},
226+
});
227+
233228
this.#initMessageStream();
234229

235230
// Angular calls the trackBy function without applying the component instance's scope.
@@ -257,10 +252,6 @@ export class SkyTokensComponent implements OnDestroy {
257252
this.#notifyTokenSelected(token);
258253
}
259254

260-
public animationDone(): void {
261-
this.tokensRendered.emit();
262-
}
263-
264255
public onTokenKeyDown(event: KeyboardEvent): void {
265256
if (!this.disabled) {
266257
switch (event.key) {
@@ -369,4 +360,26 @@ export class SkyTokensComponent implements OnDestroy {
369360
token,
370361
});
371362
}
363+
364+
/**
365+
* Debounces the tokensRendered emit so that rapid token changes
366+
* (e.g. bulk additions or removals) result in a single event.
367+
*/
368+
#queueTokensRenderedEmit(): void {
369+
if (this.#tokensRenderedQueued) {
370+
return;
371+
}
372+
373+
this.#tokensRenderedQueued = true;
374+
375+
afterNextRender(
376+
() => {
377+
this.#tokensRenderedQueued = false;
378+
this.tokensRendered.emit();
379+
},
380+
{
381+
injector: this.#injector,
382+
},
383+
);
384+
}
372385
}
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import { NgModule } from '@angular/core';
2-
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
32
import { SkyTokensModule } from '@skyux/indicators';
43

54
import { TokensHarnessTestComponent } from './tokens-harness-test.component';
65

76
@NgModule({
8-
imports: [NoopAnimationsModule, SkyTokensModule],
7+
imports: [SkyTokensModule],
98
declarations: [TokensHarnessTestComponent],
109
})
1110
export class TokensHarnessTestModule {}

libs/components/indicators/testing/src/modules/tokens/tokens-harness.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { HarnessLoader } from '@angular/cdk/testing';
22
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
33
import { ComponentFixture, TestBed } from '@angular/core/testing';
4+
import { provideNoopSkyAnimations } from '@skyux/core';
45

56
import { TokensHarnessTestComponent } from './fixtures/tokens-harness-test.component';
67
import { TokensHarnessTestModule } from './fixtures/tokens-harness-test.module';
@@ -14,6 +15,7 @@ describe('Tokens harness', () => {
1415
}> {
1516
await TestBed.configureTestingModule({
1617
imports: [TokensHarnessTestModule],
18+
providers: [provideNoopSkyAnimations()],
1719
}).compileComponents();
1820

1921
const fixture = TestBed.createComponent(TokensHarnessTestComponent);

0 commit comments

Comments
 (0)