Skip to content

Commit

Permalink
feat(module:cascader): support nzPlacement (#8935)
Browse files Browse the repository at this point in the history
  • Loading branch information
Laffery authored Dec 15, 2024
1 parent 489e0de commit 6fbd22c
Show file tree
Hide file tree
Showing 11 changed files with 218 additions and 55 deletions.
45 changes: 41 additions & 4 deletions components/cascader/cascader.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@

import { Direction, Directionality } from '@angular/cdk/bidi';
import { BACKSPACE, DOWN_ARROW, ENTER, ESCAPE, LEFT_ARROW, RIGHT_ARROW, UP_ARROW } from '@angular/cdk/keycodes';
import { CdkConnectedOverlay, ConnectionPositionPair, OverlayModule } from '@angular/cdk/overlay';
import {
CdkConnectedOverlay,
ConnectedOverlayPositionChange,
ConnectionPositionPair,
OverlayModule
} from '@angular/cdk/overlay';
import { _getEventTarget } from '@angular/cdk/platform';
import { SlicePipe } from '@angular/common';
import {
Expand Down Expand Up @@ -43,7 +48,13 @@ import { slideMotion } from 'ng-zorro-antd/core/animation';
import { NzConfigKey, NzConfigService, WithConfig } from 'ng-zorro-antd/core/config';
import { NzFormItemFeedbackIconComponent, NzFormNoStatusService, NzFormStatusService } from 'ng-zorro-antd/core/form';
import { NzNoAnimationDirective } from 'ng-zorro-antd/core/no-animation';
import { DEFAULT_CASCADER_POSITIONS, NzOverlayModule } from 'ng-zorro-antd/core/overlay';
import {
DEFAULT_CASCADER_POSITIONS,
getPlacementName,
NzOverlayModule,
POSITION_MAP,
POSITION_TYPE
} from 'ng-zorro-antd/core/overlay';
import { NzDestroyService } from 'ng-zorro-antd/core/services';
import { NzTreeBase, NzTreeNode } from 'ng-zorro-antd/core/tree';
import {
Expand All @@ -62,6 +73,7 @@ import {
NzSelectClearComponent,
NzSelectItemComponent,
NzSelectPlaceholderComponent,
NzSelectPlacementType,
NzSelectSearchComponent
} from 'ng-zorro-antd/select';
import { NZ_SPACE_COMPACT_ITEM_TYPE, NZ_SPACE_COMPACT_SIZE, NzSpaceCompactItemDirective } from 'ng-zorro-antd/space';
Expand All @@ -73,6 +85,7 @@ import {
NzCascaderComponentAsSource,
NzCascaderExpandTrigger,
NzCascaderOption,
NzCascaderPlacement,
NzCascaderSize,
NzCascaderTriggerType,
NzShowSearchOptions
Expand Down Expand Up @@ -166,9 +179,14 @@ const defaultDisplayRender = (labels: string[]): string => labels.join(' / ');
[cdkConnectedOverlayOpen]="menuVisible"
(overlayOutsideClick)="onClickOutside($event)"
(detach)="closeMenu()"
(positionChange)="onPositionChange($event)"
>
<div
class="ant-select-dropdown ant-cascader-dropdown ant-select-dropdown-placement-bottomLeft"
class="ant-select-dropdown ant-cascader-dropdown"
[class.ant-select-dropdown-placement-bottomLeft]="dropdownPosition === 'bottomLeft'"
[class.ant-select-dropdown-placement-bottomRight]="dropdownPosition === 'bottomRight'"
[class.ant-select-dropdown-placement-topLeft]="dropdownPosition === 'topLeft'"
[class.ant-select-dropdown-placement-topRight]="dropdownPosition === 'topRight'"
[class.ant-cascader-dropdown-rtl]="dir === 'rtl'"
[@slideMotion]="'enter'"
[@.disabled]="!!noAnimation?.nzNoAnimation"
Expand Down Expand Up @@ -285,9 +303,11 @@ export class NzCascaderComponent
set input(inputComponent: NzSelectSearchComponent | undefined) {
this.input$.next(inputComponent?.inputElement);
}

get input(): ElementRef<HTMLInputElement> | undefined {
return this.input$.getValue();
}

/** Used to store the native `<input type="search" />` element since it might be set asynchronously. */
private input$ = new BehaviorSubject<ElementRef<HTMLInputElement> | undefined>(undefined);

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

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

locale!: NzCascaderI18nInterface;
Expand Down Expand Up @@ -551,13 +573,23 @@ export class NzCascaderComponent
}

ngOnChanges(changes: SimpleChanges): void {
const { nzStatus, nzSize } = changes;
const { nzStatus, nzSize, nzPlacement } = changes;
if (nzStatus) {
this.setStatusStyles(this.nzStatus, this.hasFeedback);
}
if (nzSize) {
this.size.set(nzSize.currentValue);
}
if (nzPlacement) {
const { currentValue } = nzPlacement;
this.dropdownPosition = currentValue as NzCascaderPlacement;
const listOfPlacement = ['bottomLeft', 'topLeft', 'bottomRight', 'topRight'];
if (currentValue && listOfPlacement.includes(currentValue)) {
this.positions = [POSITION_MAP[currentValue as POSITION_TYPE]];
} else {
this.positions = listOfPlacement.map(e => POSITION_MAP[e as POSITION_TYPE]);
}
}
}

ngOnDestroy(): void {
Expand Down Expand Up @@ -939,6 +971,11 @@ export class NzCascaderComponent
}
}

onPositionChange(position: ConnectedOverlayPositionChange): void {
const placement = getPlacementName(position);
this.dropdownPosition = placement as NzSelectPlacementType;
}

private isActionTrigger(action: 'click' | 'hover'): boolean {
return typeof this.nzTriggerAction === 'string'
? this.nzTriggerAction === action
Expand Down
45 changes: 45 additions & 0 deletions components/cascader/cascader.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { NzCascaderModule } from './cascader.module';
import {
NzCascaderExpandTrigger,
NzCascaderOption,
NzCascaderPlacement,
NzCascaderSize,
NzCascaderTriggerType,
NzShowSearchOptions
Expand Down Expand Up @@ -1637,6 +1638,48 @@ describe('cascader', () => {
expect(itemEl1?.querySelector('.anticon-home')).toBeTruthy();
expect(cascader.nativeElement.querySelector('.ant-select-arrow .anticon')!.classList).toContain('anticon-home');
});

it('should nzPlacement works', fakeAsync(() => {
fixture.detectChanges();
testComponent.cascader.setMenuVisible(true);
fixture.detectChanges();
let element = overlayContainerElement.querySelector('.ant-select-dropdown') as HTMLElement;
expect(element.classList.contains('ant-select-dropdown-placement-bottomLeft')).toBe(true);
expect(element.classList.contains('ant-select-dropdown-placement-bottomRight')).toBe(false);
expect(element.classList.contains('ant-select-dropdown-placement-topLeft')).toBe(false);
expect(element.classList.contains('ant-select-dropdown-placement-topRight')).toBe(false);

const setNzPlacement = (placement: NzCascaderPlacement): void => {
testComponent.cascader.setMenuVisible(false);
fixture.detectChanges();
testComponent.nzPlacement = placement;
testComponent.cascader.setMenuVisible(true);
fixture.detectChanges();
tick();
fixture.detectChanges();
};

setNzPlacement('bottomRight');
element = overlayContainerElement.querySelector('.ant-select-dropdown') as HTMLElement;
expect(element.classList.contains('ant-select-dropdown-placement-bottomLeft')).toBe(false);
expect(element.classList.contains('ant-select-dropdown-placement-bottomRight')).toBe(true);
expect(element.classList.contains('ant-select-dropdown-placement-topLeft')).toBe(false);
expect(element.classList.contains('ant-select-dropdown-placement-topRight')).toBe(false);

setNzPlacement('topLeft');
element = overlayContainerElement.querySelector('.ant-select-dropdown') as HTMLElement;
expect(element.classList.contains('ant-select-dropdown-placement-bottomLeft')).toBe(false);
expect(element.classList.contains('ant-select-dropdown-placement-bottomRight')).toBe(false);
expect(element.classList.contains('ant-select-dropdown-placement-topLeft')).toBe(true);
expect(element.classList.contains('ant-select-dropdown-placement-topRight')).toBe(false);

setNzPlacement('topRight');
element = overlayContainerElement.querySelector('.ant-select-dropdown') as HTMLElement;
expect(element.classList.contains('ant-select-dropdown-placement-bottomLeft')).toBe(false);
expect(element.classList.contains('ant-select-dropdown-placement-bottomRight')).toBe(false);
expect(element.classList.contains('ant-select-dropdown-placement-topLeft')).toBe(false);
expect(element.classList.contains('ant-select-dropdown-placement-topRight')).toBe(true);
}));
});

describe('multiple', () => {
Expand Down Expand Up @@ -2290,6 +2333,7 @@ const options5: NzSafeAny[] = [];
[nzSuffixIcon]="nzSuffixIcon"
[nzValueProperty]="nzValueProperty"
[nzBackdrop]="nzBackdrop"
[nzPlacement]="nzPlacement"
(ngModelChange)="onValueChanges($event)"
(nzVisibleChange)="onVisibleChange($event)"
(nzClear)="onClear()"
Expand Down Expand Up @@ -2332,6 +2376,7 @@ export class NzDemoCascaderDefaultComponent {
nzSuffixIcon = 'down';
nzExpandIcon = 'right';
nzBackdrop = false;
nzPlacement: NzCascaderPlacement = 'bottomLeft';

onVisibleChange = jasmine.createSpy<(visible: boolean) => void>('open change');
onValueChanges = jasmine.createSpy('value change');
Expand Down
14 changes: 14 additions & 0 deletions components/cascader/demo/placement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
order: 18
title:
zh-CN: 弹出位置
en-US: Placement
---

## zh-CN

可以通过 `nzPlacement` 手动指定弹出的位置。

## en-US

You can manually specify the position of the popup via `nzPlacement`.
66 changes: 66 additions & 0 deletions components/cascader/demo/placement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Component } from '@angular/core';

import { NzCascaderModule, NzCascaderOption, NzCascaderPlacement } from 'ng-zorro-antd/cascader';
import { NzSegmentedModule } from 'ng-zorro-antd/segmented';

const options: NzCascaderOption[] = [
{
value: 'zhejiang',
label: 'Zhejiang',
children: [
{
value: 'hangzhou',
label: 'Hangzhou',
children: [
{
value: 'xihu',
label: 'West Lake',
isLeaf: true
}
]
},
{
value: 'ningbo',
label: 'Ningbo',
isLeaf: true
}
]
},
{
value: 'jiangsu',
label: 'Jiangsu',
children: [
{
value: 'nanjing',
label: 'Nanjing',
children: [
{
value: 'zhonghuamen',
label: 'Zhong Hua Men',
isLeaf: true
}
]
}
]
}
];

@Component({
selector: 'nz-demo-cascader-placement',
imports: [NzCascaderModule, NzSegmentedModule],
template: `
<nz-segmented [nzOptions]="placements" (nzValueChange)="setPlacement($event)"></nz-segmented>
<br />
<br />
<nz-cascader [nzOptions]="nzOptions" [nzPlacement]="placement"></nz-cascader>
`
})
export class NzDemoCascaderPlacementComponent {
nzOptions: NzCascaderOption[] = options;
placement: NzCascaderPlacement = 'topLeft';
readonly placements: NzCascaderPlacement[] = ['topLeft', 'topRight', 'bottomLeft', 'bottomRight'];

setPlacement(placement: string | number): void {
this.placement = placement as NzCascaderPlacement;
}
}
2 changes: 1 addition & 1 deletion components/cascader/demo/status.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
order: 18
order: 19
title:
zh-CN: 自定义状态
en-US: Status
Expand Down
1 change: 1 addition & 0 deletions components/cascader/doc/index.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { NzCascaderModule } from 'ng-zorro-antd/cascader';
| `[nzOptionRender]` | render template of cascader options | `TemplateRef<{ $implicit: NzCascaderOption, index: number }>` | |
| `[nzOptions]` | data options of cascade | `object[]` | - |
| `[nzPlaceHolder]` | input placeholder | `string` | `'Please select'` |
| `[nzPlacement]` | popup placement | `'bottomLeft' \| 'bottomRight' \| 'topLeft' \| 'topRight'` | `'bottomLeft'` |
| `[nzShowArrow]` | whether show arrow | `boolean` | `true` |
| `[nzShowInput]` | whether show input | `boolean` | `true` |
| `[nzShowSearch]` | whether support search. Cannot be used with `[nzLoadData]` at the same time | `boolean\|NzShowSearchOptions` | `false` |
Expand Down
Loading

0 comments on commit 6fbd22c

Please sign in to comment.