Skip to content

Commit

Permalink
feat(Select): add hasClear (#668)
Browse files Browse the repository at this point in the history
  • Loading branch information
Marginy605 authored Jun 26, 2023
1 parent 6aa4143 commit 93a2a00
Show file tree
Hide file tree
Showing 21 changed files with 509 additions and 113 deletions.
1 change: 1 addition & 0 deletions src/components/Select/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
| multiple | `boolean` | `false` | Indicates that multiple options can be selected in the list |
| filterable | `boolean` | `false` | Indicates that select popup have filter section |
| disabled | `boolean` | `false` | Indicates that the user cannot interact with the control |
| hasClear | `boolean` | `false` | Enable displaying icon for clear selected options |

---

Expand Down
5 changes: 4 additions & 1 deletion src/components/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(function
disabled = false,
filterable = false,
disablePortal,
hasClear = false,
onClose,
} = props;
const [mobile] = useMobile();
Expand All @@ -83,7 +84,7 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(function
const filterRef = React.useRef<SelectFilterRef>(null);
const listRef = React.useRef<List<FlattenOption>>(null);
const handleControlRef = useForkRef(ref, controlRef);
const {value, open, toggleOpen, handleSelection} = useSelect({
const {value, open, toggleOpen, handleSelection, handleClearValue} = useSelect({
onUpdate,
value: propsValue,
defaultValue,
Expand Down Expand Up @@ -212,6 +213,8 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(function
>
<SelectControl
toggleOpen={toggleOpen}
hasClear={hasClear}
clearValue={handleClearValue}
ref={handleControlRef}
className={controlClassName}
qa={qa}
Expand Down
1 change: 1 addition & 0 deletions src/components/Select/__stories__/Select.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ Showcase.args = {
disabled: false,
placeholder: 'Values',
label: '',
hasClear: false,
};
9 changes: 9 additions & 0 deletions src/components/Select/__stories__/SelectShowcase.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
@use '../../variables';

$block: '.#{variables.$ns}select-showcase';
$blockSelectClear: '.#{variables.$ns-new}select-clear';

#{$block} {
display: flex;
Expand Down Expand Up @@ -35,6 +36,14 @@ $block: '.#{variables.$ns}select-showcase';

&__user-control {
display: inline-flex;

#{$block}_has-clear #{$block}__text {
vertical-align: top;
}
}

&__user-clear-icon {
margin-left: 6px;
}

&__custom-popup {
Expand Down
14 changes: 11 additions & 3 deletions src/components/Select/__stories__/SelectShowcase.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import React from 'react';

import {TrashBin} from '@gravity-ui/icons';
import range from 'lodash/range';

import {Select} from '..';
import type {SelectOption, SelectProps} from '..';
import {Button} from '../../Button';
import {ClipboardButton} from '../../ClipboardButton';
import {Icon} from '../../Icon';
import {RadioButton} from '../../RadioButton';
import type {RadioButtonOption} from '../../RadioButton';
import {TextInput} from '../../TextInput';
Expand Down Expand Up @@ -247,12 +249,12 @@ export const SelectShowcase = (props: SelectProps) => {
<Select.Option value="val4" content="Value4" data={{color: 'purple'}} />
</ExampleItem>
<ExampleItem
title="Select with user control"
title="Select with user control and native custom icon"
code={[EXAMPLE_USER_CONTROL]}
selectProps={{
...props,
className: b('user-control'),
renderControl: ({onClick, onKeyDown, ref}) => {
renderControl: ({onClick, onKeyDown, ref, renderClear}) => {
return (
<Button
ref={ref}
Expand All @@ -261,8 +263,14 @@ export const SelectShowcase = (props: SelectProps) => {
extraProps={{
onKeyDown,
}}
className={b({'has-clear': props.hasClear})}
>
User control
<span className={b('text')}>User control</span>
{renderClear?.({
renderIcon: () => (
<Icon data={TrashBin} className={b('user-clear-icon')} />
),
})}
</Button>
);
},
Expand Down
5 changes: 4 additions & 1 deletion src/components/Select/__stories__/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export const EXAMPLE_USER_CONTROL = `const [value, setValue] = React.useState<st
<Select
value={value}
renderControl={({onClick, onKeyDown, ref}) => {
renderControl={({onClick, onKeyDown, ref, renderClear}) => {
return (
<Button
ref={ref}
Expand All @@ -145,6 +145,9 @@ export const EXAMPLE_USER_CONTROL = `const [value, setValue] = React.useState<st
}}
>
User control
{renderClear?.({
renderIcon: () => <Icon data={CrossIcon} />,
})}
</Button>
);
}},
Expand Down
16 changes: 8 additions & 8 deletions src/components/Select/__tests__/Select.base-actions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
GROUPED_OPTIONS,
GROUPED_QUICK_SEARCH_OPTIONS,
QUICK_SEARCH_OPTIONS,
SELECT_CONTROL_OPEN_CLASS,
SELECT_CONTROL_BUTTON_OPEN_CLASS,
SELECT_LIST_VIRTUALIZED_CLASS,
TEST_QA,
generateOptions,
Expand Down Expand Up @@ -60,7 +60,7 @@ describe('Select base actions', () => {
setup();
});
const selectControl = screen.getByTestId(TEST_QA);
expect(selectControl).not.toHaveClass(SELECT_CONTROL_OPEN_CLASS);
expect(selectControl).not.toHaveClass(SELECT_CONTROL_BUTTON_OPEN_CLASS);
expect(screen.queryByTestId(SelectQa.POPUP)).toBeNull();
});
test('should have [type="button"] attribute in root button', async () => {
Expand All @@ -78,7 +78,7 @@ describe('Select base actions', () => {
setup({defaultOpen: true});
});
const selectControl = screen.getByTestId(TEST_QA);
expect(selectControl).toHaveClass(SELECT_CONTROL_OPEN_CLASS);
expect(selectControl).toHaveClass(SELECT_CONTROL_BUTTON_OPEN_CLASS);
screen.getByTestId(SelectQa.POPUP);
});

Expand All @@ -88,29 +88,29 @@ describe('Select base actions', () => {
});

const selectControl = screen.getByTestId(TEST_QA);
expect(selectControl).toHaveClass(SELECT_CONTROL_OPEN_CLASS);
expect(selectControl).toHaveClass(SELECT_CONTROL_BUTTON_OPEN_CLASS);
});

test('shoult open/close by open prop', async () => {
const {rerender, getByTestId} = render(<ControlledSelect open={true} />);

const selectControl = getByTestId(TEST_QA);
expect(selectControl).toHaveClass(SELECT_CONTROL_OPEN_CLASS);
expect(selectControl).toHaveClass(SELECT_CONTROL_BUTTON_OPEN_CLASS);

rerender(<ControlledSelect open={false} />);

const rerenderedSelectControl = getByTestId(TEST_QA);
expect(rerenderedSelectControl).not.toHaveClass(SELECT_CONTROL_OPEN_CLASS);
expect(rerenderedSelectControl).not.toHaveClass(SELECT_CONTROL_BUTTON_OPEN_CLASS);
});
test('should not close when open=true prop passed', async () => {
setup({open: true});
const selectControl = screen.getByTestId(TEST_QA);

expect(selectControl).toHaveClass(SELECT_CONTROL_OPEN_CLASS);
expect(selectControl).toHaveClass(SELECT_CONTROL_BUTTON_OPEN_CLASS);

await toggleSelectPopup();

expect(selectControl).toHaveClass(SELECT_CONTROL_OPEN_CLASS);
expect(selectControl).toHaveClass(SELECT_CONTROL_BUTTON_OPEN_CLASS);
});

test('should call onOpenChange while closing', async () => {
Expand Down
76 changes: 76 additions & 0 deletions src/components/Select/__tests__/Select.clear.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {cleanup} from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import {SelectQa} from '../constants';
import type {SelectProps} from '../types';

import {DEFAULT_OPTIONS, setup} from './utils';

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

const onUpdate = jest.fn();

describe('Select clear', () => {
test.each<[string, Partial<SelectProps>]>([
['single', {hasClear: true, multiple: false}],
['multiple', {hasClear: true, multiple: true}],
])('hide clear icon with hasClear and no selected value', async () => {
const {queryByTestId} = setup({hasClear: true});
expect(queryByTestId(SelectQa.CLEAR)).toBeNull();
});

test.each<[string, Partial<SelectProps>]>([
['single', {hasClear: true, multiple: false}],
['multiple', {hasClear: true, multiple: true}],
])('display clear icon with hasClear and with selected value', async () => {
const {getByTestId} = setup({hasClear: true, value: [DEFAULT_OPTIONS[0].value]});
getByTestId(SelectQa.CLEAR);
});

test.each<[string, Partial<SelectProps>]>([
['single', {hasClear: true, multiple: false}],
['multiple', {hasClear: true, multiple: true}],
])('hide clear icon for disabled select with hasClear and with selected value', async () => {
const {queryByTestId} = setup({
hasClear: true,
disabled: true,
value: [DEFAULT_OPTIONS[0].value],
});
expect(queryByTestId(SelectQa.CLEAR)).toBeNull();
});

test.each<[string, Partial<SelectProps>]>([
['single', {hasClear: true, multiple: false}],
['multiple', {hasClear: true, multiple: true}],
])('click on clear icon will remove selected value', async () => {
const {getByTestId} = setup({
hasClear: true,
value: [DEFAULT_OPTIONS[0].value],
onUpdate,
});

const user = userEvent.setup();
await user.click(getByTestId(SelectQa.CLEAR));
expect(onUpdate).toHaveBeenCalledWith([]);
});

test.each<[string, Partial<SelectProps>]>([
['single', {hasClear: true, multiple: false}],
['multiple', {hasClear: true, multiple: true}],
])('check for correct focus on tab with hasClear and with selected value', async () => {
setup({
hasClear: true,
value: [DEFAULT_OPTIONS[0].value],
onUpdate,
});
const user = userEvent.setup();
await user.keyboard('[Tab]');
await user.keyboard('[Tab]');
// check that after double tab clear icon is focused and press enter will clear the value
await user.keyboard('[Enter]');
expect(onUpdate).toHaveBeenCalledWith([]);
});
});
32 changes: 16 additions & 16 deletions src/components/Select/__tests__/Select.muitiple.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
DEFAULT_OPTIONS,
GROUPED_OPTIONS,
OptionsListType,
SELECT_CONTROL_OPEN_CLASS,
SELECT_CONTROL_BUTTON_OPEN_CLASS,
TEST_QA,
setup,
} from './utils';
Expand Down Expand Up @@ -33,23 +33,23 @@ describe('Select multiple mode actions', () => {
const selectControl = getByTestId(TEST_QA);
// open select popup
await user.click(selectControl);
expect(selectControl).toHaveClass(SELECT_CONTROL_OPEN_CLASS);
expect(selectControl).toHaveClass(SELECT_CONTROL_BUTTON_OPEN_CLASS);
let option = getByText(selectedOptions[0].content as string);
// select first option
await user.click(option);
expect(onUpdate).toHaveBeenLastCalledWith([selectedOptions[0].value]);
expect(selectControl).toHaveClass(SELECT_CONTROL_OPEN_CLASS);
expect(selectControl).toHaveClass(SELECT_CONTROL_BUTTON_OPEN_CLASS);
option = getByText(selectedOptions[1].content as string);
// select second option
await user.click(option);
expect(onUpdate).toHaveBeenLastCalledWith([
selectedOptions[0].value,
selectedOptions[1].value,
]);
expect(selectControl).toHaveClass(SELECT_CONTROL_OPEN_CLASS);
expect(selectControl).toHaveClass(SELECT_CONTROL_BUTTON_OPEN_CLASS);
// close select popup
await user.keyboard('[Escape]');
expect(selectControl).not.toHaveClass(SELECT_CONTROL_OPEN_CLASS);
expect(selectControl).not.toHaveClass(SELECT_CONTROL_BUTTON_OPEN_CLASS);
});
});

Expand All @@ -70,15 +70,15 @@ describe('Select multiple mode actions', () => {
const selectControl = getByTestId(TEST_QA);
// open select popup
await user.click(selectControl);
expect(selectControl).toHaveClass(SELECT_CONTROL_OPEN_CLASS);
expect(selectControl).toHaveClass(SELECT_CONTROL_BUTTON_OPEN_CLASS);
const option = getByText(selectedOptions[0].content as string);
// deselect first option
await user.click(option);
expect(selectControl).toHaveClass(SELECT_CONTROL_OPEN_CLASS);
expect(selectControl).toHaveClass(SELECT_CONTROL_BUTTON_OPEN_CLASS);
expect(onUpdate).toHaveBeenLastCalledWith([selectedOptions[1].value]);
// close select popup
await user.keyboard('[Escape]');
expect(selectControl).not.toHaveClass(SELECT_CONTROL_OPEN_CLASS);
expect(selectControl).not.toHaveClass(SELECT_CONTROL_BUTTON_OPEN_CLASS);
});
});

