Skip to content

feat: support onActive #1154

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/examples/combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ class Combobox extends React.Component {
console.log('Ref:', this.textareaRef);
}

onActive = (value) => {
console.log('onActive', value);
};

onChange = (value, option) => {
console.log('onChange', value, option);
this.setState({
Expand Down Expand Up @@ -83,6 +87,7 @@ class Combobox extends React.Component {
value={value}
mode="combobox"
onChange={this.onChange}
onActive={this.onActive}
filterOption={(inputValue, option) => {
if (!inputValue) {
return true;
Expand Down
5 changes: 5 additions & 0 deletions docs/examples/controlled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ class Controlled extends React.Component<{}, ControlledState> {
this.setState({ open });
};

onActive = (value) => {
console.error('onActive', value);
};

render() {
const { open, destroy, value } = this.state;
if (destroy) {
Expand All @@ -69,6 +73,7 @@ class Controlled extends React.Component<{}, ControlledState> {
optionFilterProp="text"
onChange={this.onChange}
onPopupVisibleChange={this.onPopupVisibleChange}
onActive={this.onActive}
>
<Option value="01" text="jack" title="jack">
<b
Expand Down
26 changes: 14 additions & 12 deletions src/OptionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,20 +154,22 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
* `setActive` function will call root accessibility state update which makes re-render.
* So we need to delay to let Input component trigger onChange first.
*/
const timeoutId = setTimeout(() => {
if (!multiple && open && rawValues.size === 1) {
const value: RawValueType = Array.from(rawValues)[0];
// Scroll to the option closest to the searchValue if searching.
const index = memoFlattenOptions.findIndex(({ data }) =>
searchValue ? String(data.value).startsWith(searchValue) : data.value === value,
);

if (index !== -1) {
setActive(index);
let timeoutId: NodeJS.Timeout;
Copy link
Contributor

Choose a reason for hiding this comment

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

不应该是用 ReturnType<typeof setTimeout> 好一点么


if (!multiple && open && rawValues.size === 1) {
const value: RawValueType = Array.from(rawValues)[0];
// Scroll to the option closest to the searchValue if searching.
const index = memoFlattenOptions.findIndex(({ data }) =>
searchValue ? String(data.value).startsWith(searchValue) : data.value === value,
);

if (index !== -1) {
setActive(index);
timeoutId = setTimeout(() => {
scrollIntoView(index);
}
});
}
});
}

// Force trigger scrollbar visible when open
if (open) {
Expand Down
15 changes: 14 additions & 1 deletion src/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export interface SelectProps<ValueType = any, OptionType extends BaseOptionType
// >>> Select
onSelect?: SelectHandler<ArrayElementType<ValueType>, OptionType>;
onDeselect?: SelectHandler<ArrayElementType<ValueType>, OptionType>;
onActive?: (value: ValueType) => void;
Copy link
Contributor

Choose a reason for hiding this comment

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

info 是不是也可以一起丢出去?鼠标和键盘


// >>> Options
/**
Expand Down Expand Up @@ -185,6 +186,7 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp
// Select
onSelect,
onDeselect,
onActive,
popupMatchSelectWidth = true,

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

const activeEventRef = React.useRef<Promise<void>>();

const onActiveValue: OnActiveValue = React.useCallback(
(active, index, { source = 'keyboard' } = {}) => {
setAccessibilityIndex(index);

if (backfill && mode === 'combobox' && active !== null && source === 'keyboard') {
setActiveValue(String(active));
}

// Active will call multiple times.
// We only need trigger the last one.
const promise = Promise.resolve().then(() => {
if (activeEventRef.current === promise) {
onActive?.(active);
}
});
activeEventRef.current = promise;
},
[backfill, mode],
[backfill, mode, onActive],
);

// ========================= OptionList =========================
Expand Down
36 changes: 33 additions & 3 deletions tests/Accessibility.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ describe('Select.Accessibility', () => {
});

// https://github.com/ant-design/ant-design/issues/31850
it('active index should keep', () => {
it('active index should keep', async () => {
const onActive = jest.fn();

const { container } = render(
<Select
showSearch
Expand All @@ -40,26 +42,54 @@ describe('Select.Accessibility', () => {
value: 'light',
},
]}
onActive={onActive}
/>,
);

// First Match
fireEvent.change(container.querySelector('input')!, { target: { value: 'b' } });
jest.runAllTimers();
await act(async () => {
jest.runAllTimers();
await Promise.resolve();
});

expectOpen(container);
expect(
document.querySelector('.rc-select-item-option-active .rc-select-item-option-content')
.textContent,
).toEqual('Bamboo');
expect(onActive).toHaveBeenCalledWith('bamboo');
expect(onActive).toHaveBeenCalledTimes(1);

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

// Next Match
fireEvent.change(container.querySelector('input')!, { target: { value: '' } });
await act(async () => {
await Promise.resolve();
});
expect(onActive).toHaveBeenCalledWith('bamboo');
expect(onActive).toHaveBeenCalledTimes(2);

fireEvent.change(container.querySelector('input')!, { target: { value: 'not exist' } });
await act(async () => {
await Promise.resolve();
});
expect(onActive).toHaveBeenCalledWith(null);
expect(onActive).toHaveBeenCalledTimes(3);

fireEvent.change(container.querySelector('input')!, { target: { value: 'g' } });
jest.runAllTimers();
await act(async () => {
await Promise.resolve();
});
expect(onActive).toHaveBeenCalledWith('light');
expect(onActive).toHaveBeenCalledTimes(4);

await act(async () => {
jest.runAllTimers();
await Promise.resolve();
});

expectOpen(container);
expect(
Expand Down
Loading