@true-engineering/true-react-common-ui-kit 1.12.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (266) hide show
  1. package/LICENSE +201 -201
  2. package/dist/components/Button/Button.d.ts +2 -2
  3. package/dist/components/DateInput/DateInput.d.ts +8 -8
  4. package/dist/components/DateInput/DateInput.styles.d.ts +2 -2
  5. package/dist/components/DateInput/constants.d.ts +2 -0
  6. package/dist/components/DateInput/index.d.ts +1 -0
  7. package/dist/components/DatePicker/DatePicker.d.ts +16 -24
  8. package/dist/components/DatePicker/DatePicker.styles.d.ts +12 -24
  9. package/dist/components/DatePicker/DatePickerHeader/DatePickerHeader.d.ts +3 -9
  10. package/dist/components/DatePicker/DatePickerHeader/index.d.ts +1 -0
  11. package/dist/components/DatePicker/constants.d.ts +1 -0
  12. package/dist/components/DatePicker/helpers.d.ts +3 -0
  13. package/dist/components/DatePicker/index.d.ts +1 -2
  14. package/dist/components/DatePicker/types.d.ts +4 -0
  15. package/dist/components/FiltersPane/FilterWithDates/FilterWithDates.d.ts +1 -1
  16. package/dist/components/Flag/augment.d.ts +1 -1
  17. package/dist/components/Icon/complexIcons/augment.d.ts +1 -1
  18. package/dist/helpers/utils.d.ts +2 -1
  19. package/dist/style.css +125 -50
  20. package/dist/true-react-common-ui-kit.js +392 -427
  21. package/dist/true-react-common-ui-kit.js.map +1 -1
  22. package/dist/true-react-common-ui-kit.umd.cjs +392 -427
  23. package/dist/true-react-common-ui-kit.umd.cjs.map +1 -1
  24. package/dist/types.d.ts +1 -1
  25. package/dist/vite-env.d.ts +1 -1
  26. package/package.json +91 -91
  27. package/src/components/AccountInfo/AccountInfo.stories.tsx +35 -35
  28. package/src/components/AccountInfo/AccountInfo.styles.ts +55 -55
  29. package/src/components/AccountInfo/AccountInfo.tsx +106 -106
  30. package/src/components/AccountInfo/index.ts +2 -2
  31. package/src/components/AddButton/AddButton.stories.tsx +21 -21
  32. package/src/components/AddButton/AddButton.styles.ts +34 -34
  33. package/src/components/AddButton/AddButton.tsx +49 -49
  34. package/src/components/AddButton/index.ts +2 -2
  35. package/src/components/Button/Button.stories.tsx +61 -61
  36. package/src/components/Button/Button.styles.ts +196 -196
  37. package/src/components/Button/Button.tsx +207 -207
  38. package/src/components/Button/index.ts +2 -2
  39. package/src/components/Checkbox/Checkbox.stories.tsx +35 -35
  40. package/src/components/Checkbox/Checkbox.styles.ts +62 -62
  41. package/src/components/Checkbox/Checkbox.tsx +106 -106
  42. package/src/components/Checkbox/index.ts +2 -2
  43. package/src/components/CloseButton/CloseButton.styles.ts +34 -34
  44. package/src/components/CloseButton/CloseButton.tsx +37 -37
  45. package/src/components/CloseButton/index.ts +2 -2
  46. package/src/components/Colors/Colors.stories.tsx +7 -7
  47. package/src/components/Colors/Colors.styles.ts +38 -38
  48. package/src/components/Colors/Colors.tsx +34 -34
  49. package/src/components/Colors/index.ts +2 -2
  50. package/src/components/CssBaseline/CssBaseline.styles.ts +15 -15
  51. package/src/components/CssBaseline/CssBaseline.tsx +17 -17
  52. package/src/components/CssBaseline/index.ts +2 -2
  53. package/src/components/DateInput/DateInput.stories.tsx +67 -63
  54. package/src/components/DateInput/DateInput.styles.ts +14 -14
  55. package/src/components/DateInput/DateInput.tsx +101 -60
  56. package/src/components/DateInput/constants.ts +2 -0
  57. package/src/components/DateInput/index.ts +3 -2
  58. package/src/components/DatePicker/DatePicker.stories.tsx +90 -96
  59. package/src/components/DatePicker/DatePicker.styles.ts +44 -54
  60. package/src/components/DatePicker/DatePicker.tsx +354 -358
  61. package/src/components/DatePicker/DatePickerHeader/DatePickerHeader.styles.ts +84 -84
  62. package/src/components/DatePicker/DatePickerHeader/DatePickerHeader.tsx +80 -94
  63. package/src/components/DatePicker/DatePickerHeader/index.ts +2 -1
  64. package/src/components/DatePicker/constants.ts +1 -0
  65. package/src/components/DatePicker/helpers.ts +24 -0
  66. package/src/components/DatePicker/index.ts +3 -4
  67. package/src/components/DatePicker/types.ts +40 -0
  68. package/src/components/Description/Description.stories.tsx +29 -29
  69. package/src/components/Description/Description.styles.ts +31 -31
  70. package/src/components/Description/Description.tsx +69 -69
  71. package/src/components/Description/index.ts +2 -2
  72. package/src/components/FiltersPane/FilterInterval/FilterInterval.styles.ts +64 -64
  73. package/src/components/FiltersPane/FilterInterval/FilterInterval.tsx +162 -162
  74. package/src/components/FiltersPane/FilterInterval/index.ts +1 -1
  75. package/src/components/FiltersPane/FilterMultiSelect/FilterMultiSelect.tsx +14 -14
  76. package/src/components/FiltersPane/FilterMultiSelect/index.ts +1 -1
  77. package/src/components/FiltersPane/FilterSelect/FilterSelect.styles.ts +144 -144
  78. package/src/components/FiltersPane/FilterSelect/FilterSelect.tsx +397 -397
  79. package/src/components/FiltersPane/FilterSelect/index.ts +1 -1
  80. package/src/components/FiltersPane/FilterSelect/locales.ts +37 -37
  81. package/src/components/FiltersPane/FilterValueView/FilterValueView.styles.tsx +15 -15
  82. package/src/components/FiltersPane/FilterValueView/FilterValueView.tsx +186 -186
  83. package/src/components/FiltersPane/FilterValueView/index.tsx +1 -1
  84. package/src/components/FiltersPane/FilterWithDates/FilterWithDates.styles.ts +60 -60
  85. package/src/components/FiltersPane/FilterWithDates/FilterWithDates.tsx +210 -222
  86. package/src/components/FiltersPane/FilterWithDates/index.ts +1 -1
  87. package/src/components/FiltersPane/FilterWithPeriod/FilterWithPeriod.styles.ts +17 -17
  88. package/src/components/FiltersPane/FilterWithPeriod/FilterWithPeriod.tsx +231 -231
  89. package/src/components/FiltersPane/FilterWithPeriod/index.ts +1 -1
  90. package/src/components/FiltersPane/FilterWrapper/FilterWrapper.styles.ts +110 -110
  91. package/src/components/FiltersPane/FilterWrapper/FilterWrapper.tsx +360 -360
  92. package/src/components/FiltersPane/FilterWrapper/index.ts +1 -1
  93. package/src/components/FiltersPane/FiltersPane.stories.tsx +308 -308
  94. package/src/components/FiltersPane/FiltersPane.styles.ts +71 -71
  95. package/src/components/FiltersPane/FiltersPane.tsx +193 -193
  96. package/src/components/FiltersPane/FiltersPaneSearch/FiltersPaneSearch.styles.ts +109 -109
  97. package/src/components/FiltersPane/FiltersPaneSearch/FiltersPaneSearch.tsx +175 -175
  98. package/src/components/FiltersPane/FiltersPaneSearch/index.ts +1 -1
  99. package/src/components/FiltersPane/index.ts +20 -20
  100. package/src/components/FiltersPane/locales.ts +107 -107
  101. package/src/components/FiltersPane/types.ts +126 -126
  102. package/src/components/Flag/Flag.stories.tsx +29 -29
  103. package/src/components/Flag/Flag.styles.ts +18 -18
  104. package/src/components/Flag/Flag.tsx +28 -28
  105. package/src/components/Flag/augment.d.ts +1 -1
  106. package/src/components/Flag/index.ts +2 -2
  107. package/src/components/FlexibleTable/FlexibleTable.stories.tsx +86 -86
  108. package/src/components/FlexibleTable/FlexibleTable.styles.ts +131 -131
  109. package/src/components/FlexibleTable/FlexibleTable.tsx +243 -243
  110. package/src/components/FlexibleTable/TableRow.tsx +171 -171
  111. package/src/components/FlexibleTable/TableValue.tsx +81 -81
  112. package/src/components/FlexibleTable/fixture-test.ts +254 -254
  113. package/src/components/FlexibleTable/index.ts +3 -3
  114. package/src/components/FlexibleTable/types.ts +58 -58
  115. package/src/components/Icon/ComplexIconBoilerplate.tsx +17 -17
  116. package/src/components/Icon/Icon.stories.tsx +88 -88
  117. package/src/components/Icon/Icon.styles.ts +10 -10
  118. package/src/components/Icon/Icon.tsx +34 -34
  119. package/src/components/Icon/IconBoilerplate.tsx +42 -42
  120. package/src/components/Icon/complexIcons/augment.d.ts +1 -1
  121. package/src/components/Icon/complexIcons/avatarGreen.svg +57 -57
  122. package/src/components/Icon/complexIcons/icons.ts +7 -7
  123. package/src/components/Icon/complexIcons/index.ts +1 -1
  124. package/src/components/Icon/icons/icons.ts +838 -838
  125. package/src/components/Icon/icons/index.ts +1 -1
  126. package/src/components/Icon/index.ts +4 -4
  127. package/src/components/IncrementInput/ChangeButton.tsx +34 -34
  128. package/src/components/IncrementInput/IncrementInput.stories.tsx +34 -34
  129. package/src/components/IncrementInput/IncrementInput.styles.ts +77 -77
  130. package/src/components/IncrementInput/IncrementInput.tsx +95 -95
  131. package/src/components/IncrementInput/index.ts +2 -2
  132. package/src/components/Input/Input.stories.tsx +92 -92
  133. package/src/components/Input/Input.styles.ts +307 -307
  134. package/src/components/Input/Input.tsx +321 -321
  135. package/src/components/Input/index.ts +2 -2
  136. package/src/components/List/List.stories.tsx +62 -62
  137. package/src/components/List/List.styles.ts +52 -52
  138. package/src/components/List/List.tsx +82 -82
  139. package/src/components/List/index.ts +2 -2
  140. package/src/components/Modal/Modal.stories.tsx +113 -113
  141. package/src/components/Modal/Modal.styles.ts +308 -308
  142. package/src/components/Modal/Modal.tsx +210 -210
  143. package/src/components/Modal/index.ts +2 -2
  144. package/src/components/MoreMenu/MoreMenu.stories.tsx +46 -46
  145. package/src/components/MoreMenu/MoreMenu.styles.ts +70 -70
  146. package/src/components/MoreMenu/MoreMenu.tsx +102 -102
  147. package/src/components/MoreMenu/index.ts +2 -2
  148. package/src/components/MultiSelect/MultiSelect.stories.tsx +46 -46
  149. package/src/components/MultiSelect/MultiSelect.styles.ts +55 -55
  150. package/src/components/MultiSelect/MultiSelect.tsx +98 -98
  151. package/src/components/MultiSelect/MultiSelectInput/MultiSelectInput.styles.ts +73 -73
  152. package/src/components/MultiSelect/MultiSelectInput/MultiSelectInput.tsx +62 -62
  153. package/src/components/MultiSelect/MultiSelectInput/index.ts +1 -1
  154. package/src/components/MultiSelect/index.ts +3 -3
  155. package/src/components/MultiSelectList/MultiSelectList.styles.ts +125 -125
  156. package/src/components/MultiSelectList/MultiSelectList.tsx +519 -519
  157. package/src/components/MultiSelectList/index.ts +2 -2
  158. package/src/components/MultiSelectList/locales.ts +37 -37
  159. package/src/components/Notification/Notification.stories.tsx +51 -51
  160. package/src/components/Notification/Notification.styles.ts +50 -50
  161. package/src/components/Notification/Notification.tsx +84 -84
  162. package/src/components/Notification/index.ts +2 -2
  163. package/src/components/NumberInput/NumberInput.stories.tsx +36 -36
  164. package/src/components/NumberInput/NumberInput.tsx +154 -154
  165. package/src/components/NumberInput/helpers.ts +87 -87
  166. package/src/components/NumberInput/index.ts +1 -1
  167. package/src/components/PhoneInput/PhoneInput.stories.tsx +71 -71
  168. package/src/components/PhoneInput/PhoneInput.styles.ts +84 -84
  169. package/src/components/PhoneInput/PhoneInput.tsx +223 -223
  170. package/src/components/PhoneInput/PhoneInputCountryList/PhoneInputCountryList.stories.tsx +21 -21
  171. package/src/components/PhoneInput/PhoneInputCountryList/PhoneInputCountryList.styles.ts +100 -100
  172. package/src/components/PhoneInput/PhoneInputCountryList/PhoneInputCountryList.tsx +171 -171
  173. package/src/components/PhoneInput/PhoneInputCountryList/index.ts +2 -2
  174. package/src/components/PhoneInput/index.ts +6 -6
  175. package/src/components/PhoneInput/phone-info.ts +2167 -2167
  176. package/src/components/PhoneInput/types.ts +16 -16
  177. package/src/components/RadioButton/RadioButton.stories.tsx +46 -46
  178. package/src/components/RadioButton/RadioButton.styles.ts +37 -37
  179. package/src/components/RadioButton/RadioButton.tsx +56 -56
  180. package/src/components/RadioButton/index.ts +2 -2
  181. package/src/components/ScrollIntoViewIfNeeded/ScrollIntoViewIfNeeded.ts +66 -66
  182. package/src/components/ScrollIntoViewIfNeeded/index.ts +1 -1
  183. package/src/components/SearchInput/SearchInput.stories.tsx +24 -24
  184. package/src/components/SearchInput/SearchInput.styles.ts +50 -50
  185. package/src/components/SearchInput/SearchInput.tsx +63 -63
  186. package/src/components/SearchInput/index.ts +2 -2
  187. package/src/components/Select/MultiSelect.stories.tsx +263 -263
  188. package/src/components/Select/Select.stories.tsx +258 -258
  189. package/src/components/Select/Select.styles.ts +96 -96
  190. package/src/components/Select/Select.tsx +630 -630
  191. package/src/components/Select/SelectList/SelectList.styles.ts +72 -72
  192. package/src/components/Select/SelectList/SelectList.tsx +165 -165
  193. package/src/components/Select/SelectList/index.ts +1 -1
  194. package/src/components/Select/SelectListItem/SelectListItem.styles.ts +14 -14
  195. package/src/components/Select/SelectListItem/SelectListItem.tsx +73 -73
  196. package/src/components/Select/constants.ts +2 -2
  197. package/src/components/Select/helpers.ts +29 -29
  198. package/src/components/Select/index.ts +4 -4
  199. package/src/components/Select/types.ts +1 -1
  200. package/src/components/SmartInput/SmartInput.stories.tsx +63 -63
  201. package/src/components/SmartInput/SmartInput.tsx +180 -180
  202. package/src/components/SmartInput/helpers.ts +85 -85
  203. package/src/components/SmartInput/index.ts +1 -1
  204. package/src/components/Switch/Switch.stories.tsx +40 -40
  205. package/src/components/Switch/Switch.styles.ts +75 -75
  206. package/src/components/Switch/Switch.tsx +89 -89
  207. package/src/components/Switch/index.ts +2 -2
  208. package/src/components/TextArea/TextArea.stories.tsx +35 -35
  209. package/src/components/TextArea/TextArea.styles.ts +153 -153
  210. package/src/components/TextArea/TextArea.tsx +178 -178
  211. package/src/components/TextArea/index.ts +2 -2
  212. package/src/components/TextWithInfo/TextWithInfo.stories.tsx +53 -53
  213. package/src/components/TextWithInfo/TextWithInfo.styles.ts +60 -60
  214. package/src/components/TextWithInfo/TextWithInfo.tsx +67 -67
  215. package/src/components/TextWithInfo/index.ts +2 -2
  216. package/src/components/TextWithTooltip/TextWithTooltip.stories.tsx +58 -58
  217. package/src/components/TextWithTooltip/TextWithTooltip.styles.ts +19 -19
  218. package/src/components/TextWithTooltip/TextWithTooltip.tsx +163 -163
  219. package/src/components/TextWithTooltip/index.ts +2 -2
  220. package/src/components/ThemedPreloader/ThemedPreloader.stories.tsx +41 -41
  221. package/src/components/ThemedPreloader/ThemedPreloader.styles.ts +21 -21
  222. package/src/components/ThemedPreloader/ThemedPreloader.tsx +56 -56
  223. package/src/components/ThemedPreloader/components/DefaultPreloader/DefaultPreloader.tsx +34 -34
  224. package/src/components/ThemedPreloader/components/DefaultPreloader/index.ts +1 -1
  225. package/src/components/ThemedPreloader/components/DotsPreloader/DotsPreloader.styles.ts +54 -54
  226. package/src/components/ThemedPreloader/components/DotsPreloader/DotsPreloader.tsx +18 -18
  227. package/src/components/ThemedPreloader/components/DotsPreloader/index.ts +2 -2
  228. package/src/components/ThemedPreloader/components/SvgPreloader/SvgPreloader.styles.ts +11 -11
  229. package/src/components/ThemedPreloader/components/SvgPreloader/SvgPreloader.tsx +32 -32
  230. package/src/components/ThemedPreloader/components/SvgPreloader/index.ts +2 -2
  231. package/src/components/ThemedPreloader/components/index.ts +2 -2
  232. package/src/components/ThemedPreloader/index.ts +2 -2
  233. package/src/components/Toaster/Toaster.stories.tsx +34 -34
  234. package/src/components/Toaster/Toaster.styles.ts +59 -59
  235. package/src/components/Toaster/Toaster.tsx +113 -113
  236. package/src/components/Toaster/index.ts +2 -2
  237. package/src/components/Tooltip/Tooltip.stories.tsx +21 -21
  238. package/src/components/Tooltip/Tooltip.styles.ts +45 -45
  239. package/src/components/Tooltip/Tooltip.tsx +40 -40
  240. package/src/components/Tooltip/index.ts +3 -3
  241. package/src/components/Tooltip/types.ts +1 -1
  242. package/src/components/index.ts +36 -36
  243. package/src/helpers/colors.ts +2 -2
  244. package/src/helpers/dateHelpers/date-helpers.ts +9 -9
  245. package/src/helpers/index.ts +4 -4
  246. package/src/helpers/phone.ts +106 -106
  247. package/src/helpers/popper-helpers.ts +17 -17
  248. package/src/helpers/snippets.tsx +5 -5
  249. package/src/helpers/utils.ts +261 -250
  250. package/src/hooks/index.ts +6 -6
  251. package/src/hooks/use-did-mount-effect.ts +21 -21
  252. package/src/hooks/use-dropdown.ts +85 -85
  253. package/src/hooks/use-is-mounted.ts +15 -15
  254. package/src/hooks/use-on-click-outside.ts +92 -92
  255. package/src/hooks/use-theme.ts +36 -36
  256. package/src/hooks/use-tweak-styles.ts +14 -14
  257. package/src/index.ts +6 -6
  258. package/src/theme.ts +155 -155
  259. package/src/types.ts +105 -106
  260. package/src/vite-env.d.ts +1 -1
  261. package/dist/components/DatePicker/DatePickerInput/DatePickerInput.d.ts +0 -11
  262. package/dist/components/DatePicker/DatePickerInput/DatePickerInput.styles.d.ts +0 -20
  263. package/dist/components/DatePicker/DatePickerInput/index.d.ts +0 -1
  264. package/src/components/DatePicker/DatePickerInput/DatePickerInput.styles.ts +0 -25
  265. package/src/components/DatePicker/DatePickerInput/DatePickerInput.tsx +0 -31
  266. package/src/components/DatePicker/DatePickerInput/index.ts +0 -1
