Skip to content

Commit 6fbd22c

Browse files
authored
feat(module:cascader): support nzPlacement (#8935)
1 parent 489e0de commit 6fbd22c

11 files changed

+218
-55
lines changed

components/cascader/cascader.component.ts

+41-4
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55

66
import { Direction, Directionality } from '@angular/cdk/bidi';
77
import { BACKSPACE, DOWN_ARROW, ENTER, ESCAPE, LEFT_ARROW, RIGHT_ARROW, UP_ARROW } from '@angular/cdk/keycodes';
8-
import { CdkConnectedOverlay, ConnectionPositionPair, OverlayModule } from '@angular/cdk/overlay';
8+
import {
9+
CdkConnectedOverlay,
10+
ConnectedOverlayPositionChange,
11+
ConnectionPositionPair,
12+
OverlayModule
13+
} from '@angular/cdk/overlay';
914
import { _getEventTarget } from '@angular/cdk/platform';
1015
import { SlicePipe } from '@angular/common';
1116
import {
@@ -43,7 +48,13 @@ import { slideMotion } from 'ng-zorro-antd/core/animation';
4348
import { NzConfigKey, NzConfigService, WithConfig } from 'ng-zorro-antd/core/config';
4449
import { NzFormItemFeedbackIconComponent, NzFormNoStatusService, NzFormStatusService } from 'ng-zorro-antd/core/form';
4550
import { NzNoAnimationDirective } from 'ng-zorro-antd/core/no-animation';
46-
import { DEFAULT_CASCADER_POSITIONS, NzOverlayModule } from 'ng-zorro-antd/core/overlay';
51+
import {
52+
DEFAULT_CASCADER_POSITIONS,
53+
getPlacementName,
54+
NzOverlayModule,
55+
POSITION_MAP,
56+
POSITION_TYPE
57+
} from 'ng-zorro-antd/core/overlay';
4758
import { NzDestroyService } from 'ng-zorro-antd/core/services';
4859
import { NzTreeBase, NzTreeNode } from 'ng-zorro-antd/core/tree';
4960
import {
@@ -62,6 +73,7 @@ import {
6273
NzSelectClearComponent,
6374
NzSelectItemComponent,
6475
NzSelectPlaceholderComponent,
76+
NzSelectPlacementType,
6577
NzSelectSearchComponent
6678
} from 'ng-zorro-antd/select';
6779
import { NZ_SPACE_COMPACT_ITEM_TYPE, NZ_SPACE_COMPACT_SIZE, NzSpaceCompactItemDirective } from 'ng-zorro-antd/space';
@@ -73,6 +85,7 @@ import {
7385
NzCascaderComponentAsSource,
7486
NzCascaderExpandTrigger,
7587
NzCascaderOption,
88+
NzCascaderPlacement,
7689
NzCascaderSize,
7790
NzCascaderTriggerType,
7891
NzShowSearchOptions
@@ -166,9 +179,14 @@ const defaultDisplayRender = (labels: string[]): string => labels.join(' / ');
166179
[cdkConnectedOverlayOpen]="menuVisible"
167180
(overlayOutsideClick)="onClickOutside($event)"
168181
(detach)="closeMenu()"
182+
(positionChange)="onPositionChange($event)"
169183
>
170184
<div
171-
class="ant-select-dropdown ant-cascader-dropdown ant-select-dropdown-placement-bottomLeft"
185+
class="ant-select-dropdown ant-cascader-dropdown"
186+
[class.ant-select-dropdown-placement-bottomLeft]="dropdownPosition === 'bottomLeft'"
187+
[class.ant-select-dropdown-placement-bottomRight]="dropdownPosition === 'bottomRight'"
188+
[class.ant-select-dropdown-placement-topLeft]="dropdownPosition === 'topLeft'"
189+
[class.ant-select-dropdown-placement-topRight]="dropdownPosition === 'topRight'"
172190
[class.ant-cascader-dropdown-rtl]="dir === 'rtl'"
173191
[@slideMotion]="'enter'"
174192
[@.disabled]="!!noAnimation?.nzNoAnimation"
@@ -285,9 +303,11 @@ export class NzCascaderComponent
285303
set input(inputComponent: NzSelectSearchComponent | undefined) {
286304
this.input$.next(inputComponent?.inputElement);
287305
}
306+
288307
get input(): ElementRef<HTMLInputElement> | undefined {
289308
return this.input$.getValue();
290309
}
310+
291311
/** Used to store the native `<input type="search" />` element since it might be set asynchronously. */
292312
private input$ = new BehaviorSubject<ElementRef<HTMLInputElement> | undefined>(undefined);
293313

@@ -327,6 +347,7 @@ export class NzCascaderComponent
327347
@Input() nzStatus: NzStatus = '';
328348
@Input({ transform: booleanAttribute }) nzMultiple: boolean = false;
329349
@Input() nzMaxTagCount: number = Infinity;
350+
@Input() nzPlacement: NzCascaderPlacement = 'bottomLeft';
330351

331352
@Input() nzTriggerAction: NzCascaderTriggerType | NzCascaderTriggerType[] = ['click'] as NzCascaderTriggerType[];
332353
@Input() nzChangeOn?: (option: NzCascaderOption, level: number) => boolean;
@@ -389,6 +410,7 @@ export class NzCascaderComponent
389410
*/
390411
dropdownWidthStyle?: string;
391412
dropdownHeightStyle: 'auto' | '' = '';
413+
dropdownPosition: NzCascaderPlacement = 'bottomLeft';
392414
isFocused = false;
393415

394416
locale!: NzCascaderI18nInterface;
@@ -551,13 +573,23 @@ export class NzCascaderComponent
551573
}
552574

553575
ngOnChanges(changes: SimpleChanges): void {
554-
const { nzStatus, nzSize } = changes;
576+
const { nzStatus, nzSize, nzPlacement } = changes;
555577
if (nzStatus) {
556578
this.setStatusStyles(this.nzStatus, this.hasFeedback);
557579
}
558580
if (nzSize) {
559581
this.size.set(nzSize.currentValue);
560582
}
583+
if (nzPlacement) {
584+
const { currentValue } = nzPlacement;
585+
this.dropdownPosition = currentValue as NzCascaderPlacement;
586+
const listOfPlacement = ['bottomLeft', 'topLeft', 'bottomRight', 'topRight'];
587+
if (currentValue && listOfPlacement.includes(currentValue)) {
588+
this.positions = [POSITION_MAP[currentValue as POSITION_TYPE]];
589+
} else {
590+
this.positions = listOfPlacement.map(e => POSITION_MAP[e as POSITION_TYPE]);
591+
}
592+
}
561593
}
562594

563595
ngOnDestroy(): void {
@@ -939,6 +971,11 @@ export class NzCascaderComponent
939971
}
940972
}
941973

974+
onPositionChange(position: ConnectedOverlayPositionChange): void {
975+
const placement = getPlacementName(position);
976+
this.dropdownPosition = placement as NzSelectPlacementType;
977+
}
978+
942979
private isActionTrigger(action: 'click' | 'hover'): boolean {
943980
return typeof this.nzTriggerAction === 'string'
944981
? this.nzTriggerAction === action

components/cascader/cascader.spec.ts

+45
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { NzCascaderModule } from './cascader.module';
4141
import {
4242
NzCascaderExpandTrigger,
4343
NzCascaderOption,
44+
NzCascaderPlacement,
4445
NzCascaderSize,
4546
NzCascaderTriggerType,
4647
NzShowSearchOptions
@@ -1637,6 +1638,48 @@ describe('cascader', () => {
16371638
expect(itemEl1?.querySelector('.anticon-home')).toBeTruthy();
16381639
expect(cascader.nativeElement.querySelector('.ant-select-arrow .anticon')!.classList).toContain('anticon-home');
16391640
});
1641+
1642+
it('should nzPlacement works', fakeAsync(() => {
1643+
fixture.detectChanges();
1644+
testComponent.cascader.setMenuVisible(true);
1645+
fixture.detectChanges();
1646+
let element = overlayContainerElement.querySelector('.ant-select-dropdown') as HTMLElement;
1647+
expect(element.classList.contains('ant-select-dropdown-placement-bottomLeft')).toBe(true);
1648+
expect(element.classList.contains('ant-select-dropdown-placement-bottomRight')).toBe(false);
1649+
expect(element.classList.contains('ant-select-dropdown-placement-topLeft')).toBe(false);
1650+
expect(element.classList.contains('ant-select-dropdown-placement-topRight')).toBe(false);
1651+
1652+
const setNzPlacement = (placement: NzCascaderPlacement): void => {
1653+
testComponent.cascader.setMenuVisible(false);
1654+
fixture.detectChanges();
1655+
testComponent.nzPlacement = placement;
1656+
testComponent.cascader.setMenuVisible(true);
1657+
fixture.detectChanges();
1658+
tick();
1659+
fixture.detectChanges();
1660+
};
1661+
1662+
setNzPlacement('bottomRight');
1663+
element = overlayContainerElement.querySelector('.ant-select-dropdown') as HTMLElement;
1664+
expect(element.classList.contains('ant-select-dropdown-placement-bottomLeft')).toBe(false);
1665+
expect(element.classList.contains('ant-select-dropdown-placement-bottomRight')).toBe(true);
1666+
expect(element.classList.contains('ant-select-dropdown-placement-topLeft')).toBe(false);
1667+
expect(element.classList.contains('ant-select-dropdown-placement-topRight')).toBe(false);
1668+
1669+
setNzPlacement('topLeft');
1670+
element = overlayContainerElement.querySelector('.ant-select-dropdown') as HTMLElement;
1671+
expect(element.classList.contains('ant-select-dropdown-placement-bottomLeft')).toBe(false);
1672+
expect(element.classList.contains('ant-select-dropdown-placement-bottomRight')).toBe(false);
1673+
expect(element.classList.contains('ant-select-dropdown-placement-topLeft')).toBe(true);
1674+
expect(element.classList.contains('ant-select-dropdown-placement-topRight')).toBe(false);
1675+
1676+
setNzPlacement('topRight');
1677+
element = overlayContainerElement.querySelector('.ant-select-dropdown') as HTMLElement;
1678+
expect(element.classList.contains('ant-select-dropdown-placement-bottomLeft')).toBe(false);
1679+
expect(element.classList.contains('ant-select-dropdown-placement-bottomRight')).toBe(false);
1680+
expect(element.classList.contains('ant-select-dropdown-placement-topLeft')).toBe(false);
1681+
expect(element.classList.contains('ant-select-dropdown-placement-topRight')).toBe(true);
1682+
}));
16401683
});
16411684

16421685
describe('multiple', () => {
@@ -2290,6 +2333,7 @@ const options5: NzSafeAny[] = [];
22902333
[nzSuffixIcon]="nzSuffixIcon"
22912334
[nzValueProperty]="nzValueProperty"
22922335
[nzBackdrop]="nzBackdrop"
2336+
[nzPlacement]="nzPlacement"
22932337
(ngModelChange)="onValueChanges($event)"
22942338
(nzVisibleChange)="onVisibleChange($event)"
22952339
(nzClear)="onClear()"
@@ -2332,6 +2376,7 @@ export class NzDemoCascaderDefaultComponent {
23322376
nzSuffixIcon = 'down';
23332377
nzExpandIcon = 'right';
23342378
nzBackdrop = false;
2379+
nzPlacement: NzCascaderPlacement = 'bottomLeft';
23352380

23362381
onVisibleChange = jasmine.createSpy<(visible: boolean) => void>('open change');
23372382
onValueChanges = jasmine.createSpy('value change');

components/cascader/demo/placement.md

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
order: 18
3+
title:
4+
zh-CN: 弹出位置
5+
en-US: Placement
6+
---
7+
8+
## zh-CN
9+
10+
可以通过 `nzPlacement` 手动指定弹出的位置。
11+
12+
## en-US
13+
14+
You can manually specify the position of the popup via `nzPlacement`.

components/cascader/demo/placement.ts

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { Component } from '@angular/core';
2+
3+
import { NzCascaderModule, NzCascaderOption, NzCascaderPlacement } from 'ng-zorro-antd/cascader';
4+
import { NzSegmentedModule } from 'ng-zorro-antd/segmented';
5+
6+
const options: NzCascaderOption[] = [
7+
{
8+
value: 'zhejiang',
9+
label: 'Zhejiang',
10+
children: [
11+
{
12+
value: 'hangzhou',
13+
label: 'Hangzhou',
14+
children: [
15+
{
16+
value: 'xihu',
17+
label: 'West Lake',
18+
isLeaf: true
19+
}
20+
]
21+
},
22+
{
23+
value: 'ningbo',
24+
label: 'Ningbo',
25+
isLeaf: true
26+
}
27+
]
28+
},
29+
{
30+
value: 'jiangsu',
31+
label: 'Jiangsu',
32+
children: [
33+
{
34+
value: 'nanjing',
35+
label: 'Nanjing',
36+
children: [
37+
{
38+
value: 'zhonghuamen',
39+
label: 'Zhong Hua Men',
40+
isLeaf: true
41+
}
42+
]
43+
}
44+
]
45+
}
46+
];
47+
48+
@Component({
49+
selector: 'nz-demo-cascader-placement',
50+
imports: [NzCascaderModule, NzSegmentedModule],
51+
template: `
52+
<nz-segmented [nzOptions]="placements" (nzValueChange)="setPlacement($event)"></nz-segmented>
53+
<br />
54+
<br />
55+
<nz-cascader [nzOptions]="nzOptions" [nzPlacement]="placement"></nz-cascader>
56+
`
57+
})
58+
export class NzDemoCascaderPlacementComponent {
59+
nzOptions: NzCascaderOption[] = options;
60+
placement: NzCascaderPlacement = 'topLeft';
61+
readonly placements: NzCascaderPlacement[] = ['topLeft', 'topRight', 'bottomLeft', 'bottomRight'];
62+
63+
setPlacement(placement: string | number): void {
64+
this.placement = placement as NzCascaderPlacement;
65+
}
66+
}

components/cascader/demo/status.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
order: 18
2+
order: 19
33
title:
44
zh-CN: 自定义状态
55
en-US: Status

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

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import { NzCascaderModule } from 'ng-zorro-antd/cascader';
4949
| `[nzOptionRender]` | render template of cascader options | `TemplateRef<{ $implicit: NzCascaderOption, index: number }>` | |
5050
| `[nzOptions]` | data options of cascade | `object[]` | - |
5151
| `[nzPlaceHolder]` | input placeholder | `string` | `'Please select'` |
52+
| `[nzPlacement]` | popup placement | `'bottomLeft' \| 'bottomRight' \| 'topLeft' \| 'topRight'` | `'bottomLeft'` |
5253
| `[nzShowArrow]` | whether show arrow | `boolean` | `true` |
5354
| `[nzShowInput]` | whether show input | `boolean` | `true` |
5455
| `[nzShowSearch]` | whether support search. Cannot be used with `[nzLoadData]` at the same time | `boolean\|NzShowSearchOptions` | `false` |

0 commit comments

Comments
 (0)