Skip to content

Commit e2112e9

Browse files
committed
feat(a11y): enhance keyboard interaction in search mode
1 parent fb1b6bb commit e2112e9

File tree

2 files changed

+124
-3
lines changed

2 files changed

+124
-3
lines changed

src/OptionList.tsx

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,21 @@ const OptionList: React.ForwardRefRenderFunction<ReviseRefOptionListProps> = (_,
159159
}
160160
};
161161

162+
const getFirstMatchNode = (nodes: EventDataNode<any>[]): EventDataNode<any> | null => {
163+
for (const node of nodes) {
164+
if (filterTreeNode(node) && !node.disabled) {
165+
return node;
166+
}
167+
if (node[fieldNames.children]) {
168+
const matchInChildren = getFirstMatchNode(node[fieldNames.children]);
169+
if (matchInChildren) {
170+
return matchInChildren;
171+
}
172+
}
173+
}
174+
return null;
175+
};
176+
162177
// ========================= Keyboard =========================
163178
React.useImperativeHandle(ref, () => ({
164179
scrollTo: treeRef.current?.scrollTo,
@@ -175,7 +190,18 @@ const OptionList: React.ForwardRefRenderFunction<ReviseRefOptionListProps> = (_,
175190

176191
// >>> Select item
177192
case KeyCode.ENTER: {
178-
if (activeEntity) {
193+
if (searchValue) {
194+
const firstMatchNode = getFirstMatchNode(memoTreeData);
195+
196+
if (firstMatchNode) {
197+
if (firstMatchNode.selectable !== false) {
198+
onInternalSelect(null, {
199+
node: { key: firstMatchNode[fieldNames.value] },
200+
selected: !checkedKeys.includes(firstMatchNode[fieldNames.value]),
201+
});
202+
}
203+
}
204+
} else if (activeEntity) {
179205
const { selectable, value } = activeEntity?.node || {};
180206
if (selectable !== false) {
181207
onInternalSelect(null, {
@@ -197,10 +223,10 @@ const OptionList: React.ForwardRefRenderFunction<ReviseRefOptionListProps> = (_,
197223
}));
198224

199225
const loadDataFun = useMemo(
200-
() => searchValue ? null : (loadData as any),
226+
() => (searchValue ? null : (loadData as any)),
201227
[searchValue, treeExpandedKeys || expandedKeys],
202228
([preSearchValue], [nextSearchValue, nextExcludeSearchExpandedKeys]) =>
203-
preSearchValue !== nextSearchValue && !!(nextSearchValue || nextExcludeSearchExpandedKeys)
229+
preSearchValue !== nextSearchValue && !!(nextSearchValue || nextExcludeSearchExpandedKeys),
204230
);
205231

206232
// ========================== Render ==========================

tests/Select.SearchInput.spec.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import React, { useState } from 'react';
33
import { mount } from 'enzyme';
44
import TreeSelect, { TreeNode } from '../src';
5+
import KeyCode from 'rc-util/lib/KeyCode';
56

67
describe('TreeSelect.SearchInput', () => {
78
it('select item will clean searchInput', () => {
@@ -198,4 +199,98 @@ describe('TreeSelect.SearchInput', () => {
198199
nodes.first().simulate('click');
199200
expect(called).toBe(1);
200201
});
202+
203+
describe('keyboard events', () => {
204+
it('should select first matched node when press enter', () => {
205+
const onSelect = jest.fn();
206+
const wrapper = mount(
207+
<TreeSelect
208+
showSearch
209+
onSelect={onSelect}
210+
open
211+
treeData={[
212+
{ value: '1', label: '1' },
213+
{ value: '2', label: '2', disabled: true },
214+
{ value: '3', label: '3' },
215+
]}
216+
/>,
217+
);
218+
219+
// Search and press enter, should select first matched non-disabled node
220+
wrapper.search('1');
221+
wrapper.find('input').first().simulate('keyDown', { which: KeyCode.ENTER });
222+
expect(onSelect).toHaveBeenCalledWith('1', expect.anything());
223+
224+
onSelect.mockReset();
225+
226+
// Search disabled node and press enter, should not select
227+
wrapper.search('2');
228+
wrapper.find('input').first().simulate('keyDown', { which: KeyCode.ENTER });
229+
expect(onSelect).not.toHaveBeenCalled();
230+
});
231+
232+
it('should not select any node when no search value and press enter', () => {
233+
const onSelect = jest.fn();
234+
const wrapper = mount(
235+
<TreeSelect
236+
showSearch
237+
onSelect={onSelect}
238+
open
239+
treeData={[
240+
{ value: '1', label: '1' },
241+
{ value: '2', label: '2' },
242+
]}
243+
/>,
244+
);
245+
246+
// Press enter without search value, should not select any node
247+
wrapper.find('input').first().simulate('keyDown', { which: KeyCode.ENTER });
248+
expect(onSelect).not.toHaveBeenCalled();
249+
250+
// Search and press enter, should select first matched node
251+
wrapper.search('1');
252+
wrapper.find('input').first().simulate('keyDown', { which: KeyCode.ENTER });
253+
expect(onSelect).toHaveBeenCalledWith('1', expect.anything());
254+
});
255+
256+
it('should not select node when no matches found', () => {
257+
const onSelect = jest.fn();
258+
const wrapper = mount(
259+
<TreeSelect
260+
showSearch
261+
onSelect={onSelect}
262+
open
263+
treeData={[
264+
{ value: '1', label: '1' },
265+
{ value: '2', label: '2' },
266+
]}
267+
/>,
268+
);
269+
270+
// Search non-existent value and press enter, should not select any node
271+
wrapper.search('not-exist');
272+
wrapper.find('input').first().simulate('keyDown', { which: KeyCode.ENTER });
273+
expect(onSelect).not.toHaveBeenCalled();
274+
});
275+
276+
it('should ignore enter press when all matched nodes are disabled', () => {
277+
const onSelect = jest.fn();
278+
const wrapper = mount(
279+
<TreeSelect
280+
showSearch
281+
onSelect={onSelect}
282+
open
283+
treeData={[
284+
{ value: '1', label: '1', disabled: true },
285+
{ value: '2', label: '2', disabled: true },
286+
]}
287+
/>,
288+
);
289+
290+
// When all matched nodes are disabled, press enter should not select any node
291+
wrapper.search('1');
292+
wrapper.find('input').first().simulate('keyDown', { which: KeyCode.ENTER });
293+
expect(onSelect).not.toHaveBeenCalled();
294+
});
295+
});
201296
});

0 commit comments

Comments
 (0)