From 9f6aa9cadd3821f3aa8501f705a5980171d1d98c Mon Sep 17 00:00:00 2001 From: Kirill Revenkov Date: Mon, 8 Apr 2024 12:45:06 +0300 Subject: [PATCH] feat(Select): add selected elements counter on multiple selection (#1368) --- src/components/Select/README.md | 128 ++++++++++++------ src/components/Select/Select.tsx | 4 + .../SelectControl/SelectControl.tsx | 21 ++- .../SelectCounter/SelectCounter.scss | 48 +++++++ .../SelectCounter/SelectCounter.tsx | 26 ++++ src/components/Select/types.ts | 18 +++ 6 files changed, 200 insertions(+), 45 deletions(-) create mode 100644 src/components/Select/components/SelectCounter/SelectCounter.scss create mode 100644 src/components/Select/components/SelectCounter/SelectCounter.tsx diff --git a/src/components/Select/README.md b/src/components/Select/README.md index ad66ff2cdd..8d7146ee18 100644 --- a/src/components/Select/README.md +++ b/src/components/Select/README.md @@ -241,6 +241,45 @@ LANDING_BLOCK--> +### Counter + +You can add counter of the selected items to the component by using property `hasCounter`. + + + + + +```tsx + +``` + + + ## Filtering options To enable filter section use the `filterable` property. Default to `false`. @@ -1079,50 +1118,51 @@ LANDING_BLOCK--> ## Properties -| Name | Description | Type | Default | -| :-------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------- | :------------------------------------------------------- | -| className | Control className | `string` | | -| defaultValue | Default values that represent selected options in case of using uncontrolled state | `string[]` | | -| disabled | Indicates that the user cannot interact with the control | `boolean` | `false` | -| [filterable](#filtering-options) | Indicates that select popup have filter section | `boolean` | `false` | -| filterOption | Used to compare option with filter | `function` | | -| filterPlaceholder | Default filter input placeholder text | `string` | | -| [getOptionHeight](#render-options-with-different-heights) | Used to set height of customized user options | `function` | | -| getOptionGroupHeight | Used to set height of customized user option group | `function` | | -| hasClear | Enable displaying icon for clear selected options | `boolean` | `false` | -| id | HTML `id` attribute | `string` | | -| label | Control label | `string` | | -| loading | Add the loading item to the end of the options list. Works like persistant loading indicator while the options list is empty. | `boolean` | | -| [multiple](#selecting-multiple-options) | Indicates that multiple options can be selected in the list | `boolean` | `false` | -| name | Name of the control | `string` | | -| onBlur | Handler that is called when the element loses focus. | `function` | | -| onFilterChange | Fires every time after changing filter | `function` | | -| onFocus | Handler that is called when the element receives focus. | `function` | | -| onLoadMore | Fires when loading indicator gets visible. | `function` | | -| onOpenChange | Fires every time after changing popup visibility | `function` | | -| onUpdate | Fires when an alteration to the Select value is committed by the user | `function` | | -| [options](#options) | Options to select | `(SelectOption \| SelectOptionGroup)[]` | | -| pin | Control border view | `string` | `'round-round'` | -| placeholder | Placeholder text | `string` | | -| popupClassName | Popup with options list className | `string` | | -| popupPlacement | `Popper.js` placement | `PopupPlacement` `Array` | `['bottom-start', 'bottom-end', 'top-start', 'top-end']` | -| [popupWidth](#popup-width) | Popup width | `number \| 'fit' \| 'outfit'` | `'outfit'` | -| qa | Test id attribute (`data-qa`) | `string` | | -| [renderControl](#render-custom-control) | Used to render user control | `function` | | -| renderEmptyOptions | Used to render node for an empty options list | `function` | | -| [renderFilter](#render-custom-filter-section) | Used to render user filter section | `function` | | -| [renderOption](#render-custom-options) | Used to render user options | `function` | | -| renderOptionGroup | Used to render user option groups | `function` | | -| [renderSelectedOption](#render-custom-selected-options) | Used to render user selected options | `function` | | -| [renderPopup](#render-custom-popup) | Used to render user popup content | `function` | | -| [size](#size) | Control / options size | `string` | `'m'` | -| value | Values that represent selected options | `string[]` | | -| view | Control view | `string` | `'normal'` | -| [virtualizationThreshold](#virtualized-list) | The threshold of the options count after which virtualization is enabled | `number` | `50` | -| [width](#control-width) | Control width | `string \| number` | `undefined` | -| errorMessage | Error text | `string` | | -| errorPlacement | Error placement | `outside` `inside` | `outside` | -| validationState | Validation state | `"invalid"` | | +| Name | Description | Type | Default | +| :-------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------- | :------------------------------------------------------- | +| className | Control className | `string` | | +| defaultValue | Default values that represent selected options in case of using uncontrolled state | `string[]` | | +| disabled | Indicates that the user cannot interact with the control | `boolean` | `false` | +| [filterable](#filtering-options) | Indicates that select popup have filter section | `boolean` | `false` | +| filterOption | Used to compare option with filter | `function` | | +| filterPlaceholder | Default filter input placeholder text | `string` | | +| [getOptionHeight](#render-options-with-different-heights) | Used to set height of customized user options | `function` | | +| getOptionGroupHeight | Used to set height of customized user option group | `function` | | +| hasClear | Enable displaying icon for clear selected options | `boolean` | `false` | +| id | HTML `id` attribute | `string` | | +| label | Control label | `string` | | +| loading | Add the loading item to the end of the options list. Works like persistant loading indicator while the options list is empty. | `boolean` | | +| [multiple](#selecting-multiple-options) | Indicates that multiple options can be selected in the list | `boolean` | `false` | +| name | Name of the control | `string` | | +| onBlur | Handler that is called when the element loses focus. | `function` | | +| onFilterChange | Fires every time after changing filter | `function` | | +| onFocus | Handler that is called when the element receives focus. | `function` | | +| onLoadMore | Fires when loading indicator gets visible. | `function` | | +| onOpenChange | Fires every time after changing popup visibility | `function` | | +| onUpdate | Fires when an alteration to the Select value is committed by the user | `function` | | +| [options](#options) | Options to select | `(SelectOption \| SelectOptionGroup)[]` | | +| pin | Control border view | `string` | `'round-round'` | +| placeholder | Placeholder text | `string` | | +| popupClassName | Popup with options list className | `string` | | +| popupPlacement | `Popper.js` placement | `PopupPlacement` `Array` | `['bottom-start', 'bottom-end', 'top-start', 'top-end']` | +| [popupWidth](#popup-width) | Popup width | `number \| 'fit' \| 'outfit'` | `'outfit'` | +| qa | Test id attribute (`data-qa`) | `string` | | +| [renderControl](#render-custom-control) | Used to render user control | `function` | | +| renderEmptyOptions | Used to render node for an empty options list | `function` | | +| [renderFilter](#render-custom-filter-section) | Used to render user filter section | `function` | | +| [renderOption](#render-custom-options) | Used to render user options | `function` | | +| renderOptionGroup | Used to render user option groups | `function` | | +| [renderSelectedOption](#render-custom-selected-options) | Used to render user selected options | `function` | | +| [renderPopup](#render-custom-popup) | Used to render user popup content | `function` | | +| [size](#size) | Control / options size | `string` | `'m'` | +| value | Values that represent selected options | `string[]` | | +| view | Control view | `string` | `'normal'` | +| [virtualizationThreshold](#virtualized-list) | The threshold of the options count after which virtualization is enabled | `number` | `50` | +| [width](#control-width) | Control width | `string \| number` | `undefined` | +| errorMessage | Error text | `string` | | +| errorPlacement | Error placement | `outside` `inside` | `outside` | +| validationState | Validation state | `"invalid"` | | +| [hasCounter](#counter) | Indicates count of the selected options. Counter appears only when [multiple](#selecting-multiple-options) selection enabled. state | `boolean` | ## CSS API diff --git a/src/components/Select/Select.tsx b/src/components/Select/Select.tsx index 5bd434b314..9ee92b4d17 100644 --- a/src/components/Select/Select.tsx +++ b/src/components/Select/Select.tsx @@ -87,6 +87,8 @@ export const Select = React.forwardRef(function hasClear = false, onClose, id, + hasCounter, + renderCounter, title, } = props; const mobile = useMobile(); @@ -339,6 +341,8 @@ export const Select = React.forwardRef(function popupId={`select-popup-${selectId}`} selectId={`select-${selectId}`} activeIndex={activeIndex} + hasCounter={multiple && hasCounter} + renderCounter={renderCounter} title={title} /> diff --git a/src/components/Select/components/SelectControl/SelectControl.tsx b/src/components/Select/components/SelectControl/SelectControl.tsx index d510d07d07..09d361cf9b 100644 --- a/src/components/Select/components/SelectControl/SelectControl.tsx +++ b/src/components/Select/components/SelectControl/SelectControl.tsx @@ -14,14 +14,17 @@ import type { SelectRenderClearArgs, SelectRenderControl, SelectRenderControlProps, + SelectRenderCounter, } from '../../types'; import {SelectClear} from '../SelectClear/SelectClear'; +import {SelectCounter} from '../SelectCounter/SelectCounter'; import './SelectControl.scss'; type ControlProps = { toggleOpen: () => void; renderControl?: SelectRenderControl; + renderCounter?: SelectRenderCounter; view: NonNullable; size: NonNullable; pin: NonNullable; @@ -37,8 +40,9 @@ type ControlProps = { value: SelectProps['value']; clearValue: () => void; hasClear?: boolean; + hasCounter?: boolean; title?: string; -} & Omit; +} & Omit; export const SelectControl = React.forwardRef((props, ref) => { const { @@ -64,6 +68,8 @@ export const SelectControl = React.forwardRef(( popupId, selectId, activeIndex, + renderCounter, + hasCounter, title, } = props; const showOptionsText = Boolean(selectedOptionsContent); @@ -105,6 +111,17 @@ export const SelectControl = React.forwardRef(( clearValue(); }, [clearValue]); + const renderCounterComponent = () => { + if (!hasCounter) { + return null; + } + const count = Number(value?.length) || 0; + const counterComponent = ; + return renderCounter + ? renderCounter(counterComponent, {count, size, disabled}) + : counterComponent; + }; + const renderClearIcon = (args: SelectRenderClearArgs) => { const hideOnEmpty = !value?.[0]; if (!hasClear || !clearValue || hideOnEmpty || disabled) { @@ -128,6 +145,7 @@ export const SelectControl = React.forwardRef(( onClear: clearValue, onClick: toggleOpen, renderClear: (arg) => renderClearIcon(arg), + renderCounter: renderCounterComponent, ref, open: Boolean(open), popupId, @@ -171,6 +189,7 @@ export const SelectControl = React.forwardRef(( )} + {renderCounterComponent()} {renderClearIcon({})} {errorMessage && ( diff --git a/src/components/Select/components/SelectCounter/SelectCounter.scss b/src/components/Select/components/SelectCounter/SelectCounter.scss new file mode 100644 index 0000000000..fa092a5c22 --- /dev/null +++ b/src/components/Select/components/SelectCounter/SelectCounter.scss @@ -0,0 +1,48 @@ +@use '../../../variables'; +@use '../../variables.scss' as select-css-variables; + +$block: '.#{variables.$ns}select-counter'; + +#{$block} { + display: flex; + justify-content: center; + align-items: center; + margin-inline: 4px; + + background-color: var(--g-color-base-generic); + + &__text { + margin-inline: 4px; + flex-grow: 1; + text-align: center; + } + + &_size_xl &__text { + margin-inline: 6px; + } + + &_size { + &_s { + border-radius: var(--g-border-radius-xs); + height: 20px; + min-width: 20px; + } + &_m { + border-radius: var(--g-border-radius-s); + height: 24px; + min-width: 24px; + } + &_l { + border-radius: var(--g-border-radius-m); + height: 28px; + min-width: 28px; + } + + &_xl { + border-radius: var(--g-border-radius-l); + margin-inline: 4px; + height: 36px; + min-width: 36px; + } + } +} diff --git a/src/components/Select/components/SelectCounter/SelectCounter.tsx b/src/components/Select/components/SelectCounter/SelectCounter.tsx new file mode 100644 index 0000000000..aa9ccfbe17 --- /dev/null +++ b/src/components/Select/components/SelectCounter/SelectCounter.tsx @@ -0,0 +1,26 @@ +import React from 'react'; + +import {Text} from '../../../Text'; +import {block} from '../../../utils/cn'; +import type {SelectCounterProps} from '../../types'; + +import './SelectCounter.scss'; + +const b = block('select-counter'); + +export const SelectCounter = React.forwardRef(function SelectCouner( + {count, size, disabled}: SelectCounterProps, + ref: React.ForwardedRef, +) { + return ( +
+ + {count} + +
+ ); +}); diff --git a/src/components/Select/types.ts b/src/components/Select/types.ts index d1cdc7e18f..3aed92c62a 100644 --- a/src/components/Select/types.ts +++ b/src/components/Select/types.ts @@ -16,6 +16,7 @@ export type SelectRenderControlProps = { onClick: () => void; onKeyDown?: (e: React.KeyboardEvent) => void; renderClear?: (args: SelectRenderClearArgs) => React.ReactNode; + renderCounter?: () => React.ReactNode; ref: React.Ref; open: boolean; popupId: string; @@ -51,6 +52,11 @@ export type SelectRenderPopup = (popupItems: { export type SelectSize = InputControlSize; +export type SelectRenderCounter = ( + originalComponent: React.ReactElement, + counterProps: SelectCounterProps, +) => React.ReactElement; + export type SelectProps = QAProps & Pick & UseOpenProps & { @@ -69,6 +75,7 @@ export type SelectProps = QAProps & renderSelectedOption?: (option: SelectOption, index: number) => React.ReactElement; renderEmptyOptions?: ({filter}: {filter: string}) => React.ReactElement; renderPopup?: SelectRenderPopup; + renderCounter?: SelectRenderCounter; getOptionHeight?: (option: SelectOption, index: number) => number; getOptionGroupHeight?: (option: SelectOptionGroup, index: number) => number; filterOption?: (option: SelectOption, filter: string) => boolean; @@ -112,6 +119,8 @@ export type SelectProps = QAProps & | React.ReactElement, typeof OptionGroup> | React.ReactElement, typeof OptionGroup>[]; id?: string; + /**Shows selected options count if multiple selection is avalable */ + hasCounter?: boolean; title?: string; }; @@ -148,4 +157,13 @@ export type SelectClearProps = SelectClearIconProps & { onMouseLeave: (e: React.MouseEvent) => void; }; +export type SelectCounterProps = { + /** amount of selected elements to show */ + count: number; + /** size of the parent element */ + size: SelectSize; + /** disabled state of the parent element*/ + disabled?: boolean; +}; + export type SelectOptions = NonNullable['options']>;