Skip to content

Commit 751e850

Browse files
committed
Merge branch 'master' into feat-maxCount
2 parents ca8d966 + a16f0f0 commit 751e850

File tree

9 files changed

+363
-149
lines changed

9 files changed

+363
-149
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ dist
2525
build
2626
lib
2727
coverage
28+
.vscode
2829
yarn.lock
2930
package-lock.json
3031
es

README.md

Lines changed: 63 additions & 63 deletions
Large diffs are not rendered by default.

examples/mutiple-with-maxCount.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ export default () => {
3434
},
3535
];
3636

37-
const onChange = (value: string[]) => {
38-
setValue(value);
37+
const onChange = (val: string[]) => {
38+
setValue(val);
3939
};
4040

4141
return (

src/OptionList.tsx

Lines changed: 85 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -95,17 +95,6 @@ const OptionList: React.ForwardRefRenderFunction<ReviseRefOptionListProps> = (_,
9595
[memoTreeData, isOverMaxCount, checkedKeys],
9696
);
9797

98-
// ========================== Active ==========================
99-
const [activeKey, setActiveKey] = React.useState<Key>(null);
100-
const activeEntity = keyEntities[activeKey as SafeKey];
101-
102-
const onActiveChange = (key: Key) => {
103-
if (isOverMaxCount && !checkedKeys.includes(key)) {
104-
return;
105-
}
106-
setActiveKey(key);
107-
};
108-
10998
// ========================== Values ==========================
11099
const mergedCheckedKeys = React.useMemo(() => {
111100
if (!checkable) {
@@ -123,18 +112,31 @@ const OptionList: React.ForwardRefRenderFunction<ReviseRefOptionListProps> = (_,
123112
// Single mode should scroll to current key
124113
if (open && !multiple && checkedKeys.length) {
125114
treeRef.current?.scrollTo({ key: checkedKeys[0] });
126-
setActiveKey(checkedKeys[0]);
127115
}
128116
// eslint-disable-next-line react-hooks/exhaustive-deps
129117
}, [open]);
130118

131-
// ========================== Search ==========================
132-
const lowerSearchValue = String(searchValue).toLowerCase();
133-
const filterTreeNode = (treeNode: EventDataNode<any>) => {
134-
if (!lowerSearchValue) {
135-
return false;
119+
// ========================== Events ==========================
120+
const onListMouseDown: React.MouseEventHandler<HTMLDivElement> = event => {
121+
event.preventDefault();
122+
};
123+
124+
const onInternalSelect = (__: Key[], info: TreeEventInfo) => {
125+
const { node } = info;
126+
127+
if (checkable && isCheckDisabled(node)) {
128+
return;
129+
}
130+
131+
const isSelected = !checkedKeys.includes(node.key);
132+
133+
onSelect(node.key, {
134+
selected: isSelected,
135+
});
136+
137+
if (!multiple) {
138+
toggleOpen(false);
136139
}
137-
return String(treeNode[treeNodeFilterProp]).toLowerCase().includes(lowerSearchValue);
138140
};
139141

140142
// =========================== Keys ===========================
@@ -148,13 +150,6 @@ const OptionList: React.ForwardRefRenderFunction<ReviseRefOptionListProps> = (_,
148150
return searchValue ? searchExpandedKeys : expandedKeys;
149151
}, [expandedKeys, searchExpandedKeys, treeExpandedKeys, searchValue]);
150152

151-
React.useEffect(() => {
152-
if (searchValue) {
153-
setSearchExpandedKeys(getAllKeys(treeData, fieldNames));
154-
}
155-
// eslint-disable-next-line react-hooks/exhaustive-deps
156-
}, [searchValue]);
157-
158153
const onInternalExpand = (keys: Key[]) => {
159154
setExpandedKeys(keys);
160155
setSearchExpandedKeys(keys);
@@ -164,28 +159,78 @@ const OptionList: React.ForwardRefRenderFunction<ReviseRefOptionListProps> = (_,
164159
}
165160
};
166161

167-
// ========================== Events ==========================
168-
const onListMouseDown: React.MouseEventHandler<HTMLDivElement> = event => {
169-
event.preventDefault();
162+
// ========================== Search ==========================
163+
const lowerSearchValue = String(searchValue).toLowerCase();
164+
const filterTreeNode = (treeNode: EventDataNode<any>) => {
165+
if (!lowerSearchValue) {
166+
return false;
167+
}
168+
return String(treeNode[treeNodeFilterProp]).toLowerCase().includes(lowerSearchValue);
170169
};
171170

172-
const onInternalSelect = (__: Key[], info: TreeEventInfo) => {
173-
const { node } = info;
171+
React.useEffect(() => {
172+
if (searchValue) {
173+
setSearchExpandedKeys(getAllKeys(treeData, fieldNames));
174+
}
175+
// eslint-disable-next-line react-hooks/exhaustive-deps
176+
}, [searchValue]);
174177

175-
if (checkable && isCheckDisabled(node)) {
178+
// ========================== Get First Selectable Node ==========================
179+
const getFirstMatchingNode = (nodes: EventDataNode<any>[]): EventDataNode<any> | null => {
180+
for (const node of nodes) {
181+
if (node.disabled || node.selectable === false) {
182+
continue;
183+
}
184+
185+
if (searchValue) {
186+
if (filterTreeNode(node)) {
187+
return node;
188+
}
189+
} else {
190+
return node;
191+
}
192+
193+
if (node[fieldNames.children]) {
194+
const matchInChildren = getFirstMatchingNode(node[fieldNames.children]);
195+
if (matchInChildren) {
196+
return matchInChildren;
197+
}
198+
}
199+
}
200+
return null;
201+
};
202+
203+
// ========================== Active ==========================
204+
const [activeKey, setActiveKey] = React.useState<Key>(null);
205+
const activeEntity = keyEntities[activeKey as SafeKey];
206+
207+
const onActiveChange = (key: Key) => {
208+
if (isOverMaxCount && !checkedKeys.includes(key)) {
176209
return;
177210
}
211+
setActiveKey(key);
212+
};
178213

179-
const isSelected = !checkedKeys.includes(node.key);
214+
React.useEffect(() => {
215+
if (!open) {
216+
return;
217+
}
218+
let nextActiveKey = null;
180219

181-
onSelect(node.key, {
182-
selected: isSelected,
183-
});
220+
const getFirstNode = () => {
221+
const firstNode = getFirstMatchingNode(memoTreeData);
222+
return firstNode ? firstNode[fieldNames.value] : null;
223+
};
184224

185-
if (!multiple) {
186-
toggleOpen(false);
225+
// single mode active first checked node
226+
if (!multiple && checkedKeys.length && !searchValue) {
227+
nextActiveKey = checkedKeys[0];
228+
} else {
229+
nextActiveKey = getFirstNode();
187230
}
188-
};
231+
232+
setActiveKey(nextActiveKey);
233+
}, [open, searchValue]);
189234

190235
// ========================= Keyboard =========================
191236
React.useImperativeHandle(ref, () => ({
@@ -204,8 +249,8 @@ const OptionList: React.ForwardRefRenderFunction<ReviseRefOptionListProps> = (_,
204249
// >>> Select item
205250
case KeyCode.ENTER: {
206251
if (activeEntity) {
207-
const { selectable, value } = activeEntity?.node || {};
208-
if (selectable !== false) {
252+
const { selectable, value, disabled } = activeEntity?.node || {};
253+
if (selectable !== false && !disabled) {
209254
onInternalSelect(null, {
210255
node: { key: activeKey },
211256
selected: !checkedKeys.includes(value),

src/TreeSelect.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export interface TreeSelectProps<ValueType = any, OptionType extends DataNode =
9494
listItemHeight?: number;
9595
listItemScrollOffset?: number;
9696
onDropdownVisibleChange?: (open: boolean) => void;
97-
treeTitleRender?: (node: ValueType) => React.ReactNode;
97+
treeTitleRender?: (node: OptionType) => React.ReactNode;
9898

9999
// >>> Tree
100100
treeLine?: boolean;

tests/Select.SearchInput.spec.js

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
/* eslint-disable no-undef */
22
import React, { useState } from 'react';
33
import { mount } from 'enzyme';
4+
import { render, fireEvent } from '@testing-library/react';
45
import TreeSelect, { TreeNode } from '../src';
6+
import KeyCode from 'rc-util/lib/KeyCode';
57

68
describe('TreeSelect.SearchInput', () => {
79
it('select item will clean searchInput', () => {
@@ -198,4 +200,102 @@ describe('TreeSelect.SearchInput', () => {
198200
nodes.first().simulate('click');
199201
expect(called).toBe(1);
200202
});
203+
204+
describe('keyboard events', () => {
205+
it('should select first matched node when press enter', () => {
206+
const onSelect = jest.fn();
207+
const { getByRole } = render(
208+
<TreeSelect
209+
showSearch
210+
open
211+
onSelect={onSelect}
212+
treeData={[
213+
{ value: '1', label: '1' },
214+
{ value: '2', label: '2', disabled: true },
215+
{ value: '3', label: '3' },
216+
]}
217+
/>,
218+
);
219+
220+
// Search and press enter, should select first matched non-disabled node
221+
const input = getByRole('combobox');
222+
fireEvent.change(input, { target: { value: '1' } });
223+
fireEvent.keyDown(input, { keyCode: KeyCode.ENTER });
224+
expect(onSelect).toHaveBeenCalledWith('1', expect.anything());
225+
onSelect.mockReset();
226+
227+
// Search disabled node and press enter, should not select
228+
fireEvent.change(input, { target: { value: '2' } });
229+
fireEvent.keyDown(input, { keyCode: KeyCode.ENTER });
230+
expect(onSelect).not.toHaveBeenCalled();
231+
onSelect.mockReset();
232+
});
233+
234+
it('should not select node when no matches found', () => {
235+
const onSelect = jest.fn();
236+
const { getByRole } = render(
237+
<TreeSelect
238+
showSearch
239+
onSelect={onSelect}
240+
open
241+
treeData={[
242+
{ value: '1', label: '1' },
243+
{ value: '2', label: '2' },
244+
]}
245+
/>,
246+
);
247+
248+
// Search non-existent value and press enter, should not select any node
249+
const input = getByRole('combobox');
250+
fireEvent.change(input, { target: { value: 'not-exist' } });
251+
fireEvent.keyDown(input, { keyCode: KeyCode.ENTER });
252+
expect(onSelect).not.toHaveBeenCalled();
253+
});
254+
255+
it('should ignore enter press when all matched nodes are disabled', () => {
256+
const onSelect = jest.fn();
257+
const { getByRole } = render(
258+
<TreeSelect
259+
showSearch
260+
onSelect={onSelect}
261+
open
262+
treeData={[
263+
{ value: '1', label: '1', disabled: true },
264+
{ value: '2', label: '2', disabled: true },
265+
]}
266+
/>,
267+
);
268+
269+
// When all matched nodes are disabled, press enter should not select any node
270+
const input = getByRole('combobox');
271+
fireEvent.change(input, { target: { value: '1' } });
272+
fireEvent.keyDown(input, { keyCode: KeyCode.ENTER });
273+
expect(onSelect).not.toHaveBeenCalled();
274+
});
275+
276+
it('should activate first matched node when searching', () => {
277+
const { getByRole, container } = render(
278+
<TreeSelect
279+
showSearch
280+
open
281+
treeData={[
282+
{ value: '1', label: '1' },
283+
{ value: '2', label: '2', disabled: true },
284+
{ value: '3', label: '3' },
285+
]}
286+
/>,
287+
);
288+
289+
// When searching, first matched non-disabled node should be activated
290+
const input = getByRole('combobox');
291+
fireEvent.change(input, { target: { value: '1' } });
292+
expect(container.querySelector('.rc-tree-select-tree-treenode-active')).toHaveTextContent(
293+
'1',
294+
);
295+
296+
// Should skip disabled nodes
297+
fireEvent.change(input, { target: { value: '2' } });
298+
expect(container.querySelectorAll('.rc-tree-select-tree-treenode-active')).toHaveLength(0);
299+
});
300+
});
201301
});

tests/Select.spec.tsx

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -438,13 +438,13 @@ describe('TreeSelect.basic', () => {
438438
keyUp(KeyCode.DOWN);
439439
keyDown(KeyCode.ENTER);
440440
keyUp(KeyCode.ENTER);
441-
matchValue(['parent']);
441+
matchValue(['child']);
442442

443443
keyDown(KeyCode.UP);
444444
keyUp(KeyCode.UP);
445445
keyDown(KeyCode.ENTER);
446446
keyUp(KeyCode.ENTER);
447-
matchValue(['parent', 'child']);
447+
matchValue(['child', 'parent']);
448448
});
449449

450450
it('selectable works with keyboard operations', () => {
@@ -467,12 +467,12 @@ describe('TreeSelect.basic', () => {
467467

468468
keyDown(KeyCode.DOWN);
469469
keyDown(KeyCode.ENTER);
470-
expect(onChange).toHaveBeenCalledWith(['parent'], expect.anything(), expect.anything());
471-
onChange.mockReset();
470+
expect(onChange).not.toHaveBeenCalled();
472471

473472
keyDown(KeyCode.UP);
474473
keyDown(KeyCode.ENTER);
475-
expect(onChange).not.toHaveBeenCalled();
474+
expect(onChange).toHaveBeenCalledWith(['parent'], expect.anything(), expect.anything());
475+
onChange.mockReset();
476476
});
477477

478478
it('active index matches value', () => {
@@ -535,6 +535,24 @@ describe('TreeSelect.basic', () => {
535535
keyDown(KeyCode.UP);
536536
expect(wrapper.find('.rc-tree-select-tree-treenode-active').text()).toBe('11 label');
537537
});
538+
539+
it('should active first un-disabled option when dropdown is opened', () => {
540+
const treeData = [
541+
{ key: '0', value: '0', title: '0 label', disabled: true },
542+
{ key: '1', value: '1', title: '1 label' },
543+
{ key: '2', value: '2', title: '2 label' },
544+
];
545+
546+
const wrapper = mount(<TreeSelect treeData={treeData} />);
547+
548+
expect(wrapper.find('.rc-tree-select-tree-treenode-active')).toHaveLength(0);
549+
550+
wrapper.openSelect();
551+
552+
const activeNode = wrapper.find('.rc-tree-select-tree-treenode-active');
553+
expect(activeNode).toHaveLength(1);
554+
expect(activeNode.text()).toBe('1 label');
555+
});
538556
});
539557

540558
it('click in list should preventDefault', () => {
@@ -591,22 +609,6 @@ describe('TreeSelect.basic', () => {
591609
expect(container.querySelector('.rc-tree-select-selector').textContent).toBe('parent 1-0');
592610
});
593611

594-
it('should not add new tag when key enter is pressed if nothing is active', () => {
595-
const onSelect = jest.fn();
596-
597-
const wrapper = mount(
598-
<TreeSelect open treeDefaultExpandAll multiple onSelect={onSelect}>
599-
<TreeNode value="parent 1-0" title="parent 1-0">
600-
<TreeNode value="leaf1" title="my leaf" disabled />
601-
<TreeNode value="leaf2" title="your leaf" disabled />
602-
</TreeNode>
603-
</TreeSelect>,
604-
);
605-
606-
wrapper.find('input').first().simulate('keydown', { which: KeyCode.ENTER });
607-
expect(onSelect).not.toHaveBeenCalled();
608-
});
609-
610612
it('should not select parent if some children is disabled', () => {
611613
const onChange = jest.fn();
612614

0 commit comments

Comments
 (0)