Skip to content

Commit 52322cd

Browse files
authored
feat(module:segmented): support nzVertical (#9359)
1 parent 573d092 commit 52322cd

9 files changed

Lines changed: 212 additions & 20 deletions

File tree

components/core/animation/thumb.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ import { animate, AnimationTriggerMetadata, state, style, transition, trigger }
88
import { AnimationCurves } from './animation-consts';
99

1010
/**
11-
* a move and resize transition in the horizontal direction
11+
* a move and resize transition in the horizontal or vertical direction
1212
*/
1313
export interface ThumbAnimationProps {
1414
transform: number;
1515
width: number;
16+
height?: number;
17+
vertical?: boolean;
1618
}
1719

1820
export const thumbMotion: AnimationTriggerMetadata = trigger('thumbMotion', [
@@ -24,5 +26,22 @@ export const thumbMotion: AnimationTriggerMetadata = trigger('thumbMotion', [
2426
params: { transform: 100, width: 0 }
2527
}),
2628

27-
transition('from => to', animate(`300ms ${AnimationCurves.EASE_IN_OUT}`))
29+
state(
30+
'fromVertical',
31+
style({ transform: 'translateY({{ transform }}px)', width: '{{ width }}px', height: '{{ height }}px' }),
32+
{
33+
params: { transform: 0, width: 0, height: 0 }
34+
}
35+
),
36+
37+
state(
38+
'toVertical',
39+
style({ transform: 'translateY({{ transform }}px)', width: '{{ width }}px', height: '{{ height }}px' }),
40+
{
41+
params: { transform: 100, width: 0, height: 0 }
42+
}
43+
),
44+
45+
transition('from => to', animate(`300ms ${AnimationCurves.EASE_IN_OUT}`)),
46+
transition('fromVertical => toVertical', animate(`300ms ${AnimationCurves.EASE_IN_OUT}`))
2847
]);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
order: 13
3+
title:
4+
zh-CN: 垂直方向
5+
en-US: Vertical direction
6+
---
7+
8+
## zh-CN
9+
10+
垂直方向。
11+
12+
## en-US
13+
14+
Make it vertical.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Component } from '@angular/core';
2+
3+
import { NzSegmentedModule } from 'ng-zorro-antd/segmented';
4+
5+
@Component({
6+
selector: 'nz-demo-segmented-vertical',
7+
imports: [NzSegmentedModule],
8+
template: ` <nz-segmented [nzOptions]="options" nzVertical />`
9+
})
10+
export class NzDemoSegmentedVerticalComponent {
11+
options = [
12+
{ value: 'List', icon: 'bars' },
13+
{ value: 'Kanban', icon: 'appstore' }
14+
];
15+
}

components/segmented/doc/index.en-US.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ description: Display multiple options and allow users to select a single option.
2121
| `[nzDisabled]` | Disable all segments | `boolean` | `false` | |
2222
| `[nzOptions]` | Set children optional | `string[] \| number[] \| Array<{ label: string; value: string \| number; icon: string; disabled?: boolean }>` | - | |
2323
| `[nzSize]` | The size of the Segmented | `large \| default \| small` | - ||
24+
| `[nzVertical]` | Orientation | `boolean` | `false` | - |
2425
| `[ngModel]` | Value of the currently selected option | `string \| number` | - | |
2526
| `(nzValueChange)` | Emits when value of the currently selected option changes | `EventEmitter<string \| number>` | - | |
2627
| `(ngModelChange)` | Emits when value of the currently selected option changes | `EventEmitter<string \| number>` | - | |

components/segmented/doc/index.zh-CN.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ description: 用于展示多个选项并允许用户选择其中单个选项。
2222
| `[nzDisabled]` | 是否禁用 | `boolean` | `false` | |
2323
| `[nzOptions]` | 数据化配置选项内容 | `string[] \| number[] \| Array<{ label: string; value: string \| number; icon: string; disabled?: boolean; }>` | - | |
2424
| `[nzSize]` | 控件尺寸 | `large \| default \| small` | - ||
25+
| `[nzVertical]` | 排列方向 | `boolean` | `false` | - |
2526
| `[ngModel]` | 当前选中项目的 value | `string \| number` | - | |
2627
| `(nzValueChange)` | 当前选中项目变化时触发回调 | `EventEmitter<string \| number>` | - | |
2728
| `(ngModelChange)` | 当前选中项目变化时触发回调 | `EventEmitter<string \| number>` | - | |

