Skip to content
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

feat(Input, Combobox, Autocomplete): add showCleanCross prop #3594

Open
wants to merge 21 commits into
base: next
Choose a base branch
from
Open
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,32 @@ export const Example9: Story = () => {
return <Autocomplete source={items} value={value} onValueChange={setValue} borderless />;
};
Example9.storyName = 'Режима прозрачной рамки';

/** При showClearIcon="always" крестик отображается всегда, если в автокомплит что-либо введено.
*
* При showClearIcon="onFocus" крестик отображается при фокусировке на автокомплите, в который что-либо введено. */
export const Example10: Story = () => {
const items = ['Отображаю крестик всегда', 'Отображаю крестик по фокусу', 'Никогда не отображаю крестик'];
const [valueAlways, setValueAlways] = React.useState(items[0]);
const [valueOnFocus, setValueOnFocus] = React.useState(items[1]);

return (
<Gapped gap={10}>
<Autocomplete
showClearIcon="always"
source={items}
value={valueAlways}
onValueChange={setValueAlways}
width="250px"
/>
<Autocomplete
showClearIcon="onFocus"
source={items}
value={valueOnFocus}
onValueChange={setValueOnFocus}
width="250px"
/>
</Gapped>
);
};
Example10.storyName = 'Крестик для очистки';
Original file line number Diff line number Diff line change
Expand Up @@ -261,3 +261,7 @@ export const MenuPos = () => {
);
};
MenuPos.storyName = 'menuPos';

