Skip to content

Commit a2ee405

Browse files
authored
refactor(module:segmented): use native animation (#9619)
1 parent fcd0d55 commit a2ee405

7 files changed

Lines changed: 202 additions & 171 deletions

File tree

components/core/animation/public-api.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,5 @@ export * from './collapse';
88
export * from './move';
99
export * from './notification';
1010
export * from './slide';
11-
export * from './thumb';
1211
export * from './zoom';
1312
export * from './no-animation';

components/core/animation/thumb.ts

Lines changed: 0 additions & 47 deletions
This file was deleted.

components/segmented/segmented-item.component.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import { DOWN_ARROW, LEFT_ARROW, RIGHT_ARROW, UP_ARROW } from '@angular/cdk/keycodes';
77
import { NgTemplateOutlet } from '@angular/common';
88
import {
9-
ANIMATION_MODULE_TYPE,
109
booleanAttribute,
1110
ChangeDetectionStrategy,
1211
Component,
@@ -63,7 +62,7 @@ import { NzSegmentedService } from './segmented.service';
6362
`,
6463
host: {
6564
class: 'ant-segmented-item',
66-
'[class.ant-segmented-item-selected]': 'isChecked()',
65+
'[class.ant-segmented-item-selected]': '!showThumb() && isChecked()',
6766
'[class.ant-segmented-item-disabled]': 'finalDisabled()',
6867
'(click)': 'handleClick()',
6968
'(keydown)': 'handleKeydown($event)'
@@ -72,7 +71,6 @@ import { NzSegmentedService } from './segmented.service';
7271
encapsulation: ViewEncapsulation.None
7372
})
7473
export class NzSegmentedItemComponent implements OnInit {
75-
private readonly animationType = inject(ANIMATION_MODULE_TYPE, { optional: true });
7674
private readonly service = inject(NzSegmentedService);
7775
private readonly elementRef = inject(ElementRef);
7876
private readonly destroyRef = inject(DestroyRef);
@@ -87,6 +85,7 @@ export class NzSegmentedItemComponent implements OnInit {
8785
.rootNodes.some(node => node.textContent.trim().length > 0)
8886
);
8987

88+
protected readonly showThumb = this.service.showThumb;
9089
protected readonly name = this.service.name.asReadonly();
9190
protected readonly isChecked = signal(false);
9291
protected readonly parentDisabled = toSignal(this.service.disabled$, { initialValue: false });
@@ -102,11 +101,11 @@ export class NzSegmentedItemComponent implements OnInit {
102101
}
103102
}),
104103
switchMap(value => {
105-
if (this.animationType === 'NoopAnimations') {
104+
if (!this.service.animationEnabled()) {
106105
return of(value);
107106
}
108-
return this.service.animationDone$.pipe(
109-
filter(event => event.toState === 'to' || event.toState === 'toVertical'),
107+
return this.service.animating$.pipe(
108+
filter(animating => !animating), // done
110109
take(1),
111110
map(() => value)
112111
);

components/segmented/segmented.component.ts

Lines changed: 94 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,12 @@
33
* found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
44
*/
55

6-
import { AnimationEvent } from '@angular/animations';
76
import { Directionality } from '@angular/cdk/bidi';
87
import { DOWN_ARROW, LEFT_ARROW, RIGHT_ARROW, UP_ARROW } from '@angular/cdk/keycodes';
98
import {
109
afterNextRender,
1110
booleanAttribute,
1211
ChangeDetectionStrategy,
13-
ChangeDetectorRef,
1412
Component,
1513
computed,
1614
contentChildren,
@@ -22,18 +20,20 @@ import {
2220
Input,
2321
OnChanges,
2422
Output,
23+
signal,
2524
SimpleChanges,
2625
viewChildren,
2726
ViewEncapsulation
2827
} from '@angular/core';
2928
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
3029
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
31-
import { bufferCount, filter } from 'rxjs/operators';
30+
import { filter } from 'rxjs/operators';
3231

33-
import { ThumbAnimationProps, thumbMotion } from 'ng-zorro-antd/core/animation';
34-
import { NzConfigKey, NzConfigService, WithConfig } from 'ng-zorro-antd/core/config';
32+
import { withAnimationCheck } from 'ng-zorro-antd/core/animation';
33+
import { NzConfigKey, WithConfig } from 'ng-zorro-antd/core/config';
3534
import { NzOutletModule } from 'ng-zorro-antd/core/outlet';
36-
import { NzSizeLDSType, OnChangeType, OnTouchedType } from 'ng-zorro-antd/core/types';
35+
import { requestAnimationFrame } from 'ng-zorro-antd/core/polyfill';
36+
import { NgStyleInterface, NzSizeLDSType, OnChangeType, OnTouchedType } from 'ng-zorro-antd/core/types';
3737
import { NzIconModule } from 'ng-zorro-antd/icon';
3838

3939
import { NzSegmentedItemComponent } from './segmented-item.component';
@@ -50,11 +50,12 @@ const NZ_CONFIG_MODULE_NAME: NzConfigKey = 'segmented';
5050
template: `
5151
<!-- thumb motion div -->
5252
<div class="ant-segmented-group">
53-
@if (animationState) {
53+
@if (showThumb()) {
5454
<div
55-
class="ant-segmented-thumb ant-segmented-thumb-motion"
56-
[@thumbMotion]="animationState"
57-
(@thumbMotion.done)="handleThumbAnimationDone($event)"
55+
class="ant-segmented-thumb"
56+
[style]="thumbStyle()"
57+
[animate.enter]="thumbAnimationEnter()"
58+
(transitionend)="handleTransitionEnd($event)"
5859
></div>
5960
}
6061
@@ -89,14 +90,11 @@ const NZ_CONFIG_MODULE_NAME: NzConfigKey = 'segmented';
8990
multi: true
9091
}
9192
],
92-
animations: [thumbMotion],
9393
imports: [NzIconModule, NzOutletModule, NzSegmentedItemComponent]
9494
})
9595
export class NzSegmentedComponent implements OnChanges, ControlValueAccessor {
9696
readonly _nzModuleName: NzConfigKey = NZ_CONFIG_MODULE_NAME;
9797

98-
public readonly nzConfigService = inject(NzConfigService);
99-
private readonly cdr = inject(ChangeDetectorRef);
10098
private readonly service = inject(NzSegmentedService);
10199
private readonly injector = inject(Injector);
102100
protected readonly dir = inject(Directionality).valueSignal;
@@ -122,10 +120,9 @@ export class NzSegmentedComponent implements OnChanges, ControlValueAccessor {
122120
private isDisabledFirstChange = true;
123121

124122
protected value?: number | string;
125-
protected animationState: null | { value: string; params: ThumbAnimationProps } = {
126-
value: 'to',
127-
params: thumbAnimationParamsOf()
128-
};
123+
protected readonly thumbStyle = signal<NgStyleInterface | null>(null);
124+
protected readonly thumbAnimationEnter = withAnimationCheck(() => 'ant-segmented-thumb-motion-appear-active');
125+
protected readonly showThumb = this.service.showThumb;
129126
protected normalizedOptions: NzSegmentedOption[] = [];
130127
protected onChange: OnChangeType = () => {};
131128
protected onTouched: OnTouchedType = () => {};
@@ -135,37 +132,26 @@ export class NzSegmentedComponent implements OnChanges, ControlValueAccessor {
135132
this.value = value;
136133
});
137134

135+
this.service.activated$.pipe(takeUntilDestroyed()).subscribe(element => {
136+
this.thumbStyle.update(prevStyle => {
137+
const nextStyle = this.calcThumbStyle(element);
138+
139+
if (prevStyle && nextStyle) {
140+
// Trigger animation to end position
141+
requestAnimationFrame(() => {
142+
this.thumbStyle.set(this.getThumbStyle(nextStyle));
143+
});
144+
} else if (nextStyle) {
145+
return this.getThumbStyle(nextStyle);
146+
}
147+
return prevStyle;
148+
});
149+
});
150+
138151
this.service.change$.pipe(takeUntilDestroyed()).subscribe(value => {
139152
this.nzValueChange.emit(value);
140153
this.onChange(value);
141-
});
142-
143-
this.service.activated$.pipe(bufferCount(2, 1), takeUntilDestroyed()).subscribe(elements => {
144-
if (this.nzVertical) {
145-
this.animationState = {
146-
value: 'fromVertical',
147-
params: thumbAnimationParamsOf(elements[0], true)
148-
};
149-
this.cdr.detectChanges();
150-
151-
this.animationState = {
152-
value: 'toVertical',
153-
params: thumbAnimationParamsOf(elements[1], true)
154-
};
155-
this.cdr.detectChanges();
156-
} else {
157-
this.animationState = {
158-
value: 'from',
159-
params: thumbAnimationParamsOf(elements[0])
160-
};
161-
this.cdr.detectChanges();
162-
163-
this.animationState = {
164-
value: 'to',
165-
params: thumbAnimationParamsOf(elements[1])
166-
};
167-
this.cdr.detectChanges();
168-
}
154+
this.service.animating$.next(true);
169155
});
170156

171157
this.service.keydown$
@@ -209,13 +195,6 @@ export class NzSegmentedComponent implements OnChanges, ControlValueAccessor {
209195
}
210196
}
211197

