@true-engineering/true-react-common-ui-kit 2.7.0 → 2.7.1

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