diff --git a/docs/demo/search.md b/docs/demo/search.md new file mode 100644 index 00000000..d6a5e18f --- /dev/null +++ b/docs/demo/search.md @@ -0,0 +1,8 @@ +--- +title: search +nav: + title: Demo + path: /demo +--- + + diff --git a/examples/search.tsx b/examples/search.tsx new file mode 100644 index 00000000..8cfe19c0 --- /dev/null +++ b/examples/search.tsx @@ -0,0 +1,75 @@ +/* eslint-disable no-console */ +import React from 'react'; +import '../assets/index.less'; +import Cascader from '../src'; + +const addressOptions = [ + { + label: '福建', + value: 'fj', + children: [ + { + label: '福州', + value: 'fuzhou', + children: [ + { + label: '马尾', + value: 'mawei', + }, + ], + }, + { + label: '泉州', + value: 'quanzhou', + }, + ], + }, + { + label: '浙江', + value: 'zj', + children: [ + { + label: '杭州', + value: 'hangzhou', + children: [ + { + label: '余杭', + value: 'yuhang', + }, + { + label: '福州', + value: 'fuzhou', + children: [ + { + label: '马尾', + value: 'mawei', + }, + ], + }, + ], + }, + ], + }, + { + label: '北京', + value: 'bj', + children: [ + { + label: '朝阳区', + value: 'chaoyang', + }, + { + label: '海淀区', + value: 'haidian', + }, + ], + }, +]; + +class Demo extends React.Component { + render() { + return ; + } +} + +export default Demo; diff --git a/src/OptionList/Column.tsx b/src/OptionList/Column.tsx index a96a767e..39c46604 100644 --- a/src/OptionList/Column.tsx +++ b/src/OptionList/Column.tsx @@ -138,7 +138,8 @@ export default function Column({ key={fullPathKey} className={classNames(menuItemPrefixCls, { [`${menuItemPrefixCls}-expand`]: !isMergedLeaf, - [`${menuItemPrefixCls}-active`]: activeValue === value, + [`${menuItemPrefixCls}-active`]: + activeValue === value || activeValue === fullPathKey, [`${menuItemPrefixCls}-disabled`]: disabled, [`${menuItemPrefixCls}-loading`]: isLoading, })} diff --git a/src/OptionList/index.tsx b/src/OptionList/index.tsx index 05f24860..769834cc 100644 --- a/src/OptionList/index.tsx +++ b/src/OptionList/index.tsx @@ -6,6 +6,7 @@ import * as React from 'react'; import type { DefaultOptionType, SingleValueType } from '../Cascader'; import CascaderContext from '../context'; import { + getFullPathKeys, isLeaf, scrollIntoParentView, toPathKey, @@ -122,10 +123,14 @@ const RefOptionList = React.forwardRef((props, ref) => { const optionList = [{ options: mergedOptions }]; let currentList = mergedOptions; + const fullPathKeys = getFullPathKeys(currentList, fieldNames); + for (let i = 0; i < activeValueCells.length; i += 1) { const activeValueCell = activeValueCells[i]; const currentOption = currentList.find( - option => option[fieldNames.value] === activeValueCell, + (option, index) => + (fullPathKeys[index] ? toPathKey(fullPathKeys[index]) : option[fieldNames.value]) === + activeValueCell, ); const subOptions = currentOption?.[fieldNames.children]; diff --git a/src/OptionList/useKeyboard.ts b/src/OptionList/useKeyboard.ts index 19070baf..6131a862 100644 --- a/src/OptionList/useKeyboard.ts +++ b/src/OptionList/useKeyboard.ts @@ -1,9 +1,10 @@ -import * as React from 'react'; -import type { RefOptionListProps } from 'rc-select/lib/OptionList'; import { useBaseProps } from 'rc-select'; +import type { RefOptionListProps } from 'rc-select/lib/OptionList'; import KeyCode from 'rc-util/lib/KeyCode'; +import * as React from 'react'; import type { DefaultOptionType, InternalFieldNames, SingleValueType } from '../Cascader'; import { SEARCH_MARK } from '../hooks/useSearchOptions'; +import { getFullPathKeys, toPathKey } from '../utils/commonUtil'; export default ( ref: React.Ref, @@ -16,41 +17,46 @@ export default ( const { direction, searchValue, toggleOpen, open } = useBaseProps(); const rtl = direction === 'rtl'; - const [validActiveValueCells, lastActiveIndex, lastActiveOptions] = React.useMemo(() => { - let activeIndex = -1; - let currentOptions = options; + const [validActiveValueCells, lastActiveIndex, lastActiveOptions, fullPathKeys] = + React.useMemo(() => { + let activeIndex = -1; + let currentOptions = options; - const mergedActiveIndexes: number[] = []; - const mergedActiveValueCells: React.Key[] = []; + const mergedActiveIndexes: number[] = []; + const mergedActiveValueCells: React.Key[] = []; - const len = activeValueCells.length; + const len = activeValueCells.length; - // Fill validate active value cells and index - for (let i = 0; i < len && currentOptions; i += 1) { - // Mark the active index for current options - const nextActiveIndex = currentOptions.findIndex( - option => option[fieldNames.value] === activeValueCells[i], - ); + const pathKeys = getFullPathKeys(options, fieldNames); - if (nextActiveIndex === -1) { - break; - } + // Fill validate active value cells and index + for (let i = 0; i < len && currentOptions; i += 1) { + // Mark the active index for current options + const nextActiveIndex = currentOptions.findIndex( + (option, index) => + (pathKeys[index] ? toPathKey(pathKeys[index]) : option[fieldNames.value]) === + activeValueCells[i], + ); - activeIndex = nextActiveIndex; - mergedActiveIndexes.push(activeIndex); - mergedActiveValueCells.push(activeValueCells[i]); + if (nextActiveIndex === -1) { + break; + } - currentOptions = currentOptions[activeIndex][fieldNames.children]; - } + activeIndex = nextActiveIndex; + mergedActiveIndexes.push(activeIndex); + mergedActiveValueCells.push(activeValueCells[i]); - // Fill last active options - let activeOptions = options; - for (let i = 0; i < mergedActiveIndexes.length - 1; i += 1) { - activeOptions = activeOptions[mergedActiveIndexes[i]][fieldNames.children]; - } + currentOptions = currentOptions[activeIndex][fieldNames.children]; + } - return [mergedActiveValueCells, activeIndex, activeOptions]; - }, [activeValueCells, fieldNames, options]); + // Fill last active options + let activeOptions = options; + for (let i = 0; i < mergedActiveIndexes.length - 1; i += 1) { + activeOptions = activeOptions[mergedActiveIndexes[i]][fieldNames.children]; + } + + return [mergedActiveValueCells, activeIndex, activeOptions, pathKeys]; + }, [activeValueCells, fieldNames, options]); // Update active value cells and scroll to target element const internalSetActiveValueCells = (next: React.Key[]) => { @@ -69,10 +75,14 @@ export default ( for (let i = 0; i < len; i += 1) { currentIndex = (currentIndex + offset + len) % len; const option = lastActiveOptions[currentIndex]; - if (option && !option.disabled) { - const value = option[fieldNames.value]; - const nextActiveCells = validActiveValueCells.slice(0, -1).concat(value); + const nextActiveCells = validActiveValueCells + .slice(0, -1) + .concat( + fullPathKeys[currentIndex] + ? toPathKey(fullPathKeys[currentIndex]) + : option[fieldNames.value], + ); internalSetActiveValueCells(nextActiveCells); return; } diff --git a/src/utils/commonUtil.ts b/src/utils/commonUtil.ts index 59562678..9c5b19d8 100644 --- a/src/utils/commonUtil.ts +++ b/src/utils/commonUtil.ts @@ -1,3 +1,4 @@ +import { SEARCH_MARK } from '../hooks/useSearchOptions'; import type { DefaultOptionType, FieldNames, @@ -49,3 +50,7 @@ export function scrollIntoParentView(element: HTMLElement) { parent.scrollTo({ top: elementToParent + element.offsetHeight - parent.offsetHeight }); } } + +export function getFullPathKeys(options: DefaultOptionType[], fieldNames: FieldNames) { + return options.map(item => item[SEARCH_MARK]?.map(opt => opt[fieldNames.value])); +} diff --git a/tests/demoOptions.ts b/tests/demoOptions.ts index e0fa4ffd..9408dfd8 100644 --- a/tests/demoOptions.ts +++ b/tests/demoOptions.ts @@ -70,6 +70,16 @@ export const addressOptions: DefaultOptionType[] = [ }, ], }, + { + label: '福州', + value: 'fuzhou', + children: [ + { + label: '马尾', + value: 'mawei', + }, + ], + }, ], }, { diff --git a/tests/keyboard.spec.tsx b/tests/keyboard.spec.tsx index 08e47ad6..d0230eca 100644 --- a/tests/keyboard.spec.tsx +++ b/tests/keyboard.spec.tsx @@ -94,6 +94,24 @@ describe('Cascader.Keyboard', () => { addressOptions[1].children[0].children[0], ]); }); + it('enter on search when has same sub key', () => { + wrapper.find('input').simulate('change', { target: { value: '福' } }); + wrapper.find('input').simulate('keyDown', { which: KeyCode.DOWN }); + expect(wrapper.find('.rc-cascader-menu-item-active').length).toBe(1); + expect( + wrapper.find('.rc-cascader-menu-item-active .rc-cascader-menu-item-content').last().text(), + ).toEqual('福建 / 福州 / 马尾'); + wrapper.find('input').simulate('keyDown', { which: KeyCode.DOWN }); + expect(wrapper.find('.rc-cascader-menu-item-active').length).toBe(1); + expect( + wrapper.find('.rc-cascader-menu-item-active .rc-cascader-menu-item-content').last().text(), + ).toEqual('福建 / 泉州'); + wrapper.find('input').simulate('keyDown', { which: KeyCode.DOWN }); + expect(wrapper.find('.rc-cascader-menu-item-active').length).toBe(1); + expect( + wrapper.find('.rc-cascader-menu-item-active .rc-cascader-menu-item-content').last().text(), + ).toEqual('浙江 / 福州 / 马尾'); + }); it('rtl', () => { wrapper = mount();