Repository: gravity-ui/uikit
Pull request #668 (2023-05-18 10:54:22)

feat(Select): add hasClear property

gravity-ui-bot: [Preview](https://preview.gravity-ui.com/uikit/668/) is ready.
<!-- Sticky Pull Request Comment -->
Marginy605: > Could u add some test for new property?

added tests

Patch:
diff --git a/src/components/Select/README.md b/src/components/Select/README.md
index 25475fa2..7885a81e 100644
--- a/src/components/Select/README.md
+++ b/src/components/Select/README.md
@@ -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                                                          |
 
 ---
 
diff --git a/src/components/Select/Select.tsx b/src/components/Select/Select.tsx
index b2011c75..783aa8a9 100644
--- a/src/components/Select/Select.tsx
+++ b/src/components/Select/Select.tsx
@@ -72,6 +72,7 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(function
         disabled = false,
         filterable = false,
         disablePortal,
+        hasClear = false,
         onClose,
     } = props;
     const [mobile] = useMobile();
@@ -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,
@@ -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}
diff --git a/src/components/Select/__stories__/Select.stories.tsx b/src/components/Select/__stories__/Select.stories.tsx
index c70a04ca..bae977d4 100644
--- a/src/components/Select/__stories__/Select.stories.tsx
+++ b/src/components/Select/__stories__/Select.stories.tsx
@@ -32,4 +32,5 @@ Showcase.args = {
     disabled: false,
     placeholder: 'Values',
     label: '',
+    hasClear: false,
 };
diff --git a/src/components/Select/__stories__/SelectShowcase.scss b/src/components/Select/__stories__/SelectShowcase.scss
index f0a2ea21..e480ff41 100644
--- a/src/components/Select/__stories__/SelectShowcase.scss
+++ b/src/components/Select/__stories__/SelectShowcase.scss
@@ -1,6 +1,7 @@
 @use '../../variables';
 
 $block: '.#{variables.$ns}select-showcase';
+$blockSelectClear: '.#{variables.$ns-new}select-clear';
 
 #{$block} {
     display: flex;
@@ -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 {
diff --git a/src/components/Select/__stories__/SelectShowcase.tsx b/src/components/Select/__stories__/SelectShowcase.tsx
index 423d867f..7c6db643 100644
--- a/src/components/Select/__stories__/SelectShowcase.tsx
+++ b/src/components/Select/__stories__/SelectShowcase.tsx
@@ -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';
@@ -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}
@@ -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>
                         );
                     },
