diff --git a/packages/react-ui/.creevey/images/Autocomplete/Clear Cross/chrome2022.png b/packages/react-ui/.creevey/images/Autocomplete/Clear Cross/chrome2022.png
new file mode 100644
index 00000000000..6c8800dcfb9
--- /dev/null
+++ b/packages/react-ui/.creevey/images/Autocomplete/Clear Cross/chrome2022.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0e9b1d6fe36bdcc20ba38328696bf2c4a13a78e34e8759e20aa464e49770cf12
+size 1737
diff --git a/packages/react-ui/.creevey/images/Autocomplete/Clear Cross/chrome2022Dark.png b/packages/react-ui/.creevey/images/Autocomplete/Clear Cross/chrome2022Dark.png
new file mode 100644
index 00000000000..bb15e90cd4d
--- /dev/null
+++ b/packages/react-ui/.creevey/images/Autocomplete/Clear Cross/chrome2022Dark.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:de49085c8925952a36e1b4f979260cdddda62ea7ec51da85ba18a270de0cfed0
+size 1786
diff --git a/packages/react-ui/.creevey/images/Autocomplete/Clear Cross/firefox2022.png b/packages/react-ui/.creevey/images/Autocomplete/Clear Cross/firefox2022.png
new file mode 100644
index 00000000000..e3ba2bb3c0e
--- /dev/null
+++ b/packages/react-ui/.creevey/images/Autocomplete/Clear Cross/firefox2022.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:171b1d4524ce3f4cb9ee08694d731c4b75220c2f250806864d974681aa649cca
+size 3363
diff --git a/packages/react-ui/.creevey/images/Autocomplete/Clear Cross/firefox2022Dark.png b/packages/react-ui/.creevey/images/Autocomplete/Clear Cross/firefox2022Dark.png
new file mode 100644
index 00000000000..9457b96cb68
--- /dev/null
+++ b/packages/react-ui/.creevey/images/Autocomplete/Clear Cross/firefox2022Dark.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:45cd74402ab5fad9bae129aadaf79861a503296161cbe8cd868413b5c9bac96c
+size 3325
diff --git a/packages/react-ui/.creevey/images/ComboBox/Combobox With Clear Cross/chrome2022.png b/packages/react-ui/.creevey/images/ComboBox/Combobox With Clear Cross/chrome2022.png
new file mode 100644
index 00000000000..69c49299994
--- /dev/null
+++ b/packages/react-ui/.creevey/images/ComboBox/Combobox With Clear Cross/chrome2022.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d7ec45988f3e06047ea22582bb111938cedfc7df782ddc534625b403b1e2b583
+size 1323
diff --git a/packages/react-ui/.creevey/images/ComboBox/Combobox With Clear Cross/chrome2022Dark.png b/packages/react-ui/.creevey/images/ComboBox/Combobox With Clear Cross/chrome2022Dark.png
new file mode 100644
index 00000000000..3a155039eab
--- /dev/null
+++ b/packages/react-ui/.creevey/images/ComboBox/Combobox With Clear Cross/chrome2022Dark.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5cf9f8efaafbf38828ee7a7faa28b6041e215663b98896872a2154310fd8915f
+size 1456
diff --git a/packages/react-ui/.creevey/images/ComboBox/Combobox With Clear Cross/firefox2022.png b/packages/react-ui/.creevey/images/ComboBox/Combobox With Clear Cross/firefox2022.png
new file mode 100644
index 00000000000..496ed76edb8
--- /dev/null
+++ b/packages/react-ui/.creevey/images/ComboBox/Combobox With Clear Cross/firefox2022.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:94dfd8c2cf7629fb691b49943389fcd5a2e8c443d9c90bb1644b1b2f9c81c9e3
+size 1608
diff --git a/packages/react-ui/.creevey/images/ComboBox/Combobox With Clear Cross/firefox2022Dark.png b/packages/react-ui/.creevey/images/ComboBox/Combobox With Clear Cross/firefox2022Dark.png
new file mode 100644
index 00000000000..f7384279d63
--- /dev/null
+++ b/packages/react-ui/.creevey/images/ComboBox/Combobox With Clear Cross/firefox2022Dark.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:162f2d29684ecc748b35fc4b964f9027dc99a48a70a3d7a2b503ae2502c01077
+size 1584
diff --git a/packages/react-ui/.creevey/images/Input/Clear Cross Sizes/chrome2022.png b/packages/react-ui/.creevey/images/Input/Clear Cross Sizes/chrome2022.png
new file mode 100644
index 00000000000..3ec0b5ef19d
--- /dev/null
+++ b/packages/react-ui/.creevey/images/Input/Clear Cross Sizes/chrome2022.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ca0f02ccae6de676e28d2a0d786e6eaa53018d753b98787c39c6245b044013ef
+size 3346
diff --git a/packages/react-ui/.creevey/images/Input/Clear Cross Sizes/chrome2022Dark.png b/packages/react-ui/.creevey/images/Input/Clear Cross Sizes/chrome2022Dark.png
new file mode 100644
index 00000000000..5bdfe0182b3
--- /dev/null
+++ b/packages/react-ui/.creevey/images/Input/Clear Cross Sizes/chrome2022Dark.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:303ae571327d840dbd469e0d13b80b2f3a5284dde657643ba1909c3a311e6e7e
+size 3402
diff --git a/packages/react-ui/.creevey/images/Input/Clear Cross Sizes/firefox2022.png b/packages/react-ui/.creevey/images/Input/Clear Cross Sizes/firefox2022.png
new file mode 100644
index 00000000000..ca335164974
--- /dev/null
+++ b/packages/react-ui/.creevey/images/Input/Clear Cross Sizes/firefox2022.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d469d8109dfa980fea6721dbd7404a6e162e8aad377c40b226777b803bdbb329
+size 4312
diff --git a/packages/react-ui/.creevey/images/Input/Clear Cross Sizes/firefox2022Dark.png b/packages/react-ui/.creevey/images/Input/Clear Cross Sizes/firefox2022Dark.png
new file mode 100644
index 00000000000..54ef16de5d3
--- /dev/null
+++ b/packages/react-ui/.creevey/images/Input/Clear Cross Sizes/firefox2022Dark.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ddc4ff9f6522babde4daaeed57d83012c1b67518af0d65be571308d32b041a1f
+size 4194
diff --git a/packages/react-ui/components/Autocomplete/__docs__/Autocomplete.docs.stories.tsx b/packages/react-ui/components/Autocomplete/__docs__/Autocomplete.docs.stories.tsx
index 40c11ce19ec..2ac29f43738 100644
--- a/packages/react-ui/components/Autocomplete/__docs__/Autocomplete.docs.stories.tsx
+++ b/packages/react-ui/components/Autocomplete/__docs__/Autocomplete.docs.stories.tsx
@@ -110,3 +110,32 @@ export const Example9: Story = () => {
return ;
};
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 (
+
+
+
+
+ );
+};
+Example10.storyName = 'Крестик для очистки';
diff --git a/packages/react-ui/components/Autocomplete/__stories__/Autocomplete.stories.tsx b/packages/react-ui/components/Autocomplete/__stories__/Autocomplete.stories.tsx
index 41c7443413b..2b77a942a47 100644
--- a/packages/react-ui/components/Autocomplete/__stories__/Autocomplete.stories.tsx
+++ b/packages/react-ui/components/Autocomplete/__stories__/Autocomplete.stories.tsx
@@ -261,3 +261,7 @@ export const MenuPos = () => {
);
};
MenuPos.storyName = 'menuPos';
+
+export const ClearCross = () => {
+ return {}} />;
+};
diff --git a/packages/react-ui/components/Autocomplete/__tests__/Autocomplete-test.tsx b/packages/react-ui/components/Autocomplete/__tests__/Autocomplete-test.tsx
index e7e44167424..89ef7ea0fd6 100644
--- a/packages/react-ui/components/Autocomplete/__tests__/Autocomplete-test.tsx
+++ b/packages/react-ui/components/Autocomplete/__tests__/Autocomplete-test.tsx
@@ -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';
@@ -164,6 +164,45 @@ describe('', () => {
expect(screen.getByTestId('my-testy-icon')).toBeInTheDocument();
});
+ it('passes showClearIcon prop to input', async () => {
+ const ControlledAutocomplete = ({ clear }: { clear?: ShowClearIcon }) => {
+ const [value, setValue] = useState('');
+ return ;
+ };
+ const { rerender } = render();
+ const autocomplete = screen.getByRole('textbox');
+
+ expect(screen.queryByTestId(InputDataTids.clearCross)).toBeNull();
+ await userEvent.type(autocomplete, 'hello');
+ expect(screen.queryByTestId(InputDataTids.clearCross)).toBeNull();
+
+ rerender();
+ await userEvent.clear(autocomplete);
+ expect(screen.queryByTestId(InputDataTids.clearCross)).toBeNull();
+ await userEvent.type(autocomplete, 'hello');
+ expect(screen.queryByTestId(InputDataTids.clearCross)).toBeNull();
+
+ rerender();
+ 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();
+ 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[] = [];
diff --git a/packages/react-ui/components/ComboBox/ComboBox.tsx b/packages/react-ui/components/ComboBox/ComboBox.tsx
index 13820f4def1..639a5d7af28 100644
--- a/packages/react-ui/components/ComboBox/ComboBox.tsx
+++ b/packages/react-ui/components/ComboBox/ComboBox.tsx
@@ -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';
@@ -13,6 +13,12 @@ export interface ComboBoxProps
extends Pick,
Pick, 'id'>,
CommonProps {
+ /** Устанавливает иконку крестика, при нажатии на который комбобокс очищается.
+ * При значении "always" крестик отображается всегда, если поле непустое.
+ * При значении "onFocus" крестик отображается при фокусировке на непустом поле.
+ * @default never */
+ showClearIcon?: ShowClearIcon;
+
/** Задает выравнивание контента. */
align?: 'left' | 'center' | 'right';
@@ -170,7 +176,14 @@ export type ComboBoxExtendedItem = T | (() => React.ReactElement) | React.
type DefaultProps = Required<
Pick<
ComboBoxProps,
- 'itemToValue' | 'valueToString' | 'renderValue' | 'renderItem' | 'menuAlign' | 'searchOnFocus' | 'drawArrow'
+ | 'itemToValue'
+ | 'valueToString'
+ | 'renderValue'
+ | 'renderItem'
+ | 'menuAlign'
+ | 'searchOnFocus'
+ | 'drawArrow'
+ | 'showClearIcon'
>
>;
@@ -197,6 +210,7 @@ export class ComboBox extends React.Component
menuAlign: 'left',
searchOnFocus: true,
drawArrow: true,
+ showClearIcon: 'never',
};
private getProps = createPropsGetter(ComboBox.defaultProps);
diff --git a/packages/react-ui/components/ComboBox/__docs__/ComboBox.docs.stories.tsx b/packages/react-ui/components/ComboBox/__docs__/ComboBox.docs.stories.tsx
index 4e7f294bc3b..e99ae5e2d61 100644
--- a/packages/react-ui/components/ComboBox/__docs__/ComboBox.docs.stories.tsx
+++ b/packages/react-ui/components/ComboBox/__docs__/ComboBox.docs.stories.tsx
@@ -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 (
+
+
+
+
+ );
+};
+Example10.storyName = 'Крестик для очистки';
diff --git a/packages/react-ui/components/ComboBox/__stories__/Combobox.stories.tsx b/packages/react-ui/components/ComboBox/__stories__/Combobox.stories.tsx
index c3bef9eca2a..86ff13962e8 100644
--- a/packages/react-ui/components/ComboBox/__stories__/Combobox.stories.tsx
+++ b/packages/react-ui/components/ComboBox/__stories__/Combobox.stories.tsx
@@ -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 (
+
+
+
+ );
+};
diff --git a/packages/react-ui/components/ComboBox/__tests__/ComboBox-test.tsx b/packages/react-ui/components/ComboBox/__tests__/ComboBox-test.tsx
index 3b7e40853ac..3a1868c1bb7 100644
--- a/packages/react-ui/components/ComboBox/__tests__/ComboBox-test.tsx
+++ b/packages/react-ui/components/ComboBox/__tests__/ComboBox-test.tsx
@@ -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 ;
+ };
+ render();
+
+ 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();
+
+ 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', () => {
diff --git a/packages/react-ui/components/CurrencyInput/CurrencyInput.tsx b/packages/react-ui/components/CurrencyInput/CurrencyInput.tsx
index b9029887752..b4d7fa55de3 100644
--- a/packages/react-ui/components/CurrencyInput/CurrencyInput.tsx
+++ b/packages/react-ui/components/CurrencyInput/CurrencyInput.tsx
@@ -24,7 +24,7 @@ export interface CurrencyInputProps
extends Pick,
CommonProps,
Override<
- InputProps,
+ Omit,
{
/** Задает значение инпута. */
value?: Nullable;
diff --git a/packages/react-ui/components/Input/Input.tsx b/packages/react-ui/components/Input/Input.tsx
index 928536d1d28..ff4a6e7c8da 100644
--- a/packages/react-ui/components/Input/Input.tsx
+++ b/packages/react-ui/components/Input/Input.tsx
@@ -13,10 +13,12 @@ import { ThemeContext } from '../../lib/theming/ThemeContext';
import { Theme } from '../../lib/theming/Theme';
import { CommonProps, CommonWrapper, CommonWrapperRestProps } from '../../internal/CommonWrapper';
import { cx } from '../../lib/theming/Emotion';
-import { rootNode, TSetRootNode } from '../../lib/rootNode';
+import { getRootNode, rootNode, TSetRootNode } from '../../lib/rootNode';
import { createPropsGetter } from '../../lib/createPropsGetter';
import { SizeProp } from '../../lib/types/props';
import { FocusControlWrapper } from '../../internal/FocusControlWrapper';
+import { ClearCrossIcon } from '../../internal/ClearCrossIcon/ClearCrossIcon';
+import { UnreachableError } from '../../lib/utils';
import { InputElement, InputElementProps } from './Input.typings';
import { styles } from './Input.styles';
@@ -26,6 +28,7 @@ import { PolyfillPlaceholder } from './InputLayout/PolyfillPlaceholder';
export const inputTypes = ['password', 'text', 'number', 'tel', 'search', 'time', 'date', 'url', 'email'] as const;
export type InputAlign = 'left' | 'center' | 'right';
+export type ShowClearIcon = 'always' | 'onFocus' | 'never';
export type InputType = (typeof inputTypes)[number];
export type InputIconType = React.ReactNode | (() => React.ReactNode);
@@ -53,6 +56,12 @@ export interface InputProps
Override<
React.InputHTMLAttributes,
{
+ /** Устанавливает иконку крестика, при нажатии на который инпут очищается.
+ * При значении "always" крестик отображается всегда, если поле непустое.
+ * При значении "onFocus" крестик отображается при фокусировке на непустом поле.
+ * @default never */
+ showClearIcon?: ShowClearIcon;
+
/** Задает иконку слева.
* При использовании `ReactNode` применяются дефолтные стили для иконки.
* При использовании `() => ReactNode` применяются только стили для позиционирования. */
@@ -149,13 +158,15 @@ export interface InputState {
blinking: boolean;
focused: boolean;
needsPolyfillPlaceholder: boolean;
+ clearCrossShowed: boolean;
}
export const InputDataTids = {
root: 'Input__root',
+ clearCross: 'Input__clearCross',
} as const;
-type DefaultProps = Required>;
+type DefaultProps = Required>;
/**
* Поле ввода `Input` дает возможность указать значение с помощью клавиатуры.
@@ -178,22 +189,48 @@ export class Input extends React.Component {
public static defaultProps: DefaultProps = {
size: 'small',
type: 'text',
+ showClearIcon: 'never',
};
private getProps = createPropsGetter(Input.defaultProps);
- public state: InputState = {
- needsPolyfillPlaceholder,
- blinking: false,
- focused: false,
- };
-
private selectAllId: number | null = null;
private theme!: Theme;
private blinkTimeout: SafeTimer;
public input: HTMLInputElement | null = null;
private setRootNode!: TSetRootNode;
+ private getClearCrossShowed = ({
+ focused,
+ hasInitialValue,
+ }: {
+ focused?: boolean;
+ hasInitialValue?: boolean;
+ }): boolean => {
+ const showClearIcon = this.getProps().showClearIcon;
+ const notEmptyValue = Boolean(this.input?.value || hasInitialValue);
+ switch (showClearIcon) {
+ case 'always':
+ return notEmptyValue;
+ case 'onFocus':
+ return Boolean(focused && notEmptyValue);
+ case 'never':
+ return false;
+ default:
+ throw new UnreachableError(showClearIcon);
+ }
+ };
+
+ public state: InputState = {
+ needsPolyfillPlaceholder,
+ blinking: false,
+ focused: false,
+ clearCrossShowed: this.getClearCrossShowed({
+ focused: false,
+ hasInitialValue: Boolean(this.props.value || this.props.defaultValue),
+ }),
+ };
+
private outputMaskError() {
warning(!(this.props.mask && this.canBeUsedWithMask), maskErrorMessage(this.getProps().type));
}
@@ -393,9 +430,12 @@ export class Input extends React.Component {
'aria-controls': ariaControls,
'aria-label': ariaLabel,
element,
+ showClearIcon: initialShowClearIcon,
...rest
} = props;
+ const { showClearIcon } = this.getProps();
+
const { blinking, focused } = this.state;
const labelProps = {
@@ -444,10 +484,27 @@ export class Input extends React.Component {
{this.getInput(inputProps)}
);
+ const getRightIcon = () => {
+ return this.state.clearCrossShowed ? (
+ {
+ this.setState({
+ clearCrossShowed: showClearIcon === 'always' && !!this.input?.value.toString(),
+ });
+ }}
+ />
+ ) : (
+ rightIcon
+ );
+ };
+
return (
{
}
};
+ private handleClearInput = () => {
+ if (this.props.onValueChange) {
+ this.props.onValueChange('');
+ }
+ if (this.input) {
+ this.input.value = '';
+ }
+ this.setState({ clearCrossShowed: false });
+ };
+
private handleChange = (event: React.ChangeEvent) => {
+ this.setState({
+ clearCrossShowed: this.getClearCrossShowed({ focused: this.state.focused }),
+ });
+
if (needsPolyfillPlaceholder) {
const fieldIsEmpty = event.target.value === '';
if (this.state.needsPolyfillPlaceholder !== fieldIsEmpty) {
@@ -532,6 +603,7 @@ export class Input extends React.Component {
private handleFocus = (event: React.FocusEvent) => {
this.setState({
focused: true,
+ clearCrossShowed: this.getClearCrossShowed({ focused: true }),
});
if (this.props.selectAllOnFocus) {
@@ -582,7 +654,15 @@ export class Input extends React.Component {
private resetFocus = () => this.setState({ focused: false });
private handleBlur = (event: React.FocusEvent) => {
- this.resetFocus();
- this.props.onBlur?.(event);
+ const showClearIcon = this.props.showClearIcon;
+ if (showClearIcon && getRootNode(this)?.contains(event.relatedTarget)) {
+ this.setState({ focused: false });
+ } else {
+ this.setState({
+ focused: false,
+ clearCrossShowed: this.getClearCrossShowed({ focused: false }),
+ });
+ this.props.onBlur?.(event);
+ }
};
}
diff --git a/packages/react-ui/components/Input/__docs__/Input.docs.stories.tsx b/packages/react-ui/components/Input/__docs__/Input.docs.stories.tsx
index 777e9fb9fc0..b6303d4d797 100644
--- a/packages/react-ui/components/Input/__docs__/Input.docs.stories.tsx
+++ b/packages/react-ui/components/Input/__docs__/Input.docs.stories.tsx
@@ -34,7 +34,7 @@ export const Example3: Story = () => {
Example3.storyName = 'Очистка значения';
export const Example4: Story = () => {
- return } />;
+ return } />;
};
Example4.storyName = 'Префикс';
@@ -84,3 +84,18 @@ export const Example5: Story = () => {
);
};
Example5.storyName = 'type';
+
+/** При showClearIcon="always" крестик отображается всегда, если в инпут что-либо введено.
+ *
+ * При showClearIcon="onFocus" крестик отображается при фокусировке на инпуте, в который что-либо введено. */
+export const Example6: Story = () => {
+ const [valueAlways, setValueAlways] = React.useState('Отображаю крестик всегда');
+ const [valueOnFocus, setValueOnFocus] = React.useState('Отображаю крестик по фокусу');
+ return (
+
+
+
+
+ );
+};
+Example6.storyName = 'Крестик для очистки';
diff --git a/packages/react-ui/components/Input/__stories__/Input.stories.tsx b/packages/react-ui/components/Input/__stories__/Input.stories.tsx
index 02d75e14265..b84167be803 100644
--- a/packages/react-ui/components/Input/__stories__/Input.stories.tsx
+++ b/packages/react-ui/components/Input/__stories__/Input.stories.tsx
@@ -458,3 +458,16 @@ export const WithMaskAndSelectAllProp: Story = () => {
export const SearchTypeApi: Story = () => ;
export const InputTypeApi: Story = () => ;
+
+export const ClearCrossSizes: Story = () => {
+ const [valueSmall, setValueSmall] = React.useState('Small');
+ const [valueMedium, setValueMedium] = React.useState('Medium');
+ const [valueLarge, setValueLarge] = React.useState('Large');
+ return (
+
+
+
+
+
+ );
+};
diff --git a/packages/react-ui/components/Input/__tests__/Input-test.tsx b/packages/react-ui/components/Input/__tests__/Input-test.tsx
index 38eeb31bee7..ef01a144fc5 100644
--- a/packages/react-ui/components/Input/__tests__/Input-test.tsx
+++ b/packages/react-ui/components/Input/__tests__/Input-test.tsx
@@ -526,6 +526,97 @@ describe('', () => {
expect(screen.getByRole('textbox')).toHaveAttribute('aria-label', ariaLabel);
});
});
+
+ describe('clear cross', () => {
+ it('clears uncontrolled input', async () => {
+ render();
+
+ await userEvent.type(screen.getByRole('textbox'), 'z');
+ expect(screen.getByRole('textbox')).toHaveValue('z');
+
+ const cross = screen.getByTestId(InputDataTids.clearCross);
+ await userEvent.click(cross);
+
+ expect(screen.getByRole('textbox')).toHaveValue('');
+ });
+
+ it('clears uncontrolled input with default value', async () => {
+ render();
+
+ expect(screen.getByRole('textbox')).toHaveValue('z');
+
+ const cross = screen.getByTestId(InputDataTids.clearCross);
+ await userEvent.click(cross);
+
+ expect(screen.getByRole('textbox')).toHaveValue('');
+ });
+
+ it('clears controlled input', async () => {
+ const ControlledInput = () => {
+ const [value, setValue] = useState('z');
+ return ;
+ };
+ render();
+
+ const cross = screen.getByTestId(InputDataTids.clearCross);
+ await userEvent.click(cross);
+
+ expect(screen.getByRole('textbox')).toHaveValue('');
+ });
+
+ it('tests always clear cross', () => {
+ const ControlledInput = () => {
+ const [value, setValue] = useState('z');
+ return ;
+ };
+ render();
+
+ expect(screen.getByTestId(InputDataTids.clearCross)).toBeInTheDocument();
+ });
+
+ it('tests onFocus clear cross', async () => {
+ const ControlledInput = () => {
+ const [value, setValue] = useState('z');
+ return ;
+ };
+ render();
+
+ expect(screen.queryByTestId(InputDataTids.clearCross)).not.toBeInTheDocument();
+
+ await userEvent.click(screen.getByRole('textbox'));
+ expect(screen.getByTestId(InputDataTids.clearCross)).toBeInTheDocument();
+ });
+
+ it('tests never clear cross', async () => {
+ const ControlledInput = () => {
+ const [value, setValue] = useState('z');
+ return ;
+ };
+ render();
+
+ expect(screen.queryByTestId(InputDataTids.clearCross)).not.toBeInTheDocument();
+
+ await userEvent.click(screen.getByRole('textbox'));
+ expect(screen.queryByTestId(InputDataTids.clearCross)).not.toBeInTheDocument();
+ });
+
+ it('tests clear cross when rightIcon', async () => {
+ const rightIcon = ;
+ const ControlledInput = () => {
+ const [value, setValue] = useState('z');
+ return ;
+ };
+ render();
+
+ const cross = screen.getByTestId(InputDataTids.clearCross);
+ expect(cross).toBeInTheDocument();
+ expect(screen.queryByTestId('my-testy-icon')).not.toBeInTheDocument();
+
+ await userEvent.click(cross);
+ expect(cross).not.toBeInTheDocument();
+ expect(screen.queryByTestId('my-testy-icon')).toBeInTheDocument();
+ });
+ });
});
const renderEnzyme = (props: InputProps) =>
diff --git a/packages/react-ui/components/MaskedInput/MaskedInput.tsx b/packages/react-ui/components/MaskedInput/MaskedInput.tsx
index ae1f4081076..4587afb812b 100644
--- a/packages/react-ui/components/MaskedInput/MaskedInput.tsx
+++ b/packages/react-ui/components/MaskedInput/MaskedInput.tsx
@@ -55,7 +55,10 @@ export type MaskInputType = Exclude {
+ Omit<
+ InputProps,
+ 'showClearIcon' | 'mask' | 'maxLength' | 'type' | 'alwaysShowMask' | 'onUnexpectedInput' | 'maskChar'
+ > {
type?: MaskInputType;
}
diff --git a/packages/react-ui/components/PasswordInput/PasswordInput.tsx b/packages/react-ui/components/PasswordInput/PasswordInput.tsx
index 69d1287f018..ae3f946a065 100644
--- a/packages/react-ui/components/PasswordInput/PasswordInput.tsx
+++ b/packages/react-ui/components/PasswordInput/PasswordInput.tsx
@@ -21,7 +21,10 @@ import { styles } from './PasswordInput.styles';
import { PasswordInputIcon } from './PasswordInputIcon';
import { PasswordInputLocale, PasswordInputLocaleHelper } from './locale';
-export interface PasswordInputProps extends Pick, CommonProps, InputProps {
+export interface PasswordInputProps
+ extends Pick,
+ CommonProps,
+ Omit {
/** Включает CapsLock детектор. */
detectCapsLock?: boolean;
}
diff --git a/packages/react-ui/internal/ClearCrossIcon/ClearCrossIcon.styles.tsx b/packages/react-ui/internal/ClearCrossIcon/ClearCrossIcon.styles.tsx
new file mode 100644
index 00000000000..c4f3706810c
--- /dev/null
+++ b/packages/react-ui/internal/ClearCrossIcon/ClearCrossIcon.styles.tsx
@@ -0,0 +1,58 @@
+import { css, memoizeStyle } from '../../lib/theming/Emotion';
+import { Theme } from '../../lib/theming/Theme';
+import { resetButton } from '../../lib/styles/Mixins';
+
+export const styles = memoizeStyle({
+ root(t: Theme) {
+ return css`
+ ${resetButton()}
+ color: ${t.cleanCrossIconColor};
+ cursor: pointer;
+ transition: color ${t.transitionDuration} ${t.transitionTimingFunction};
+ &:hover {
+ color: ${t.cleanCrossIconHoverColor};
+ }
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ `;
+ },
+
+ rootDisabled(t: Theme) {
+ return css`
+ color: ${t.clearCrossIconDisabledColor};
+ `;
+ },
+
+ focus(t: Theme) {
+ return css`
+ color: ${t.clearCrossIconHoverColor};
+ box-shadow: ${t.clearCrossIconFocusShadow};
+ `;
+ },
+
+ clearCrossSmall(t: Theme) {
+ return css`
+ width: ${t.clearCrossIconWidthSmall};
+ height: ${t.clearCrossIconHeightSmall};
+ margin-right: ${t.clearCrossIconRightMarginSmall};
+ border-radius: ${t.clearCrossIconBorderRadiusSmall};
+ `;
+ },
+ clearCrossMedium(t: Theme) {
+ return css`
+ width: ${t.clearCrossIconWidthMedium};
+ height: ${t.clearCrossIconHeightMedium};
+ margin-right: ${t.clearCrossIconRightMarginMedium};
+ border-radius: ${t.clearCrossIconBorderRadiusMedium};
+ `;
+ },
+ clearCrossLarge(t: Theme) {
+ return css`
+ width: ${t.clearCrossIconWidthLarge};
+ height: ${t.clearCrossIconHeightLarge};
+ margin-right: ${t.clearCrossIconRightMarginLarge};
+ border-radius: ${t.clearCrossIconBorderRadiusLarge};
+ `;
+ },
+});
diff --git a/packages/react-ui/internal/ClearCrossIcon/ClearCrossIcon.tsx b/packages/react-ui/internal/ClearCrossIcon/ClearCrossIcon.tsx
new file mode 100644
index 00000000000..bd4b2d3ec67
--- /dev/null
+++ b/packages/react-ui/internal/ClearCrossIcon/ClearCrossIcon.tsx
@@ -0,0 +1,81 @@
+import React, { AriaAttributes } from 'react';
+import { globalObject } from '@skbkontur/global-object';
+
+import { cx } from '../../lib/theming/Emotion';
+import { keyListener } from '../../lib/events/keyListener';
+import { ThemeContext } from '../../lib/theming/ThemeContext';
+import { CommonWrapper, CommonProps } from '../CommonWrapper';
+import { SizeProp } from '../../lib/types/props';
+import { TokenSize } from '../../components/Token';
+import { ThemeFactory } from '../../lib/theming/ThemeFactory';
+
+import { styles } from './ClearCrossIcon.styles';
+import { CrossIcon } from './CrossIcon';
+
+export interface ClearCrossIconProps
+ extends Pick,
+ React.ButtonHTMLAttributes,
+ CommonProps {
+ /** Ширина и высота иконки крестика
+ * @default small */
+ size?: SizeProp;
+}
+
+export const ClearCrossIcon: React.FunctionComponent = ({ size = 'small', style, ...rest }) => {
+ const _theme = React.useContext(ThemeContext);
+ const theme = ThemeFactory.create(
+ {
+ clearCrossIconColor: _theme.clearCrossIconColor,
+ clearCrossIconHoverColor: _theme.clearCrossIconHoverColor,
+ },
+ _theme,
+ );
+ const getSizeClassName = (size: TokenSize) => {
+ switch (size) {
+ case 'large':
+ return styles.clearCrossLarge(theme);
+ case 'medium':
+ return styles.clearCrossMedium(theme);
+ case 'small':
+ default:
+ return styles.clearCrossSmall(theme);
+ }
+ };
+
+ const [focusedByTab, setFocusedByTab] = React.useState(false);
+
+ const handleFocus = () => {
+ // focus event fires before keyDown eventlistener so we should check tabPressed in async way
+ globalObject.requestAnimationFrame?.(() => {
+ if (keyListener.isTabPressed) {
+ setFocusedByTab(true);
+ }
+ });
+ };
+ const handleBlur = () => setFocusedByTab(false);
+
+ return (
+
+
+
+ );
+};
+
+ClearCrossIcon.__KONTUR_REACT_UI__ = 'ClearCrossIcon';
+ClearCrossIcon.displayName = 'ClearCrossIcon';
diff --git a/packages/react-ui/internal/ClearCrossIcon/CrossIcon.tsx b/packages/react-ui/internal/ClearCrossIcon/CrossIcon.tsx
new file mode 100644
index 00000000000..2dc711c7e1e
--- /dev/null
+++ b/packages/react-ui/internal/ClearCrossIcon/CrossIcon.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+
+import { iconSizer } from '../icons2022/iconSizer';
+import { XIcon16Light } from '../icons2022/XIcon/XIcon16Light';
+import { XIcon20Light } from '../icons2022/XIcon/XIcon20Light';
+import { XIcon24Regular } from '../icons2022/XIcon/XIcon24Regular';
+
+export const CrossIcon = iconSizer(
+ {
+ small: () => ,
+ medium: () => ,
+ large: () => ,
+ },
+ 'CrossIcon',
+);
diff --git a/packages/react-ui/internal/ClearCrossIcon/__tests__/ClearCrossIcon.test.tsx b/packages/react-ui/internal/ClearCrossIcon/__tests__/ClearCrossIcon.test.tsx
new file mode 100644
index 00000000000..331b6f7ee24
--- /dev/null
+++ b/packages/react-ui/internal/ClearCrossIcon/__tests__/ClearCrossIcon.test.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+import { ClearCrossIcon } from '../ClearCrossIcon';
+
+describe('ClearCrossIcon', () => {
+ it('focuses on click', async () => {
+ render();
+
+ await userEvent.click(screen.getByRole('button'));
+ expect(screen.getByRole('button')).toHaveFocus();
+ });
+
+ it('blurs on click', async () => {
+ render(
+ <>
+
+
+ >,
+ );
+ const button = screen.getByRole('button');
+
+ await userEvent.click(button);
+ await userEvent.click(screen.getByTestId('next-focus'));
+
+ expect(button).not.toHaveFocus();
+ });
+
+ it('disabled clear cross dont get focus', async () => {
+ render();
+
+ await userEvent.tab();
+ expect(screen.getByRole('button')).not.toHaveFocus();
+ });
+
+ it('gets data-tid', async () => {
+ render();
+
+ expect(screen.getByRole('button')).toHaveAttribute('data-tid', 'tid');
+ });
+});
diff --git a/packages/react-ui/internal/CustomComboBox/ComboBoxView.tsx b/packages/react-ui/internal/CustomComboBox/ComboBoxView.tsx
index d8f4c32b0f3..1998c869650 100644
--- a/packages/react-ui/internal/CustomComboBox/ComboBoxView.tsx
+++ b/packages/react-ui/internal/CustomComboBox/ComboBoxView.tsx
@@ -1,7 +1,7 @@
import React, { AriaAttributes, HTMLAttributes } from 'react';
import { getRandomID, isNonNullable } from '../../lib/utils';
-import { Input, InputIconType, InputProps } from '../../components/Input';
+import { Input, InputDataTids, InputIconType, InputProps, ShowClearIcon } from '../../components/Input';
import { InputLikeText } from '../InputLikeText';
import { Menu } from '../Menu';
import { MenuItemState } from '../../components/MenuItem';
@@ -21,6 +21,7 @@ import { SizeProp } from '../../lib/types/props';
import { Popup } from '../Popup';
import { getMenuPositions } from '../../lib/getMenuPositions';
import { ZIndex } from '../ZIndex';
+import { ClearCrossIcon } from '../ClearCrossIcon/ClearCrossIcon';
import { ArrowDownIcon } from './ArrowDownIcon';
import { ComboBoxMenu } from './ComboBoxMenu';
@@ -57,6 +58,7 @@ interface ComboBoxViewProps
textValue?: string;
totalCount?: number;
value?: Nullable;
+ showClearIcon?: ShowClearIcon;
/**
* Cостояние валидации при предупреждении.
*/
@@ -77,6 +79,7 @@ interface ComboBoxViewProps
onInputValueChange?: (value: string) => void;
onInputFocus?: () => void;
onInputClick?: () => void;
+ onClearCrossClick?: () => void;
onInputKeyDown?: (e: React.KeyboardEvent) => void;
onMouseEnter?: (e: React.MouseEvent) => void;
onMouseOver?: (e: React.MouseEvent) => void;
@@ -106,6 +109,7 @@ type DefaultProps = Required<
| 'onFocusOutside'
| 'size'
| 'width'
+ | 'showClearIcon'
>
>;
@@ -115,6 +119,7 @@ export const ComboBoxViewIds = {
interface ComboBoxViewState {
anchorElement: Nullable;
+ clearCrossShowed: boolean;
}
@responsiveLayout
@@ -137,6 +142,7 @@ export class ComboBoxView extends React.Component, Combo
},
size: 'small',
width: 250,
+ showClearIcon: 'never',
};
private getProps = createPropsGetter(ComboBoxView.defaultProps);
@@ -151,6 +157,7 @@ export class ComboBoxView extends React.Component, Combo
public state = {
anchorElement: null,
+ clearCrossShowed: this.props.showClearIcon === 'always' && !!this.props.value?.toString(),
};
public componentDidMount() {
@@ -340,6 +347,7 @@ export class ComboBoxView extends React.Component, Combo
size,
'aria-describedby': ariaDescribedby,
'aria-label': ariaLabel,
+ showClearIcon,
} = this.props;
const { renderValue } = this.getProps();
@@ -373,6 +381,7 @@ export class ComboBoxView extends React.Component, Combo
aria-describedby={ariaDescribedby}
aria-controls={this.menuId}
aria-label={ariaLabel}
+ showClearIcon={showClearIcon}
/>
);
}
@@ -436,6 +445,17 @@ export class ComboBoxView extends React.Component, Combo
return ;
}
+ if (this.getProps().showClearIcon !== 'never' && this.state.clearCrossShowed) {
+ return (
+ event.stopPropagation()}
+ onClick={this.props.onClearCrossClick}
+ />
+ );
+ }
+
if (rightIcon || drawArrow) {
return rightIcon || ;
}
diff --git a/packages/react-ui/internal/CustomComboBox/CustomComboBox.tsx b/packages/react-ui/internal/CustomComboBox/CustomComboBox.tsx
index 49ada5c8a3e..1404658b739 100644
--- a/packages/react-ui/internal/CustomComboBox/CustomComboBox.tsx
+++ b/packages/react-ui/internal/CustomComboBox/CustomComboBox.tsx
@@ -3,7 +3,7 @@ import ReactDOM from 'react-dom';
import { globalObject } from '@skbkontur/global-object';
import { Nullable } from '../../typings/utility-types';
-import { Input, InputIconType } from '../../components/Input';
+import { Input, InputIconType, ShowClearIcon } from '../../components/Input';
import { Menu } from '../Menu';
import { InputLikeText } from '../InputLikeText';
import { MenuItemState } from '../../components/MenuItem';
@@ -62,6 +62,7 @@ export interface CustomComboBoxProps
size?: SizeProp;
totalCount?: number;
value?: Nullable;
+ showClearIcon?: ShowClearIcon;
/**
* Cостояние валидации при предупреждении.
*/
@@ -271,6 +272,7 @@ export class CustomComboBox extends React.PureComponent extends React.PureComponent this.dispatch({ type: 'TextChange', value }),
onInputFocus: this.handleFocus,
onInputClick: this.handleInputClick,
+ onClearCrossClick: () => this.dispatch({ type: 'ClearCrossClick' }),
onInputKeyDown: (event: React.KeyboardEvent) => {
event.persist();
this.dispatch({ type: 'KeyPress', event });
diff --git a/packages/react-ui/internal/CustomComboBox/CustomComboBoxReducer.tsx b/packages/react-ui/internal/CustomComboBox/CustomComboBoxReducer.tsx
index 59903ae0597..8811256b4b2 100644
--- a/packages/react-ui/internal/CustomComboBox/CustomComboBoxReducer.tsx
+++ b/packages/react-ui/internal/CustomComboBox/CustomComboBoxReducer.tsx
@@ -25,6 +25,7 @@ export type CustomComboBoxAction =
| { type: 'Mount' }
| { type: 'Focus' }
| { type: 'InputClick' }
+ | { type: 'ClearCrossClick' }
| { type: 'Blur' }
| { type: 'Reset' }
| { type: 'Open' }
@@ -55,6 +56,7 @@ interface EffectFactory {
focus: Effect;
valueChange: (value: any) => Effect;
+ valueClear: Effect;
unexpectedInput: (textValue: string, items: Nullable) => Effect;
inputChange: Effect;
inputFocus: Effect;
@@ -103,6 +105,9 @@ export const Effect: EffectFactory = {
onValueChange(value);
}
},
+ valueClear: (dispatch) => {
+ dispatch({ type: 'TextChange', value: '' });
+ },
unexpectedInput: (textValue, items) => (dispatch, getState, getProps) => {
const { onUnexpectedInput, valueToString } = getProps();
if (Array.isArray(items) && items.length === 1) {
@@ -349,6 +354,13 @@ export function reducer(
}
return state;
}
+ case 'ClearCrossClick': {
+ const newState = {
+ editing: true,
+ loading: true,
+ };
+ return [newState, [Effect.focus, Effect.search(''), Effect.valueClear]];
+ }
case 'Blur': {
const { inputChanged, items } = state;
if (!inputChanged) {
diff --git a/packages/react-ui/internal/InputLikeText/InputLikeText.tsx b/packages/react-ui/internal/InputLikeText/InputLikeText.tsx
index 2a1d7bc6f4b..24ac69bee91 100644
--- a/packages/react-ui/internal/InputLikeText/InputLikeText.tsx
+++ b/packages/react-ui/internal/InputLikeText/InputLikeText.tsx
@@ -25,7 +25,7 @@ import { FocusControlWrapper } from '../FocusControlWrapper';
import { HiddenInput } from './HiddenInput';
import { styles } from './InputLikeText.styles';
-export interface InputLikeTextProps extends CommonProps, InputProps {
+export interface InputLikeTextProps extends CommonProps, Omit {
children?: React.ReactNode;
innerRef?: (el: HTMLElement | null) => void;
onFocus?: React.FocusEventHandler;
@@ -35,7 +35,7 @@ export interface InputLikeTextProps extends CommonProps, InputProps {
takeContentWidth?: boolean;
}
-export type InputLikeTextState = Omit;
+export type InputLikeTextState = Omit;
export const InputLikeTextDataTids = {
root: 'InputLikeText__root',
diff --git a/packages/react-ui/internal/themes/BasicDarkTheme.ts b/packages/react-ui/internal/themes/BasicDarkTheme.ts
index 8fa6ab89991..3ca6878e56f 100644
--- a/packages/react-ui/internal/themes/BasicDarkTheme.ts
+++ b/packages/react-ui/internal/themes/BasicDarkTheme.ts
@@ -43,6 +43,14 @@ export class BasicDarkThemeInternal extends (class {} as typeof BasicLightTheme)
//#region CloseIcon, CloseButtonIcon
public static closeBtnIconColor = 'rgba(255, 255, 255, 0.32)';
//#endregion CloseIcon, CloseButtonIcon
+
+ //#region ClearCrossIcon
+ public static clearCrossIconColor = 'rgba(255, 255, 255, 0.32)';
+ public static get clearCrossIconHoverColor() {
+ return this.inputBorderColorFocus;
+ }
+ //#endregion ClearCrossIcon
+
//#region Link
public static linkColor = 'rgba(255, 255, 255, 0.87)';
public static linkHoverColor = '#ffffff';
diff --git a/packages/react-ui/internal/themes/BasicLightTheme.ts b/packages/react-ui/internal/themes/BasicLightTheme.ts
index 2ae1a22315c..5f1f60c19ee 100644
--- a/packages/react-ui/internal/themes/BasicLightTheme.ts
+++ b/packages/react-ui/internal/themes/BasicLightTheme.ts
@@ -2435,6 +2435,61 @@ export class BasicLightThemeInternal {
'0px 0px 0px 3px rgb(149, 149, 149), 0px 0px 0px 8px rgba(61, 61, 61, 0.2)';
//#endregion FileUploader
+ //#region ClearCrossIcon
+ public static get clearCrossIconWidthSmall() {
+ return this.inputHeightSmall;
+ }
+ public static get clearCrossIconWidthMedium() {
+ return this.inputHeightMedium;
+ }
+ public static get clearCrossIconWidthLarge() {
+ return this.inputHeightLarge;
+ }
+ public static get clearCrossIconHeightSmall() {
+ return this.inputHeightSmall;
+ }
+ public static get clearCrossIconHeightMedium() {
+ return this.inputHeightMedium;
+ }
+ public static get clearCrossIconHeightLarge() {
+ return this.inputHeightLarge;
+ }
+
+ public static get clearCrossIconRightMarginSmall() {
+ const inputPaddingXSmall = parseInt(this.inputPaddingXSmall);
+ const inputBorderWidth = parseInt(this.inputBorderWidth);
+ return `-${inputPaddingXSmall + inputBorderWidth}px`;
+ }
+ public static get clearCrossIconRightMarginMedium() {
+ const inputPaddingXMedium = parseInt(this.inputPaddingXMedium);
+ const inputBorderWidth = parseInt(this.inputBorderWidth);
+ return `-${inputPaddingXMedium + inputBorderWidth}px`;
+ }
+ public static get clearCrossIconRightMarginLarge() {
+ const inputPaddingXLarge = parseInt(this.inputPaddingXLarge);
+ const inputBorderWidth = parseInt(this.inputBorderWidth);
+ return `-${inputPaddingXLarge + inputBorderWidth}px`;
+ }
+
+ public static get clearCrossIconBorderRadiusSmall() {
+ return this.inputBorderRadiusSmall;
+ }
+ public static get clearCrossIconBorderRadiusMedium() {
+ return this.inputBorderRadiusMedium;
+ }
+ public static get clearCrossIconBorderRadiusLarge() {
+ return this.inputBorderRadiusLarge;
+ }
+
+ public static clearCrossIconColor = '#757575';
+ public static clearCrossIconDisabledColor = '#8b8b8b';
+ public static clearCrossIconHoverColor = '#222222';
+ public static get clearCrossIconFocusShadow() {
+ return `inset 0 0 0 2px ${this.borderColorFocus}`;
+ }
+
+ //#endregion ClearCrossIcon
+
//#region CloseIcon
public static closeBtnIconColor = 'rgba(0, 0, 0, 0.32)';
public static closeBtnIconDisabledColor = '#8b8b8b';
diff --git a/packages/react-ui/lib/utils.ts b/packages/react-ui/lib/utils.ts
index 0279dee1e0f..2d6bc02d0d1 100644
--- a/packages/react-ui/lib/utils.ts
+++ b/packages/react-ui/lib/utils.ts
@@ -233,3 +233,9 @@ export function clickOutside() {
document.body.dispatchEvent(event);
}
+
+export class UnreachableError extends Error {
+ public constructor(guard: never) {
+ super(`Unsupported kind: ${JSON.stringify(guard)}`);
+ }
+}