Skip to content

Commit

Permalink
feat(Select): add filter prop (#1669)
Browse files Browse the repository at this point in the history
  • Loading branch information
ValeraS authored Jun 19, 2024
1 parent c61fe11 commit 98750c6
Show file tree
Hide file tree
Showing 9 changed files with 108 additions and 68 deletions.
48 changes: 21 additions & 27 deletions src/components/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import React from 'react';

import {KeyCode} from '../../constants';
import {useFocusWithin, useForkRef, useSelect, useUniqId} from '../../hooks';
import {useControlledState, useFocusWithin, useForkRef, useSelect, useUniqId} from '../../hooks';
import type {List} from '../List';
import {OuterAdditionalContent} from '../controls/common/OuterAdditionalContent/OuterAdditionalContent';
import {errorPropsMapper} from '../controls/utils';
Expand All @@ -21,7 +21,6 @@ import {
import {DEFAULT_VIRTUALIZATION_THRESHOLD, selectBlock} from './constants';
import {useQuickSearch} from './hooks';
import {getSelectFilteredOptions, useSelectOptions} from './hooks-public';
import {initialState, reducer} from './store';
import {Option, OptionGroup} from './tech-components';
import type {SelectProps, SelectRenderPopup} from './types';
import type {SelectFilterRef} from './types-misc';
Expand Down Expand Up @@ -93,6 +92,7 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(function
multiple = false,
disabled = false,
filterable = false,
filter: propsFilter,
disablePortal,
hasClear = false,
onClose,
Expand All @@ -102,7 +102,7 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(function
title,
} = props;
const mobile = useMobile();
const [{filter}, dispatch] = React.useReducer(reducer, initialState);
const [filter, setFilter] = useControlledState(propsFilter, '', onFilterChange);
// to avoid problem with incorrect popper offset calculation
// for example: https://github.com/radix-ui/primitives/issues/1567
const controlWrapRef = React.useRef<HTMLDivElement>(null);
Expand All @@ -111,28 +111,6 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(function
const listRef = React.useRef<List<FlattenOption>>(null);
const handleControlRef = useForkRef(ref, controlRef);

const handleFilterChange = React.useCallback(
(nextFilter: string) => {
onFilterChange?.(nextFilter);
dispatch({type: 'SET_FILTER', payload: {filter: nextFilter}});
},
[onFilterChange],
);

const handleOpenChange = React.useCallback(
(open: boolean) => {
onOpenChange?.(open);

if (!open && filterable) {
// FIXME: rework after https://github.com/gravity-ui/uikit/issues/1354
setTimeout(() => {
handleFilterChange('');
}, 100);
}
},
[filterable, onOpenChange, handleFilterChange],
);

const {
value,
open,
Expand All @@ -150,9 +128,18 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(function
multiple,
open: propsOpen,
onClose,
onOpenChange: handleOpenChange,
onOpenChange,
});

React.useEffect(() => {
if (!open && filterable && mobile) {
// FIXME: add handlers to Sheet like in https://github.com/gravity-ui/uikit/issues/1354
setTimeout(() => {
setFilter('');
}, 300);
}
}, [open, filterable, setFilter, mobile]);

const propsOptions = props.options || getOptionsFromChildren(props.children);
const options = useSelectOptions({
options: propsOptions,
Expand Down Expand Up @@ -283,7 +270,7 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(function
size={size}
value={filter}
placeholder={filterPlaceholder}
onChange={handleFilterChange}
onChange={setFilter}
onKeyDown={handleFilterKeyDown}
renderFilter={renderFilter}
/>
Expand Down Expand Up @@ -369,6 +356,13 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(function
virtualized={virtualized}
mobile={mobile}
placement={popupPlacement}
onAfterClose={
filterable
? () => {
setFilter('');
}
: undefined
}
>
{renderPopup({renderFilter: _renderFilter, renderList: _renderList})}
</SelectPopup>
Expand Down
71 changes: 68 additions & 3 deletions src/components/Select/__tests__/Select.filter.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import React from 'react';

import userEvent from '@testing-library/user-event';

import {cleanup} from '../../../../test-utils/utils';
import {render, screen} from '../../../../test-utils/utils';
import {TextInput} from '../../controls';
import {MobileProvider} from '../../mobile';
import {Select} from '../Select';
import type {SelectOption, SelectProps, SelectRenderPopup} from '../types';

import {TEST_QA, generateOptions, generateOptionsGroups, setup} from './utils';
import {TEST_QA, generateOptions, generateOptionsGroups, setup, timeout} from './utils';

afterEach(() => {
cleanup();
jest.clearAllMocks();
});

Expand Down Expand Up @@ -127,4 +128,68 @@ describe('Select filter', () => {
await user.keyboard('definitely not option');
expect(queryAllByRole('option').length).toBe(0);
});

test('should not clear filter onClose if open is true', async () => {
const onClose = jest.fn();
render(
<MobileProvider mobile>
<Select
open
filterable
onFilterChange={onFilterChange}
onClose={onClose}
qa="select"
filterPlaceholder="filter"
>
<Select.Option value="one">One</Select.Option>
<Select.Option value="two">Two</Select.Option>
<Select.Option value="three">Three</Select.Option>
</Select>
</MobileProvider>,
);

await userEvent.click(screen.getByPlaceholderText('filter'));
await userEvent.keyboard('test');

expect(onFilterChange).toHaveBeenCalledTimes(4);
onFilterChange.mockClear();

await userEvent.click(document.body);
await timeout(400);

expect(onClose).toHaveBeenCalledTimes(1);
expect(onFilterChange).toHaveBeenCalledTimes(0);
});

test('should not clear filter onClose', async () => {
const onClose = jest.fn();
render(
<MobileProvider mobile>
<Select
defaultOpen={true}
filterable
onFilterChange={onFilterChange}
onClose={onClose}
qa="select"
filterPlaceholder="filter"
>
<Select.Option value="one">One</Select.Option>
<Select.Option value="two">Two</Select.Option>
<Select.Option value="three">Three</Select.Option>
</Select>
</MobileProvider>,
);

await userEvent.click(screen.getByPlaceholderText('filter'));
await userEvent.keyboard('test');

expect(onFilterChange).toHaveBeenCalledTimes(4);
onFilterChange.mockClear();

await userEvent.click(document.body);
await timeout(400);

expect(onClose).toHaveBeenCalledTimes(1);
expect(onFilterChange).toHaveBeenCalledTimes(1);
});
});
2 changes: 2 additions & 0 deletions src/components/Select/components/SelectPopup/SelectPopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const SelectPopup = React.forwardRef<HTMLDivElement, SelectPopupProps>(
(
{
handleClose,
onAfterClose,
width,
open,
placement = DEFAULT_PLACEMENT,
Expand Down Expand Up @@ -57,6 +58,7 @@ export const SelectPopup = React.forwardRef<HTMLDivElement, SelectPopupProps>(
restoreFocusRef={controlRef}
modifiers={getModifiers({width, disablePortal, virtualized})}
id={id}
onTransitionExited={onAfterClose}
>
{children}
</Popup>
Expand Down
1 change: 1 addition & 0 deletions src/components/Select/components/SelectPopup/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ export type SelectPopupProps = {
disablePortal?: boolean;
virtualized?: boolean;
id?: string;
onAfterClose?: () => void;
};
2 changes: 0 additions & 2 deletions src/components/Select/store/index.ts

This file was deleted.

15 changes: 0 additions & 15 deletions src/components/Select/store/reducer.ts

This file was deleted.

10 changes: 0 additions & 10 deletions src/components/Select/store/types.ts

This file was deleted.

3 changes: 2 additions & 1 deletion src/components/Select/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ export type SelectRenderCounter = (
export type SelectProps<T = any> = QAProps &
UseOpenProps & {
onUpdate?: (value: string[]) => void;
onFilterChange?: (filter: string) => void;
renderControl?: SelectRenderControl;
renderFilter?: (props: {
onChange: (filter: string) => void;
Expand Down Expand Up @@ -106,6 +105,8 @@ export type SelectProps<T = any> = QAProps &
validationState?: 'invalid';
multiple?: boolean;
filterable?: boolean;
filter?: string;
onFilterChange?: (filter: string) => void;
disablePortal?: boolean;
hasClear?: boolean;
onFocus?: (e: React.FocusEvent) => void;
Expand Down
24 changes: 14 additions & 10 deletions src/hooks/useSelect/useOpenState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,29 @@ import {useControlledState} from '../useControlledState/useControlledState';
import type {UseOpenProps} from './types';

export const useOpenState = (props: UseOpenProps) => {
const {onOpenChange, onClose} = props;
const handleOpenChange = React.useCallback(
(newOpen: boolean) => {
onOpenChange?.(newOpen);
if (newOpen === false && onClose) {
onClose();
}
},
[onOpenChange, onClose],
);

const [open, setOpenState] = useControlledState(
props.open,
props.defaultOpen ?? false,
props.onOpenChange,
handleOpenChange,
);

const {onClose} = props;
const toggleOpen = React.useCallback(
(val?: boolean) => {
const newOpen = typeof val === 'boolean' ? val : !open;
if (newOpen !== open) {
setOpenState(newOpen);
}

if (newOpen === false && onClose) {
onClose();
}
setOpenState(newOpen);
},
[open, setOpenState, onClose],
[open, setOpenState],
);

return {
Expand Down

0 comments on commit 98750c6

Please sign in to comment.