@@ -1,630 +1,630 @@
1
- import {
2
- ReactNode,
3
- FocusEvent,
4
- KeyboardEvent,
5
- MouseEvent,
6
- useCallback,
7
- useEffect,
8
- useMemo,
9
- useRef,
10
- useState,
11
- SyntheticEvent,
12
- } from 'react';
13
- import { Styles } from 'jss';
14
- import clsx from 'clsx';
15
- import merge from 'lodash-es/merge';
16
- import { debounce } from 'ts-debounce';
17
- import { Portal } from 'react-overlays';
18
- import { SelectList } from './SelectList';
19
- import { IInputProps, Input } from '../Input';
20
- import { IIconType, Icon } from '../Icon';
21
- import {
22
- useIsMounted,
23
- useTheme,
24
- useOnClickOutsideWithRef,
25
- useDropdown,
26
- useTweakStyles,
27
- } from '../../hooks';
28
- import { IDropdownWithPopperOptions } from '../../types';
29
- import {
30
- createFilter,
31
- getTestId,
32
- hasExactParent,
33
- isNotEmpty,
34
- isStringNotEmpty,
35
- } from '../../helpers';
36
- import {
37
- defaultConvertFunction,
38
- defaultCompareFunction,
39
- defaultIsOptionDisabled,
40
- getDefaultConvertToIdFunction,
41
- isMultiSelectValue,
42
- } from './helpers';
43
- import { SelectStyles, styles } from './Select.styles';
44
- import { ISearchInputProps, SearchInput } from '../SearchInput';
45
- import { IMultipleSelectValue } from './types';
46
- import { ALL_OPTION_INDEX, DEFAULT_OPTION_INDEX } from './constants';
47
- import { renderIcon } from '../../helpers/snippets';
48
-
49
- export interface ISelectProps<Value>
50
- extends Omit<IInputProps, 'value' | 'onChange' | 'onBlur' | 'type'> {
51
- tweakStyles?: SelectStyles;
52
- defaultOptionLabel?: string;
53
- allOptionsLabel?: string;
54
- noMatchesLabel?: string;
55
- loadingLabel?: ReactNode;
56
- optionsMode?: 'search' | 'dynamic' | 'normal';
57
- debounceTime?: number;
58
- minSymbolsCountToOpenList?: number;
59
- dropdownOptions?: IDropdownWithPopperOptions;
60
- dropdownIcon?: IIconType;
61
- options: Value[];
62
- value: Value | undefined;
63
- shouldScrollToList?: boolean;
64
- isMultiSelect?: boolean;
65
- searchInput?: { shouldRenderInList: true } & Pick<
66
- ISearchInputProps,
67
- 'placeholder'
68
- >;
69
- isOptionDisabled?(option: Value): boolean;
70
- onChange(value?: Value): void; // подумать как возвращать индекс
71
- onBlur?(event: Event | SyntheticEvent): void;
72
- onType?(value: string): Promise<void>;
73
- optionsFilter?(options: Value[], query: string): Value[];
74
- onOpen?(): void;
75
- compareValuesOnChange?(v1?: Value, v2?: Value): boolean;
76
- // Для избежания проблем юзайте useCallback на эти функции
77
- // или выносите их из компонента (чтобы не было сайдэфектов от перерендеринга их)
78
- convertValueToString?(value: Value): string | undefined;
79
- convertValueToReactNode?(value: Value, isDisabled: boolean): ReactNode;
80
- convertValueToId?(value: Value): string | undefined;
81
- }
82
-
83
- export interface IMultipleSelectProps<Value>
84
- extends Omit<
85
- ISelectProps<Value>,
86
- 'value' | 'onChange' | 'compareValuesOnChange'
87
- > {
88
- isMultiSelect: true;
89
- value: IMultipleSelectValue<Value> | undefined;
90
- onChange(value?: IMultipleSelectValue<Value>): void;
91
- compareValuesOnChange?(
92
- v1?: IMultipleSelectValue<Value>,
93
- v2?: IMultipleSelectValue<Value>,
94
- ): boolean;
95
- }
96
-
97
- export function Select<Value>(
98
- props: ISelectProps<Value> | IMultipleSelectProps<Value>,
99
- ): JSX.Element {
100
- const {
101
- options,
102
- value,
103
- defaultOptionLabel,
104
- allOptionsLabel,
105
- debounceTime = 400,
106
- optionsMode = 'normal',
107
- noMatchesLabel,
108
- loadingLabel,
109
- tweakStyles,
110
- testId,
111
- isReadonly,
112
- isDisabled,
113
- dropdownOptions,
114
- minSymbolsCountToOpenList = 0,
115
- dropdownIcon = 'chevron-down',
116
- shouldScrollToList = true,
117
- searchInput,
118
- iconType,
119
- onChange,
120
- onFocus,
121
- onBlur,
122
- onType,
123
- onOpen,
124
- isOptionDisabled = defaultIsOptionDisabled,
125
- compareValuesOnChange = defaultCompareFunction,
126
- convertValueToString = defaultConvertFunction,
127
- convertValueToId,
128
- convertValueToReactNode,
129
- optionsFilter,
130
- ...inputProps
131
- } = props;
132
- const { classes, componentStyles } = useTheme('Select', styles, tweakStyles);
133
- const isMounted = useIsMounted();
134
- const [isListOpen, setIsListOpen] = useState(false);
135
- const [areOptionsLoading, setAreOptionsLoading] = useState(false);
136
- const hasDefaultOption = isStringNotEmpty(defaultOptionLabel);
137
-
138
- const [focusedListCellIndex, setFocusedListCellIndex] =
139
- useState(DEFAULT_OPTION_INDEX);
140
- const [searchValue, setSearchValue] = useState('');
141
- // если мы ввели что то в строку поиска - то этот булеан будет отключаться
142
- // вынесен отдельно, из-за проблем с дебаунсом при динамич. опциях
143
- const [shouldShowDefaultOption, setShouldShowDefaultOption] = useState(true);
144
-
145
- const inputWrapper = useRef<HTMLDivElement>(null);
146
- const list = useRef<HTMLDivElement>(null);
147
- const input = useRef<HTMLInputElement>(null); // TODO ref снаружи?
148
-
149
- const shouldRenderSearchInputInList =
150
- searchInput?.shouldRenderInList === true;
151
- const hasSearchInputInList =
152
- optionsMode !== 'normal' && shouldRenderSearchInputInList;
153
-
154
- const isMultiSelect = isMultiSelectValue(props, value);
155
- const strValue = isMultiSelect ? value?.[0] : value;
156
- const shouldShowAllOption =
157
- isMultiSelect && isNotEmpty(allOptionsLabel) && searchValue === '';
158
-
159
- const filteredOptions = useMemo(() => {
160
- if (optionsMode !== 'search') {
161
- return options;
162
- }
163
-
164
- const filter =
165
- optionsFilter ??
166
- createFilter<Value>((option) => [convertValueToString(option) ?? '']);
167
-
168
- return filter(options, searchValue);
169
- }, [optionsFilter, options, convertValueToString, searchValue, optionsMode]);
170
-
171
- const availableOptions = useMemo(
172
- () => options.filter((o) => !isOptionDisabled(o)),
173
- [options, isOptionDisabled],
174
- );
175
-
176
- const areAllOptionsSelected =
177
- isMultiSelect && value?.length === availableOptions.length;
178
- const shouldShowMultiSelectCounter =
179
- isMultiSelect &&
180
- isNotEmpty(value) &&
181
- value.length > 1 &&
182
- !areAllOptionsSelected;
183
-
184
- const optionsIndexesForNavigation = useMemo(() => {
185
- const result: number[] = [];
186
- if (shouldShowDefaultOption && hasDefaultOption) {
187
- result.push(DEFAULT_OPTION_INDEX);
188
- }
189
- if (shouldShowAllOption) {
190
- result.push(ALL_OPTION_INDEX);
191
- }
192
- return result.concat(
193
- filteredOptions.reduce((acc, cur, i) => {
194
- if (!isOptionDisabled(cur)) {
195
- acc.push(i);
196
- }
197
- return acc;
198
- }, [] as number[]),
199
- );
200
- }, [filteredOptions]);
201
-
202
- const stringValue = isNotEmpty(strValue)
203
- ? convertValueToString(strValue)
204
- : undefined;
205
- // Для мультиселекта пытаемся показать "Все опции" если выбраны все опции
206
- const showedStringValue =
207
- areAllOptionsSelected && isNotEmpty(allOptionsLabel)
208
- ? allOptionsLabel
209
- : stringValue;
210
-
211
- const convertToId = useCallback(
212
- (v: Value) =>
213
- (convertValueToId ?? getDefaultConvertToIdFunction(convertValueToString))(
214
- v,
215
- ),
216
- [convertValueToId, convertValueToString],
217
- );
218
-
219
- const handleListClose = useCallback(
220
- (event: Event | SyntheticEvent) => {
221
- setIsListOpen(false);
222
- setSearchValue('');
223
- setShouldShowDefaultOption(true);
224
- onBlur?.(event);
225
- },
226
- [onBlur],
227
- );
228
-
229
- const handleListOpen = () => {
230
- if (!isListOpen) {
231
- setIsListOpen(true);
232
- }
233
- };
234
-
235
- const handleFocus = (event: FocusEvent<HTMLInputElement>) => {
236
- onFocus?.(event);
237
- handleListOpen();
238
- };
239
-
240
- const handleOnClick = () => {
241
- handleListOpen();
242
- };
243
-
244
- const handleBlur = (event: FocusEvent<HTMLInputElement>) => {
245
- // Когда что-то блокирует открытие листа, но блур все равно должен сработать
246
- // например minSymbolsCount
247
- if (isListOpen && !isOpen) {
248
- handleListClose(event);
249
- return;
250
- }
251
-
252
- if (
253
- !isNotEmpty(event.relatedTarget) ||
254
- !isNotEmpty(list.current) ||
255
- !isNotEmpty(inputWrapper.current)
256
- ) {
257
- return;
258
- }
259
-
260
- const isActionInsideSelect =
261
- hasExactParent(event.relatedTarget, list.current) ||
262
- hasExactParent(event.relatedTarget, inputWrapper.current);
263
-
264
- // Ниче не делаем если клик был внутри селекта
265
- if (!isActionInsideSelect) {
266
- handleListClose(event);
267
- }
268
- };
269
-
270
- const handleOnChange = useCallback(
271
- (newValue: Value | IMultipleSelectValue<Value> | undefined) => {
272
- // Тут беда с типами, сорри
273
- if (!compareValuesOnChange(value as never, newValue as never)) {
274
- onChange(newValue as (Value & IMultipleSelectValue<Value>) | undefined);
275
- }
276
- },
277
- [value, compareValuesOnChange, onChange],
278
- );
279
-
280
- const handleOptionSelect = useCallback(
281
- (index: number, event: MouseEvent<HTMLElement> | KeyboardEvent) => {
282
- handleOnChange(
283
- index === DEFAULT_OPTION_INDEX ? undefined : filteredOptions[index],
284
- );
285
- handleListClose(event);
286
- input.current?.blur();
287
- },
288
- [handleOnChange, handleListClose, filteredOptions],
289
- );
290
-
291
- // MultiSelect
292
- const handleToggleOptionCheckbox = useCallback(
293
- (index: number, isSelected: boolean) => {
294
- if (!isMultiSelect) {
295
- return;
296
- }
297
-
298
- // Если выбрана не дефолтная опция, которая сетит андеф
299
- if (
300
- index === DEFAULT_OPTION_INDEX ||
301
- (index === ALL_OPTION_INDEX && !isSelected)
302
- ) {
303
- handleOnChange(undefined);
304
- return;
305
- }
306
- if (index === ALL_OPTION_INDEX && isSelected) {
307
- handleOnChange(availableOptions as IMultipleSelectValue<Value>);
308
- return;
309
- }
310
- const option = filteredOptions[index];
311
- handleOnChange(
312
- isSelected
313
- ? // Добавляем
314
- ([...(value ?? []), option] as IMultipleSelectValue<Value>)
315
- : // Убираем
316
- value?.filter((o) => convertToId(o) !== convertToId(option)),
317
- );
318
- },
319
- [handleOnChange, filteredOptions, isMultiSelect, value],
320
- );
321
-
322
- const handleOnType = useCallback(
323
- async (v: string) => {
324
- if (onType === undefined) {
325
- return;
326
- }
327
- if (isMounted()) {
328
- setAreOptionsLoading(true);
329
- }
330
- await onType(v);
331
- if (isMounted()) {
332
- setAreOptionsLoading(false);
333
- }
334
- if (optionsMode === 'dynamic') {
335
- setShouldShowDefaultOption(v === '');
336
- }
337
- },
338
- [onType, optionsMode],
339
- );
340
-
341
- const debounceHandleOnType = useCallback(
342
- debounce(handleOnType, debounceTime),
343
- [handleOnType, debounceTime],
344
- );
345
-
346
- const handleInputChange = (v: string) => {
347
- if (onType !== undefined) {
348
- debounceHandleOnType(v);
349
- }
350
-
351
- if (optionsMode !== 'dynamic') {
352
- setShouldShowDefaultOption(v === '');
353
- }
354
-
355
- if (v === '' && !hasSearchInputInList) {
356
- handleOnChange(undefined);
357
- }
358
-
359
- setSearchValue(v);
360
- };
361
-
362
- const handleKeyDown = (event: KeyboardEvent) => {
363
- if (!isListOpen) {
364
- return;
365
- }
366
-
367
- event.stopPropagation();
368
- const curIndexInNavigation = optionsIndexesForNavigation.findIndex(
369
- (index) => index === focusedListCellIndex,
370
- );
371
-
372
- switch (event.code) {
373
- case 'Enter':
374
- case 'NumpadEnter': {
375
- let indexToSelect = focusedListCellIndex;
376
-
377
- // если осталась одна опция в списке,
378
- // то выбираем ее нажатием на enter
379
- if (
380
- indexToSelect === DEFAULT_OPTION_INDEX &&
381
- filteredOptions.length === 1
382
- ) {
383
- indexToSelect = 0;
384
- }
385
-
386
- if (isMultiSelect) {
387
- let isThisValueAlreadySelected: boolean;
388
- if (indexToSelect === ALL_OPTION_INDEX) {
389
- isThisValueAlreadySelected = areAllOptionsSelected;
390
- } else {
391
- // подумать над концептом реального фокуса на опциях, а не вот эти вот focusedCell
392
- const valueIdToSelect = convertToId(filteredOptions[indexToSelect]);
393
- isThisValueAlreadySelected =
394
- value?.some((opt) => convertToId(opt) === valueIdToSelect) ??
395
- false;
396
- }
397
- handleToggleOptionCheckbox(
398
- indexToSelect,
399
- !isThisValueAlreadySelected,
400
- );
401
- } else {
402
- handleOptionSelect(indexToSelect, event);
403
- }
404
- break;
405
- }
406
-
407
- case 'ArrowDown': {
408
- // чтобы убрать перемещение курсора в инпуте
409
- event.preventDefault();
410
- const targetIndexInNavigation =
411
- (curIndexInNavigation + 1) % optionsIndexesForNavigation.length;
412
- setFocusedListCellIndex(
413
- optionsIndexesForNavigation[targetIndexInNavigation],
414
- );
415
- break;
416
- }
417
-
418
- case 'ArrowUp': {
419
- // чтобы убрать перемещение курсора в инпуте
420
- event.preventDefault();
421
- const targetIndexInNavigation =
422
- (curIndexInNavigation - 1 + optionsIndexesForNavigation.length) %
423
- optionsIndexesForNavigation.length;
424
- setFocusedListCellIndex(
425
- optionsIndexesForNavigation[targetIndexInNavigation],
426
- );
427
- break;
428
- }
429
- }
430
- };
431
-
432
- const onArrowClick = () => {
433
- if (isListOpen) {
434
- input.current?.blur();
435
- } else {
436
- input.current?.focus();
437
- }
438
- };
439
-
440
- useOnClickOutsideWithRef(list, handleListClose, inputWrapper);
441
-
442
- const hasEnoughSymbolsToSearch =
443
- searchValue.trim().length >= minSymbolsCountToOpenList;
444
-
445
- const isOpen =
446
- // Пользователь пытается открыть лист
447
- isListOpen &&
448
- // Нам есть что показать:
449
- // Есть опции
450
- (filteredOptions.length > 0 ||
451
- // Дефолтная опция
452
- (defaultOptionLabel !== undefined && !hasEnoughSymbolsToSearch) ||
453
- // Текст "Загрузка..."
454
- inputProps.isLoading ||
455
- // Текст "Совпадений не найдено"
456
- noMatchesLabel !== undefined ||
457
- // У нас есть инпут с поиском внутри листа
458
- hasSearchInputInList) &&
459
- // Последняя проверка на случай, если мы че то ищем в опциях
460
- (optionsMode === 'normal' || hasEnoughSymbolsToSearch);
461
-
462
- const hasReadonlyInput =
463
- isReadonly || optionsMode === 'normal' || shouldRenderSearchInputInList;
464
-
465
- const tweakInputStyles = useMemo(
466
- () =>
467
- merge(
468
- {},
469
- componentStyles.tweakInput,
470
- { ...(hasReadonlyInput && { input: { cursor: 'pointer' } }) },
471
- { ...(isMultiSelect && { inputIcon: { width: 'auto' } }) },
472
- tweakStyles?.tweakInput,
473
- ) as Styles,
474
- [tweakStyles?.tweakInput, hasReadonlyInput],
475
- );
476
-
477
- const tweakSearchInputStyles = useTweakStyles(
478
- componentStyles,
479
- tweakStyles,
480
- 'tweakSearchInput',
481
- );
482
-
483
- // Эти значения ставятся в false по дефолту также в useDropdown
484
- const {
485
- shouldUsePopper = false,
486
- shouldRenderInBody = false,
487
- shouldHideOnScroll = false,
488
- } = dropdownOptions ?? {};
489
-
490
- const popperData = useDropdown({
491
- isOpen,
492
- onDropdownClose: handleListClose,
493
- referenceElement: inputWrapper.current,
494
- dropdownElement: list.current,
495
- options: dropdownOptions,
496
- dependenciesForPositionUpdating: [
497
- inputProps.isLoading,
498
- filteredOptions.length,
499
- ],
500
- });
501
-
502
- useEffect(() => {
503
- const val = isMultiSelect ? value?.[0] : value;
504
- setFocusedListCellIndex(
505
- optionsIndexesForNavigation.find(
506
- (index) => filteredOptions[index] === val,
507
- ) ?? optionsIndexesForNavigation[0],
508
- );
509
-
510
- if (isOpen) {
511
- onOpen?.();
512
- }
513
- }, [isOpen]);
514
-
515
- const listEl = (
516
- <div
517
- className={clsx(classes.listWrapper, {
518
- [classes.withoutPopper]: !shouldUsePopper,
519
- [classes.listWrapperInBody]: shouldRenderInBody,
520
- })}
521
- ref={list}
522
- style={popperData?.styles.popper as Styles}
523
- onBlur={handleBlur} // обработка для Tab из списка
524
- {...popperData?.attributes.popper}
525
- >
526
- {isOpen && (
527
- <SelectList
528
- options={filteredOptions}
529
- defaultOptionLabel={
530
- hasDefaultOption && shouldShowDefaultOption
531
- ? defaultOptionLabel
532
- : undefined
533
- }
534
- allOptionsLabel={shouldShowAllOption ? allOptionsLabel : undefined}
535
- areAllOptionsSelected={areAllOptionsSelected}
536
- customListHeader={
537
- hasSearchInputInList ? (
538
- <SearchInput
539
- value={searchValue}
540
- onChange={handleInputChange}
541
- tweakStyles={tweakSearchInputStyles}
542
- placeholder="Поиск"
543
- {...searchInput}
544
- />
545
- ) : undefined
546
- }
547
- noMatchesLabel={noMatchesLabel}
548
- focusedIndex={focusedListCellIndex}
549
- activeValue={value}
550
- isLoading={inputProps.isLoading}
551
- loadingLabel={loadingLabel}
552
- tweakStyles={tweakStyles?.tweakSelectList as Styles}
553
- testId={getTestId(testId, 'list')}
554
- // скролл не работает с включеным поппером
555
- shouldScrollToList={
556
- shouldScrollToList && !shouldUsePopper && !shouldHideOnScroll
557
- }
558
- isOptionDisabled={isOptionDisabled}
559
- convertValueToString={convertValueToString}
560
- convertValueToReactNode={convertValueToReactNode}
561
- convertValueToId={convertToId}
562
- onOptionSelect={handleOptionSelect}
563
- onToggleCheckbox={
564
- isMultiSelect ? handleToggleOptionCheckbox : undefined
565
- }
566
- />
567
- )}
568
- </div>
569
- );
570
-
571
- const multiSelectCounterWithIcon =
572
- shouldShowMultiSelectCounter || isNotEmpty(iconType) ? (
573
- <>
574
- {shouldShowMultiSelectCounter && (
575
- <div className={classes.counter}>(+{value.length - 1})</div>
576
- )}
577
- {isNotEmpty(iconType) && (
578
- <div className={classes.icon}>{renderIcon(iconType)}</div>
579
- )}
580
- </>
581
- ) : undefined;
582
-
583
- return (
584
- <div className={classes.root} onKeyDown={handleKeyDown}>
585
- <div
586
- className={clsx(classes.inputWrapper, isDisabled && classes.disabled)}
587
- onClick={isDisabled ? undefined : handleOnClick}
588
- ref={inputWrapper}
589
- >
590
- <Input
591
- value={
592
- searchValue !== '' && !shouldRenderSearchInputInList
593
- ? searchValue
594
- : showedStringValue
595
- }
596
- onChange={handleInputChange}
597
- isActive={isListOpen}
598
- isReadonly={hasReadonlyInput}
599
- onFocus={handleFocus}
600
- onBlur={handleBlur}
601
- isDisabled={isDisabled}
602
- ref={input}
603
- isLoading={areOptionsLoading}
604
- tweakStyles={tweakInputStyles}
605
- testId={testId}
606
- iconType={isMultiSelect ? multiSelectCounterWithIcon : iconType}
607
- {...inputProps}
608
- />
609
- <div
610
- onMouseDown={(event: MouseEvent) => {
611
- event.preventDefault();
612
- }}
613
- onClick={onArrowClick}
614
- className={clsx(classes.arrow, isOpen && classes.activeArrow)}
615
- >
616
- <Icon type={dropdownIcon} />
617
- </div>
618
- </div>
619
- {shouldUsePopper ? (
620
- <Portal
621
- container={shouldRenderInBody ? document.body : inputWrapper.current}
622
- >
623
- <>{listEl}</>
624
- </Portal>
625
- ) : (
626
- <>{isOpen && listEl}</>
627
- )}
628
- </div>
629
- );
630
- }
1
+ import {
2
+ ReactNode,
3
+ FocusEvent,
4
+ KeyboardEvent,
5
+ MouseEvent,
6
+ useCallback,
7
+ useEffect,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ SyntheticEvent,
12
+ } from 'react';
13
+ import { Styles } from 'jss';
14
+ import clsx from 'clsx';
15
+ import merge from 'lodash-es/merge';
16
+ import { debounce } from 'ts-debounce';
17
+ import { Portal } from 'react-overlays';
18
+ import { SelectList } from './SelectList';
19
+ import { IInputProps, Input } from '../Input';
20
+ import { IIconType, Icon } from '../Icon';
21
+ import {
22
+ useIsMounted,
23
+ useTheme,
24
+ useOnClickOutsideWithRef,
25
+ useDropdown,
26
+ useTweakStyles,
27
+ } from '../../hooks';
28
+ import { IDropdownWithPopperOptions } from '../../types';
29
+ import {
30
+ createFilter,
31
+ getTestId,
32
+ hasExactParent,
33
+ isNotEmpty,
34
+ isStringNotEmpty,
35
+ } from '../../helpers';
36
+ import {
37
+ defaultConvertFunction,
38
+ defaultCompareFunction,
39
+ defaultIsOptionDisabled,
40
+ getDefaultConvertToIdFunction,
41
+ isMultiSelectValue,
42
+ } from './helpers';
43
+ import { SelectStyles, styles } from './Select.styles';
44
+ import { ISearchInputProps, SearchInput } from '../SearchInput';
45
+ import { IMultipleSelectValue } from './types';
46
+ import { ALL_OPTION_INDEX, DEFAULT_OPTION_INDEX } from './constants';
47
+ import { renderIcon } from '../../helpers/snippets';
48
+
49
+ export interface ISelectProps<Value>
50
+ extends Omit<IInputProps, 'value' | 'onChange' | 'onBlur' | 'type'> {
51
+ tweakStyles?: SelectStyles;
52
+ defaultOptionLabel?: string;
53
+ allOptionsLabel?: string;
54
+ noMatchesLabel?: string;
55
+ loadingLabel?: ReactNode;
56
+ optionsMode?: 'search' | 'dynamic' | 'normal';
57
+ debounceTime?: number;
58
+ minSymbolsCountToOpenList?: number;
59
+ dropdownOptions?: IDropdownWithPopperOptions;
60
+ dropdownIcon?: IIconType;
61
+ options: Value[];
62
+ value: Value | undefined;
63
+ shouldScrollToList?: boolean;
64
+ isMultiSelect?: boolean;
65
+ searchInput?: { shouldRenderInList: true } & Pick<
66
+ ISearchInputProps,
67
+ 'placeholder'
68
+ >;
69
+ isOptionDisabled?(option: Value): boolean;
70
+ onChange(value?: Value): void; // подумать как возвращать индекс
71
+ onBlur?(event: Event | SyntheticEvent): void;
72
+ onType?(value: string): Promise<void>;
73
+ optionsFilter?(options: Value[], query: string): Value[];
74
+ onOpen?(): void;
75
+ compareValuesOnChange?(v1?: Value, v2?: Value): boolean;
76
+ // Для избежания проблем юзайте useCallback на эти функции
77
+ // или выносите их из компонента (чтобы не было сайдэфектов от перерендеринга их)
78
+ convertValueToString?(value: Value): string | undefined;
79
+ convertValueToReactNode?(value: Value, isDisabled: boolean): ReactNode;
80
+ convertValueToId?(value: Value): string | undefined;
81
+ }
82
+
83
+ export interface IMultipleSelectProps<Value>
84
+ extends Omit<
85
+ ISelectProps<Value>,
86
+ 'value' | 'onChange' | 'compareValuesOnChange'
87
+ > {
88
+ isMultiSelect: true;
89
+ value: IMultipleSelectValue<Value> | undefined;
90
+ onChange(value?: IMultipleSelectValue<Value>): void;
91
+ compareValuesOnChange?(
92
+ v1?: IMultipleSelectValue<Value>,
93
+ v2?: IMultipleSelectValue<Value>,
94
+ ): boolean;
95
+ }
96
+
97
+ export function Select<Value>(
98
+ props: ISelectProps<Value> | IMultipleSelectProps<Value>,
99
+ ): JSX.Element {
100
+ const {
101
+ options,
102
+ value,
103
+ defaultOptionLabel,
104
+ allOptionsLabel,
105
+ debounceTime = 400,
106
+ optionsMode = 'normal',
107
+ noMatchesLabel,
108
+ loadingLabel,
109
+ tweakStyles,
110
+ testId,
111
+ isReadonly,
112
+ isDisabled,
113
+ dropdownOptions,
114
+ minSymbolsCountToOpenList = 0,
115
+ dropdownIcon = 'chevron-down',
116
+ shouldScrollToList = true,
117
+ searchInput,
118
+ iconType,
119
+ onChange,
120
+ onFocus,
121
+ onBlur,
122
+ onType,
123
+ onOpen,
124
+ isOptionDisabled = defaultIsOptionDisabled,
125
+ compareValuesOnChange = defaultCompareFunction,
126
+ convertValueToString = defaultConvertFunction,
127
+ convertValueToId,
128
+ convertValueToReactNode,
129
+ optionsFilter,
130
+ ...inputProps
131
+ } = props;
132
+ const { classes, componentStyles } = useTheme('Select', styles, tweakStyles);
133
+ const isMounted = useIsMounted();
134
+ const [isListOpen, setIsListOpen] = useState(false);
135
+ const [areOptionsLoading, setAreOptionsLoading] = useState(false);
136
+ const hasDefaultOption = isStringNotEmpty(defaultOptionLabel);
137
+
138
+ const [focusedListCellIndex, setFocusedListCellIndex] =
139
+ useState(DEFAULT_OPTION_INDEX);
140
+ const [searchValue, setSearchValue] = useState('');
141
+ // если мы ввели что то в строку поиска - то этот булеан будет отключаться
142
+ // вынесен отдельно, из-за проблем с дебаунсом при динамич. опциях
143
+ const [shouldShowDefaultOption, setShouldShowDefaultOption] = useState(true);
144
+
145
+ const inputWrapper = useRef<HTMLDivElement>(null);
146
+ const list = useRef<HTMLDivElement>(null);
147
+ const input = useRef<HTMLInputElement>(null); // TODO ref снаружи?
148
+
149
+ const shouldRenderSearchInputInList =
150
+ searchInput?.shouldRenderInList === true;
151
+ const hasSearchInputInList =
152
+ optionsMode !== 'normal' && shouldRenderSearchInputInList;
153
+
154
+ const isMultiSelect = isMultiSelectValue(props, value);
155
+ const strValue = isMultiSelect ? value?.[0] : value;
156
+ const shouldShowAllOption =
157
+ isMultiSelect && isNotEmpty(allOptionsLabel) && searchValue === '';
158
+
159
+ const filteredOptions = useMemo(() => {
160
+ if (optionsMode !== 'search') {
161
+ return options;
162
+ }
163
+
164
+ const filter =
165
+ optionsFilter ??
166
+ createFilter<Value>((option) => [convertValueToString(option) ?? '']);
167
+
168
+ return filter(options, searchValue);
169
+ }, [optionsFilter, options, convertValueToString, searchValue, optionsMode]);
170
+
171
+ const availableOptions = useMemo(
172
+ () => options.filter((o) => !isOptionDisabled(o)),
173
+ [options, isOptionDisabled],
174
+ );
175
+
176
+ const areAllOptionsSelected =
177
+ isMultiSelect && value?.length === availableOptions.length;
178
+ const shouldShowMultiSelectCounter =
179
+ isMultiSelect &&
180
+ isNotEmpty(value) &&
181
+ value.length > 1 &&
182
+ !areAllOptionsSelected;
183
+
184
+ const optionsIndexesForNavigation = useMemo(() => {
185
+ const result: number[] = [];
186
+ if (shouldShowDefaultOption && hasDefaultOption) {
187
+ result.push(DEFAULT_OPTION_INDEX);
188
+ }
189
+ if (shouldShowAllOption) {
190
+ result.push(ALL_OPTION_INDEX);
191
+ }
192
+ return result.concat(
193
+ filteredOptions.reduce((acc, cur, i) => {
194
+ if (!isOptionDisabled(cur)) {
195
+ acc.push(i);
196
+ }
197
+ return acc;
198
+ }, [] as number[]),
199
+ );
200
+ }, [filteredOptions]);
201
+
202
+ const stringValue = isNotEmpty(strValue)
203
+ ? convertValueToString(strValue)
204
+ : undefined;
205
+ // Для мультиселекта пытаемся показать "Все опции" если выбраны все опции
206
+ const showedStringValue =
207
+ areAllOptionsSelected && isNotEmpty(allOptionsLabel)
208
+ ? allOptionsLabel
209
+ : stringValue;
210
+
211
+ const convertToId = useCallback(
212
+ (v: Value) =>
213
+ (convertValueToId ?? getDefaultConvertToIdFunction(convertValueToString))(
214
+ v,
215
+ ),
216
+ [convertValueToId, convertValueToString],
217
+ );
218
+
219
+ const handleListClose = useCallback(
220
+ (event: Event | SyntheticEvent) => {
221
+ setIsListOpen(false);
222
+ setSearchValue('');
223
+ setShouldShowDefaultOption(true);
224
+ onBlur?.(event);
225
+ },
226
+ [onBlur],
227
+ );
228
+
229
+ const handleListOpen = () => {
230
+ if (!isListOpen) {
231
+ setIsListOpen(true);
232
+ }
233
+ };
234
+
235
+ const handleFocus = (event: FocusEvent<HTMLInputElement>) => {
236
+ onFocus?.(event);
237
+ handleListOpen();
238
+ };
239
+
240
+ const handleOnClick = () => {
241
+ handleListOpen();
242
+ };
243
+
244
+ const handleBlur = (event: FocusEvent<HTMLInputElement>) => {
245
+ // Когда что-то блокирует открытие листа, но блур все равно должен сработать
246
+ // например minSymbolsCount
247
+ if (isListOpen && !isOpen) {
248
+ handleListClose(event);
249
+ return;
250
+ }
251
+
252
+ if (
253
+ !isNotEmpty(event.relatedTarget) ||
254
+ !isNotEmpty(list.current) ||
255
+ !isNotEmpty(inputWrapper.current)
256
+ ) {
257
+ return;
258
+ }
259
+
260
+ const isActionInsideSelect =
261
+ hasExactParent(event.relatedTarget, list.current) ||
262
+ hasExactParent(event.relatedTarget, inputWrapper.current);
263
+
264
+ // Ниче не делаем если клик был внутри селекта
265
+ if (!isActionInsideSelect) {
266
+ handleListClose(event);
267
+ }
268
+ };
269
+
270
+ const handleOnChange = useCallback(
271
+ (newValue: Value | IMultipleSelectValue<Value> | undefined) => {
272
+ // Тут беда с типами, сорри
273
+ if (!compareValuesOnChange(value as never, newValue as never)) {
274
+ onChange(newValue as (Value & IMultipleSelectValue<Value>) | undefined);
275
+ }
276
+ },
277
+ [value, compareValuesOnChange, onChange],
278
+ );
279
+
280
+ const handleOptionSelect = useCallback(
281
+ (index: number, event: MouseEvent<HTMLElement> | KeyboardEvent) => {
282
+ handleOnChange(
283
+ index === DEFAULT_OPTION_INDEX ? undefined : filteredOptions[index],
284
+ );
285
+ handleListClose(event);
286
+ input.current?.blur();
287
+ },
288
+ [handleOnChange, handleListClose, filteredOptions],
289
+ );
290
+
291
+ // MultiSelect
292
+ const handleToggleOptionCheckbox = useCallback(
293
+ (index: number, isSelected: boolean) => {
294
+ if (!isMultiSelect) {
295
+ return;
296
+ }
297
+
298
+ // Если выбрана не дефолтная опция, которая сетит андеф
299
+ if (
300
+ index === DEFAULT_OPTION_INDEX ||
301
+ (index === ALL_OPTION_INDEX && !isSelected)
302
+ ) {
303
+ handleOnChange(undefined);
304
+ return;
305
+ }
306
+ if (index === ALL_OPTION_INDEX && isSelected) {
307
+ handleOnChange(availableOptions as IMultipleSelectValue<Value>);
308
+ return;
309
+ }
310
+ const option = filteredOptions[index];
311
+ handleOnChange(
312
+ isSelected
313
+ ? // Добавляем
314
+ ([...(value ?? []), option] as IMultipleSelectValue<Value>)
315
+ : // Убираем
316
+ value?.filter((o) => convertToId(o) !== convertToId(option)),
317
+ );
318
+ },
319
+ [handleOnChange, filteredOptions, isMultiSelect, value],
320
+ );
321
+
322
+ const handleOnType = useCallback(
323
+ async (v: string) => {
324
+ if (onType === undefined) {
325
+ return;
326
+ }
327
+ if (isMounted()) {
328
+ setAreOptionsLoading(true);
329
+ }
330
+ await onType(v);
331
+ if (isMounted()) {
332
+ setAreOptionsLoading(false);
333
+ }
334
+ if (optionsMode === 'dynamic') {
335
+ setShouldShowDefaultOption(v === '');
336
+ }
337
+ },
338
+ [onType, optionsMode],
339
+ );
340
+
341
+ const debounceHandleOnType = useCallback(
342
+ debounce(handleOnType, debounceTime),
343
+ [handleOnType, debounceTime],
344
+ );
345
+
346
+ const handleInputChange = (v: string) => {
347
+ if (onType !== undefined) {
348
+ debounceHandleOnType(v);
349
+ }
350
+
351
+ if (optionsMode !== 'dynamic') {
352
+ setShouldShowDefaultOption(v === '');
353
+ }
354
+
355
+ if (v === '' && !hasSearchInputInList) {
356
+ handleOnChange(undefined);
357
+ }
358
+
359
+ setSearchValue(v);
360
+ };
361
+
362
+ const handleKeyDown = (event: KeyboardEvent) => {
363
+ if (!isListOpen) {
364
+ return;
365
+ }
366
+
367
+ event.stopPropagation();
368
+ const curIndexInNavigation = optionsIndexesForNavigation.findIndex(
369
+ (index) => index === focusedListCellIndex,
370
+ );
371
+
372
+ switch (event.code) {
373
+ case 'Enter':
374
+ case 'NumpadEnter': {
375
+ let indexToSelect = focusedListCellIndex;
376
+
377
+ // если осталась одна опция в списке,
378
+ // то выбираем ее нажатием на enter
379
+ if (
380
+ indexToSelect === DEFAULT_OPTION_INDEX &&
381
+ filteredOptions.length === 1
382
+ ) {
383
+ indexToSelect = 0;
384
+ }
385
+
386
+ if (isMultiSelect) {
387
+ let isThisValueAlreadySelected: boolean;
388
+ if (indexToSelect === ALL_OPTION_INDEX) {
389
+ isThisValueAlreadySelected = areAllOptionsSelected;
390
+ } else {
391
+ // подумать над концептом реального фокуса на опциях, а не вот эти вот focusedCell
392
+ const valueIdToSelect = convertToId(filteredOptions[indexToSelect]);
393
+ isThisValueAlreadySelected =
394
+ value?.some((opt) => convertToId(opt) === valueIdToSelect) ??
395
+ false;
396
+ }
397
+ handleToggleOptionCheckbox(
398
+ indexToSelect,
399
+ !isThisValueAlreadySelected,
400
+ );
401
+ } else {
402
+ handleOptionSelect(indexToSelect, event);
403
+ }
404
+ break;
405
+ }
406
+
407
+ case 'ArrowDown': {
408
+ // чтобы убрать перемещение курсора в инпуте
409
+ event.preventDefault();
410
+ const targetIndexInNavigation =
411
+ (curIndexInNavigation + 1) % optionsIndexesForNavigation.length;
412
+ setFocusedListCellIndex(
413
+ optionsIndexesForNavigation[targetIndexInNavigation],
414
+ );
415
+ break;
416
+ }
417
+
418
+ case 'ArrowUp': {
419
+ // чтобы убрать перемещение курсора в инпуте
420
+ event.preventDefault();
421
+ const targetIndexInNavigation =
422
+ (curIndexInNavigation - 1 + optionsIndexesForNavigation.length) %
423
+ optionsIndexesForNavigation.length;
424
+ setFocusedListCellIndex(
425
+ optionsIndexesForNavigation[targetIndexInNavigation],
426
+ );
427
+ break;
428
+ }
429
+ }
430
+ };
431
+
432
+ const onArrowClick = () => {
433
+ if (isListOpen) {
434
+ input.current?.blur();
435
+ } else {
436
+ input.current?.focus();
437
+ }
438
+ };
439
+
440
+ useOnClickOutsideWithRef(list, handleListClose, inputWrapper);
441
+
442
+ const hasEnoughSymbolsToSearch =
443
+ searchValue.trim().length >= minSymbolsCountToOpenList;
444
+
445
+ const isOpen =
446
+ // Пользователь пытается открыть лист
447
+ isListOpen &&
448
+ // Нам есть что показать:
449
+ // Есть опции
450
+ (filteredOptions.length > 0 ||
451
+ // Дефолтная опция
452
+ (defaultOptionLabel !== undefined && !hasEnoughSymbolsToSearch) ||
453
+ // Текст "Загрузка..."
454
+ inputProps.isLoading ||
455
+ // Текст "Совпадений не найдено"
456
+ noMatchesLabel !== undefined ||
457
+ // У нас есть инпут с поиском внутри листа
458
+ hasSearchInputInList) &&
459
+ // Последняя проверка на случай, если мы че то ищем в опциях
460
+ (optionsMode === 'normal' || hasEnoughSymbolsToSearch);
461
+
462
+ const hasReadonlyInput =
463
+ isReadonly || optionsMode === 'normal' || shouldRenderSearchInputInList;
464
+
465
+ const tweakInputStyles = useMemo(
466
+ () =>
467
+ merge(
468
+ {},
469
+ componentStyles.tweakInput,
470
+ { ...(hasReadonlyInput && { input: { cursor: 'pointer' } }) },
471
+ { ...(isMultiSelect && { inputIcon: { width: 'auto' } }) },
472
+ tweakStyles?.tweakInput,
473
+ ) as Styles,
474
+ [tweakStyles?.tweakInput, hasReadonlyInput],
475
+ );
476
+
477
+ const tweakSearchInputStyles = useTweakStyles(
478
+ componentStyles,
479
+ tweakStyles,
480
+ 'tweakSearchInput',
481
+ );
482
+
483
+ // Эти значения ставятся в false по дефолту также в useDropdown
484
+ const {
485
+ shouldUsePopper = false,
486
+ shouldRenderInBody = false,
487
+ shouldHideOnScroll = false,
488
+ } = dropdownOptions ?? {};
489
+
490
+ const popperData = useDropdown({
491
+ isOpen,
492
+ onDropdownClose: handleListClose,
493
+ referenceElement: inputWrapper.current,
494
+ dropdownElement: list.current,
495
+ options: dropdownOptions,
496
+ dependenciesForPositionUpdating: [
497
+ inputProps.isLoading,
498
+ filteredOptions.length,
499
+ ],
500
+ });
501
+
502
+ useEffect(() => {
503
+ const val = isMultiSelect ? value?.[0] : value;
504
+ setFocusedListCellIndex(
505
+ optionsIndexesForNavigation.find(
506
+ (index) => filteredOptions[index] === val,
507
+ ) ?? optionsIndexesForNavigation[0],
508
+ );
509
+
510
+ if (isOpen) {
511
+ onOpen?.();
512
+ }
513
+ }, [isOpen]);
514
+
515
+ const listEl = (
516
+ <div
517
+ className={clsx(classes.listWrapper, {
518
+ [classes.withoutPopper]: !shouldUsePopper,
519
+ [classes.listWrapperInBody]: shouldRenderInBody,
520
+ })}
521
+ ref={list}
522
+ style={popperData?.styles.popper as Styles}
523
+ onBlur={handleBlur} // обработка для Tab из списка
524
+ {...popperData?.attributes.popper}
525
+ >
526
+ {isOpen && (
527
+ <SelectList
528
+ options={filteredOptions}
529
+ defaultOptionLabel={
530
+ hasDefaultOption && shouldShowDefaultOption
531
+ ? defaultOptionLabel
532
+ : undefined
533
+ }
534
+ allOptionsLabel={shouldShowAllOption ? allOptionsLabel : undefined}
535
+ areAllOptionsSelected={areAllOptionsSelected}
536
+ customListHeader={
537
+ hasSearchInputInList ? (
538
+ <SearchInput
539
+ value={searchValue}
540
+ onChange={handleInputChange}
541
+ tweakStyles={tweakSearchInputStyles}
542
+ placeholder="Поиск"
543
+ {...searchInput}
544
+ />
545
+ ) : undefined
546
+ }
547
+ noMatchesLabel={noMatchesLabel}
548
+ focusedIndex={focusedListCellIndex}
549
+ activeValue={value}
550
+ isLoading={inputProps.isLoading}
551
+ loadingLabel={loadingLabel}
552
+ tweakStyles={tweakStyles?.tweakSelectList as Styles}
553
+ testId={getTestId(testId, 'list')}
554
+ // скролл не работает с включеным поппером
555
+ shouldScrollToList={
556
+ shouldScrollToList && !shouldUsePopper && !shouldHideOnScroll
557
+ }
558
+ isOptionDisabled={isOptionDisabled}
559
+ convertValueToString={convertValueToString}
560
+ convertValueToReactNode={convertValueToReactNode}
561
+ convertValueToId={convertToId}
562
+ onOptionSelect={handleOptionSelect}
563
+ onToggleCheckbox={
564
+ isMultiSelect ? handleToggleOptionCheckbox : undefined
565
+ }
566
+ />
567
+ )}
568
+ </div>
569
+ );
570
+
571
+ const multiSelectCounterWithIcon =
572
+ shouldShowMultiSelectCounter || isNotEmpty(iconType) ? (
573
+ <>
574
+ {shouldShowMultiSelectCounter && (
575
+ <div className={classes.counter}>(+{value.length - 1})</div>
576
+ )}
577
+ {isNotEmpty(iconType) && (
578
+ <div className={classes.icon}>{renderIcon(iconType)}</div>
579
+ )}
580
+ </>
581
+ ) : undefined;
582
+
583
+ return (
584
+ <div className={classes.root} onKeyDown={handleKeyDown}>
585
+ <div
586
+ className={clsx(classes.inputWrapper, isDisabled && classes.disabled)}
587
+ onClick={isDisabled ? undefined : handleOnClick}
588
+ ref={inputWrapper}
589
+ >
590
+ <Input
591
+ value={
592
+ searchValue !== '' && !shouldRenderSearchInputInList
593
+ ? searchValue
594
+ : showedStringValue
595
+ }
596
+ onChange={handleInputChange}
597
+ isActive={isListOpen}
598
+ isReadonly={hasReadonlyInput}
599
+ onFocus={handleFocus}
600
+ onBlur={handleBlur}
601
+ isDisabled={isDisabled}
602
+ ref={input}
603
+ isLoading={areOptionsLoading}
604
+ tweakStyles={tweakInputStyles}
605
+ testId={testId}
606
+ iconType={isMultiSelect ? multiSelectCounterWithIcon : iconType}
607
+ {...inputProps}
608
+ />
609
+ <div
610
+ onMouseDown={(event: MouseEvent) => {
611
+ event.preventDefault();
612
+ }}
613
+ onClick={onArrowClick}
614
+ className={clsx(classes.arrow, isOpen && classes.activeArrow)}
615
+ >
616
+ <Icon type={dropdownIcon} />
617
+ </div>
618
+ </div>
619
+ {shouldUsePopper ? (
620
+ <Portal
621
+ container={shouldRenderInBody ? document.body : inputWrapper.current}
622
+ >
623
+ <>{listEl}</>
624
+ </Portal>
625
+ ) : (
626
+ <>{isOpen && listEl}</>
627
+ )}
628
+ </div>
629
+ );
630
+ }