export const ClearCross = () => {
return <Autocomplete showClearIcon="always" value="hello" onValueChange={() => {}} />;
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import OkIcon from '@skbkontur/react-icons/Ok';
import userEvent from '@testing-library/user-event';
import { mount } from 'enzyme';

import { InputDataTids } from '../../../components/Input';
import { InputDataTids, ShowClearIcon } from '../../../components/Input';
import { Autocomplete, AutocompleteProps, AutocompleteIds, AutocompleteDataTids } from '../Autocomplete';
import { delay, clickOutside } from '../../../lib/utils';

Expand Down Expand Up @@ -164,6 +164,45 @@ describe('<Autocomplete />', () => {
expect(screen.getByTestId('my-testy-icon')).toBeInTheDocument();
});

it('passes showClearIcon prop to input', async () => {
const ControlledAutocomplete = ({ clear }: { clear?: ShowClearIcon }) => {
const [value, setValue] = useState<string>('');
return <Autocomplete showClearIcon={clear} value={value} onValueChange={setValue} />;
};
const { rerender } = render(<ControlledAutocomplete />);
const autocomplete = screen.getByRole('textbox');

expect(screen.queryByTestId(InputDataTids.clearCross)).toBeNull();
await userEvent.type(autocomplete, 'hello');
expect(screen.queryByTestId(InputDataTids.clearCross)).toBeNull();

rerender(<ControlledAutocomplete clear="never" />);
await userEvent.clear(autocomplete);
expect(screen.queryByTestId(InputDataTids.clearCross)).toBeNull();
await userEvent.type(autocomplete, 'hello');
expect(screen.queryByTestId(InputDataTids.clearCross)).toBeNull();

rerender(<ControlledAutocomplete clear="onFocus" />);
await userEvent.clear(autocomplete);
expect(screen.queryByTestId(InputDataTids.clearCross)).toBeNull();
await userEvent.type(autocomplete, 'hello');
await userEvent.tab();
expect(autocomplete).not.toHaveFocus();
expect(screen.queryByTestId(InputDataTids.clearCross)).toBeNull();
await userEvent.click(autocomplete);
expect(screen.queryByTestId(InputDataTids.clearCross)).toBeInTheDocument();

rerender(<ControlledAutocomplete clear="always" />);
await userEvent.clear(autocomplete);
expect(screen.queryByTestId(InputDataTids.clearCross)).toBeNull();
await userEvent.type(autocomplete, 'hello');
await userEvent.tab();
expect(autocomplete).not.toHaveFocus();
expect(screen.queryByTestId(InputDataTids.clearCross)).toBeInTheDocument();
await userEvent.click(autocomplete);
expect(screen.queryByTestId(InputDataTids.clearCross)).toBeInTheDocument();
});

it('passes id prop to input', () => {
const onValueChange = jest.fn();
const source: any[] = [];
Expand Down
18 changes: 16 additions & 2 deletions packages/react-ui/components/ComboBox/ComboBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React, { AriaAttributes, HTMLAttributes } from 'react';
import { CustomComboBox } from '../../internal/CustomComboBox';
import { Nullable } from '../../typings/utility-types';
import { MenuItemState } from '../MenuItem';
import { InputIconType } from '../Input';
import { InputIconType, ShowClearIcon } from '../Input';
import { CommonProps } from '../../internal/CommonWrapper';
import { rootNode, TSetRootNode } from '../../lib/rootNode';
import { createPropsGetter } from '../../lib/createPropsGetter';
Expand All @@ -13,6 +13,12 @@ export interface ComboBoxProps<T>
extends Pick<AriaAttributes, 'aria-describedby' | 'aria-label'>,
Pick<HTMLAttributes<HTMLElement>, 'id'>,
CommonProps {
/** Устанавливает иконку крестика, при нажатии на который комбобокс очищается.
* При значении "always" крестик отображается всегда, если поле непустое.
* При значении "onFocus" крестик отображается при фокусировке на непустом поле.
* @default never */
showClearIcon?: ShowClearIcon;

/** Задает выравнивание контента. */
align?: 'left' | 'center' | 'right';

Expand Down Expand Up @@ -170,7 +176,14 @@ export type ComboBoxExtendedItem<T> = T | (() => React.ReactElement<T>) | React.
type DefaultProps<T> = Required<
Pick<
ComboBoxProps<T>,
'itemToValue' | 'valueToString' | 'renderValue' | 'renderItem' | 'menuAlign' | 'searchOnFocus' | 'drawArrow'
| 'itemToValue'
| 'valueToString'
| 'renderValue'
| 'renderItem'
| 'menuAlign'
| 'searchOnFocus'
| 'drawArrow'
| 'showClearIcon'
>
>;

Expand All @@ -197,6 +210,7 @@ export class ComboBox<T = ComboBoxItem> extends React.Component<ComboBoxProps<T>
menuAlign: 'left',
searchOnFocus: true,
drawArrow: true,
showClearIcon: 'never',
};

private getProps = createPropsGetter(ComboBox.defaultProps);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -537,3 +537,54 @@ export const Example9: Story = () => {
);
};
Example9.storyName = 'Размер';

/** При showClearIcon="always" крестик отображается всегда, если в комбобокс что-либо введено.
*
* При showClearIcon="onFocus" крестик отображается при фокусировке на комбобоксе, в который что-либо введено. */
export const Example10: Story = () => {
const [valueAlways, setValueAlways] = React.useState({
value: 1,
label: 'Отображаю крестик всегда',
});
const [valueOnFocus, setValueOnFocus] = React.useState({
value: 2,
label: 'Отображаю крестик по фокусу',
});
const getItems = (q: string) => {
return Promise.resolve(
[
{
value: 1,
label: 'Отображаю крестик всегда',
},
{
value: 2,
label: 'Отображаю крестик по фокусу',
},
{
value: 3,
label: 'Никогда не отображаю крестик',
},
].filter((x) => x.label.toLowerCase().includes(q.toLowerCase()) || x.value.toString(10) === q),
);
};
return (
<Gapped gap={10}>
<ComboBox
showClearIcon="always"
getItems={getItems}
value={valueAlways}
onValueChange={setValueAlways}
width="250px"
/>
<ComboBox
showClearIcon="onFocus"
getItems={getItems}
value={valueOnFocus}
onValueChange={setValueOnFocus}
width="250px"
/>
</Gapped>
);
};
Example10.storyName = 'Крестик для очистки';
Original file line number Diff line number Diff line change
Expand Up @@ -954,3 +954,29 @@ export const WithMenuAlignAndMenuPos: Story = () => {
WithMenuAlignAndMenuPos.parameters = {
creevey: { skip: { 'no themes': { in: /^(?!\b(chrome2022)\b)/ } } },
};

export const ComboboxWithClearCross: Story = () => {
const [value, setValue] = React.useState({
value: 2,
label: 'Second',
});
const getItems = (q: string) => {
return Promise.resolve(
[
{
value: 1,
label: 'First',
},
{
value: 2,
label: 'Second',
},
].filter((x) => x.label.toLowerCase().includes(q.toLowerCase()) || x.value.toString(10) === q),
);
};
return (
<Gapped>
<ComboBox showClearIcon="always" getItems={getItems} value={value} onValueChange={setValue} />
</Gapped>
);
};
55 changes: 55 additions & 0 deletions packages/react-ui/components/ComboBox/__tests__/ComboBox-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1404,6 +1404,61 @@ describe('ComboBox', () => {
expect(await screen.findByTestId(ComboBoxMenuDataTids.item)).toHaveTextContent(testValues[1].label);
});
});

describe('with clear cross', () => {
it('clears controlled combobox', async () => {
const ControlledCombobox = () => {
const [value, setValue] = React.useState({
value: 2,
label: 'Second',
});
const getItems = (q: string) => {
return Promise.resolve(
[
{
value: 1,
label: 'First',
},
{
value: 2,
label: 'Second',
},
].filter((x) => x.label.toLowerCase().includes(q.toLowerCase()) || x.value.toString(10) === q),
);
};
return <ComboBox getItems={getItems} showClearIcon="always" value={value} onValueChange={setValue} />;
};
render(<ControlledCombobox />);

expect(screen.getByText('Second')).toBeInTheDocument();
const cross = screen.getByTestId(InputDataTids.clearCross);
await userEvent.click(cross);
expect(screen.queryByText('Second')).not.toBeInTheDocument();
});

it('clears uncontrolled combobox', async () => {
const testValues = [
{ value: '1', label: 'One' },
{ value: '2', label: 'Two' },
{ value: '3', label: 'Three' },
{ value: '4', label: 'Four' },
];
const getItems = jest.fn((searchQuery) =>
Promise.resolve(testValues.filter((x) => x.label.includes(searchQuery))),
);
render(<ComboBox showClearIcon="always" getItems={getItems} ref={comboboxRef} />);

comboboxRef.current?.focus();
await userEvent.type(screen.getByRole('textbox'), 'z');
expect(screen.getByRole('textbox')).toHaveValue('z');
const cross = screen.getByTestId(InputDataTids.clearCross);
expect(cross).toBeInTheDocument();

await userEvent.click(cross);
expect(cross).not.toBeInTheDocument();
expect(screen.getByRole('textbox')).toHaveValue('');
});
});
});

describe('mobile comboBox', () => {
Expand Down
Copy link
Member Author

Choose a reason for hiding this comment

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

В CurrencyInput не наследуем свойство по договоренности с дизайном

Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export interface CurrencyInputProps
extends Pick<AriaAttributes, 'aria-label'>,
CommonProps,
Override<
InputProps,
Omit<InputProps, 'showClearIcon'>,
{
/** Задает значение инпута. */
value?: Nullable<number>;
Expand Down
Loading