Skip to content

Commit

Permalink
fix: 修复因子key相同导致搜索时激活项展示异常问题 (#390)
Browse files Browse the repository at this point in the history
* fix: 修复因子key相同导致搜索时激活项展示异常问题

* feat: add demo

* style: code style

* style: code style

* feat: optimize code
  • Loading branch information
kiner-tang authored Feb 20, 2023
1 parent d14633b commit 3e0eaa9
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 34 deletions.
8 changes: 8 additions & 0 deletions docs/demo/search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: search
nav:
title: Demo
path: /demo
---

<code src="../../examples/search.tsx"></code>
75 changes: 75 additions & 0 deletions examples/search.tsx
Original file line number Diff line number Diff line change
@@ -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 <Cascader options={addressOptions} showSearch />;
}
}

export default Demo;
3 changes: 2 additions & 1 deletion src/OptionList/Column.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})}
Expand Down
7 changes: 6 additions & 1 deletion src/OptionList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as React from 'react';
import type { DefaultOptionType, SingleValueType } from '../Cascader';
import CascaderContext from '../context';
import {
getFullPathKeys,
isLeaf,
scrollIntoParentView,
toPathKey,
Expand Down Expand Up @@ -122,10 +123,14 @@ const RefOptionList = React.forwardRef<RefOptionListProps>((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];
Expand Down
74 changes: 42 additions & 32 deletions src/OptionList/useKeyboard.ts
Original file line number Diff line number Diff line change
@@ -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<RefOptionListProps>,
Expand All @@ -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[]) => {
Expand All @@ -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;
}
Expand Down
5 changes: 5 additions & 0 deletions src/utils/commonUtil.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SEARCH_MARK } from '../hooks/useSearchOptions';
import type {
DefaultOptionType,
FieldNames,
Expand Down Expand Up @@ -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]));
}
10 changes: 10 additions & 0 deletions tests/demoOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ export const addressOptions: DefaultOptionType[] = [
},
],
},
{
label: '福州',
value: 'fuzhou',
children: [
{
label: '马尾',
value: 'mawei',
},
],
},
],
},
{
Expand Down
18 changes: 18 additions & 0 deletions tests/keyboard.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<Cascader options={addressOptions} onChange={onChange} direction="rtl" />);
Expand Down

1 comment on commit 3e0eaa9

@vercel
Copy link

@vercel vercel bot commented on 3e0eaa9 Feb 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

cascader – ./

cascader-git-master-react-component.vercel.app
cascader-react-component.vercel.app

Please sign in to comment.