Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(module: select): support placement #7537

Merged
merged 1 commit into from Sep 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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';