Skip to content

Commit

Permalink
feat(module: select): support placement (#7537)
Browse files Browse the repository at this point in the history
Co-authored-by: luolei <luolei@kuaishou.com>
  • Loading branch information
rorry121 and luolei committed Sep 26, 2022
1 parent 1f10a9c commit dda0e6d
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 8 deletions.
14 changes: 14 additions & 0 deletions 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`.
33 changes: 33 additions & 0 deletions 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: `
<nz-radio-group [(ngModel)]="placement">
<label nz-radio-button nzValue="topLeft">topLeft</label>
<label nz-radio-button nzValue="topRight">topRight</label>
<label nz-radio-button nzValue="bottomLeft">bottomLeft</label>
<label nz-radio-button nzValue="bottomRight">bottomRight</label>
</nz-radio-group>
<br />
<br />
<nz-select [(ngModel)]="selectedValue" [nzDropdownMatchSelectWidth]="false" [nzPlacement]="placement">
<nz-option nzValue="HangZhou" nzLabel="HangZhou #310000"></nz-option>
<nz-option nzValue="NingBo" nzLabel="NingBo #315000"></nz-option>
<nz-option nzValue="WenZhou" nzLabel="WenZhou #325000"></nz-option>
</nz-select>
`,
styles: [
`
nz-select {
width: 120px;
}
`
]
})
export class NzDemoSelectPlacementComponent {
placement: NzSelectPlacementType = 'topLeft';
selectedValue = 'HangZhou';
}
1 change: 1 addition & 0 deletions components/select/doc/index.en-US.md
Expand Up @@ -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<void>` | `'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'` |
Expand Down
1 change: 1 addition & 0 deletions components/select/doc/index.zh-CN.md
Expand Up @@ -49,6 +49,7 @@ import { NzSelectModule } from 'ng-zorro-antd/select';
| `[nzMode]` | 设置 nz-select 的模式 | `'multiple' \| 'tags' \| 'default'` | `'default'` |
| `[nzNotFoundContent]` | 当下拉列表为空时显示的内容 | `string \| TemplateRef<void>` | - |
| `[nzPlaceHolder]` | 选择框默认文字 | `string` | - |
| `[nzPlacement]` | 选择框弹出的位置 | `'bottomLeft' \| 'bottomRight' \| 'topLeft' \| 'topRight'` | `'bottomLeft'` |
| `[nzShowArrow]` | 是否显示下拉小箭头 | `boolean` | 单选为 `true`,多选为 `false` |
| `[nzShowSearch]` | 使单选模式可搜索 | `boolean` | `false` |
| `[nzSize]` | 选择框大小 | `'large' \| 'small' \| 'default'` | `'default'` |
Expand Down
42 changes: 35 additions & 7 deletions components/select/select.component.ts
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand All @@ -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) {
Expand Down Expand Up @@ -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)"
Expand All @@ -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"
Expand Down Expand Up @@ -205,6 +220,7 @@ export class NzSelectComponent implements ControlValueAccessor, OnInit, AfterCon
@Input() nzDropdownStyle: { [key: string]: string } | null = null;
@Input() nzNotFoundContent: string | TemplateRef<NzSafeAny> | undefined = undefined;
@Input() nzPlaceHolder: string | TemplateRef<NzSafeAny> | null = null;
@Input() nzPlacement: NzSelectPlacementType | null = null;
@Input() nzMaxTagCount = Infinity;
@Input() nzDropdownRender: TemplateRef<NzSafeAny> | null = null;
@Input() nzCustomTemplate: TemplateRef<{ $implicit: NzSelectItemInterface }> | null = null;
Expand Down Expand Up @@ -264,14 +280,15 @@ 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[] = [];
activatedValue: NzSafeAny | null = null;
listOfValue: NzSafeAny[] = [];
focused = false;
dir: Direction = 'ltr';
positions: ConnectionPositionPair[] = [];

// status
prefixCls: string = 'ant-select';
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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 {
Expand Down
73 changes: 72 additions & 1 deletion components/select/select.spec.ts
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -1331,6 +1336,70 @@ describe('select', () => {
expect(selectElement.querySelector('nz-form-item-feedback-icon')).toBeNull();
});
});
describe('placement', () => {
let testBed: ComponentBed<TestSelectTemplateDefaultComponent>;
let component: TestSelectTemplateDefaultComponent;
let fixture: ComponentFixture<TestSelectTemplateDefaultComponent>;
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({
Expand All @@ -1357,6 +1426,7 @@ describe('select', () => {
[nzDisabled]="nzDisabled"
[nzBackdrop]="nzBackdrop"
[(nzOpen)]="nzOpen"
[nzPlacement]="nzPlacement"
(ngModelChange)="valueChange($event)"
(nzOnSearch)="searchValueChange($event)"
(nzOpenChange)="openChange($event)"
Expand Down Expand Up @@ -1418,6 +1488,7 @@ export class TestSelectTemplateDefaultComponent {
nzDisabled = false;
nzOpen = false;
nzBackdrop = false;
nzPlacement: NzSelectPlacementType | null = 'bottomLeft';
}

@Component({
Expand Down
2 changes: 2 additions & 0 deletions components/select/select.types.ts
Expand Up @@ -34,3 +34,5 @@ export type NzSelectTopControlItemType = Partial<NzSelectItemInterface> & {
};

export type NzFilterOptionType = (input: string, option: NzSelectItemInterface) => boolean;

export type NzSelectPlacementType = 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';

0 comments on commit dda0e6d

Please sign in to comment.