components/segmented/segmented-item.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export class NzSegmentedItemComponent implements OnInit {
7777
}),
7878
switchMap(value =>
7979
this.service.animationDone$.pipe(
80-
filter(event => event.toState === 'to'),
80+
filter(event => event.toState === 'to' || event.toState === 'toVertical'),
8181
take(1),
8282
map(() => value)
8383
)

components/segmented/segmented.component.ts

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ const NZ_CONFIG_MODULE_NAME: NzConfigKey = 'segmented';
6969
'[class.ant-segmented-rtl]': `dir === 'rtl'`,
7070
'[class.ant-segmented-lg]': `nzSize === 'large'`,
7171
'[class.ant-segmented-sm]': `nzSize === 'small'`,
72-
'[class.ant-segmented-block]': `nzBlock`
72+
'[class.ant-segmented-block]': `nzBlock`,
73+
'[class.ant-segmented-vertical]': `nzVertical`
7374
},
7475
providers: [
7576
NzSegmentedService,
@@ -93,6 +94,7 @@ export class NzSegmentedComponent implements OnChanges, ControlValueAccessor {
9394
@Input({ transform: booleanAttribute }) nzBlock: boolean = false;
9495
@Input({ transform: booleanAttribute }) nzDisabled = false;
9596
@Input() nzOptions: NzSegmentedOptions = [];
97+
@Input({ transform: booleanAttribute }) nzVertical: boolean = false;
9698
@Input() @WithConfig() nzSize: NzSizeLDSType = 'default';
9799

98100
@Output() readonly nzValueChange = new EventEmitter<number | string>();
@@ -127,17 +129,31 @@ export class NzSegmentedComponent implements OnChanges, ControlValueAccessor {
127129
});
128130

129131
this.service.activated$.pipe(bufferCount(2, 1), takeUntilDestroyed()).subscribe(elements => {
130-
this.animationState = {
131-
value: 'from',
132-
params: thumbAnimationParamsOf(elements[0])
133-
};
134-
this.cdr.detectChanges();
135-
136-
this.animationState = {
137-
value: 'to',
138-
params: thumbAnimationParamsOf(elements[1])
139-
};
140-
this.cdr.detectChanges();
132+
if (this.nzVertical) {
133+
this.animationState = {
134+
value: 'fromVertical',
135+
params: thumbAnimationParamsOf(elements[0], true)
136+
};
137+
this.cdr.detectChanges();
138+
139+
this.animationState = {
140+
value: 'toVertical',
141+
params: thumbAnimationParamsOf(elements[1], true)
142+
};
143+
this.cdr.detectChanges();
144+
} else {
145+
this.animationState = {
146+
value: 'from',
147+
params: thumbAnimationParamsOf(elements[0])
148+
};
149+
this.cdr.detectChanges();
150+
151+
this.animationState = {
152+
value: 'to',
153+
params: thumbAnimationParamsOf(elements[1])
154+
};
155+
this.cdr.detectChanges();
156+
}
141157
});
142158

143159
effect(() => {
@@ -167,7 +183,7 @@ export class NzSegmentedComponent implements OnChanges, ControlValueAccessor {
167183
}
168184

169185
handleThumbAnimationDone(event: AnimationEvent): void {
170-
if (event.toState === 'to') {
186+
if (event.toState === 'to' || event.toState === 'toVertical') {
171187
this.animationState = null;
172188
}
173189
this.service.animationDone$.next(event);
@@ -191,7 +207,15 @@ export class NzSegmentedComponent implements OnChanges, ControlValueAccessor {
191207
}
192208
}
193209

194-
function thumbAnimationParamsOf(element?: HTMLElement): ThumbAnimationProps {
210+
function thumbAnimationParamsOf(element?: HTMLElement, vertical: boolean = false): ThumbAnimationProps {
211+
if (vertical) {
212+
return {
213+
transform: element?.offsetTop ?? 0,
214+
width: element?.clientWidth ?? 0,
215+
height: element?.clientHeight ?? 0,
216+
vertical
217+
};
218+
}
195219
return {
196220
transform: element?.offsetLeft ?? 0,
197221
width: element?.clientWidth ?? 0

components/segmented/segmented.spec.ts

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import { Component, DebugElement } from '@angular/core';
7-
import { ComponentFixture, TestBed } from '@angular/core/testing';
7+
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
88
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
99
import { By } from '@angular/platform-browser';
1010
import { provideNoopAnimations } from '@angular/platform-browser/animations';
@@ -55,6 +55,14 @@ describe('nz-segmented', () => {
5555
expect(segmentedElement.classList).toContain('ant-segmented-sm');
5656
});
5757

58+
it('should support vertical mode', () => {
59+
const segmentedElement: HTMLElement = segmentedComponent.nativeElement;
60+
expect(segmentedElement.classList).not.toContain('ant-segmented-vertical');
61+
component.vertical = true;
62+
fixture.detectChanges();
63+
expect(segmentedElement.classList).toContain('ant-segmented-vertical');
64+
});
65+
5866
it('should be auto selected the first option when if no value is set', async () => {
5967
const theFirstElement = getSegmentedOptionByIndex(0);
6068
await fixture.whenStable();
@@ -132,6 +140,33 @@ describe('nz-segmented', () => {
132140
expect(theFirstElement.classList).toContain('ant-segmented-item-selected');
133141
expect(theSecondElement.classList).not.toContain('ant-segmented-item-selected');
134142
});
143+
144+
it('should animate thumb correctly in vertical mode', fakeAsync(() => {
145+
component.vertical = true;
146+
fixture.detectChanges();
147+
148+
const segmentedComponentInstance = fixture.debugElement.query(
149+
By.directive(NzSegmentedComponent)
150+
).componentInstance;
151+
152+
expect(segmentedComponentInstance.nzVertical).toBe(true);
153+
154+
const theSecondElement = getSegmentedOptionByIndex(1);
155+
156+
tick(100);
157+
fixture.detectChanges();
158+
159+
dispatchMouseEvent(theSecondElement, 'click');
160+
tick(100);
161+
fixture.detectChanges();
162+
163+
expect(theSecondElement.classList).toContain('ant-segmented-item-selected');
164+
expect(segmentedComponentInstance.animationState).toBeDefined();
165+
166+
if (segmentedComponentInstance.animationState) {
167+
expect(['fromVertical', 'toVertical']).toContain(segmentedComponentInstance.animationState.value);
168+
}
169+
}));
135170
});
136171

137172
describe('ng model', () => {
@@ -217,6 +252,54 @@ describe('nz-segmented', () => {
217252
expect(theThirdElement.classList).toContain('ant-segmented-item-selected');
218253
});
219254
});
255+
256+
describe('vertical segmented', () => {
257+
let fixture: ComponentFixture<NzSegmentedVerticalTestComponent>;
258+
let component: NzSegmentedVerticalTestComponent;
259+
let segmentedComponent: DebugElement;
260+
261+
function getSegmentedOptionByIndex(index: number): HTMLElement {
262+
return segmentedComponent.nativeElement.querySelectorAll('.ant-segmented-item')[index];
263+
}
264+
265+
beforeEach(() => {
266+
TestBed.configureTestingModule({
267+
providers: [provideNoopAnimations()]
268+
});
269+
fixture = TestBed.createComponent(NzSegmentedVerticalTestComponent);
270+
component = fixture.componentInstance;
271+
spyOn(component, 'handleValueChange');
272+
segmentedComponent = fixture.debugElement.query(By.directive(NzSegmentedComponent));
273+
fixture.detectChanges();
274+
});
275+
276+
it('should render in vertical mode', () => {
277+
const segmentedElement: HTMLElement = segmentedComponent.nativeElement;
278+
expect(segmentedElement.classList).toContain('ant-segmented-vertical');
279+
const groupElement = segmentedElement.querySelector('.ant-segmented-group') as HTMLElement;
280+
expect(groupElement).toBeTruthy();
281+
});
282+
283+
it('should change selection in vertical mode', async () => {
284+
const theFirstElement = getSegmentedOptionByIndex(0);
285+
const theSecondElement = getSegmentedOptionByIndex(1);
286+
287+
await fixture.whenStable();
288+
fixture.detectChanges();
289+
290+
expect(theFirstElement.classList).toContain('ant-segmented-item-selected');
291+
expect(component.handleValueChange).toHaveBeenCalledTimes(0);
292+
293+
dispatchMouseEvent(theSecondElement, 'click');
294+
await fixture.whenStable();
295+
fixture.detectChanges();
296+
297+
expect(theFirstElement.classList).not.toContain('ant-segmented-item-selected');
298+
expect(theSecondElement.classList).toContain('ant-segmented-item-selected');
299+
expect(component.handleValueChange).toHaveBeenCalledTimes(1);
300+
expect(component.handleValueChange).toHaveBeenCalledWith('Weekly');
301+
});
302+
});
220303
});
221304

222305
@Component({
@@ -227,15 +310,17 @@ describe('nz-segmented', () => {
227310
[nzOptions]="options"
228311
[nzDisabled]="disabled"
229312
[nzBlock]="block"
313+
[nzVertical]="vertical"
230314
(nzValueChange)="handleValueChange($event)"
231-
></nz-segmented>
315+
/>
232316
`
233317
})
234318
export class NzSegmentedTestComponent {
235319
size: NzSizeLDSType = 'default';
236320
options: NzSegmentedOptions = [1, 2, 3];
237321
block = false;
238322
disabled = false;
323+
vertical = false;
239324

240325
handleValueChange(_e: string | number): void {
241326
// empty
@@ -263,3 +348,15 @@ export class NzSegmentedInReactiveFormTestComponent {
263348
options = ['Daily', 'Weekly', 'Monthly', 'Quarterly', 'Yearly'];
264349
formControl = new FormControl('Weekly');
265350
}
351+
352+
@Component({
353+
imports: [FormsModule, NzSegmentedModule],
354+
template: ` <nz-segmented [nzOptions]="options" [nzVertical]="true" (nzValueChange)="handleValueChange($event)" /> `
355+
})
356+
export class NzSegmentedVerticalTestComponent {
357+
options = ['Daily', 'Weekly', 'Monthly', 'Quarterly', 'Yearly'];
358+
359+
handleValueChange(_e: string | number): void {
360+
return void 0;
361+
}
362+
}

components/segmented/style/index.less

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,25 @@
2323
width: 100%;
2424
}
2525

26+
&-vertical {
27+
.@{segmented-prefix-cls}-group {
28+
flex-direction: column;
29+
}
30+
31+
.@{segmented-prefix-cls}-thumb {
32+
width: 100%;
33+
height: 0;
34+
}
35+
36+
.@{segmented-prefix-cls}-thumb-motion-appear-active {
37+
transition:
38+
transform 0.3s @ease-in-out,
39+
width 0.3s @ease-in-out,
40+
height 0.3s @ease-in-out;
41+
will-change: transform, width, height;
42+
}
43+
}
44+
2645
// block styles
2746
&&-block {
2847
display: flex;
@@ -114,7 +133,9 @@
114133

115134
// transition effect when `appear-active`
116135
&-thumb-motion-appear-active {
117-
transition: transform 0.3s @ease-in-out, width 0.3s @ease-in-out;
136+
transition:
137+
transform 0.3s @ease-in-out,
138+
width 0.3s @ease-in-out;
118139
will-change: transform, width;
119140
}
120141
}

0 commit comments

Comments
 (0)