diff --git a/components/select/demo/placement.md b/components/select/demo/placement.md
new file mode 100644
index 0000000000..c6874cd59b
--- /dev/null
+++ b/components/select/demo/placement.md
@@ -0,0 +1,14 @@
+---
+order: 25
+title:
+ zh-CN: 弹出位置
+ en-US: Placement
+---
+
+## zh-CN
+
+可以通过 `placement` 手动指定弹出的位置。
+
+## en-US
+
+You can manually specify the position of the popup via `placement`.
\ No newline at end of file
diff --git a/components/select/demo/placement.ts b/components/select/demo/placement.ts
new file mode 100644
index 0000000000..0fd5d051c4
--- /dev/null
+++ b/components/select/demo/placement.ts
@@ -0,0 +1,33 @@
+import { Component } from '@angular/core';
+
+import { NzSelectPlacementType } from 'ng-zorro-antd/select';
+
+@Component({
+ selector: 'nz-demo-select-placement',
+ template: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ styles: [
+ `
+ nz-select {
+ width: 120px;
+ }
+ `
+ ]
+})
+export class NzDemoSelectPlacementComponent {
+ placement: NzSelectPlacementType = 'topLeft';
+ selectedValue = 'HangZhou';
+}
diff --git a/components/select/doc/index.en-US.md b/components/select/doc/index.en-US.md
index 6fb7f61d89..cf74d8e65c 100644
--- a/components/select/doc/index.en-US.md
+++ b/components/select/doc/index.en-US.md
@@ -48,6 +48,7 @@ import { NzSelectModule } from 'ng-zorro-antd/select';
| `[nzMode]` | Set mode of Select | `'multiple' \| 'tags' \| 'default'` | `'default'` |
| `[nzNotFoundContent]` | Specify content to show when no result matches.. | `string \| TemplateRef` | `'Not Found'` |
| `[nzPlaceHolder]` | Placeholder of select | `string` | - |
+| `[nzPlacement]` | The position where the selection box pops up | `'bottomLeft' \| 'bottomRight' \| 'topLeft' \| 'topRight'` | `'bottomLeft'` |
| `[nzShowArrow]` | Whether to show the drop-down arrow | `boolean` | `true`(for single select), `false`(for multiple select) |
| `[nzShowSearch]` | Whether show search input in single mode. | `boolean` | `false` |
| `[nzSize]` | Size of Select input | `'large' \| 'small' \| 'default'` | `'default'` |
diff --git a/components/select/doc/index.zh-CN.md b/components/select/doc/index.zh-CN.md
index de168777a4..9e05c6bdaa 100644
--- a/components/select/doc/index.zh-CN.md
+++ b/components/select/doc/index.zh-CN.md
@@ -49,6 +49,7 @@ import { NzSelectModule } from 'ng-zorro-antd/select';
| `[nzMode]` | 设置 nz-select 的模式 | `'multiple' \| 'tags' \| 'default'` | `'default'` |
| `[nzNotFoundContent]` | 当下拉列表为空时显示的内容 | `string \| TemplateRef` | - |
| `[nzPlaceHolder]` | 选择框默认文字 | `string` | - |
+| `[nzPlacement]` | 选择框弹出的位置 | `'bottomLeft' \| 'bottomRight' \| 'topLeft' \| 'topRight'` | `'bottomLeft'` |
| `[nzShowArrow]` | 是否显示下拉小箭头 | `boolean` | 单选为 `true`,多选为 `false` |
| `[nzShowSearch]` | 使单选模式可搜索 | `boolean` | `false` |
| `[nzSize]` | 选择框大小 | `'large' \| 'small' \| 'default'` | `'default'` |
diff --git a/components/select/select.component.ts b/components/select/select.component.ts
index 4130fbfd93..7aa74d855c 100644
--- a/components/select/select.component.ts
+++ b/components/select/select.component.ts
@@ -6,7 +6,12 @@
import { FocusMonitor } from '@angular/cdk/a11y';
import { Direction, Directionality } from '@angular/cdk/bidi';
import { DOWN_ARROW, ENTER, ESCAPE, SPACE, TAB, UP_ARROW } from '@angular/cdk/keycodes';
-import { CdkConnectedOverlay, CdkOverlayOrigin, ConnectedOverlayPositionChange } from '@angular/cdk/overlay';
+import {
+ CdkConnectedOverlay,
+ CdkOverlayOrigin,
+ ConnectedOverlayPositionChange,
+ ConnectionPositionPair
+} from '@angular/cdk/overlay';
import { Platform } from '@angular/cdk/platform';
import {
AfterContentInit,
@@ -40,6 +45,7 @@ import { slideMotion } from 'ng-zorro-antd/core/animation';
import { NzConfigKey, NzConfigService, WithConfig } from 'ng-zorro-antd/core/config';
import { NzFormNoStatusService, NzFormStatusService } from 'ng-zorro-antd/core/form';
import { NzNoAnimationDirective } from 'ng-zorro-antd/core/no-animation';
+import { getPlacementName, POSITION_MAP, POSITION_TYPE } from 'ng-zorro-antd/core/overlay';
import { cancelRequestAnimationFrame, reqAnimFrame } from 'ng-zorro-antd/core/polyfill';
import { NzDestroyService } from 'ng-zorro-antd/core/services';
import {
@@ -56,7 +62,13 @@ import { getStatusClassNames, InputBoolean, isNotNil } from 'ng-zorro-antd/core/
import { NzOptionGroupComponent } from './option-group.component';
import { NzOptionComponent } from './option.component';
import { NzSelectTopControlComponent } from './select-top-control.component';
-import { NzFilterOptionType, NzSelectItemInterface, NzSelectModeType, NzSelectOptionInterface } from './select.types';
+import {
+ NzFilterOptionType,
+ NzSelectItemInterface,
+ NzSelectModeType,
+ NzSelectOptionInterface,
+ NzSelectPlacementType
+} from './select.types';
const defaultFilterOption: NzFilterOptionType = (searchValue: string, item: NzSelectItemInterface): boolean => {
if (item && item.nzLabel) {
@@ -137,6 +149,7 @@ export type NzSelectSizeType = 'large' | 'default' | 'small';
[cdkConnectedOverlayTransformOriginOn]="'.ant-select-dropdown'"
[cdkConnectedOverlayPanelClass]="nzDropdownClassName!"
[cdkConnectedOverlayOpen]="nzOpen"
+ [cdkConnectedOverlayPositions]="positions"
(overlayOutsideClick)="onClickOutside($event)"
(detach)="setOpenState(false)"
(positionChange)="onPositionChange($event)"
@@ -146,8 +159,10 @@ export type NzSelectSizeType = 'large' | 'default' | 'small';
[itemSize]="nzOptionHeightPx"
[maxItemLength]="nzOptionOverflowSize"
[matchWidth]="nzDropdownMatchSelectWidth"
- [class.ant-select-dropdown-placement-bottomLeft]="dropDownPosition === 'bottom'"
- [class.ant-select-dropdown-placement-topLeft]="dropDownPosition === 'top'"
+ [class.ant-select-dropdown-placement-bottomLeft]="dropDownPosition === 'bottomLeft'"
+ [class.ant-select-dropdown-placement-topLeft]="dropDownPosition === 'topLeft'"
+ [class.ant-select-dropdown-placement-bottomRight]="dropDownPosition === 'bottomRight'"
+ [class.ant-select-dropdown-placement-topRight]="dropDownPosition === 'topRight'"
[@slideMotion]="'enter'"
[@.disabled]="noAnimation?.nzNoAnimation"
[nzNoAnimation]="noAnimation?.nzNoAnimation"
@@ -205,6 +220,7 @@ export class NzSelectComponent implements ControlValueAccessor, OnInit, AfterCon
@Input() nzDropdownStyle: { [key: string]: string } | null = null;
@Input() nzNotFoundContent: string | TemplateRef | undefined = undefined;
@Input() nzPlaceHolder: string | TemplateRef | null = null;
+ @Input() nzPlacement: NzSelectPlacementType | null = null;
@Input() nzMaxTagCount = Infinity;
@Input() nzDropdownRender: TemplateRef | null = null;
@Input() nzCustomTemplate: TemplateRef<{ $implicit: NzSelectItemInterface }> | null = null;
@@ -264,7 +280,7 @@ export class NzSelectComponent implements ControlValueAccessor, OnInit, AfterCon
private requestId: number = -1;
onChange: OnChangeType = () => {};
onTouched: OnTouchedType = () => {};
- dropDownPosition: 'top' | 'center' | 'bottom' = 'bottom';
+ dropDownPosition: NzSelectPlacementType = 'bottomLeft';
triggerWidth: number | null = null;
listOfContainerItem: NzSelectItemInterface[] = [];
listOfTopItem: NzSelectItemInterface[] = [];
@@ -272,6 +288,7 @@ export class NzSelectComponent implements ControlValueAccessor, OnInit, AfterCon
listOfValue: NzSafeAny[] = [];
focused = false;
dir: Direction = 'ltr';
+ positions: ConnectionPositionPair[] = [];
// status
prefixCls: string = 'ant-select';
@@ -500,7 +517,8 @@ export class NzSelectComponent implements ControlValueAccessor, OnInit, AfterCon
}
onPositionChange(position: ConnectedOverlayPositionChange): void {
- this.dropDownPosition = position.connectionPair.originY;
+ const placement = getPlacementName(position);
+ this.dropDownPosition = placement as NzSelectPlacementType;
}
updateCdkConnectedOverlayStatus(): void {
@@ -579,7 +597,7 @@ export class NzSelectComponent implements ControlValueAccessor, OnInit, AfterCon
}
ngOnChanges(changes: SimpleChanges): void {
- const { nzOpen, nzDisabled, nzOptions, nzStatus } = changes;
+ const { nzOpen, nzDisabled, nzOptions, nzStatus, nzPlacement } = changes;
if (nzOpen) {
this.onOpenChange();
}
@@ -607,6 +625,16 @@ export class NzSelectComponent implements ControlValueAccessor, OnInit, AfterCon
if (nzStatus) {
this.setStatusStyles(this.nzStatus, this.hasFeedback);
}
+ if (nzPlacement) {
+ const { currentValue } = nzPlacement;
+ this.dropDownPosition = currentValue as NzSelectPlacementType;
+ 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]);
+ }
+ }
}
ngOnInit(): void {
diff --git a/components/select/select.spec.ts b/components/select/select.spec.ts
index b3d93ad45b..b3dff48306 100644
--- a/components/select/select.spec.ts
+++ b/components/select/select.spec.ts
@@ -20,7 +20,12 @@ import { NzSelectSearchComponent } from './select-search.component';
import { NzSelectTopControlComponent } from './select-top-control.component';
import { NzSelectComponent, NzSelectSizeType } from './select.component';
import { NzSelectModule } from './select.module';
-import { NzFilterOptionType, NzSelectItemInterface, NzSelectOptionInterface } from './select.types';
+import {
+ NzFilterOptionType,
+ NzSelectItemInterface,
+ NzSelectOptionInterface,
+ NzSelectPlacementType
+} from './select.types';
describe('select', () => {
describe('default template mode', () => {
@@ -1331,6 +1336,70 @@ describe('select', () => {
expect(selectElement.querySelector('nz-form-item-feedback-icon')).toBeNull();
});
});
+ describe('placement', () => {
+ let testBed: ComponentBed;
+ let component: TestSelectTemplateDefaultComponent;
+ let fixture: ComponentFixture;
+ let overlayContainerElement: HTMLElement;
+
+ beforeEach(() => {
+ testBed = createComponentBed(TestSelectTemplateDefaultComponent, {
+ imports: [NzSelectModule, NzIconTestModule, FormsModule]
+ });
+ component = testBed.component;
+ fixture = testBed.fixture;
+ });
+
+ beforeEach(inject([OverlayContainer], (oc: OverlayContainer) => {
+ overlayContainerElement = oc.getContainerElement();
+ }));
+
+ it('should nzPlacement work', fakeAsync(() => {
+ component.nzOpen = 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);
+ component.nzOpen = false;
+ component.nzPlacement = 'bottomRight';
+ fixture.detectChanges();
+ component.nzOpen = true;
+ tick();
+ fixture.detectChanges();
+ 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);
+ component.nzOpen = false;
+ component.nzPlacement = 'topLeft';
+ fixture.detectChanges();
+ component.nzOpen = true;
+ tick();
+ fixture.detectChanges();
+ 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);
+ component.nzOpen = false;
+ component.nzPlacement = 'topRight';
+ fixture.detectChanges();
+ component.nzOpen = true;
+ tick();
+ fixture.detectChanges();
+ 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);
+ component.nzOpen = false;
+ fixture.detectChanges();
+ flush();
+ }));
+ });
});
@Component({
@@ -1357,6 +1426,7 @@ describe('select', () => {
[nzDisabled]="nzDisabled"
[nzBackdrop]="nzBackdrop"
[(nzOpen)]="nzOpen"
+ [nzPlacement]="nzPlacement"
(ngModelChange)="valueChange($event)"
(nzOnSearch)="searchValueChange($event)"
(nzOpenChange)="openChange($event)"
@@ -1418,6 +1488,7 @@ export class TestSelectTemplateDefaultComponent {
nzDisabled = false;
nzOpen = false;
nzBackdrop = false;
+ nzPlacement: NzSelectPlacementType | null = 'bottomLeft';
}
@Component({
diff --git a/components/select/select.types.ts b/components/select/select.types.ts
index 25edc7ec06..ea53766ff3 100644
--- a/components/select/select.types.ts
+++ b/components/select/select.types.ts
@@ -34,3 +34,5 @@ export type NzSelectTopControlItemType = Partial & {
};
export type NzFilterOptionType = (input: string, option: NzSelectItemInterface) => boolean;
+
+export type NzSelectPlacementType = 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';