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