Skip to content

Commit dda0e6d

Browse files
rorry121luolei
andauthored
feat(module: select): support placement (#7537)
Co-authored-by: luolei <luolei@kuaishou.com>
1 parent 1f10a9c commit dda0e6d

7 files changed

Lines changed: 158 additions & 8 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
order: 25
3+
title:
4+
zh-CN: 弹出位置
5+
en-US: Placement
6+
---
7+
8+
## zh-CN
9+
10+
可以通过 `placement` 手动指定弹出的位置。
11+
12+
## en-US
13+
14+
You can manually specify the position of the popup via `placement`.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Component } from '@angular/core';
2+
3+
import { NzSelectPlacementType } from 'ng-zorro-antd/select';
4+
5+
@Component({
6+
selector: 'nz-demo-select-placement',
7+
template: `
8+
<nz-radio-group [(ngModel)]="placement">
9+
<label nz-radio-button nzValue="topLeft">topLeft</label>
10+
<label nz-radio-button nzValue="topRight">topRight</label>
11+
<label nz-radio-button nzValue="bottomLeft">bottomLeft</label>
12+
<label nz-radio-button nzValue="bottomRight">bottomRight</label>
13+
</nz-radio-group>
14+
<br />
15+
<br />
16+
<nz-select [(ngModel)]="selectedValue" [nzDropdownMatchSelectWidth]="false" [nzPlacement]="placement">
17+
<nz-option nzValue="HangZhou" nzLabel="HangZhou #310000"></nz-option>
18+
<nz-option nzValue="NingBo" nzLabel="NingBo #315000"></nz-option>
19+
<nz-option nzValue="WenZhou" nzLabel="WenZhou #325000"></nz-option>
20+
</nz-select>
21+
`,
22+
styles: [
23+
`
24+
nz-select {
25+
width: 120px;
26+
}
27+
`
28+
]
29+
})
30+
export class NzDemoSelectPlacementComponent {
31+
placement: NzSelectPlacementType = 'topLeft';
32+
selectedValue = 'HangZhou';
33+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import { NzSelectModule } from 'ng-zorro-antd/select';
4848
| `[nzMode]` | Set mode of Select | `'multiple' \| 'tags' \| 'default'` | `'default'` |
4949
| `[nzNotFoundContent]` | Specify content to show when no result matches.. | `string \| TemplateRef<void>` | `'Not Found'` |
5050
| `[nzPlaceHolder]` | Placeholder of select | `string` | - |
51+
| `[nzPlacement]` | The position where the selection box pops up | `'bottomLeft' \| 'bottomRight' \| 'topLeft' \| 'topRight'` | `'bottomLeft'` |
5152
| `[nzShowArrow]` | Whether to show the drop-down arrow | `boolean` | `true`(for single select), `false`(for multiple select) |
5253
| `[nzShowSearch]` | Whether show search input in single mode. | `boolean` | `false` |
5354
| `[nzSize]` | Size of Select input | `'large' \| 'small' \| 'default'` | `'default'` |

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import { NzSelectModule } from 'ng-zorro-antd/select';
4949
| `[nzMode]` | 设置 nz-select 的模式 | `'multiple' \| 'tags' \| 'default'` | `'default'` |
5050
| `[nzNotFoundContent]` | 当下拉列表为空时显示的内容 | `string \| TemplateRef<void>` | - |
5151
| `[nzPlaceHolder]` | 选择框默认文字 | `string` | - |
52+
| `[nzPlacement]` | 选择框弹出的位置 | `'bottomLeft' \| 'bottomRight' \| 'topLeft' \| 'topRight'` | `'bottomLeft'` |
5253
| `[nzShowArrow]` | 是否显示下拉小箭头 | `boolean` | 单选为 `true`,多选为 `false` |
5354
| `[nzShowSearch]` | 使单选模式可搜索 | `boolean` | `false` |
5455
| `[nzSize]` | 选择框大小 | `'large' \| 'small' \| 'default'` | `'default'` |

components/select/select.component.ts

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
import { FocusMonitor } from '@angular/cdk/a11y';
77
import { Direction, Directionality } from '@angular/cdk/bidi';
88
import { DOWN_ARROW, ENTER, ESCAPE, SPACE, TAB, UP_ARROW } from '@angular/cdk/keycodes';
9-
import { CdkConnectedOverlay, CdkOverlayOrigin, ConnectedOverlayPositionChange } from '@angular/cdk/overlay';
9+
import {
10+
CdkConnectedOverlay,
11+
CdkOverlayOrigin,
12+
ConnectedOverlayPositionChange,
13+
ConnectionPositionPair
14+
} from '@angular/cdk/overlay';
1015
import { Platform } from '@angular/cdk/platform';
1116
import {
1217
AfterContentInit,
@@ -40,6 +45,7 @@ import { slideMotion } from 'ng-zorro-antd/core/animation';
4045
import { NzConfigKey, NzConfigService, WithConfig } from 'ng-zorro-antd/core/config';
4146
import { NzFormNoStatusService, NzFormStatusService } from 'ng-zorro-antd/core/form';
4247
import { NzNoAnimationDirective } from 'ng-zorro-antd/core/no-animation';
48+
import { getPlacementName, POSITION_MAP, POSITION_TYPE } from 'ng-zorro-antd/core/overlay';
4349
import { cancelRequestAnimationFrame, reqAnimFrame } from 'ng-zorro-antd/core/polyfill';
4450
import { NzDestroyService } from 'ng-zorro-antd/core/services';
4551
import {
@@ -56,7 +62,13 @@ import { getStatusClassNames, InputBoolean, isNotNil } from 'ng-zorro-antd/core/
5662
import { NzOptionGroupComponent } from './option-group.component';
5763
import { NzOptionComponent } from './option.component';
5864
import { NzSelectTopControlComponent } from './select-top-control.component';
59-
import { NzFilterOptionType, NzSelectItemInterface, NzSelectModeType, NzSelectOptionInterface } from './select.types';
65+
import {
66+
NzFilterOptionType,
67+
NzSelectItemInterface,
68+
NzSelectModeType,
69+
NzSelectOptionInterface,
70+
NzSelectPlacementType
71+
} from './select.types';
6072

6173
const defaultFilterOption: NzFilterOptionType = (searchValue: string, item: NzSelectItemInterface): boolean => {
6274
if (item && item.nzLabel) {
@@ -137,6 +149,7 @@ export type NzSelectSizeType = 'large' | 'default' | 'small';
137149
[cdkConnectedOverlayTransformOriginOn]="'.ant-select-dropdown'"
138150
[cdkConnectedOverlayPanelClass]="nzDropdownClassName!"
139151
[cdkConnectedOverlayOpen]="nzOpen"
152+
[cdkConnectedOverlayPositions]="positions"
140153
(overlayOutsideClick)="onClickOutside($event)"
141154
(detach)="setOpenState(false)"
142155
(positionChange)="onPositionChange($event)"
@@ -146,8 +159,10 @@ export type NzSelectSizeType = 'large' | 'default' | 'small';
146159
[itemSize]="nzOptionHeightPx"
147160
[maxItemLength]="nzOptionOverflowSize"
148161
[matchWidth]="nzDropdownMatchSelectWidth"
149-
[class.ant-select-dropdown-placement-bottomLeft]="dropDownPosition === 'bottom'"
150-
[class.ant-select-dropdown-placement-topLeft]="dropDownPosition === 'top'"
162+
[class.ant-select-dropdown-placement-bottomLeft]="dropDownPosition === 'bottomLeft'"
163+
[class.ant-select-dropdown-placement-topLeft]="dropDownPosition === 'topLeft'"
164+
[class.ant-select-dropdown-placement-bottomRight]="dropDownPosition === 'bottomRight'"
165+
[class.ant-select-dropdown-placement-topRight]="dropDownPosition === 'topRight'"
151166
[@slideMotion]="'enter'"
152167
[@.disabled]="noAnimation?.nzNoAnimation"
153168
[nzNoAnimation]="noAnimation?.nzNoAnimation"
@@ -205,6 +220,7 @@ export class NzSelectComponent implements ControlValueAccessor, OnInit, AfterCon
205220
@Input() nzDropdownStyle: { [key: string]: string } | null = null;
206221
@Input() nzNotFoundContent: string | TemplateRef<NzSafeAny> | undefined = undefined;
207222
@Input() nzPlaceHolder: string | TemplateRef<NzSafeAny> | null = null;
223+
@Input() nzPlacement: NzSelectPlacementType | null = null;
208224
@Input() nzMaxTagCount = Infinity;
209225
@Input() nzDropdownRender: TemplateRef<NzSafeAny> | null = null;
210226
@Input() nzCustomTemplate: TemplateRef<{ $implicit: NzSelectItemInterface }> | null = null;
@@ -264,14 +280,15 @@ export class NzSelectComponent implements ControlValueAccessor, OnInit, AfterCon
264280
private requestId: number = -1;
265281
onChange: OnChangeType = () => {};
266282
onTouched: OnTouchedType = () => {};
267-
dropDownPosition: 'top' | 'center' | 'bottom' = 'bottom';
283+
dropDownPosition: NzSelectPlacementType = 'bottomLeft';
268284
triggerWidth: number | null = null;
269285
listOfContainerItem: NzSelectItemInterface[] = [];
270286
listOfTopItem: NzSelectItemInterface[] = [];
271287
activatedValue: NzSafeAny | null = null;
272288
listOfValue: NzSafeAny[] = [];
273289
focused = false;
274290
dir: Direction = 'ltr';
291+
positions: ConnectionPositionPair[] = [];
275292

276293
// status
277294
prefixCls: string = 'ant-select';
@@ -500,7 +517,8 @@ export class NzSelectComponent implements ControlValueAccessor, OnInit, AfterCon
500517
}
501518

502519
onPositionChange(position: ConnectedOverlayPositionChange): void {
503-
this.dropDownPosition = position.connectionPair.originY;
520+
const placement = getPlacementName(position);
521+
this.dropDownPosition = placement as NzSelectPlacementType;
504522
}
505523

506524
updateCdkConnectedOverlayStatus(): void {
@@ -579,7 +597,7 @@ export class NzSelectComponent implements ControlValueAccessor, OnInit, AfterCon
579597
}
580598

581599
ngOnChanges(changes: SimpleChanges): void {
582-
const { nzOpen, nzDisabled, nzOptions, nzStatus } = changes;
600+
const { nzOpen, nzDisabled, nzOptions, nzStatus, nzPlacement } = changes;
583601
if (nzOpen) {
584602
this.onOpenChange();
585603
}
@@ -607,6 +625,16 @@ export class NzSelectComponent implements ControlValueAccessor, OnInit, AfterCon
607625
if (nzStatus) {
608626
this.setStatusStyles(this.nzStatus, this.hasFeedback);
609627
}
628+
if (nzPlacement) {
629+
const { currentValue } = nzPlacement;
630+
this.dropDownPosition = currentValue as NzSelectPlacementType;
631+
const listOfPlacement = ['bottomLeft', 'topLeft', 'bottomRight', 'topRight'];
632+
if (currentValue && listOfPlacement.includes(currentValue)) {
633+
this.positions = [POSITION_MAP[currentValue as POSITION_TYPE]];
634+
} else {
635+
this.positions = listOfPlacement.map(e => POSITION_MAP[e as POSITION_TYPE]);
636+
}
637+
}
610638
}
611639

612640
ngOnInit(): void {

components/select/select.spec.ts

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ import { NzSelectSearchComponent } from './select-search.component';
2020
import { NzSelectTopControlComponent } from './select-top-control.component';
2121
import { NzSelectComponent, NzSelectSizeType } from './select.component';
2222
import { NzSelectModule } from './select.module';
23-
import { NzFilterOptionType, NzSelectItemInterface, NzSelectOptionInterface } from './select.types';
23+
import {
24+
NzFilterOptionType,
25+
NzSelectItemInterface,
26+
NzSelectOptionInterface,
27+
NzSelectPlacementType
28+
} from './select.types';
2429

2530
describe('select', () => {
2631
describe('default template mode', () => {
@@ -1331,6 +1336,70 @@ describe('select', () => {
13311336
expect(selectElement.querySelector('nz-form-item-feedback-icon')).toBeNull();
13321337
});
13331338
});
1339+
describe('placement', () => {
1340+
let testBed: ComponentBed<TestSelectTemplateDefaultComponent>;
1341+
let component: TestSelectTemplateDefaultComponent;
1342+
let fixture: ComponentFixture<TestSelectTemplateDefaultComponent>;
1343+
let overlayContainerElement: HTMLElement;
1344+
1345+
beforeEach(() => {
1346+
testBed = createComponentBed(TestSelectTemplateDefaultComponent, {
1347+
imports: [NzSelectModule, NzIconTestModule, FormsModule]
1348+
});
1349+
component = testBed.component;
1350+
fixture = testBed.fixture;
1351+
});
1352+
1353+
beforeEach(inject([OverlayContainer], (oc: OverlayContainer) => {
1354+
overlayContainerElement = oc.getContainerElement();
1355+
}));
1356+
1357+
it('should nzPlacement work', fakeAsync(() => {
1358+
component.nzOpen = true;
1359+
fixture.detectChanges();
1360+
let element = overlayContainerElement.querySelector('.ant-select-dropdown') as HTMLElement;
1361+
expect(element.classList.contains('ant-select-dropdown-placement-bottomLeft')).toBe(true);
1362+
expect(element.classList.contains('ant-select-dropdown-placement-bottomRight')).toBe(false);
1363+
expect(element.classList.contains('ant-select-dropdown-placement-topLeft')).toBe(false);
1364+
expect(element.classList.contains('ant-select-dropdown-placement-topRight')).toBe(false);
1365+
component.nzOpen = false;
1366+
component.nzPlacement = 'bottomRight';
1367+
fixture.detectChanges();
1368+
component.nzOpen = true;
1369+
tick();
1370+
fixture.detectChanges();
1371+
element = overlayContainerElement.querySelector('.ant-select-dropdown') as HTMLElement;
1372+
expect(element.classList.contains('ant-select-dropdown-placement-bottomLeft')).toBe(false);
1373+
expect(element.classList.contains('ant-select-dropdown-placement-bottomRight')).toBe(true);
1374+
expect(element.classList.contains('ant-select-dropdown-placement-topLeft')).toBe(false);
1375+
expect(element.classList.contains('ant-select-dropdown-placement-topRight')).toBe(false);
1376+
component.nzOpen = false;
1377+
component.nzPlacement = 'topLeft';
1378+
fixture.detectChanges();
1379+
component.nzOpen = true;
1380+
tick();
1381+
fixture.detectChanges();
1382+
element = overlayContainerElement.querySelector('.ant-select-dropdown') as HTMLElement;
1383+
expect(element.classList.contains('ant-select-dropdown-placement-bottomLeft')).toBe(false);
1384+
expect(element.classList.contains('ant-select-dropdown-placement-bottomRight')).toBe(false);
1385+
expect(element.classList.contains('ant-select-dropdown-placement-topLeft')).toBe(true);
1386+
expect(element.classList.contains('ant-select-dropdown-placement-topRight')).toBe(false);
1387+
component.nzOpen = false;
1388+
component.nzPlacement = 'topRight';
1389+
fixture.detectChanges();
1390+
component.nzOpen = true;
1391+
tick();
1392+
fixture.detectChanges();
1393+
element = overlayContainerElement.querySelector('.ant-select-dropdown') as HTMLElement;
1394+
expect(element.classList.contains('ant-select-dropdown-placement-bottomLeft')).toBe(false);
1395+
expect(element.classList.contains('ant-select-dropdown-placement-bottomRight')).toBe(false);
1396+
expect(element.classList.contains('ant-select-dropdown-placement-topLeft')).toBe(false);
1397+
expect(element.classList.contains('ant-select-dropdown-placement-topRight')).toBe(true);
1398+
component.nzOpen = false;
1399+
fixture.detectChanges();
1400+
flush();
1401+
}));
1402+
});
13341403
});
13351404

13361405
@Component({
@@ -1357,6 +1426,7 @@ describe('select', () => {
13571426
[nzDisabled]="nzDisabled"
13581427
[nzBackdrop]="nzBackdrop"
13591428
[(nzOpen)]="nzOpen"
1429+
[nzPlacement]="nzPlacement"
13601430
(ngModelChange)="valueChange($event)"
13611431
(nzOnSearch)="searchValueChange($event)"
13621432
(nzOpenChange)="openChange($event)"
@@ -1418,6 +1488,7 @@ export class TestSelectTemplateDefaultComponent {
14181488
nzDisabled = false;
14191489
nzOpen = false;
14201490
nzBackdrop = false;
1491+
nzPlacement: NzSelectPlacementType | null = 'bottomLeft';
14211492
}
14221493

14231494
@Component({

components/select/select.types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,5 @@ export type NzSelectTopControlItemType = Partial<NzSelectItemInterface> & {
3434
};
3535

3636
export type NzFilterOptionType = (input: string, option: NzSelectItemInterface) => boolean;
37+
38+
export type NzSelectPlacementType = 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';

0 commit comments

Comments
 (0)