8000 feat: add onActive by Wxh16144 · Pull Request #1153 · react-component/select · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

feat: add onActive #1153

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

Closed
Closed
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
58 changes: 27 additions & 31 deletions src/OptionList.tsx
8000
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import useBaseProps from './hooks/useBaseProps';
import type { FlattenOptionData } from './interface';
import { isPlatformMac } from './utils/platformUtil';
import { isValidCount } from './utils/valueUtil';
import { useEvent } from 'rc-util';

// export interface OptionListProps<OptionsType extends object[]> {
export type OptionListProps = Record<string, never>;
Expand Down Expand Up @@ -128,11 +129,6 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
onActiveValue(flattenItem.value, index, info);
};

// Auto active first item when list length or searchValue changed
useEffect(() => {
setActive(defaultActiveFirstOption !== false ? getEnabledActiveIndex(0) : -1);
}, [memoFlattenOptions.length, searchValue]);

// https://github.com/ant-design/ant-design/issues/48036
const isAriaSelected = React.useCallback(
(value: RawValueType) => {
Expand All @@ -144,35 +140,35 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
[mode, searchValue, [...rawValues].toString(), rawValues.size],
);

// Auto scroll to item position in single mode
useEffect(() => {
/**
* React will skip `onChange` when component update.
* `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);
scrollIntoView(index);
}
}
});
const handleActive = useEvent(() => {
if (!open) return;

let index = -1;

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

// Force trigger scrollbar visible when open
if (open) {
listRef.current?.scrollTo(undefined);
if (index === -1 && defaultActiveFirstOption !== false) {
// If no value found, scroll to the first enabled item
index = getEnabledActiveIndex(0);
}

return () => clearTimeout(timeoutId);
}, [open, searchValue]);
setActive(index);

if (index !== -1) {
scrollIntoView(index);
}

listRef.current?.scrollTo(undefined);
});

useEffect(handleActive, [open, searchValue, memoFlattenOptions.length]);

// ========================== Values ==========================
const RawValueType) => {
Expand Down
39 changes: 20 additions & 19 deletions src/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,11 @@ import useFilterOptions from './hooks/useFilterOptions';
import useId from './hooks/useId';
import useOptions from './hooks/useOptions';
import useRefFunc from './hooks/useRefFunc';
import type { FlattenOptionData } from './interface';
import type { FlattenOptionData, RawValueType } from './interface';
import { hasValue, isComboNoValue, toArray } from './utils/commonUtil';
import { fillFieldNames, flattenOptions, injectPropsWithOption } from './utils/valueUtil';
import warningProps, { warningNullOptions } from './utils/warningPropsUtil';
import { useEvent } from 'rc-util';

const OMIT_DOM_PROPS = ['inputValue'];

Expand All @@ -66,7 +67,6 @@ export type >

export type RawValueType, info: { selected: boolean }) => void;

export type RawValueType = string | number;
export interface LabelInValueType {
label: React.ReactNode;
value: RawValueType;
Expand Down Expand Up @@ -130,6 +130,9 @@ export interface SelectProps<ValueType = any, OptionType extends BaseOptionType
onSelect?: SelectHandler<ArrayElementType<ValueType>, OptionType>;
onDeselect?: SelectHandler<ArrayElementType<ValueType>, OptionType>;

// >>> active
onActive?: OnActiveValue;

// >>> Options
/**
* In Select, `false` means do nothing.
Expand Down Expand Up @@ -210,6 +213,7 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp
labelInValue,
onChange,
maxCount,
onActive,

...restProps
} = props;
Expand Down Expand Up @@ -488,21 +492,18 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp
};

// ======================= Accessibility ========================
const [activeValue, setActiveValue] = React.useState<string>(null);
const [innerActiveValue, setInnerActiveValue] = React.useState<string>(null);
const [accessibilityIndex, setAccessibilityIndex] = React.useState(0);
const mergedDefaultActiveFirstOption =
defaultActiveFirstOption !== undefined ? defaultActiveFirstOption : mode !== 'combobox';

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

if (backfill && mode === 'combobox' && active !== null && source === 'keyboard') {
setActiveValue(String(active));
}
},
[backfill, mode],
);
const handleActiveValueChange = useEvent<OnActiveValue>((active, index, info = {}) => {
setAccessibilityIndex(index);
onActive?.(active, index, info);
if (backfill && mode === 'combobox' && active !== null && info.source === 'keyboard') {
setInnerActiveValue(String(active));
}
});

// ========================= OptionList =========================
const triggerSelect = (val: RawValueType, selected: boolean, type?: DisplayInfoType) => {
Expand Down Expand Up @@ -548,10 +549,10 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp
// Clean search value if single or configured
if (mode === 'combobox') {
// setSearchValue(String(val));
setActiveValue('');
setInnerActiveValue('');
} else if (!isMultiple || autoClearSearchValue) {
setSearchValue('');
setActiveValue('');
setInnerActiveValue('');
}
});

Expand All @@ -571,7 +572,7 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp
// =========================== Search ===========================
const onInternalSearch: BaseSelectProps['onSearch'] = (searchText, info) => {
setSearchValue(searchText);
setActiveValue(null);
setInnerActiveValue(null);

// [Submit] Tag mode should flush input
if (info.source === 'submit') {
Expand Down Expand Up @@ -621,7 +622,7 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp
return {
...parsedOptions,
flattenOptions: displayOptions,
onActiveValue,
onActiveValue: handleActiveValueChange,
defaultActiveFirstOption: mergedDefaultActiveFirstOption,
onSelect: onInternalSelect,
menuItemSelectedIcon,
Expand All @@ -639,7 +640,7 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp
maxCount,
parsedOptions,
displayOptions,
onActiveValue,
handleActiveValueChange,
mergedDefaultActiveFirstOption,
onInternalSelect,
menuItemSelectedIcon,
Expand Down Expand Up @@ -688,7 +689,7 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp
OptionList={OptionList}
emptyOptions={!displayOptions.length}
// >>> Accessibility
activeValue={activeValue}
activeValue={innerActiveValue}
activeDescendantId={`${mergedId}_list_${accessibilityIndex}`}
/>
</SelectContext.Provider>
Expand Down
1 change: 1 addition & 0 deletions tests/OptionList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ describe('OptionList', () => {

render(
generateList({
open: true,
options: [{ value: '1' }, { value: '2' }],
values: new Set('1'),
onActiveValue,
Expand Down
35 changes: 35 additions & 0 deletions tests/Select.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1648,6 +1648,41 @@ describe('Select.Basic', () => {
expect(onKeyUp).toHaveBeenCalled();
});

it('onActive', () => {
const >
const { getByRole, container } = render(
<Select open>
<Option value="1">One</Option>
<Option value="2">Two</Option>
<Option value={3}>Three</Option>
</Select>,
);

const input = getByRole('combobox');

keyDown(input, KeyCode.DOWN);
keyDown(input, KeyCode.DOWN);
expect(onActive).toHaveBeenCalledTimes(3);
expect(onActive).toHaveBeenLastCalledWith(
3,
2,
expect.objectContaining({
source: 'keyboard',
}),
);

fireEvent.mouseMove(container.querySelector('.rc-select-item-option'));
expect(onActive).toHaveBeenCalledTimes(4);

expect(onActive).toHaveBeenLastCalledWith(
'1',
0,
expect.objectContaining({
source: 'mouse',
}),
);
});

describe('warning if label not same as option', () => {
it('should work', () => {
resetWarned();
Expand Down
Loading
0