Expand All @@ -96,11 +96,11 @@ describe('Select multiple mode actions', () => {
await user.keyboard('[Tab]');
// open select popup
await user.keyboard(`[${key}]`);
expect(selectControl).toHaveClass(SELECT_CONTROL_OPEN_CLASS);
expect(selectControl).toHaveClass(SELECT_CONTROL_BUTTON_OPEN_CLASS);
// select active option (first option by default)
await user.keyboard(`[${key}]`);
expect(onUpdate).toHaveBeenLastCalledWith([selectedOptions[0].value]);
expect(selectControl).toHaveClass(SELECT_CONTROL_OPEN_CLASS);
expect(selectControl).toHaveClass(SELECT_CONTROL_BUTTON_OPEN_CLASS);
// focus next option
await user.keyboard('[ArrowDown]');
// select next option
Expand All @@ -109,10 +109,10 @@ describe('Select multiple mode actions', () => {
selectedOptions[0].value,
selectedOptions[1].value,
]);
expect(selectControl).toHaveClass(SELECT_CONTROL_OPEN_CLASS);
expect(selectControl).toHaveClass(SELECT_CONTROL_BUTTON_OPEN_CLASS);
// close select popup
await user.keyboard('[Escape]');
expect(selectControl).not.toHaveClass(SELECT_CONTROL_OPEN_CLASS);
expect(selectControl).not.toHaveClass(SELECT_CONTROL_BUTTON_OPEN_CLASS);
});
});

