From 97a512d7649c6bfd9865f4b3aa0c2a218610fa0c Mon Sep 17 00:00:00 2001 From: Wendell Date: Thu, 12 Jul 2018 21:00:50 +0800 Subject: [PATCH] feat(cascader): add cascader search close #1773 fix toggle transition fix lint another solution with arrow support fix lint add test --- .vscode/launch.json | 12 ++ components/cascader/demo/basic.ts | 3 +- components/cascader/demo/search.md | 14 ++ components/cascader/demo/search.ts | 113 ++++++++++++ components/cascader/doc/index.en-US.md | 8 + components/cascader/doc/index.zh-CN.md | 8 + .../cascader/nz-cascader.component.html | 15 +- components/cascader/nz-cascader.component.ts | 144 +++++++++++++-- components/cascader/nz-cascader.spec.ts | 170 +++++++++++++++++- components/cascader/style/index.less | 5 + 10 files changed, 474 insertions(+), 18 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 components/cascader/demo/search.md create mode 100644 components/cascader/demo/search.ts diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000000..12513a1b076 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Chrome", + "url": "http://localhost:9876", + "webRoot": "${workspaceRoot}" + } + ] +} \ No newline at end of file diff --git a/components/cascader/demo/basic.ts b/components/cascader/demo/basic.ts index 43bd60c3cbb..5cd1a260370 100644 --- a/components/cascader/demo/basic.ts +++ b/components/cascader/demo/basic.ts @@ -65,6 +65,7 @@ const otherOptions = [{ [(ngModel)]="values" (ngModelChange)="onChanges($event)"> +   Change Options @@ -92,7 +93,7 @@ export class NzDemoCascaderBasicComponent implements OnInit { ngOnInit(): void { // let's set nzOptions in a asynchronous way setTimeout(() => { - this.nzOptions = options; + this.nzOptions = options; }, 100); } diff --git a/components/cascader/demo/search.md b/components/cascader/demo/search.md new file mode 100644 index 00000000000..9b633396755 --- /dev/null +++ b/components/cascader/demo/search.md @@ -0,0 +1,14 @@ +--- +order: 11 +title: + zh-CN: 搜索 + en-US: Search +--- + +## zh-CN + +可以直接搜索选项并选择。 + +## en-US + +Search and select an option directly. diff --git a/components/cascader/demo/search.ts b/components/cascader/demo/search.ts new file mode 100644 index 00000000000..e9367131bce --- /dev/null +++ b/components/cascader/demo/search.ts @@ -0,0 +1,113 @@ +// tslint:disable:no-any +import { Component, OnInit } from '@angular/core'; + +const options = [ { + value: 'zhejiang', + label: 'Zhejiang', + children: [ { + value: 'hangzhou', + label: 'Hangzhou', + children: [ { + value: 'xihu', + label: 'West Lake', + isLeaf: true + } ] + }, { + value: 'ningbo', + label: 'Ningbo', + isLeaf: true, + disabled: true + } ] +}, { + value: 'jiangsu', + label: 'Jiangsu', + children: [ { + value: 'nanjing', + label: 'Nanjing', + children: [ { + value: 'zhonghuamen', + label: 'Zhong Hua Men', + isLeaf: true + } ] + } ] +} ]; + +const otherOptions = [ { + value: 'fujian', + label: 'Fujian', + children: [ { + value: 'xiamen', + label: 'Xiamen', + children: [ { + value: 'Kulangsu', + label: 'Kulangsu', + isLeaf: true + } ] + } ] +}, { + value: 'guangxi', + label: 'Guangxi', + children: [ { + value: 'guilin', + label: 'Guilin', + children: [ { + value: 'Lijiang', + label: 'Li Jiang River', + isLeaf: true + } ] + } ] +} ]; + +@Component({ + selector: 'nz-demo-cascader-search', + template: ` + + +   + + Change Options + + `, + styles: [ + ` + .ant-cascader-picker { + width: 300px; + } + .change-options { + display: inline-block; + font-size: 12px; + margin-top: 8px; + } + ` + ] +}) +export class NzDemoCascaderSearchComponent implements OnInit { + /** init data */ + public nzOptions = null; + + /** ngModel value */ + public values: any[] = null; + + ngOnInit(): void { + // let's set nzOptions in a asynchronous way + setTimeout(() => { + this.nzOptions = options; + }, 100); + } + + public changeNzOptions(): void { + if (this.nzOptions === options) { + this.nzOptions = otherOptions; + } else { + this.nzOptions = options; + } + } + + public onChanges(values: any): void { + console.log(values, this.values); + } +} diff --git a/components/cascader/doc/index.en-US.md b/components/cascader/doc/index.en-US.md index e5555693a07..91d8f7d28dc 100755 --- a/components/cascader/doc/index.en-US.md +++ b/components/cascader/doc/index.en-US.md @@ -39,6 +39,7 @@ Cascade selection box. | `[nzPlaceHolder]` | input placeholder | string | 'Please select' | | `[nzShowArrow]` | Whether show arrow | boolean | true | | `[nzShowInput]` | Whether show input | boolean | true | +| `[nzShowSearch]` | Whether support search | `boolean` `NzShowSearchOptions` | `false` | | `[nzSize]` | input size, one of `large` `default` `small` | string | `default` | | `[nzValueProperty]` | the value property name of options | string | 'value' | | `(ngModelChange)` | Emit on values change | `EventEmitter` | - | @@ -47,6 +48,13 @@ Cascade selection box. | `(nzSelect)` | Emit on select | `EventEmitter<{option: any, index: number}>` | - | | `(nzSelectionChange)` | Emit on selection change | `EventEmitter` | - | +When `nzShowSearch` is an object it should implements `NzShowSearchOptions`: + +| Params | Explanation | Type | Default | +| --- | --- | --- | --- | +| `filter` | Optional. Be aware that all non-leaf CascaderOptions would be filtered | `(inputValue: string, path: CascaderOption[]): boolean` | - | +| `sorter` | Optional | `(a: CascaderOption[], b: CascaderOption[], inputValue: string): number` | - | + #### Methods | Name | Description | diff --git a/components/cascader/doc/index.zh-CN.md b/components/cascader/doc/index.zh-CN.md index 075703f566d..f6d40a2e925 100755 --- a/components/cascader/doc/index.zh-CN.md +++ b/components/cascader/doc/index.zh-CN.md @@ -40,6 +40,7 @@ subtitle: 级联选择 | `[nzPlaceHolder]` | 输入框占位文本 | string | '请选择' | | `[nzShowArrow]` | 是否显示箭头 | boolean | true | | `[nzShowInput]` | 显示输入框 | boolean | true | +| `[nzShowSearch]` | 是否支持搜索,默认情况下对 `label` 进行全匹配搜索 | `boolean` `NzShowSearchOptions` | `false` | | `[nzSize]` | 输入框大小,可选 `large` `default` `small` | string | `default` | | `[nzValueProperty]` | 选项的实际值的属性名 | string | 'value' | | `(ngModelChange)` | 值发生变化时触发 | `EventEmitter` | - | @@ -48,6 +49,13 @@ subtitle: 级联选择 | `(nzSelect)` | 选中菜单选项时触发 | `EventEmitter<{option: any, index: number}>` | - | | `(nzSelectionChange)` | 选中菜单选项时触发 | `EventEmitter` |- | +`nzShowSearch` 为对象时需遵守 `NzShowSearchOptions` 接口: + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| `filter` | 可选,选择是否保留选项的过滤函数,每级菜单的选项都会被匹配 | `(inputValue: string, path: CascaderOption[]): boolean` | - | +| `sorter` | 可选,按照到每个最终选项的路径进行排序,默认按照原始数据的顺序 | `(a: CascaderOption[], b: CascaderOption[], inputValue: string): number` | - | + #### 方法 | 名称 | 描述 | diff --git a/components/cascader/nz-cascader.component.html b/components/cascader/nz-cascader.component.html index fc9bd1c7266..330d6ac6592 100644 --- a/components/cascader/nz-cascader.component.html +++ b/components/cascader/nz-cascader.component.html @@ -49,15 +49,24 @@ [ngClass]="menuCls" [ngStyle]="nzMenuStyle" [@dropDownAnimation]="dropDownPosition" (mouseleave)="onTriggerMouseLeave($event)"> -
    +
    • - {{ getOptionLabel(option) }} + + + + + {{ getOptionLabel(option) }} + +
    • +
    • + Not Found
    - \ No newline at end of file + diff --git a/components/cascader/nz-cascader.component.ts b/components/cascader/nz-cascader.component.ts index 3b160fb96e5..e6521e3c640 100644 --- a/components/cascader/nz-cascader.component.ts +++ b/components/cascader/nz-cascader.component.ts @@ -68,6 +68,15 @@ export interface CascaderOption { [ key: string ]: any; } +export interface CascaderSearchOption extends CascaderOption { + path: CascaderOption[]; +} + +export interface NzShowSearchOptions { + filter?(inputValue: string, path: CascaderOption[]): boolean; + sorter?(a: CascaderOption[], b: CascaderOption[], inputValue: string): number; +} + @Component({ selector : 'nz-cascader,[nz-cascader]', preserveWhitespaces: false, @@ -110,7 +119,7 @@ export class NzCascaderComponent implements OnInit, OnDestroy, ControlValueAcces private menuClassName; private columnClassName; private changeOnSelect = false; - // private showSearch = false; + private showSearch: boolean | NzShowSearchOptions; private defaultValue: any[]; public dropDownPosition = 'bottom'; @@ -157,6 +166,22 @@ export class NzCascaderComponent implements OnInit, OnDestroy, ControlValueAcces set inputValue(inputValue: string) { this._inputValue = inputValue; + + if (!this.inSearch) { + this.oldActivatedOptions = this.activatedOptions; + this.activatedOptions = []; + } else { + this.activatedOptions = this.oldActivatedOptions; + } + + this.inSearch = !!inputValue; + if (this.inSearch) { + this.searchWidthStyle = `${this.input.nativeElement.offsetWidth}px`; + this.prepareSearchValue(); + } else { + this.nzColumns = this.oldColumnsHolder; + this.searchWidthStyle = ''; + } this.setClassMap(); } @@ -229,16 +254,20 @@ export class NzCascaderComponent implements OnInit, OnDestroy, ControlValueAcces } /** Whether can search. Defaults to `false`. */ - - /* // not support yet @Input() - set nzShowSearch(value: boolean) { - this.showSearch = toBoolean(value); + set nzShowSearch(value: boolean | NzShowSearchOptions) { + this.showSearch = value; } - get nzShowSearch(): boolean { + get nzShowSearch(): boolean | NzShowSearchOptions { return this.showSearch; } - */ + + public searchWidthStyle: string; + private oldColumnsHolder; + private oldActivatedOptions; + + /** If cascader is in search mode. */ + public inSearch = false; /** Whether allow clear. Defaults to `true`. */ @Input() @@ -294,7 +323,7 @@ export class NzCascaderComponent implements OnInit, OnDestroy, ControlValueAcces /** Options for first column, sub column will be load async */ @Input() set nzOptions(options: CascaderOption[] | null) { - this.nzColumns = options && options.length ? [ options ] : []; + this.oldColumnsHolder = this.nzColumns = options && options.length ? [ options ] : []; if (this.defaultValue && this.nzColumns.length) { this.initOptions(0); } @@ -370,6 +399,7 @@ export class NzCascaderComponent implements OnInit, OnDestroy, ControlValueAcces /** Event: emit on the clear button clicked */ @Output() nzClear = new EventEmitter(); + @ViewChild('input') input: ElementRef; /** 浮层菜单 */ @ViewChild('menu') menu: ElementRef; @@ -404,6 +434,7 @@ export class NzCascaderComponent implements OnInit, OnDestroy, ControlValueAcces } this.isFocused = false; this.setClassMap(); + this.setLabelClass(); } } @@ -428,7 +459,9 @@ export class NzCascaderComponent implements OnInit, OnDestroy, ControlValueAcces private setLabelClass(): void { this._labelCls = { - [ `${this.prefixCls}-picker-label` ]: true + [ `${this.prefixCls}-picker-label` ]: true, + [ `${this.prefixCls}-show-search`]: !!this.nzShowSearch, + [ `${this.prefixCls}-focused`]: !!this.nzShowSearch && this.isFocused && !this._inputValue }; } @@ -543,6 +576,7 @@ export class NzCascaderComponent implements OnInit, OnDestroy, ControlValueAcces } */ this.focus(); + this.setLabelClass(); } private hasInput(): boolean { @@ -609,6 +643,14 @@ export class NzCascaderComponent implements OnInit, OnDestroy, ControlValueAcces return; } + if (this.inSearch && ( + keyCode === BACKSPACE || + keyCode === LEFT_ARROW || + keyCode === RIGHT_ARROW + )) { + return; + } + // Press any keys above to reopen menu if (!this.isMenuVisible() && keyCode !== BACKSPACE && @@ -644,6 +686,7 @@ export class NzCascaderComponent implements OnInit, OnDestroy, ControlValueAcces return; } this.onTouched(); // set your control to 'touched' + if (this.nzShowSearch) { this.focus(); } if (this.isClickTiggerAction()) { this.delaySetMenuVisible(!this.menuVisible, 100); @@ -697,6 +740,7 @@ export class NzCascaderComponent implements OnInit, OnDestroy, ControlValueAcces } public closeMenu(): void { + this.blur(); this.clearDelayTimer(); this.setMenuVisible(false); } @@ -897,7 +941,7 @@ export class NzCascaderComponent implements OnInit, OnDestroy, ControlValueAcces * @param event 鼠标事件 */ onOptionClick(option: CascaderOption, index: number, event: Event): void { - event.preventDefault(); + if (event) { event.preventDefault(); } // Keep focused state for keyboard support this.el.focus(); @@ -905,7 +949,12 @@ export class NzCascaderComponent implements OnInit, OnDestroy, ControlValueAcces if (option && option.disabled) { return; } - this.setActiveOption(option, index, true); + + if (this.inSearch) { + this.setSearchActiveOption(option as CascaderSearchOption, event); + } else { + this.setActiveOption(option, index, true); + } } /** 按下回车键时选择 */ @@ -913,7 +962,11 @@ export class NzCascaderComponent implements OnInit, OnDestroy, ControlValueAcces const columnIndex = Math.max(this.activatedOptions.length - 1, 0); const activeOption = this.activatedOptions[ columnIndex ]; if (activeOption && !activeOption.disabled) { - this.onSelectOption(activeOption, columnIndex); + if (this.inSearch) { + this.setSearchActiveOption(activeOption as CascaderSearchOption, null); + } else { + this.onSelectOption(activeOption, columnIndex); + } } } @@ -1134,6 +1187,73 @@ export class NzCascaderComponent implements OnInit, OnDestroy, ControlValueAcces this.nzDisabled = isDisabled; } + private prepareSearchValue(): void { + const results: CascaderSearchOption[] = []; + const path: CascaderOption[] = []; + const defaultFilter = (inputValue: string, p: CascaderOption[]): boolean => { + let flag = false; + p.forEach(n => { + if (n.label.indexOf(inputValue) > -1) { flag = true; } + }); + return flag; + }; + const filter: (inputValue: string, p: CascaderOption[]) => boolean = + this.nzShowSearch instanceof Object && (this.nzShowSearch as NzShowSearchOptions).filter ? + (this.nzShowSearch as NzShowSearchOptions).filter : + defaultFilter; + const sorter: (a: CascaderOption[], b: CascaderOption[], inputValue: string) => number = + this.nzShowSearch instanceof Object && (this.nzShowSearch as NzShowSearchOptions).sorter; + const loopParent = (node: CascaderOption, forceDisabled = false) => { + const disabled = forceDisabled || node.disabled; + path.push(node); + node.children.forEach((sNode) => { + if (!sNode.parent) { sNode.parent = node; } /** 搜索的同时建立 parent 连接,因为用户直接搜索的话是没有建立连接的,会提升从叶子节点回溯的难度 */ + if (!sNode.isLeaf) { loopParent(sNode, disabled); } + if (sNode.isLeaf || !sNode.children) { loopChild(sNode, disabled); } + }); + path.pop(); + }; + const loopChild = (node: CascaderOption, forceDisabled = false) => { + path.push(node); + const cPath = Array.from(path); + if (filter(this._inputValue, cPath)) { + const disabled = forceDisabled || node.disabled; + results.push({ + disabled, + isLeaf: true, + path: cPath, + label: cPath.map(p => p.label).join(' / ') + } as CascaderSearchOption); + } + path.pop(); + }; + + this.oldColumnsHolder[0].forEach(node => loopParent(node)); + if (sorter) { results.sort((a, b) => sorter(a.path, b.path, this._inputValue)); } + this.nzColumns = [ results ]; + } + + renderSearchString(str: string): string { + return str.replace(new RegExp(this._inputValue, 'g'), + `${this._inputValue}`); + } + + setSearchActiveOption(result: CascaderSearchOption, event: Event): void { + this.activatedOptions = [ result ]; + this.delaySetMenuVisible(false, 200); + + setTimeout(() => { + this.inputValue = ''; // Not only remove `inputValue` but also reverse `nzColumns` in the hook. + const index = result.path.length - 1; + const destiNode = result.path[index]; + const mockClickParent = (node: CascaderOption, cIndex: number) => { + if (node && node.parent) { mockClickParent(node.parent, cIndex - 1); } + this.onOptionClick(node, cIndex, event); + }; + mockClickParent(destiNode, index); + }, 300); + } + ngOnInit(): void { // 设置样式 this.setClassMap(); diff --git a/components/cascader/nz-cascader.spec.ts b/components/cascader/nz-cascader.spec.ts index 54c1002f5ae..228da7ffe8c 100644 --- a/components/cascader/nz-cascader.spec.ts +++ b/components/cascader/nz-cascader.spec.ts @@ -13,8 +13,7 @@ import { dispatchMouseEvent } from '../core/testing'; -// import { NzDemoCascaderBasicComponent } from './demo/basic'; -import { NzCascaderComponent } from './nz-cascader.component'; +import { CascaderOption, NzCascaderComponent, NzShowSearchOptions } from './nz-cascader.component'; import { NzCascaderModule } from './nz-cascader.module'; describe('cascader', () => { @@ -1309,6 +1308,140 @@ describe('cascader', () => { fixture.detectChanges(); expect(testComponent.cascader.dropDownPosition).toBe('bottom'); }); + it('should support search', (done) => { + fixture.detectChanges(); + testComponent.nzShowSearch = true; + testComponent.cascader.inputValue = 'o'; + testComponent.cascader.setMenuVisible(true); + fixture.detectChanges(); + const itemEl1 = overlayContainerElement.querySelector('.ant-cascader-menu:nth-child(1) .ant-cascader-menu-item:nth-child(1)') as HTMLElement; + expect(testComponent.cascader.inSearch).toBe(true); + expect(itemEl1.innerText).toBe('Zhejiang / Hangzhou / West Lake'); + itemEl1.click(); + fixture.whenStable().then(() => { + expect(testComponent.cascader.inSearch).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); + expect(testComponent.cascader.inputValue).toBe(''); + expect(testComponent.values.join(',')).toBe('zhejiang,hangzhou,xihu'); + done(); + }); + }); + it('should support custom filter', (done) => { + testComponent.nzShowSearch = { + filter(inputValue: string, path: CascaderOption[]): boolean { + let flag = false; + path.forEach(p => { + if (p.label.indexOf(inputValue) > -1) { flag = true; } + }); + return flag; + } + } as NzShowSearchOptions; + fixture.detectChanges(); + testComponent.cascader.inputValue = 'o'; + testComponent.cascader.setMenuVisible(true); + fixture.detectChanges(); + const itemEl1 = overlayContainerElement.querySelector('.ant-cascader-menu:nth-child(1) .ant-cascader-menu-item:nth-child(1)') as HTMLElement; + expect(testComponent.cascader.inSearch).toBe(true); + expect(itemEl1.innerText).toBe('Zhejiang / Hangzhou / West Lake'); + itemEl1.click(); + fixture.whenStable().then(() => { + expect(testComponent.cascader.inSearch).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); + expect(testComponent.cascader.inputValue).toBe(''); + expect(testComponent.values.join(',')).toBe('zhejiang,hangzhou,xihu'); + done(); + }); + }); + it('should support custom sorter', (done) => { + testComponent.nzShowSearch = { + sorter(a: CascaderOption[], b: CascaderOption[], inputValue: string): number { + return 1; // all reversed, just to be sure it works + } + } as NzShowSearchOptions; + fixture.detectChanges(); + testComponent.cascader.inputValue = 'o'; + testComponent.cascader.setMenuVisible(true); + fixture.detectChanges(); + const itemEl1 = overlayContainerElement.querySelector('.ant-cascader-menu:nth-child(1) .ant-cascader-menu-item:nth-child(1)') as HTMLElement; + expect(testComponent.cascader.inSearch).toBe(true); + expect(itemEl1.innerText).toBe('Jiangsu / Nanjing / Zhong Hua Men'); + itemEl1.click(); + fixture.whenStable().then(() => { + expect(testComponent.cascader.inSearch).toBe(false); + expect(testComponent.cascader.menuVisible).toBe(false); + expect(testComponent.cascader.inputValue).toBe(''); + expect(testComponent.values.join(',')).toBe('jiangsu,nanjing,zhonghuamen'); + done(); + }); + }); + it('should forbid disabled search options to be clicked', fakeAsync(() => { + testComponent.nzOptions = options4; + fixture.detectChanges(); + testComponent.cascader.inputValue = 'o'; + testComponent.cascader.setMenuVisible(true); + fixture.detectChanges(); + const itemEl1 = overlayContainerElement.querySelector('.ant-cascader-menu:nth-child(1) .ant-cascader-menu-item:nth-child(1)') as HTMLElement; + expect(itemEl1.innerText).toBe('Zhejiang / Hangzhou / West Lake'); + expect(testComponent.cascader.nzColumns[0][0].disabled).toBe(true); + itemEl1.click(); + tick(300); + fixture.detectChanges(); + expect(testComponent.cascader.inSearch).toBe(true); + expect(testComponent.cascader.menuVisible).toBe(true); + expect(testComponent.cascader.inputValue).toBe('o'); + expect(testComponent.values).toBe(null); + })); + it('should pass disabled property to children when searching', () => { + testComponent.nzOptions = options4; + fixture.detectChanges(); + testComponent.cascader.inputValue = 'o'; + testComponent.cascader.setMenuVisible(true); + fixture.detectChanges(); + expect(testComponent.cascader.nzColumns[0][0].disabled).toBe(true); + expect(testComponent.cascader.nzColumns[0][1].disabled).toBe(undefined); + expect(testComponent.cascader.nzColumns[0][2].disabled).toBe(true); + }); + it('should support arrow in search mode', (done) => { + const DOWN_ARROW = 40; + const ENTER = 13; + testComponent.nzOptions = options2; + fixture.detectChanges(); + testComponent.cascader.inputValue = 'o'; + testComponent.cascader.setMenuVisible(true); + fixture.detectChanges(); + const itemEl2 = overlayContainerElement.querySelector('.ant-cascader-menu:nth-child(1) .ant-cascader-menu-item:nth-child(2)') as HTMLElement; + const itemEl4 = overlayContainerElement.querySelector('.ant-cascader-menu:nth-child(1) .ant-cascader-menu-item:nth-child(4)') as HTMLElement; + dispatchKeyboardEvent(cascader.nativeElement, 'keydown', DOWN_ARROW); + fixture.detectChanges(); + expect(itemEl2.classList).toContain('ant-cascader-menu-item-active'); + dispatchKeyboardEvent(cascader.nativeElement, 'keydown', DOWN_ARROW); + fixture.detectChanges(); + expect(itemEl2.classList).not.toContain('ant-cascader-menu-item-active'); + expect(itemEl4.classList).toContain('ant-cascader-menu-item-active'); + dispatchKeyboardEvent(cascader.nativeElement, 'keydown', ENTER); + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(testComponent.values.join(',')).toBe('option1,option14'); + done(); + }); + }); + // How can I test BACKSPACE? + it('should not preventDefault left/right arrow in search mode', () => { + const LEFT_ARROW = 37; + const RIGHT_ARROW = 39; + fixture.detectChanges(); + testComponent.nzShowSearch = true; + testComponent.cascader.inputValue = 'o'; + testComponent.cascader.setMenuVisible(true); + fixture.detectChanges(); + dispatchKeyboardEvent(cascader.nativeElement, 'keydown', LEFT_ARROW); + const itemEl1 = overlayContainerElement.querySelector('.ant-cascader-menu:nth-child(1) .ant-cascader-menu-item:nth-child(1)') as HTMLElement; + fixture.detectChanges(); + expect(itemEl1.classList).not.toContain('ant-cascader-menu-item-active'); + dispatchKeyboardEvent(cascader.nativeElement, 'keydown', RIGHT_ARROW); + fixture.detectChanges(); + expect(itemEl1.classList).not.toContain('ant-cascader-menu-item-active'); + }); }); describe('load data lazily', () => { @@ -1533,6 +1666,37 @@ const options3 = [ { } ] } ]; +const options4 = [ { + value: 'zhejiang', + label: 'Zhejiang', + children: [ { + value: 'hangzhou', + label: 'Hangzhou', + disabled: true, + children: [ { + value: 'xihu', + label: 'West Lake', + isLeaf: true + } ] + }, { + value: 'ningbo', + label: 'Ningbo', + isLeaf: true + } ] +}, { + value: 'jiangsu', + label: 'Jiangsu', + disabled: true, + children: [ { + value: 'nanjing', + label: 'Nanjing', + children: [ { + value: 'zhonghuamen', + label: 'Zhong Hua Men', + isLeaf: true + } ] + } ] +} ]; @Component({ selector: 'nz-demo-cascader-default', template: ` @@ -1553,6 +1717,7 @@ const options3 = [ { [nzPrefixCls]="nzPrefixCls" [nzShowArrow]="nzShowArrow" [nzShowInput]="nzShowInput" + [nzShowSearch]="nzShowSearch" [nzSize]="nzSize" [nzTriggerAction]="nzTriggerAction" [nzMouseEnterDelay]="nzMouseEnterDelay" @@ -1597,6 +1762,7 @@ export class NzDemoCascaderDefaultComponent { nzPrefixCls = 'ant-cascader'; nzShowArrow = true; nzShowInput = true; + nzShowSearch: boolean | NzShowSearchOptions = false; nzSize = 'default'; nzLabelRender = null; nzChangeOn = null; diff --git a/components/cascader/style/index.less b/components/cascader/style/index.less index 73c0b32b627..65b72b46517 100644 --- a/components/cascader/style/index.less +++ b/components/cascader/style/index.less @@ -21,6 +21,10 @@ position: relative; } + &-show-search&-focused { + color: @disabled-color; + } + &-picker { .reset-component; position: relative; @@ -29,6 +33,7 @@ background-color: @component-background; border-radius: @border-radius-base; outline: 0; + transition: color .3s; &-with-value &-label { color: transparent;