Skip to content

Commit 3e0eaa9

Browse files
authored
fix: 修复因子key相同导致搜索时激活项展示异常问题 (#390)
* fix: 修复因子key相同导致搜索时激活项展示异常问题 * feat: add demo * style: code style * style: code style * feat: optimize code
1 parent d14633b commit 3e0eaa9

File tree

8 files changed

+166
-34
lines changed

8 files changed

+166
-34
lines changed

docs/demo/search.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
title: search
3+
nav:
4+
title: Demo
5+
path: /demo
6+
---
7+
8+
<code src="../../examples/search.tsx"></code>

examples/search.tsx

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/* eslint-disable no-console */
2+
import React from 'react';
3+
import '../assets/index.less';
4+
import Cascader from '../src';
5+
6+
const addressOptions = [
7+
{
8+
label: '福建',
9+
value: 'fj',
10+
children: [
11+
{
12+
label: '福州',
13+
value: 'fuzhou',
14+
children: [
15+
{
16+
label: '马尾',
17+
value: 'mawei',
18+
},
19+
],
20+
},
21+
{
22+
label: '泉州',
23+
value: 'quanzhou',
24+
},
25+
],
26+
},
27+
{
28+
label: '浙江',
29+
value: 'zj',
30+
children: [
31+
{
32+
label: '杭州',
33+
value: 'hangzhou',
34+
children: [
35+
{
36+
label: '余杭',
37+
value: 'yuhang',
38+
},
39+
{
40+
label: '福州',
41+
value: 'fuzhou',
42+
children: [
43+
{
44+
label: '马尾',
45+
value: 'mawei',
46+
},
47+
],
48+
},
49+
],
50+
},
51+
],
52+
},
53+
{
54+
label: '北京',
55+
value: 'bj',
56+
children: [
57+
{
58+
label: '朝阳区',
59+
value: 'chaoyang',
60+
},
61+
{
62+
label: '海淀区',
63+
value: 'haidian',
64+
},
65+
],
66+
},
67+
];
68+
69+
class Demo extends React.Component {
70+
render() {
71+
return <Cascader options={addressOptions} showSearch />;
72+
}
73+
}
74+
75+
export default Demo;

src/OptionList/Column.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ export default function Column({
138138
key={fullPathKey}
139139
className={classNames(menuItemPrefixCls, {
140140
[`${menuItemPrefixCls}-expand`]: !isMergedLeaf,
141-
[`${menuItemPrefixCls}-active`]: activeValue === value,
141+
[`${menuItemPrefixCls}-active`]:
142+
activeValue === value || activeValue === fullPathKey,
142143
[`${menuItemPrefixCls}-disabled`]: disabled,
143144
[`${menuItemPrefixCls}-loading`]: isLoading,
144145
})}

src/OptionList/index.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as React from 'react';
66
import type { DefaultOptionType, SingleValueType } from '../Cascader';
77
import CascaderContext from '../context';
88
import {
9+
getFullPathKeys,
910
isLeaf,
1011
scrollIntoParentView,
1112
toPathKey,
@@ -122,10 +123,14 @@ const RefOptionList = React.forwardRef<RefOptionListProps>((props, ref) => {
122123
const optionList = [{ options: mergedOptions }];
123124
let currentList = mergedOptions;
124125

126+
const fullPathKeys = getFullPathKeys(currentList, fieldNames);
127+
125128
for (let i = 0; i < activeValueCells.length; i += 1) {
126129
const activeValueCell = activeValueCells[i];
127130
const currentOption = currentList.find(
128-
option => option[fieldNames.value] === activeValueCell,
131+
(option, index) =>
132+
(fullPathKeys[index] ? toPathKey(fullPathKeys[index]) : option[fieldNames.value]) ===
133+
activeValueCell,
129134
);
130135

131136
const subOptions = currentOption?.[fieldNames.children];

src/OptionList/useKeyboard.ts

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import * as React from 'react';
2-
import type { RefOptionListProps } from 'rc-select/lib/OptionList';
31
import { useBaseProps } from 'rc-select';
2+
import type { RefOptionListProps } from 'rc-select/lib/OptionList';
43
import KeyCode from 'rc-util/lib/KeyCode';
4+
import * as React from 'react';
55
import type { DefaultOptionType, InternalFieldNames, SingleValueType } from '../Cascader';
66
import { SEARCH_MARK } from '../hooks/useSearchOptions';
7+
import { getFullPathKeys, toPathKey } from '../utils/commonUtil';
78

89
export default (
910
ref: React.Ref<RefOptionListProps>,
@@ -16,41 +17,46 @@ export default (
1617
const { direction, searchValue, toggleOpen, open } = useBaseProps();
1718
const rtl = direction === 'rtl';
1819

19-
const [validActiveValueCells, lastActiveIndex, lastActiveOptions] = React.useMemo(() => {
20-
let activeIndex = -1;
21-
let currentOptions = options;
20+
const [validActiveValueCells, lastActiveIndex, lastActiveOptions, fullPathKeys] =
21+
React.useMemo(() => {
22+
let activeIndex = -1;
23+
let currentOptions = options;
2224

23-
const mergedActiveIndexes: number[] = [];
24-
const mergedActiveValueCells: React.Key[] = [];
25+
const mergedActiveIndexes: number[] = [];
26+
const mergedActiveValueCells: React.Key[] = [];
2527

26-
const len = activeValueCells.length;
28+
const len = activeValueCells.length;
2729

28-
// Fill validate active value cells and index
29-
for (let i = 0; i < len && currentOptions; i += 1) {
30-
// Mark the active index for current options
31-
const nextActiveIndex = currentOptions.findIndex(
32-
option => option[fieldNames.value] === activeValueCells[i],
33-
);
30+
const pathKeys = getFullPathKeys(options, fieldNames);
3431

35-
if (nextActiveIndex === -1) {
36-
break;
37-
}
32+
// Fill validate active value cells and index
33+
for (let i = 0; i < len && currentOptions; i += 1) {
34+
// Mark the active index for current options
35+
const nextActiveIndex = currentOptions.findIndex(
36+
(option, index) =>
37+
(pathKeys[index] ? toPathKey(pathKeys[index]) : option[fieldNames.value]) ===
38+
activeValueCells[i],
39+
);
3840

39-
activeIndex = nextActiveIndex;
40-
mergedActiveIndexes.push(activeIndex);
41-
mergedActiveValueCells.push(activeValueCells[i]);
41+
if (nextActiveIndex === -1) {
42+
break;
43+
}
4244

43-
currentOptions = currentOptions[activeIndex][fieldNames.children];
44-
}
45+
activeIndex = nextActiveIndex;
46+
mergedActiveIndexes.push(activeIndex);
47+
mergedActiveValueCells.push(activeValueCells[i]);
4548

46-
// Fill last active options
47-
let activeOptions = options;
48-
for (let i = 0; i < mergedActiveIndexes.length - 1; i += 1) {
49-
activeOptions = activeOptions[mergedActiveIndexes[i]][fieldNames.children];
50-
}
49+
currentOptions = currentOptions[activeIndex][fieldNames.children];
50+
}
5151

52-
return [mergedActiveValueCells, activeIndex, activeOptions];
53-
}, [activeValueCells, fieldNames, options]);
52+
// Fill last active options
53+
let activeOptions = options;
54+
for (let i = 0; i < mergedActiveIndexes.length - 1; i += 1) {
55+
activeOptions = activeOptions[mergedActiveIndexes[i]][fieldNames.children];
56+
}
57+
58+
return [mergedActiveValueCells, activeIndex, activeOptions, pathKeys];
59+
}, [activeValueCells, fieldNames, options]);
5460

5561
// Update active value cells and scroll to target element
5662
const internalSetActiveValueCells = (next: React.Key[]) => {
@@ -69,10 +75,14 @@ export default (
6975
for (let i = 0; i < len; i += 1) {
7076
currentIndex = (currentIndex + offset + len) % len;
7177
const option = lastActiveOptions[currentIndex];
72-
7378
if (option && !option.disabled) {
74-
const value = option[fieldNames.value];
75-
const nextActiveCells = validActiveValueCells.slice(0, -1).concat(value);
79+
const nextActiveCells = validActiveValueCells
80+
.slice(0, -1)
81+
.concat(
82+
fullPathKeys[currentIndex]
83+
? toPathKey(fullPathKeys[currentIndex])
84+
: option[fieldNames.value],
85+
);
7686
internalSetActiveValueCells(nextActiveCells);
7787
return;
7888
}

src/utils/commonUtil.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { SEARCH_MARK } from '../hooks/useSearchOptions';
12
import type {
23
DefaultOptionType,
34
FieldNames,
@@ -49,3 +50,7 @@ export function scrollIntoParentView(element: HTMLElement) {
4950
parent.scrollTo({ top: elementToParent + element.offsetHeight - parent.offsetHeight });
5051
}
5152
}
53+
54+
export function getFullPathKeys(options: DefaultOptionType[], fieldNames: FieldNames) {
55+
return options.map(item => item[SEARCH_MARK]?.map(opt => opt[fieldNames.value]));
56+
}

tests/demoOptions.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,16 @@ export const addressOptions: DefaultOptionType[] = [
7070
},
7171
],
7272
},
73+
{
74+
label: '福州',
75+
value: 'fuzhou',
76+
children: [
77+
{
78+
label: '马尾',
79+
value: 'mawei',
80+
},
81+
],
82+
},
7383
],
7484
},
7585
{

tests/keyboard.spec.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,24 @@ describe('Cascader.Keyboard', () => {
9494
addressOptions[1].children[0].children[0],
9595
]);
9696
});
97+
it('enter on search when has same sub key', () => {
98+
wrapper.find('input').simulate('change', { target: { value: '福' } });
99+
wrapper.find('input').simulate('keyDown', { which: KeyCode.DOWN });
100+
expect(wrapper.find('.rc-cascader-menu-item-active').length).toBe(1);
101+
expect(
102+
wrapper.find('.rc-cascader-menu-item-active .rc-cascader-menu-item-content').last().text(),
103+
).toEqual('福建 / 福州 / 马尾');
104+
wrapper.find('input').simulate('keyDown', { which: KeyCode.DOWN });
105+
expect(wrapper.find('.rc-cascader-menu-item-active').length).toBe(1);
106+
expect(
107+
wrapper.find('.rc-cascader-menu-item-active .rc-cascader-menu-item-content').last().text(),
108+
).toEqual('福建 / 泉州');
109+
wrapper.find('input').simulate('keyDown', { which: KeyCode.DOWN });
110+
expect(wrapper.find('.rc-cascader-menu-item-active').length).toBe(1);
111+
expect(
112+
wrapper.find('.rc-cascader-menu-item-active .rc-cascader-menu-item-content').last().text(),
113+
).toEqual('浙江 / 福州 / 马尾');
114+
});
97115

98116
it('rtl', () => {
99117
wrapper = mount(<Cascader options={addressOptions} onChange={onChange} direction="rtl" />);

0 commit comments

Comments
 (0)