Skip to content

Commit 3bdb3b5

Browse files
authored
feat: support onActive (#1154)
* feat: support onActive * chore: enhance logic * test: update test case * test: update test case
1 parent faa487c commit 3bdb3b5

File tree

5 files changed

+71
-16
lines changed

5 files changed

+71
-16
lines changed

docs/examples/combobox.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ class Combobox extends React.Component {
1818
console.log('Ref:', this.textareaRef);
1919
}
2020

21+
onActive = (value) => {
22+
console.log('onActive', value);
23+
};
24+
2125
onChange = (value, option) => {
2226
console.log('onChange', value, option);
2327
this.setState({
@@ -83,6 +87,7 @@ class Combobox extends React.Component {
8387
value={value}
8488
mode="combobox"
8589
onChange={this.onChange}
90+
onActive={this.onActive}
8691
filterOption={(inputValue, option) => {
8792
if (!inputValue) {
8893
return true;

docs/examples/controlled.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ class Controlled extends React.Component<{}, ControlledState> {
4747
this.setState({ open });
4848
};
4949

50+
onActive = (value) => {
51+
console.error('onActive', value);
52+
};
53+
5054
render() {
5155
const { open, destroy, value } = this.state;
5256
if (destroy) {
@@ -69,6 +73,7 @@ class Controlled extends React.Component<{}, ControlledState> {
6973
optionFilterProp="text"
7074
onChange={this.onChange}
7175
onPopupVisibleChange={this.onPopupVisibleChange}
76+
onActive={this.onActive}
7277
>
7378
<Option value="01" text="jack" title="jack">
7479
<b

src/OptionList.tsx

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -154,20 +154,22 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
154154
* `setActive` function will call root accessibility state update which makes re-render.
155155
* So we need to delay to let Input component trigger onChange first.
156156
*/
157-
const timeoutId = setTimeout(() => {
158-
if (!multiple && open && rawValues.size === 1) {
159-
const value: RawValueType = Array.from(rawValues)[0];
160-
// Scroll to the option closest to the searchValue if searching.
161-
const index = memoFlattenOptions.findIndex(({ data }) =>
162-
searchValue ? String(data.value).startsWith(searchValue) : data.value === value,
163-
);
164-
165-
if (index !== -1) {
166-
setActive(index);
157+
let timeoutId: NodeJS.Timeout;
158+
159+
if (!multiple && open && rawValues.size === 1) {
160+
const value: RawValueType = Array.from(rawValues)[0];
161+
// Scroll to the option closest to the searchValue if searching.
162+
const index = memoFlattenOptions.findIndex(({ data }) =>
163+
searchValue ? String(data.value).startsWith(searchValue) : data.value === value,
164+
);
165+
166+
if (index !== -1) {
167+
setActive(index);
168+
timeoutId = setTimeout(() => {
167169
scrollIntoView(index);
168-
}
170+
});
169171
}
170-
});
172+
}
171173

172174
// Force trigger scrollbar visible when open
173175
if (open) {

src/Select.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ export interface SelectProps<ValueType = any, OptionType extends BaseOptionType
127127
// >>> Select
128128
onSelect?: SelectHandler<ArrayElementType<ValueType>, OptionType>;
129129
onDeselect?: SelectHandler<ArrayElementType<ValueType>, OptionType>;
130+
onActive?: (value: ValueType) => void;
130131

131132
// >>> Options
132133
/**
@@ -185,6 +186,7 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp
185186
// Select
186187
onSelect,
187188
onDeselect,
189+
onActive,
188190
popupMatchSelectWidth = true,
189191

190192
// Options
@@ -493,15 +495,26 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp
493495
const mergedDefaultActiveFirstOption =
494496
defaultActiveFirstOption !== undefined ? defaultActiveFirstOption : mode !== 'combobox';
495497

498+
const activeEventRef = React.useRef<Promise<void>>();
499+
496500
const onActiveValue: OnActiveValue = React.useCallback(
497501
(active, index, { source = 'keyboard' } = {}) => {
498502
setAccessibilityIndex(index);
499503

500504
if (backfill && mode === 'combobox' && active !== null && source === 'keyboard') {
501505
setActiveValue(String(active));
502506
}
507+
508+
// Active will call multiple times.
509+
// We only need trigger the last one.
510+
const promise = Promise.resolve().then(() => {
511+
if (activeEventRef.current === promise) {
512+
onActive?.(active);
513+
}
514+
});
515+
activeEventRef.current = promise;
503516
},
504-
[backfill, mode],
517+
[backfill, mode, onActive],
505518
);
506519

507520
// ========================= OptionList =========================

tests/Accessibility.test.tsx

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ describe('Select.Accessibility', () => {
2222
});
2323

2424
// https://github.com/ant-design/ant-design/issues/31850
25-
it('active index should keep', () => {
25+
it('active index should keep', async () => {
26+
const onActive = jest.fn();
27+
2628
const { container } = render(
2729
<Select
2830
showSearch
@@ -40,26 +42,54 @@ describe('Select.Accessibility', () => {
4042
value: 'light',
4143
},
4244
]}
45+
onActive={onActive}
4346
/>,
4447
);
4548

4649
// First Match
4750
fireEvent.change(container.querySelector('input')!, { target: { value: 'b' } });
48-
jest.runAllTimers();
51+
await act(async () => {
52+
jest.runAllTimers();
53+
await Promise.resolve();
54+
});
4955

5056
expectOpen(container);
5157
expect(
5258
document.querySelector('.rc-select-item-option-active .rc-select-item-option-content')
5359
.textContent,
5460
).toEqual('Bamboo');
61+
expect(onActive).toHaveBeenCalledWith('bamboo');
62+
expect(onActive).toHaveBeenCalledTimes(1);
5563

5664
keyDown(container.querySelector('input')!, KeyCode.ENTER);
5765
expectOpen(container, false);
5866

5967
// Next Match
6068
fireEvent.change(container.querySelector('input')!, { target: { value: '' } });
69+
await act(async () => {
70+
await Promise.resolve();
71+
});
72+
expect(onActive).toHaveBeenCalledWith('bamboo');
73+
expect(onActive).toHaveBeenCalledTimes(2);
74+
75+
fireEvent.change(container.querySelector('input')!, { target: { value: 'not exist' } });
76+
await act(async () => {
77+
await Promise.resolve();
78+
});
79+
expect(onActive).toHaveBeenCalledWith(null);
80+
expect(onActive).toHaveBeenCalledTimes(3);
81+
6182
fireEvent.change(container.querySelector('input')!, { target: { value: 'g' } });
62-
jest.runAllTimers();
83+
await act(async () => {
84+
await Promise.resolve();
85+
});
86+
expect(onActive).toHaveBeenCalledWith('light');
87+
expect(onActive).toHaveBeenCalledTimes(4);
88+
89+
await act(async () => {
90+
jest.runAllTimers();
91+
await Promise.resolve();
92+
});
6393

6494
expectOpen(container);
6595
expect(

0 commit comments

Comments
 (0)