Expand All @@ -136,20 +136,20 @@ describe('Select multiple mode actions', () => {
await user.keyboard('[Tab]');
// open select popup
await user.keyboard(`[${key}]`);
expect(selectControl).toHaveClass(SELECT_CONTROL_OPEN_CLASS);
expect(selectControl).toHaveClass(SELECT_CONTROL_BUTTON_OPEN_CLASS);
// select active option (first option by default)
await user.keyboard(`[${key}]`);
expect(onUpdate).toHaveBeenLastCalledWith([selectedOptions[1].value]);
expect(selectControl).toHaveClass(SELECT_CONTROL_OPEN_CLASS);
expect(selectControl).toHaveClass(SELECT_CONTROL_BUTTON_OPEN_CLASS);
// focus next option
await user.keyboard('[ArrowDown]');
// select next option
await user.keyboard(`[${key}]`);
expect(onUpdate).toHaveBeenCalledWith([]);
expect(selectControl).toHaveClass(SELECT_CONTROL_OPEN_CLASS);
expect(selectControl).toHaveClass(SELECT_CONTROL_BUTTON_OPEN_CLASS);
// close select popup
await user.keyboard('[Escape]');
expect(selectControl).not.toHaveClass(SELECT_CONTROL_OPEN_CLASS);
expect(selectControl).not.toHaveClass(SELECT_CONTROL_BUTTON_OPEN_CLASS);
});
});
});
Loading

0 comments on commit 93a2a00

Please sign in to comment.