diff --git a/src/components/Select/__stories__/constants.ts b/src/components/Select/__stories__/constants.ts
index 2c3d312d..2421ae4c 100644
--- a/src/components/Select/__stories__/constants.ts
+++ b/src/components/Select/__stories__/constants.ts
@@ -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}
@@ -145,6 +145,9 @@ export const EXAMPLE_USER_CONTROL = `const [value, setValue] = React.useState<st
                 }}
             >
                 User control
+                {renderClear?.({
+                    renderIcon: () => <Icon data={CrossIcon} />,
+                })}
             </Button>
         );
     }},
diff --git a/src/components/Select/components/SelectClear/SelectClear.scss b/src/components/Select/components/SelectClear/SelectClear.scss
new file mode 100644
index 00000000..2afa8b7a
--- /dev/null
+++ b/src/components/Select/components/SelectClear/SelectClear.scss
@@ -0,0 +1,52 @@
+@use '../../../../../styles/mixins';
+@use '../../../variables';
+
+$block: '.#{variables.$ns-new}select-clear';
+
+#{$block} {
+    @include mixins.button-reset();
+    display: inline-flex;
+    justify-content: center;
+    align-items: center;
+    margin-left: auto;
+    z-index: 1;
+
+    &:focus-visible {
+        border: 1px solid var(--yc-color-line-generic-active);
+    }
+
+    &_size_s {
+        height: #{variables.$s-height};
+        width: #{variables.$s-height};
+        border-radius: var(--yc-border-radius-s);
+    }
+
+    &_size_m {
+        height: #{variables.$m-height};
+        width: #{variables.$m-height};
+        border-radius: var(--yc-border-radius-m);
+    }
+
+    &_size_l {
+        height: #{variables.$l-height};
+        width: #{variables.$l-height};
+        border-radius: var(--yc-border-radius-l);
+    }
+
+    &_size_xl {
+        height: #{variables.$xl-height};
+        width: #{variables.$xl-height};
+        border-radius: var(--yc-border-radius-xl);
+    }
+
+    &__clear {
+        --yc-button-background-color: transparent;
+        --yc-button-background-color-hover: transparent;
+
+        color: var(--yc-color-text-secondary);
+
+        #{$block}:hover & {
+            color: var(--yc-color-text-primary);
+        }
+    }
+}
diff --git a/src/components/Select/components/SelectClear/SelectClear.tsx b/src/components/Select/components/SelectClear/SelectClear.tsx
new file mode 100644
index 00000000..e1966173
--- /dev/null
+++ b/src/components/Select/components/SelectClear/SelectClear.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+
+import {Xmark} from '@gravity-ui/icons';
+
+import {Icon} from '../../../Icon';
+import {SelectQa, selectClearBlock} from '../../constants';
+import i18n from '../../i18n';
+import type {SelectClearProps} from '../../types';
+
+import './SelectClear.scss';
+
+export const SelectClear = (props: SelectClearProps) => {
+    const {size, onClick, onMouseEnter, onMouseLeave, renderIcon} = props;
+    const icon = renderIcon ? (
+        renderIcon()
+    ) : (
+        <Icon className={selectClearBlock('clear')} data={Xmark} />
+    );
+    return (
+        <button
+            className={selectClearBlock({size})}
+            aria-label={i18n('label_clear')}
+            onClick={onClick}
+            onMouseEnter={onMouseEnter}
+            onMouseLeave={onMouseLeave}
+            data-qa={SelectQa.CLEAR}
+        >
+            {icon}
+        </button>
+    );
+};
+
+SelectClear.displayName = 'SelectClear';
diff --git a/src/components/Select/components/SelectControl/SelectControl.scss b/src/components/Select/components/SelectControl/SelectControl.scss
index 70c486a1..a78a351e 100644
--- a/src/components/Select/components/SelectControl/SelectControl.scss
+++ b/src/components/Select/components/SelectControl/SelectControl.scss
@@ -1,102 +1,187 @@
 @use '../../../../../styles/mixins';
 @use '../../../variables';
 @use '../../variables.scss' as select-css-variables;
+@use '../SelectClear/SelectClear';
 
 $block: '.#{variables.$ns-new}select-control';
+$blockButton: '.#{variables.$ns-new}select-control__button';
 
-@mixin block_style($size: m) {
+@mixin block_control($size: m) {
     @if $size == 's' {
-        @include mixins.text-body-short;
-
-        height: #{variables.$s-height};
-        padding: 4px #{select-css-variables.$s-hor-padding};
-        --_--select-border-radius: var(--yc-border-radius-s);
         --_--select-options-text-right-padding: #{select-css-variables.$s-hor-padding};
+        --_--select-border-radius: var(--yc-border-radius-s);
+        height: #{variables.$s-height};
+        padding: 4px calc(var(--_--select-options-text-right-padding) + 1px); // plus border-width
     }
     @if $size == 'm' {
-        @include mixins.text-body-short;
-
-        height: #{variables.$m-height};
-        padding: 6px #{select-css-variables.$m-hor-padding};
-        --_--select-border-radius: var(--yc-border-radius-m);
         --_--select-options-text-right-padding: #{select-css-variables.$m-hor-padding};
+        --_--select-border-radius: var(--yc-border-radius-m);
+        height: #{variables.$m-height};
+        padding: 6px calc(var(--_--select-options-text-right-padding) + 1px); // plus border-width
     }
     @if $size == 'l' {
-        @include mixins.text-body-short;
-
-        height: #{variables.$l-height};
-        padding: 10px #{select-css-variables.$l-hor-padding};
-        --_--select-border-radius: var(--yc-border-radius-l);
         --_--select-options-text-right-padding: #{select-css-variables.$l-hor-padding};
+        --_--select-border-radius: var(--yc-border-radius-l);
+        height: #{variables.$l-height};
+        padding: 10px calc(var(--_--select-options-text-right-padding) + 1px); // plus border-width
+    }
+    @if $size == 'xl' {
+        --_--select-options-text-right-padding: #{select-css-variables.$xl-hor-padding};
+        --_--select-border-radius: var(--yc-border-radius-xl);
+        height: #{variables.$xl-height};
+        padding: 12px calc(var(--_--select-options-text-right-padding) + 1px); // plus border-width
+    }
+}
+
+@mixin block_button_style($size: m) {
+    @if $size == 's' {
+        @include mixins.text-body-short;
+    }
+    @if $size == 'm' {
+        @include mixins.text-body-short;
+    }
+    @if $size == 'l' {
+        @include mixins.text-body-short;
     }
     @if $size == 'xl' {
         @include mixins.text-body-2;
+    }
+}
 
-        height: #{variables.$xl-height};
-        padding: 12px #{select-css-variables.$xl-hor-padding};
-        --_--select-border-radius: var(--yc-border-radius-xl);
-        --_--select-options-text-right-padding: #{select-css-variables.$xl-hor-padding};
+@mixin block_clear_reserved_width() {
+    // reserving place for clear icon to fix width changing on displaying clear
+    #{$block}_has-clear:not(#{$block}_has-value)#{$block}_size_s & {
+        padding-right: calc(#{variables.$s-height} + var(--_--select-options-text-right-padding));
+    }
+    #{$block}_has-clear:not(#{$block}_has-value)#{$block}_size_m & {
+        padding-right: calc(#{variables.$m-height} + var(--_--select-options-text-right-padding));
+    }
+    #{$block}_has-clear:not(#{$block}_has-value)#{$block}_size_l & {
+        padding-right: calc(#{variables.$l-height} + var(--_--select-options-text-right-padding));
+    }
+    #{$block}_has-clear:not(#{$block}_has-value)#{$block}_size_xl & {
+        padding-right: calc(#{variables.$xl-height} + var(--_--select-options-text-right-padding));
+    }
+}
+
+@mixin block_clear_reserved_disabled_width() {
+    // reserving place for clear icon to fix width changing on displaying clear for disabled select
+    #{$block}_has-clear#{$block}_size_s #{$blockButton}_disabled & {
+        padding-right: calc(#{variables.$s-height} + var(--_--select-options-text-right-padding));
+    }
+    #{$block}_has-clear#{$block}_size_m #{$blockButton}_disabled & {
+        padding-right: calc(#{variables.$m-height} + var(--_--select-options-text-right-padding));
+    }
+    #{$block}_has-clear#{$block}_size_l #{$blockButton}_disabled & {
+        padding-right: calc(#{variables.$l-height} + var(--_--select-options-text-right-padding));
+    }
+    #{$block}_has-clear#{$block}_size_xl #{$blockButton}_disabled & {
+        padding-right: calc(#{variables.$xl-height} + var(--_--select-options-text-right-padding));
     }
 }
 
 #{$block} {
     @include mixins.button-reset();
-    @include mixins.pin($block, $block, var(--_--select-border-radius));
 
-    width: 100%;
-    box-sizing: border-box;
+    position: relative;
     display: inline-flex;
     align-items: center;
-    border: 1px solid var(--yc-color-line-generic);
-    overflow: hidden;
+    box-sizing: border-box;
     transition: transform 0.1s ease-out, color 0.15s linear, background-color 0.15s linear;
-
-    &_view_clear {
-        border-color: transparent;
-    }
+    width: 100%;
 
     &_size_s {
-        @include block_style(s);
+        @include block_control(s);
     }
 
     &_size_m {
-        @include block_style(m);
+        @include block_control(m);
     }
 
     &_size_l {
-        @include block_style(l);
+        @include block_control(l);
     }
 
     &_size_xl {
-        @include block_style(xl);
+        @include block_control(xl);
     }
 
-    &_error {
-        border-color: var(--yc-color-line-danger);
-    }
+    &__button {
+        @include mixins.button-reset();
 
-    &_disabled {
-        background-color: var(--yc-color-base-generic-accent-disabled);
-        color: var(--yc-color-text-hint);
-        border-color: transparent;
-        pointer-events: none;
-    }
+        overflow: hidden;
 
-    &:active {
-        transform: scale(0.96);
-    }
+        &::before {
+            @include mixins.pin($block, $block, var(--_--select-border-radius));
+
+            content: '';
+            position: absolute;
+            inset: 0;
+            border: 1px solid var(--yc-color-line-generic);
+            border-radius: var(--_--select-border-radius);
+        }
+
+        &::after {
+            content: '';
+            position: absolute;
+            inset: 0;
+            z-index: -1;
+            border-radius: var(--_--select-border-radius);
+        }
+
+        &_view_clear {
+            border-color: transparent;
+        }
+
+        &_size_s {
+            @include block_button_style(s);
+        }
 
-    &:hover {
-        background-color: var(--yc-color-base-simple-hover);
+        &_size_m {
+            @include block_button_style(m);
+        }
+
+        &_size_l {
+            @include block_button_style(l);
+        }
+
+        &_size_xl {
+            @include block_button_style(xl);
+        }
+
+        &_error::before {
+            border-color: var(--yc-color-line-danger);
+        }
+
+        &:hover::after {
+            background-color: var(--yc-color-base-simple-hover);
+        }
+
+        &_disabled {
+            color: var(--yc-color-text-hint);
+            pointer-events: none;
+
+            &::after {
+                background-color: var(--yc-color-base-generic-accent-disabled);
+            }
+
+            &::before {
+                border-color: transparent;
+            }
+        }
 
-        &:not(#{$block}_view_clear):not(#{$block}_error) {
+        &:not(&_error):not(&_disabled)::before {
             border-color: var(--yc-color-line-generic-hover);
         }
+
+        &_open:not(&_error)::before,
+        &:not(&_error):focus-visible::before {
+            border-color: var(--yc-color-line-generic-active);
+        }
     }
 
-    &:focus-visible:not(#{$block}_error),
-    &_open:not(#{$block}_error) {
-        border-color: var(--yc-color-line-generic-active);
+    &:not(&_disabled):not(&_no-active):active {
+        transform: scale(0.96);
     }
 
     &__label {
@@ -112,12 +197,17 @@ $block: '.#{variables.$ns-new}select-control';
     &__placeholder,
     &__option-text {
         @include mixins.overflow-ellipsis();
+        @include block_clear_reserved_disabled_width();
 
         padding-right: var(--_--select-options-text-right-padding);
     }
 
     &__placeholder {
         color: var(--yc-color-text-hint);
+
+        #{$blockButton}:not(#{$blockButton}_disabled) & {
+            @include block_clear_reserved_width();
+        }
     }
 
     &__chevron-icon {
@@ -130,6 +220,10 @@ $block: '.#{variables.$ns-new}select-control';
         }
     }
 
+    #{SelectClear.$block} + &__chevron-icon {
+        margin-left: 0;
+    }
+
     &__error {
         @include mixins.text-body-1();
 
diff --git a/src/components/Select/components/SelectControl/SelectControl.tsx b/src/components/Select/components/SelectControl/SelectControl.tsx
index ea750859..12a677cb 100644
--- a/src/components/Select/components/SelectControl/SelectControl.tsx
+++ b/src/components/Select/components/SelectControl/SelectControl.tsx
@@ -1,12 +1,19 @@
 import React from 'react';
 
 import {ChevronDown} from '@gravity-ui/icons';
+import isEmpty from 'lodash/isEmpty';
 
 import {Icon} from '../../../Icon';
 import type {CnMods} from '../../../utils/cn';
 import {useForkRef} from '../../../utils/useForkRef';
-import {selectControlBlock} from '../../constants';
-import type {SelectProps, SelectRenderControl, SelectRenderControlProps} from '../../types';
+import {selectControlBlock, selectControlButtonBlock} from '../../constants';
+import type {
+    SelectProps,
+    SelectRenderClearArgs,
+    SelectRenderControl,
+    SelectRenderControlProps,
+} from '../../types';
+import {SelectClear} from '../SelectClear/SelectClear';
 
 import './SelectControl.scss';
 
@@ -25,11 +32,14 @@ type ControlProps = {
     error?: SelectProps['error'];
     disabled?: boolean;
     value: SelectProps['value'];
+    clearValue: () => void;
+    hasClear?: boolean;
 } & Omit<SelectRenderControlProps, 'onClick'>;
 
 export const SelectControl = React.forwardRef<HTMLElement, ControlProps>((props, ref) => {
     const {
         toggleOpen,
+        clearValue,
         onKeyDown,
         renderControl,
         view,
@@ -45,12 +55,63 @@ export const SelectControl = React.forwardRef<HTMLElement, ControlProps>((props,
         open,
         disabled,
         value,
+        hasClear,
     } = props;
     const controlRef = React.useRef<HTMLElement>(null);
     const handleControlRef = useForkRef<HTMLElement>(ref, controlRef);
     const showOptionsText = Boolean(selectedOptionsContent);
     const showPlaceholder = Boolean(placeholder && !showOptionsText);
-    const mods: CnMods = {open, size, view, pin, disabled, error: Boolean(error)};
+    const hasValue = Array.isArray(value) && !isEmpty(value.filter(Boolean));
+
+    const [isDisabledButtonAnimation, setIsDisabledButtonAnimation] = React.useState(false);
+
+    const controlMods: CnMods = {
+        open,
+        size,
+        pin,
+        disabled,
+        error: Boolean(error),
+        'has-clear': hasClear,
+        'no-active': isDisabledButtonAnimation,
+        'has-value': hasValue,
+    };
+
+    const buttonMods: CnMods = {
+        open,
+        size,
+        view,
+        pin,
+        disabled,
+        error: Boolean(error),
+    };
+
+    const disableButtonAnimation = React.useCallback(() => {
+        setIsDisabledButtonAnimation(true);
+    }, []);
+    const enableButtonAnimation = React.useCallback(() => {
+        setIsDisabledButtonAnimation(false);
+    }, []);
+    const handleOnClearIconClick = React.useCallback(() => {
+        // return animation on clear click
+        setIsDisabledButtonAnimation(false);
+        clearValue();
+    }, [clearValue]);
+
+    const renderClearIcon = (args: SelectRenderClearArgs) => {
+        const hideOnEmpty = !value?.[0];
+        if (!hasClear || !clearValue || hideOnEmpty || disabled) {
+            return null;
+        }
+        return (
+            <SelectClear
+                size={size}
+                onClick={handleOnClearIconClick}
+                onMouseEnter={disableButtonAnimation}
+                onMouseLeave={enableButtonAnimation}
+                renderIcon={args.renderIcon}
+            />
+        );
+    };
 
     if (renderControl) {
         return renderControl(
@@ -59,6 +120,7 @@ export const SelectControl = React.forwardRef<HTMLElement, ControlProps>((props,
                 onClick: toggleOpen,
                 ref: handleControlRef,
                 open: Boolean(open),
+                renderClear: (arg) => renderClearIcon(arg),
             },
             {value},
         );
@@ -66,32 +128,35 @@ export const SelectControl = React.forwardRef<HTMLElement, ControlProps>((props,
 
     return (
         <React.Fragment>
-            <button
-                ref={handleControlRef as React.Ref<HTMLButtonElement>}
-                className={selectControlBlock(mods, className)}
-                aria-haspopup="listbox"
-                aria-expanded={open}
-                data-qa={qa}
-                name={name}
-                disabled={disabled}
-                onClick={toggleOpen}
-                onKeyDown={onKeyDown}
-                type="button"
-            >
-                {label && <span className={selectControlBlock('label')}>{label}</span>}
-                {showPlaceholder && (
-                    <span className={selectControlBlock('placeholder')}>{placeholder}</span>
-                )}
-                {showOptionsText && (
-                    <span className={selectControlBlock('option-text')}>
-                        {selectedOptionsContent}
-                    </span>
-                )}
+            <div className={selectControlBlock(controlMods)} ref={handleControlRef} role="group">
+                <button
+                    className={selectControlButtonBlock(buttonMods, className)}
+                    aria-haspopup="listbox"
+                    aria-expanded={open}
+                    data-qa={qa}
+                    name={name}
+                    disabled={disabled}
+                    onClick={toggleOpen}
+                    onKeyDown={onKeyDown}
+                    type="button"
+                >
+                    {label && <span className={selectControlBlock('label')}>{label}</span>}
+                    {showPlaceholder && (
+                        <span className={selectControlBlock('placeholder')}>{placeholder}</span>
+                    )}
+                    {showOptionsText && (
+                        <span className={selectControlBlock('option-text')}>
+                            {selectedOptionsContent}
+                        </span>
+                    )}
+                </button>
+                {renderClearIcon({})}
                 <Icon
                     className={selectControlBlock('chevron-icon', {disabled})}
                     data={ChevronDown}
+                    aria-hidden="true"
                 />
-            </button>
+            </div>
             {typeof error === 'string' && (
                 <div className={selectControlBlock('error')}>{error}</div>
             )}
diff --git a/src/components/Select/constants.ts b/src/components/Select/constants.ts
index b20c7435..d89cbcd0 100644
--- a/src/components/Select/constants.ts
+++ b/src/components/Select/constants.ts
@@ -6,8 +6,12 @@ export const selectBlock = block('select');
 
 export const selectControlBlock = block('select-control');
 
+export const selectControlButtonBlock = block('select-control__button');
+
 export const selectListBlock = block('select-list');
 
+export const selectClearBlock = block('select-clear');
+
 export const SIZE_TO_ITEM_HEIGHT: Record<SelectSize, number> = {
     s: 28,
     m: 28,
@@ -31,4 +35,5 @@ export const SelectQa = {
     LIST: 'select-list',
     POPUP: 'select-popup',
     SHEET: 'select-sheet',
+    CLEAR: 'select-clear',
 };
diff --git a/src/components/Select/i18n/en.json b/src/components/Select/i18n/en.json
new file mode 100644
index 00000000..dab10b78
--- /dev/null
+++ b/src/components/Select/i18n/en.json
@@ -0,0 +1,3 @@
+{
+  "label_clear": "Clear"
+}
diff --git a/src/components/Select/i18n/index.ts b/src/components/Select/i18n/index.ts
new file mode 100644
index 00000000..15ec9d12
--- /dev/null
+++ b/src/components/Select/i18n/index.ts
@@ -0,0 +1,8 @@
+import {addComponentKeysets} from '../../utils/addComponentKeysets';
+
+import en from './en.json';
+import ru from './ru.json';
+
+const COMPONENT = 'Select';
+
+export default addComponentKeysets({en, ru}, COMPONENT);
diff --git a/src/components/Select/i18n/ru.json b/src/components/Select/i18n/ru.json
new file mode 100644
index 00000000..1cc44f14
--- /dev/null
+++ b/src/components/Select/i18n/ru.json
@@ -0,0 +1,3 @@
+{
+  "label_clear": "Очистить"
+}
diff --git a/src/components/Select/types.ts b/src/components/Select/types.ts
index 75dd06f7..e242c495 100644
--- a/src/components/Select/types.ts
+++ b/src/components/Select/types.ts
@@ -1,14 +1,21 @@
+import type React from 'react';
+
 import type {TextInputPin, TextInputSize, TextInputView} from '../TextInput';
 import type {ControlGroupOption, ControlGroupProps, QAProps} from '../types';
 import type {UseOpenProps} from '../utils/useSelect/types';
 
 import type {Option, OptionGroup} from './tech-components';
 
+export type SelectRenderClearArgs = {
+    renderIcon?: () => React.ReactNode;
+};
+
 export type SelectRenderControlProps = {
     onClick: (e: React.MouseEvent<HTMLElement>) => void;
     onKeyDown: (e: React.KeyboardEvent<HTMLElement>) => void;
     ref: React.Ref<HTMLElement>;
     open: boolean;
+    renderClear?: (args: SelectRenderClearArgs) => React.ReactNode;
 };
 export type SelectRenderControlOptions = {
     value: SelectProps['value'];
@@ -65,6 +72,7 @@ export type SelectProps<T = any> = QAProps &
         multiple?: boolean;
         filterable?: boolean;
         disablePortal?: boolean;
+        hasClear?: boolean;
         children?:
             | React.ReactElement<SelectOption<T>, typeof Option>
             | React.ReactElement<SelectOption<T>, typeof Option>[]
@@ -87,3 +95,20 @@ export type SelectOptionGroup<T = any> = {
         | React.ReactElement<SelectOption, typeof Option>
         | React.ReactElement<SelectOption, typeof Option>[];
 };
+
+type SelectClearIconProps = {
+    size: SelectSize;
+    renderIcon: SelectRenderClearArgs['renderIcon'];
+};
+
+export type SelectClearProps = SelectClearIconProps & {
+    onClick: (e: React.MouseEvent) => void;
+    /**
+     * select control (button) has styles on focus, focus-in with animation on click event
+     * to prevent this on click by clear icon need to set class on button
+     * with disabling animation on button
+     * @param e
+     */
+    onMouseEnter: (e: React.MouseEvent) => void;
+    onMouseLeave: (e: React.MouseEvent) => void;
+};
diff --git a/src/components/utils/useSelect/useSelect.ts b/src/components/utils/useSelect/useSelect.ts
index 6330379a..af651ce5 100644
--- a/src/components/utils/useSelect/useSelect.ts
+++ b/src/components/utils/useSelect/useSelect.ts
@@ -53,9 +53,15 @@ export const useSelect = <T extends unknown>(props: UseSelectProps) => {
         [multiple, handleSingleSelection, handleMultipleSelection],
     );
 
+    const handleClearValue = React.useCallback(() => {
+        onUpdate?.([]);
+        setInnerValue([]);
+    }, [onUpdate]);
+
     return {
         value,
         handleSelection,
+        handleClearValue,
         /**
          * @deprecated use toggleOpen
          */


Test patch:
diff --git a/src/components/Select/__tests__/Select.base-actions.test.tsx b/src/components/Select/__tests__/Select.base-actions.test.tsx
index d90a01a5..a039a3c5 100644
--- a/src/components/Select/__tests__/Select.base-actions.test.tsx
+++ b/src/components/Select/__tests__/Select.base-actions.test.tsx
@@ -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,
@@ -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 () => {
@@ -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);
         });
 
@@ -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 () => {
diff --git a/src/components/Select/__tests__/Select.clear.test.tsx b/src/components/Select/__tests__/Select.clear.test.tsx
new file mode 100644
index 00000000..16e8dd97
--- /dev/null
+++ b/src/components/Select/__tests__/Select.clear.test.tsx
@@ -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([]);
+    });
+});
diff --git a/src/components/Select/__tests__/Select.muitiple.test.tsx b/src/components/Select/__tests__/Select.muitiple.test.tsx
index c3aede5a..f70262ea 100644
--- a/src/components/Select/__tests__/Select.muitiple.test.tsx
+++ b/src/components/Select/__tests__/Select.muitiple.test.tsx
@@ -5,7 +5,7 @@ import {
     DEFAULT_OPTIONS,
     GROUPED_OPTIONS,
     OptionsListType,
-    SELECT_CONTROL_OPEN_CLASS,
+    SELECT_CONTROL_BUTTON_OPEN_CLASS,
     TEST_QA,
     setup,
 } from './utils';
@@ -33,12 +33,12 @@ 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);
@@ -46,10 +46,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);
         });
     });
 
@@ -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);
         });
     });
 
@@ -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
@@ -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);
         });
     });
 
@@ -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);
         });
     });
 });
diff --git a/src/components/Select/__tests__/Select.single.test.tsx b/src/components/Select/__tests__/Select.single.test.tsx
index 08012896..d43e0fdb 100644
--- a/src/components/Select/__tests__/Select.single.test.tsx
+++ b/src/components/Select/__tests__/Select.single.test.tsx
@@ -5,7 +5,7 @@ import {
     DEFAULT_OPTIONS,
     GROUPED_OPTIONS,
     OptionsListType,
-    SELECT_CONTROL_OPEN_CLASS,
+    SELECT_CONTROL_BUTTON_OPEN_CLASS,
     TEST_QA,
     setup,
 } from './utils';
@@ -31,12 +31,12 @@ describe('Select single 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(selectedOption.content as string);
             // select option
             await user.click(option);
             expect(onUpdate).toHaveBeenCalledWith([selectedOption.value]);
-            expect(selectControl).not.toHaveClass(SELECT_CONTROL_OPEN_CLASS);
+            expect(selectControl).not.toHaveClass(SELECT_CONTROL_BUTTON_OPEN_CLASS);
         });
     });
 
@@ -54,11 +54,11 @@ describe('Select single 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).toHaveBeenCalledWith([selectedOption.value]);
-            expect(selectControl).not.toHaveClass(SELECT_CONTROL_OPEN_CLASS);
+            expect(selectControl).not.toHaveClass(SELECT_CONTROL_BUTTON_OPEN_CLASS);
         });
     });
 
@@ -71,12 +71,12 @@ describe('Select single 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(searchText);
             // select option
             await user.click(option);
             expect(onUpdate).not.toBeCalled();
-            expect(selectControl).toHaveClass(SELECT_CONTROL_OPEN_CLASS);
+            expect(selectControl).toHaveClass(SELECT_CONTROL_BUTTON_OPEN_CLASS);
         });
     });
 });
diff --git a/src/components/Select/__tests__/utils.tsx b/src/components/Select/__tests__/utils.tsx
index 20882a9f..aeded7b9 100644
--- a/src/components/Select/__tests__/utils.tsx
+++ b/src/components/Select/__tests__/utils.tsx
@@ -6,7 +6,7 @@ import {range} from 'lodash';
 import {Select} from '..';
 import type {SelectOption, SelectOptionGroup, SelectProps} from '..';
 import {MobileProvider} from '../../mobile';
-import {selectControlBlock, selectListBlock} from '../constants';
+import {selectControlBlock, selectControlButtonBlock, selectListBlock} from '../constants';
 
 export const OptionsListType = {
     FLAT: 'flat',
@@ -14,6 +14,7 @@ export const OptionsListType = {
 } as const;
 export const TEST_QA = 'select-test-qa';
 export const SELECT_CONTROL_OPEN_CLASS = selectControlBlock({open: true});
+export const SELECT_CONTROL_BUTTON_OPEN_CLASS = selectControlButtonBlock({open: true});
 export const SELECT_LIST_VIRTUALIZED_CLASS = selectListBlock({virtualized: true});
 export const DEFAULT_OPTIONS = generateOptions([
     ['js', 'JavaScript'],