diff --git a/components/select/demo/placement.md b/components/select/demo/placement.md new file mode 100644 index 00000000000..c6874cd59bf --- /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 00000000000..0fd5d051c4a --- /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 6fb7f61d891..cf74d8e65c3 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 de168777a46..9e05c6bdaab 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 4130fbfd93e..7aa74d855c8 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 b3d93ad45bd..6e11423f8f7 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', () => { @@ -244,6 +249,53 @@ describe('select', () => { fixture.detectChanges(); expect(selectElement.querySelector('input')!.attributes.getNamedItem('autofocus')).toBeFalsy(); }); + + 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(16); + 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(16); + 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(16); + 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(); + })); + it('should nzServerSearch works', fakeAsync(() => { component.listOfOption = [ { nzValue: '1', nzLabel: '1' }, @@ -1357,6 +1409,7 @@ describe('select', () => { [nzDisabled]="nzDisabled" [nzBackdrop]="nzBackdrop" [(nzOpen)]="nzOpen" + [nzPlacement]="nzPlacement" (ngModelChange)="valueChange($event)" (nzOnSearch)="searchValueChange($event)" (nzOpenChange)="openChange($event)" @@ -1418,6 +1471,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 25edc7ec06d..ea53766ff34 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';