212-
handleThumbAnimationDone(event: AnimationEvent): void {
213-
if (event.toState === 'to' || event.toState === 'toVertical') {
214-
this.animationState = null;
215-
}
216-
this.service.animationDone$.next(event);
217-
}
218-
219198
private onOffset(offset: number): void {
220199
const items = this.renderedItemCmps();
221200
const total = items.length;
@@ -272,19 +251,71 @@ export class NzSegmentedComponent implements OnChanges, ControlValueAccessor {
272251
this.nzDisabled = (this.isDisabledFirstChange && this.nzDisabled) || disabled;
273252
this.isDisabledFirstChange = false;
274253
}
275-
}
276254

277-
function thumbAnimationParamsOf(element?: HTMLElement, vertical: boolean = false): ThumbAnimationProps {
278-
if (vertical) {
255+
/************* Thumb Animation *************/
256+
257+
private calcThumbStyle(element?: HTMLElement): NgStyleInterface | null {
258+
if (!element || !element.offsetParent) {
259+
return null;
260+
}
261+
262+
const parentElement = element.parentElement;
263+
if (!parentElement) {
264+
return null;
265+
}
266+
267+
const style: NgStyleInterface = {
268+
left: element.offsetLeft,
269+
right: parentElement.clientWidth - element.clientWidth - element.offsetLeft,
270+
width: element.clientWidth,
271+
top: element.offsetTop,
272+
bottom: parentElement.clientHeight - element.clientHeight - element.offsetTop,
273+
height: element.clientHeight
274+
};
275+
276+
if (this.nzVertical) {
277+
return {
278+
left: 0,
279+
right: 0,
280+
width: 0,
281+
top: style.top,
282+
bottom: style.bottom,
283+
height: style.height
284+
};
285+
}
286+
287+
return {
288+
left: style.left,
289+
right: style.right,
290+
width: style.width,
291+
top: 0,
292+
bottom: 0,
293+
height: 0
294+
};
295+
}
296+
297+
private getThumbStyle(targetStyle: NgStyleInterface): NgStyleInterface {
298+
if (this.nzVertical) {
299+
return {
300+
transform: `translateY(${targetStyle.top}px)`,
301+
width: '100%',
302+
height: `${targetStyle.height}px`
303+
};
304+
}
305+
306+
const isRtl = this.dir() === 'rtl';
307+
const transformValue = isRtl ? -targetStyle.right : targetStyle.left;
308+
279309
return {
280-
transform: element?.offsetTop ?? 0,
281-
width: element?.clientWidth ?? 0,
282-
height: element?.clientHeight ?? 0,
283-
vertical
310+
transform: `translateX(${transformValue}px)`,
311+
width: `${targetStyle.width}px`,
312+
height: '100%'
284313
};
285314
}
286-
return {
287-
transform: element?.offsetLeft ?? 0,
288-
width: element?.clientWidth ?? 0
289-
};
315+
316+
protected handleTransitionEnd(event: TransitionEvent): void {
317+
if (event.propertyName === 'transform') {
318+
this.service.animating$.next(false);
319+
}
320+
}
290321
}

components/segmented/segmented.service.ts

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,41 @@
33
* found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
44
*/
55

6-
import { AnimationEvent } from '@angular/animations';
76
import { _IdGenerator } from '@angular/cdk/a11y';
8-
import { inject, Injectable, OnDestroy, signal } from '@angular/core';
9-
import { ReplaySubject, Subject } from 'rxjs';
7+
import { computed, DestroyRef, inject, Injectable, signal } from '@angular/core';
8+
import { toSignal } from '@angular/core/rxjs-interop';
9+
import { BehaviorSubject, ReplaySubject, Subject } from 'rxjs';
10+
11+
import { isAnimationEnabled } from 'ng-zorro-antd/core/animation';
1012

1113
@Injectable()
12-
export class NzSegmentedService implements OnDestroy {
14+
export class NzSegmentedService {
1315
private readonly defaultName = inject(_IdGenerator).getId('segmented_');
1416

1517
readonly name = signal<string | null>(this.defaultName);
1618
readonly selected$ = new ReplaySubject<string | number>(1);
1719
readonly activated$ = new ReplaySubject<HTMLElement>(1);
1820
readonly change$ = new Subject<string | number>();
1921
readonly disabled$ = new ReplaySubject<boolean>(1);
20-
readonly animationDone$ = new Subject<AnimationEvent>();
22+
readonly animating$ = new BehaviorSubject<boolean>(false);
2123
readonly keydown$ = new Subject<KeyboardEvent>();
2224

23-
setName(name: string | null | undefined): void {
24-
this.name.set(typeof name === 'undefined' ? this.defaultName : name);
25+
private readonly _animating = toSignal(this.animating$, { initialValue: false });
26+
readonly animationEnabled = isAnimationEnabled(() => true);
27+
readonly showThumb = computed(() => this.animationEnabled() && this._animating());
28+
29+
constructor() {
30+
inject(DestroyRef).onDestroy(() => {
31+
this.selected$.complete();
32+
this.activated$.complete();
33+
this.change$.complete();
34+
this.disabled$.complete();
35+
this.animating$.complete();
36+
this.keydown$.complete();
37+
});
2538
}
2639

27-
ngOnDestroy(): void {
28-
this.selected$.complete();
29-
this.activated$.complete();
30-
this.change$.complete();
31-
this.disabled$.complete();
32-
this.animationDone$.complete();
33-
this.keydown$.complete();
40+
setName(name: string | null | undefined): void {
41+
this.name.set(typeof name === 'undefined' ? this.defaultName : name);
3442
}
3543
}

0 commit comments

Comments
 (0)