From dda0e6d6b8e0abba46946a6ba04142500ba38328 Mon Sep 17 00:00:00 2001 From: rorry121 Date: Mon, 26 Sep 2022 10:26:01 +0800 Subject: [PATCH] feat(module: select): support placement (#7537) Co-authored-by: luolei --- components/select/demo/placement.md | 14 +++++ components/select/demo/placement.ts | 33 ++++++++++++ components/select/doc/index.en-US.md | 1 + components/select/doc/index.zh-CN.md | 1 + components/select/select.component.ts | 42 ++++++++++++--- components/select/select.spec.ts | 73 ++++++++++++++++++++++++++- components/select/select.types.ts | 2 + 7 files changed, 158 insertions(+), 8 deletions(-) create mode 100644 components/select/demo/placement.md create mode 100644 components/select/demo/placement.ts 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';