@alfalab/core-components-select 19.1.10 → 19.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 (251) hide show
  1. package/components/arrow/index.css +4 -4
  2. package/components/arrow/index.module.css.js +1 -1
  3. package/components/base-checkmark/index.css +9 -9
  4. package/components/base-checkmark/index.module.css.js +1 -1
  5. package/components/base-option/index.css +14 -14
  6. package/components/base-option/index.module.css.js +1 -1
  7. package/components/base-select/Component.js +36 -49
  8. package/components/base-select/Component.js.map +1 -1
  9. package/components/base-select/index.css +70 -20
  10. package/components/base-select/index.module.css.js +1 -1
  11. package/components/base-select/index.module.css.js.map +1 -1
  12. package/components/base-select/mobile.css +12 -11
  13. package/components/base-select/mobile.module.css.js +1 -1
  14. package/components/base-select/mobile.module.css.js.map +1 -1
  15. package/components/checkmark/index.css +11 -11
  16. package/components/checkmark/index.module.css.js +1 -1
  17. package/components/checkmark-mobile/index.css +6 -6
  18. package/components/checkmark-mobile/index.module.css.js +1 -1
  19. package/components/clear-button/index.css +4 -4
  20. package/components/clear-button/index.module.css.js +1 -1
  21. package/components/field/index.css +13 -13
  22. package/components/field/index.module.css.js +1 -1
  23. package/components/footer/index.css +4 -4
  24. package/components/footer/index.module.css.js +1 -1
  25. package/components/optgroup/index.css +4 -4
  26. package/components/optgroup/index.module.css.js +1 -1
  27. package/components/option/desktop/index.css +23 -19
  28. package/components/option/desktop/index.module.css.js +1 -1
  29. package/components/option/desktop/index.module.css.js.map +1 -1
  30. package/components/option/mobile/index.css +20 -16
  31. package/components/option/mobile/index.module.css.js +1 -1
  32. package/components/option/mobile/index.module.css.js.map +1 -1
  33. package/components/options-list/Component.d.ts +4 -0
  34. package/components/options-list/Component.js +32 -53
  35. package/components/options-list/Component.js.map +1 -1
  36. package/components/options-list/index.css +13 -33
  37. package/components/options-list/index.module.css.js +1 -1
  38. package/components/options-list/index.module.css.js.map +1 -1
  39. package/components/search/index.css +1 -1
  40. package/components/search/index.module.css.js +1 -1
  41. package/components/virtual-options-list/Component.d.ts +4 -0
  42. package/components/virtual-options-list/Component.js +61 -48
  43. package/components/virtual-options-list/Component.js.map +1 -1
  44. package/cssm/components/base-select/Component.js +36 -49
  45. package/cssm/components/base-select/Component.js.map +1 -1
  46. package/cssm/components/base-select/index.module.css +61 -11
  47. package/cssm/components/base-select/mobile.module.css +1 -0
  48. package/cssm/components/option/desktop/index.module.css +4 -0
  49. package/cssm/components/option/mobile/index.module.css +4 -0
  50. package/cssm/components/options-list/Component.d.ts +4 -0
  51. package/cssm/components/options-list/Component.js +32 -53
  52. package/cssm/components/options-list/Component.js.map +1 -1
  53. package/cssm/components/options-list/index.module.css +6 -26
  54. package/cssm/components/virtual-options-list/Component.d.ts +4 -0
  55. package/cssm/components/virtual-options-list/Component.js +61 -48
  56. package/cssm/components/virtual-options-list/Component.js.map +1 -1
  57. package/cssm/presets/useSelectWithApply/hook.d.ts +12 -0
  58. package/cssm/presets/useSelectWithApply/options-list-with-apply/Component.d.ts +4 -0
  59. package/cssm/presets/useSelectWithApply/options-list-with-apply/footer/index.module.css +0 -5
  60. package/cssm/presets/useSelectWithApply/options-list-with-apply/header/index.module.css +9 -6
  61. package/cssm/shared/index.js +0 -1
  62. package/cssm/shared/index.js.map +1 -1
  63. package/cssm/typings.d.ts +21 -1
  64. package/cssm/utils.d.ts +1 -6
  65. package/cssm/utils.js +19 -28
  66. package/cssm/utils.js.map +1 -1
  67. package/esm/components/arrow/index.css +4 -4
  68. package/esm/components/arrow/index.module.css.js +1 -1
  69. package/esm/components/base-checkmark/index.css +9 -9
  70. package/esm/components/base-checkmark/index.module.css.js +1 -1
  71. package/esm/components/base-option/index.css +14 -14
  72. package/esm/components/base-option/index.module.css.js +1 -1
  73. package/esm/components/base-select/Component.js +37 -50
  74. package/esm/components/base-select/Component.js.map +1 -1
  75. package/esm/components/base-select/index.css +70 -20
  76. package/esm/components/base-select/index.module.css.js +1 -1
  77. package/esm/components/base-select/index.module.css.js.map +1 -1
  78. package/esm/components/base-select/mobile.css +12 -11
  79. package/esm/components/base-select/mobile.module.css.js +1 -1
  80. package/esm/components/base-select/mobile.module.css.js.map +1 -1
  81. package/esm/components/checkmark/index.css +11 -11
  82. package/esm/components/checkmark/index.module.css.js +1 -1
  83. package/esm/components/checkmark-mobile/index.css +6 -6
  84. package/esm/components/checkmark-mobile/index.module.css.js +1 -1
  85. package/esm/components/clear-button/index.css +4 -4
  86. package/esm/components/clear-button/index.module.css.js +1 -1
  87. package/esm/components/field/index.css +13 -13
  88. package/esm/components/field/index.module.css.js +1 -1
  89. package/esm/components/footer/index.css +4 -4
  90. package/esm/components/footer/index.module.css.js +1 -1
  91. package/esm/components/optgroup/index.css +4 -4
  92. package/esm/components/optgroup/index.module.css.js +1 -1
  93. package/esm/components/option/desktop/index.css +23 -19
  94. package/esm/components/option/desktop/index.module.css.js +1 -1
  95. package/esm/components/option/desktop/index.module.css.js.map +1 -1
  96. package/esm/components/option/mobile/index.css +20 -16
  97. package/esm/components/option/mobile/index.module.css.js +1 -1
  98. package/esm/components/option/mobile/index.module.css.js.map +1 -1
  99. package/esm/components/options-list/Component.d.ts +4 -0
  100. package/esm/components/options-list/Component.js +33 -54
  101. package/esm/components/options-list/Component.js.map +1 -1
  102. package/esm/components/options-list/index.css +13 -33
  103. package/esm/components/options-list/index.module.css.js +1 -1
  104. package/esm/components/options-list/index.module.css.js.map +1 -1
  105. package/esm/components/search/index.css +1 -1
  106. package/esm/components/search/index.module.css.js +1 -1
  107. package/esm/components/virtual-options-list/Component.d.ts +4 -0
  108. package/esm/components/virtual-options-list/Component.js +63 -49
  109. package/esm/components/virtual-options-list/Component.js.map +1 -1
  110. package/esm/mobile/modal.mobile.css +1 -1
  111. package/esm/mobile/modal.mobile.module.css.js +1 -1
  112. package/esm/presets/useSelectWithApply/hook.d.ts +12 -0
  113. package/esm/presets/useSelectWithApply/options-list-with-apply/Component.d.ts +4 -0
  114. package/esm/presets/useSelectWithApply/options-list-with-apply/footer/index.css +2 -7
  115. package/esm/presets/useSelectWithApply/options-list-with-apply/footer/index.module.css.js +1 -1
  116. package/esm/presets/useSelectWithApply/options-list-with-apply/footer/index.module.css.js.map +1 -1
  117. package/esm/presets/useSelectWithApply/options-list-with-apply/header/index.css +12 -9
  118. package/esm/presets/useSelectWithApply/options-list-with-apply/header/index.module.css.js +1 -1
  119. package/esm/presets/useSelectWithApply/options-list-with-apply/header/index.module.css.js.map +1 -1
  120. package/esm/presets/useSelectWithLoading/index.css +1 -1
  121. package/esm/presets/useSelectWithLoading/index.module.css.js +1 -1
  122. package/esm/shared/index.js +1 -1
  123. package/esm/typings.d.ts +21 -1
  124. package/esm/utils.d.ts +1 -6
  125. package/esm/utils.js +21 -29
  126. package/esm/utils.js.map +1 -1
  127. package/mobile/modal.mobile.css +1 -1
  128. package/mobile/modal.mobile.module.css.js +1 -1
  129. package/modern/components/arrow/index.css +4 -4
  130. package/modern/components/arrow/index.module.css.js +1 -1
  131. package/modern/components/base-checkmark/index.css +9 -9
  132. package/modern/components/base-checkmark/index.module.css.js +1 -1
  133. package/modern/components/base-option/index.css +14 -14
  134. package/modern/components/base-option/index.module.css.js +1 -1
  135. package/modern/components/base-select/Component.js +34 -47
  136. package/modern/components/base-select/Component.js.map +1 -1
  137. package/modern/components/base-select/index.css +70 -20
  138. package/modern/components/base-select/index.module.css.js +1 -1
  139. package/modern/components/base-select/index.module.css.js.map +1 -1
  140. package/modern/components/base-select/mobile.css +12 -11
  141. package/modern/components/base-select/mobile.module.css.js +1 -1
  142. package/modern/components/base-select/mobile.module.css.js.map +1 -1
  143. package/modern/components/checkmark/index.css +11 -11
  144. package/modern/components/checkmark/index.module.css.js +1 -1
  145. package/modern/components/checkmark-mobile/index.css +6 -6
  146. package/modern/components/checkmark-mobile/index.module.css.js +1 -1
  147. package/modern/components/clear-button/index.css +4 -4
  148. package/modern/components/clear-button/index.module.css.js +1 -1
  149. package/modern/components/field/index.css +13 -13
  150. package/modern/components/field/index.module.css.js +1 -1
  151. package/modern/components/footer/index.css +4 -4
  152. package/modern/components/footer/index.module.css.js +1 -1
  153. package/modern/components/optgroup/index.css +4 -4
  154. package/modern/components/optgroup/index.module.css.js +1 -1
  155. package/modern/components/option/desktop/index.css +23 -19
  156. package/modern/components/option/desktop/index.module.css.js +1 -1
  157. package/modern/components/option/desktop/index.module.css.js.map +1 -1
  158. package/modern/components/option/mobile/index.css +20 -16
  159. package/modern/components/option/mobile/index.module.css.js +1 -1
  160. package/modern/components/option/mobile/index.module.css.js.map +1 -1
  161. package/modern/components/options-list/Component.d.ts +4 -0
  162. package/modern/components/options-list/Component.js +29 -49
  163. package/modern/components/options-list/Component.js.map +1 -1
  164. package/modern/components/options-list/index.css +13 -33
  165. package/modern/components/options-list/index.module.css.js +1 -1
  166. package/modern/components/options-list/index.module.css.js.map +1 -1
  167. package/modern/components/search/index.css +1 -1
  168. package/modern/components/search/index.module.css.js +1 -1
  169. package/modern/components/virtual-options-list/Component.d.ts +4 -0
  170. package/modern/components/virtual-options-list/Component.js +59 -45
  171. package/modern/components/virtual-options-list/Component.js.map +1 -1
  172. package/modern/mobile/modal.mobile.css +1 -1
  173. package/modern/mobile/modal.mobile.module.css.js +1 -1
  174. package/modern/presets/useSelectWithApply/hook.d.ts +12 -0
  175. package/modern/presets/useSelectWithApply/options-list-with-apply/Component.d.ts +4 -0
  176. package/modern/presets/useSelectWithApply/options-list-with-apply/footer/index.css +2 -7
  177. package/modern/presets/useSelectWithApply/options-list-with-apply/footer/index.module.css.js +1 -1
  178. package/modern/presets/useSelectWithApply/options-list-with-apply/footer/index.module.css.js.map +1 -1
  179. package/modern/presets/useSelectWithApply/options-list-with-apply/header/index.css +12 -9
  180. package/modern/presets/useSelectWithApply/options-list-with-apply/header/index.module.css.js +1 -1
  181. package/modern/presets/useSelectWithApply/options-list-with-apply/header/index.module.css.js.map +1 -1
  182. package/modern/presets/useSelectWithLoading/index.css +1 -1
  183. package/modern/presets/useSelectWithLoading/index.module.css.js +1 -1
  184. package/modern/shared/index.js +1 -1
  185. package/modern/typings.d.ts +21 -1
  186. package/modern/utils.d.ts +1 -6
  187. package/modern/utils.js +21 -29
  188. package/modern/utils.js.map +1 -1
  189. package/moderncssm/components/base-select/Component.js +34 -47
  190. package/moderncssm/components/base-select/Component.js.map +1 -1
  191. package/moderncssm/components/base-select/index.module.css +67 -9
  192. package/moderncssm/components/base-select/mobile.module.css +1 -0
  193. package/moderncssm/components/option/desktop/index.module.css +3 -0
  194. package/moderncssm/components/option/mobile/index.module.css +3 -0
  195. package/moderncssm/components/options-list/Component.d.ts +4 -0
  196. package/moderncssm/components/options-list/Component.js +29 -49
  197. package/moderncssm/components/options-list/Component.js.map +1 -1
  198. package/moderncssm/components/options-list/index.module.css +7 -32
  199. package/moderncssm/components/virtual-options-list/Component.d.ts +4 -0
  200. package/moderncssm/components/virtual-options-list/Component.js +59 -45
  201. package/moderncssm/components/virtual-options-list/Component.js.map +1 -1
  202. package/moderncssm/presets/useSelectWithApply/hook.d.ts +12 -0
  203. package/moderncssm/presets/useSelectWithApply/options-list-with-apply/Component.d.ts +4 -0
  204. package/moderncssm/presets/useSelectWithApply/options-list-with-apply/footer/index.module.css +0 -4
  205. package/moderncssm/presets/useSelectWithApply/options-list-with-apply/header/index.module.css +8 -6
  206. package/moderncssm/shared/index.js +1 -1
  207. package/moderncssm/typings.d.ts +21 -1
  208. package/moderncssm/utils.d.ts +1 -6
  209. package/moderncssm/utils.js +21 -29
  210. package/moderncssm/utils.js.map +1 -1
  211. package/package.json +13 -14
  212. package/presets/useSelectWithApply/hook.d.ts +12 -0
  213. package/presets/useSelectWithApply/options-list-with-apply/Component.d.ts +4 -0
  214. package/presets/useSelectWithApply/options-list-with-apply/footer/index.css +2 -7
  215. package/presets/useSelectWithApply/options-list-with-apply/footer/index.module.css.js +1 -1
  216. package/presets/useSelectWithApply/options-list-with-apply/footer/index.module.css.js.map +1 -1
  217. package/presets/useSelectWithApply/options-list-with-apply/header/index.css +12 -9
  218. package/presets/useSelectWithApply/options-list-with-apply/header/index.module.css.js +1 -1
  219. package/presets/useSelectWithApply/options-list-with-apply/header/index.module.css.js.map +1 -1
  220. package/presets/useSelectWithLoading/index.css +1 -1
  221. package/presets/useSelectWithLoading/index.module.css.js +1 -1
  222. package/shared/index.js +0 -1
  223. package/shared/index.js.map +1 -1
  224. package/src/components/base-select/Component.tsx +41 -55
  225. package/src/components/base-select/index.module.css +60 -8
  226. package/src/components/base-select/mobile.module.css +1 -0
  227. package/src/components/option/index.module.css +4 -0
  228. package/src/components/options-list/Component.tsx +54 -97
  229. package/src/components/options-list/index.module.css +7 -31
  230. package/src/components/virtual-options-list/Component.tsx +95 -72
  231. package/src/presets/useSelectWithApply/options-list-with-apply/footer/index.module.css +0 -4
  232. package/src/presets/useSelectWithApply/options-list-with-apply/header/index.module.css +4 -4
  233. package/src/typings.ts +26 -0
  234. package/src/utils.ts +30 -34
  235. package/src/vars.css +3 -0
  236. package/typings.d.ts +21 -1
  237. package/utils.d.ts +1 -6
  238. package/utils.js +19 -28
  239. package/utils.js.map +1 -1
  240. package/components/virtual-options-list/index.css +0 -83
  241. package/components/virtual-options-list/index.module.css.js +0 -8
  242. package/components/virtual-options-list/index.module.css.js.map +0 -1
  243. package/cssm/components/virtual-options-list/index.module.css +0 -83
  244. package/esm/components/virtual-options-list/index.css +0 -83
  245. package/esm/components/virtual-options-list/index.module.css.js +0 -6
  246. package/esm/components/virtual-options-list/index.module.css.js.map +0 -1
  247. package/modern/components/virtual-options-list/index.css +0 -83
  248. package/modern/components/virtual-options-list/index.module.css.js +0 -6
  249. package/modern/components/virtual-options-list/index.module.css.js.map +0 -1
  250. package/moderncssm/components/virtual-options-list/index.module.css +0 -81
  251. package/src/components/virtual-options-list/index.module.css +0 -77
@@ -1,8 +1,8 @@
1
- import React, { forwardRef, useCallback, useRef, useState } from 'react';
1
+ import React, { forwardRef, useRef, useState } from 'react';
2
2
  import mergeRefs from 'react-merge-refs';
3
3
  import cn from 'classnames';
4
4
 
5
- import { Scrollbar } from '@alfalab/core-components-scrollbar';
5
+ import { ScrollbarPrivate } from '@alfalab/core-components-scrollbar-private';
6
6
 
7
7
  import { DEFAULT_VISIBLE_OPTIONS } from '../../consts';
8
8
  import { useNativeScrollbar } from '../../hooks/use-native-scrollbar';
@@ -40,7 +40,6 @@ export const OptionsList = forwardRef<HTMLDivElement, OptionsListProps>(
40
40
  header,
41
41
  footer,
42
42
  showFooter = true,
43
- optionsListWidth,
44
43
  nativeScrollbar: nativeScrollbarProp,
45
44
  flatOptions = [],
46
45
  setHighlightedIndex,
@@ -50,9 +49,24 @@ export const OptionsList = forwardRef<HTMLDivElement, OptionsListProps>(
50
49
  multiple,
51
50
  limitDynamicOptionGroupSize = false,
52
51
  client,
52
+ scrollableNodeClassName,
53
+ contentNodeClassName,
54
+ listNodeClassName,
53
55
  },
54
56
  ref,
55
57
  ) => {
58
+ const actualOptionsCount = limitDynamicOptionGroupSize && options.length > 0;
59
+ const listRef = useRef<HTMLDivElement>(null);
60
+ const scrollableNodeRef = useRef<HTMLDivElement>(null);
61
+ const [, maxHeight] = useVisibleOptions({
62
+ visibleOptions,
63
+ listRef,
64
+ open,
65
+ options,
66
+ actualOptionsCount,
67
+ size: actualOptionsCount ? size : undefined,
68
+ });
69
+ const noOptions = options.length === 0;
56
70
  const [scrollTop, setScrollTop] = useState(true);
57
71
  const [scrollBottom, setScrollBottom] = useState(false);
58
72
 
@@ -61,27 +75,22 @@ export const OptionsList = forwardRef<HTMLDivElement, OptionsListProps>(
61
75
  client,
62
76
  });
63
77
 
64
- const handleScroll = useCallback(
65
- (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
66
- const scrolledToHeader = event.currentTarget.scrollTop <= 0;
67
- const scrolledToFooter =
68
- event.currentTarget.scrollHeight - event.currentTarget.offsetHeight <=
69
- event.currentTarget.scrollTop;
78
+ const handleScroll: React.MouseEventHandler<HTMLDivElement> = (event) => {
79
+ const scrolledToHeader = event.currentTarget.scrollTop <= 0;
80
+ const scrolledToFooter =
81
+ event.currentTarget.scrollHeight - event.currentTarget.offsetHeight <=
82
+ event.currentTarget.scrollTop;
70
83
 
71
- setScrollTop(scrolledToHeader);
72
- setScrollBottom(scrolledToFooter);
84
+ setScrollTop(scrolledToHeader);
85
+ setScrollBottom(scrolledToFooter);
73
86
 
74
- onScroll?.(event);
75
- },
76
- [onScroll],
77
- );
87
+ onScroll?.(event);
88
+ };
78
89
 
79
90
  const renderOption = (option: OptionShape, index: number) => (
80
91
  <Option key={option.key} {...getOptionProps(option, index)} />
81
92
  );
82
93
 
83
- const listRef = useRef<HTMLDivElement>(null);
84
- const scrollbarRef = useRef<HTMLDivElement>(null);
85
94
  const counter = createCounter();
86
95
  const renderGroup = (group: GroupShape) => {
87
96
  const groupSelectedItems = selectedItems?.filter(({ key: selectedItemKey }) =>
@@ -116,89 +125,10 @@ export const OptionsList = forwardRef<HTMLDivElement, OptionsListProps>(
116
125
  );
117
126
  };
118
127
 
119
- const actualOptionsCount = limitDynamicOptionGroupSize && options.length > 0;
120
-
121
- const [measured, maxHeight] = useVisibleOptions({
122
- visibleOptions,
123
- listRef,
124
- open,
125
- options,
126
- actualOptionsCount,
127
- size: actualOptionsCount
128
- ? (() => {
129
- switch (typeof size) {
130
- case 'string':
131
- throw new Error(
132
- 'OptionsList with `limitDynamicOptionGroupSize` enabled needs a `size` with number type',
133
- );
134
- default:
135
- return size;
136
- }
137
- })()
138
- : undefined,
139
- });
140
-
141
128
  if (options.length === 0 && !emptyPlaceholder && !header && !footer) {
142
129
  return null;
143
130
  }
144
131
 
145
- const renderListItems = () => (
146
- <React.Fragment>
147
- {options.map((option) =>
148
- isGroup(option) ? renderGroup(option) : renderOption(option, counter()),
149
- )}
150
-
151
- {emptyPlaceholder && options.length === 0 && (
152
- <div className={styles.emptyPlaceholder}>{emptyPlaceholder}</div>
153
- )}
154
- </React.Fragment>
155
- );
156
-
157
- const renderWithCustomScrollbar = () => {
158
- const scrollableNodeProps = {
159
- onScroll: handleScroll,
160
- 'data-test-id': dataTestId,
161
- ref: ref as React.RefObject<HTMLDivElement>,
162
- };
163
-
164
- return (
165
- <Scrollbar
166
- className={cn(styles.scrollable, scrollbarClassName)}
167
- verticalBarClassName={cn(styles.verticalBar, {
168
- [styles.verticalBarWithHeader]: Boolean(header),
169
- [styles.verticalBarWithFooter]: Boolean(footer),
170
- })}
171
- ref={scrollbarRef}
172
- style={{ maxHeight }}
173
- horizontalAutoStretch={optionsListWidth === 'content'}
174
- scrollableNodeProps={scrollableNodeProps}
175
- contentNodeProps={{ ref: listRef }}
176
- maskProps={{
177
- /*
178
- * Для корректного подсчета высоты опций(иначе для optionsListWidth: 'field'
179
- * высота опции всегда будет равна высоте одной строчки)
180
- */
181
- className: cn({
182
- [styles.mask]: optionsListWidth === 'content' && !measured,
183
- }),
184
- }}
185
- >
186
- {renderListItems()}
187
- </Scrollbar>
188
- );
189
- };
190
-
191
- const renderWithNativeScrollbar = () => (
192
- <div
193
- className={cn(styles.scrollable, scrollbarClassName)}
194
- ref={mergeRefs([listRef, ref])}
195
- onScroll={handleScroll}
196
- style={{ maxHeight }}
197
- >
198
- {renderListItems()}
199
- </div>
200
- );
201
-
202
132
  const resetHighlightedIndex = () => setHighlightedIndex?.(-1);
203
133
 
204
134
  return (
@@ -217,7 +147,34 @@ export const OptionsList = forwardRef<HTMLDivElement, OptionsListProps>(
217
147
  </div>
218
148
  )}
219
149
 
220
- {nativeScrollbar ? renderWithNativeScrollbar() : renderWithCustomScrollbar()}
150
+ {!noOptions && (
151
+ <ScrollbarPrivate
152
+ native={nativeScrollbar}
153
+ className={scrollbarClassName}
154
+ style={{ maxHeight }}
155
+ scrollableNodeProps={{
156
+ ref: mergeRefs([scrollableNodeRef, ref]),
157
+ onScroll: handleScroll,
158
+ className: scrollableNodeClassName,
159
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
160
+ // @ts-expect-error
161
+ 'data-test-id': nativeScrollbar ? undefined : dataTestId,
162
+ }}
163
+ contentNodeProps={{ className: contentNodeClassName }}
164
+ >
165
+ <div className={listNodeClassName} ref={listRef}>
166
+ {options.map((option) =>
167
+ isGroup(option)
168
+ ? renderGroup(option)
169
+ : renderOption(option, counter()),
170
+ )}
171
+ </div>
172
+ </ScrollbarPrivate>
173
+ )}
174
+
175
+ {emptyPlaceholder && noOptions && (
176
+ <div className={styles.emptyPlaceholder}>{emptyPlaceholder}</div>
177
+ )}
221
178
 
222
179
  {showFooter && footer && (
223
180
  <div
@@ -6,37 +6,11 @@
6
6
  outline: none;
7
7
  box-sizing: border-box;
8
8
  position: relative;
9
-
10
- & .scrollable {
11
- overflow: auto;
12
- width: 100%;
13
-
14
- & .verticalBar.verticalBar {
15
- top: var(--select-options-list-scrollbar-gap);
16
- height: calc(100% - 2 * var(--select-options-list-scrollbar-gap));
17
-
18
- &.verticalBarWithHeader {
19
- top: 0;
20
- height: calc(100% - var(--select-options-list-scrollbar-gap));
21
- }
22
-
23
- &.verticalBarWithFooter {
24
- top: var(--select-options-list-scrollbar-gap);
25
- height: calc(100% - var(--select-options-list-scrollbar-gap));
26
- }
27
-
28
- &.verticalBarWithHeader.verticalBarWithFooter {
29
- top: 0;
30
- height: 100%;
31
- }
32
- }
33
- }
34
9
  }
35
10
 
36
11
  .optionsListHeader {
37
12
  box-sizing: border-box;
38
13
  border-bottom: 1px solid transparent;
39
- transition: border-color 0.2s ease;
40
14
 
41
15
  &.headerHighlighted {
42
16
  border-bottom-color: var(--color-light-neutral-300);
@@ -48,22 +22,24 @@
48
22
  bottom: var(--gap-0);
49
23
  box-sizing: border-box;
50
24
  border-top: 1px solid transparent;
51
- transition: border-color 0.2s ease;
52
25
 
53
26
  &.withBorder {
54
27
  border-top-color: var(--color-light-neutral-300);
55
28
  }
56
29
  }
57
30
 
58
- .mask {
59
- min-width: 4000px;
60
- }
61
-
62
31
  .emptyPlaceholder {
63
32
  padding: var(--gap-16) var(--gap-12);
64
33
  color: var(--select-options-list-empty-placeholder-color);
34
+ flex: 1; /* needed in mobile */
65
35
  }
66
36
 
37
+ .size-64 .emptyPlaceholder,
67
38
  .size-72 .emptyPlaceholder {
68
39
  padding: var(--gap-24) var(--gap-16);
69
40
  }
41
+
42
+ .list {
43
+ will-change: transform;
44
+ height: auto;
45
+ }
@@ -1,17 +1,24 @@
1
- import React, { forwardRef, useEffect, useMemo, useRef, useState } from 'react';
2
- import mergeRefs from 'react-merge-refs';
1
+ import React, {
2
+ forwardRef,
3
+ useEffect,
4
+ useImperativeHandle,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ } from 'react';
3
9
  import { useVirtual } from 'react-virtual';
4
10
  import cn from 'classnames';
5
11
 
6
- import { Scrollbar } from '@alfalab/core-components-scrollbar';
12
+ import { ScrollbarPrivate } from '@alfalab/core-components-scrollbar-private';
13
+ import { getElementWindow } from '@alfalab/core-components-shared';
7
14
 
8
15
  import { DEFAULT_VISIBLE_OPTIONS } from '../../consts';
9
16
  import { useNativeScrollbar } from '../../hooks/use-native-scrollbar';
10
17
  import { type GroupShape, type OptionShape, type OptionsListProps } from '../../typings';
11
- import { isGroup, lastIndexOf, usePrevious, useVirtualVisibleOptions } from '../../utils';
18
+ import { isGroup, usePrevious, useVirtualVisibleOptions } from '../../utils';
12
19
  import { Optgroup as DefaultOptgroup } from '../optgroup';
13
20
 
14
- import styles from './index.module.css';
21
+ import styles from '../options-list/index.module.css';
15
22
 
16
23
  export const VirtualOptionsList = forwardRef<HTMLDivElement, OptionsListProps>(
17
24
  (
@@ -33,7 +40,6 @@ export const VirtualOptionsList = forwardRef<HTMLDivElement, OptionsListProps>(
33
40
  header,
34
41
  footer,
35
42
  showFooter = true,
36
- optionsListWidth,
37
43
  onScroll,
38
44
  nativeScrollbar: nativeScrollbarProp,
39
45
  setHighlightedIndex,
@@ -43,18 +49,43 @@ export const VirtualOptionsList = forwardRef<HTMLDivElement, OptionsListProps>(
43
49
  multiple,
44
50
  scrollbarClassName,
45
51
  client,
52
+ scrollableNodeClassName,
53
+ contentNodeClassName,
54
+ listNodeClassName,
55
+ footerClassName,
56
+ ctrlRef,
46
57
  },
47
58
  ref,
48
59
  ) => {
49
60
  const listRef = useRef<HTMLDivElement>(null);
50
- const parentRef = useRef<HTMLDivElement>(null);
51
- const scrollbarRef = useRef<HTMLDivElement>(null);
61
+ const scrollableNodeRef = useRef<HTMLDivElement>(null);
62
+ const noOptions = options.length === 0;
63
+ const [scrollTop, setScrollTop] = useState(true);
64
+ const [scrollBottom, setScrollBottom] = useState(false);
52
65
  const [visibleOptionsInvalidateKey, setVisibleOptionsInvalidateKey] = useState('');
53
66
  const prevHighlightedIndex = usePrevious(highlightedIndex) || -1;
54
67
 
55
68
  const rowVirtualizer = useVirtual({
69
+ scrollToFn(offset, defaultScrollToFn) {
70
+ let delta = 0;
71
+ const list = listRef.current;
72
+
73
+ if (list) {
74
+ const win = getElementWindow(list);
75
+
76
+ delta = ['::before', '::after']
77
+ .map((pseudo) => {
78
+ const { paddingTop } = win.getComputedStyle(list, pseudo);
79
+
80
+ return parseFloat(paddingTop) || 0;
81
+ })
82
+ .reduce((a, b) => a + b);
83
+ }
84
+
85
+ defaultScrollToFn?.(offset === 0 ? offset : offset + delta);
86
+ },
56
87
  size: flatOptions.length,
57
- parentRef: (ref || parentRef) as React.RefObject<HTMLDivElement>,
88
+ parentRef: (ref as React.RefObject<HTMLDivElement>) || scrollableNodeRef,
58
89
  overscan: 15,
59
90
  });
60
91
 
@@ -71,21 +102,13 @@ export const VirtualOptionsList = forwardRef<HTMLDivElement, OptionsListProps>(
71
102
  // eslint-disable-next-line react-hooks/exhaustive-deps
72
103
  }, [open]);
73
104
 
74
- // Скролл к пункту, которого нет на экране
75
- useEffect(() => {
76
- if (highlightedIndex === -1) return;
77
-
78
- if (!rowVirtualizer.virtualItems.some((option) => option.index === highlightedIndex)) {
79
- rowVirtualizer.scrollToIndex(highlightedIndex, { align: 'end' });
80
- }
81
- // eslint-disable-next-line react-hooks/exhaustive-deps
82
- }, [highlightedIndex]);
105
+ useImperativeHandle(ctrlRef, () => rowVirtualizer, [rowVirtualizer]);
83
106
 
84
107
  // Циклическая навигация
85
108
  useEffect(() => {
86
109
  const notDisabled = (option: OptionShape) => !option.disabled;
87
110
  const firstNonDisabled = flatOptions.findIndex(notDisabled);
88
- const lastNonDisabled = lastIndexOf(flatOptions, notDisabled);
111
+ const lastNonDisabled = flatOptions.findLastIndex(notDisabled);
89
112
 
90
113
  if (
91
114
  prevHighlightedIndex <= firstNonDisabled &&
@@ -115,11 +138,10 @@ export const VirtualOptionsList = forwardRef<HTMLDivElement, OptionsListProps>(
115
138
  // eslint-disable-next-line react-hooks/exhaustive-deps
116
139
  }, [rowVirtualizer.virtualItems.length, flatOptions]);
117
140
 
118
- useVirtualVisibleOptions({
141
+ const maxHeight = useVirtualVisibleOptions({
119
142
  visibleOptions,
120
143
  invalidate: visibleOptionsInvalidateKey,
121
144
  listRef,
122
- styleTargetRef: nativeScrollbar ? parentRef : scrollbarRef,
123
145
  open,
124
146
  });
125
147
 
@@ -139,6 +161,18 @@ export const VirtualOptionsList = forwardRef<HTMLDivElement, OptionsListProps>(
139
161
  }, {});
140
162
  }, [options]);
141
163
 
164
+ const handleScroll: React.MouseEventHandler<HTMLDivElement> = (event) => {
165
+ const scrolledToHeader = event.currentTarget.scrollTop <= 0;
166
+ const scrolledToFooter =
167
+ event.currentTarget.scrollHeight - event.currentTarget.offsetHeight <=
168
+ event.currentTarget.scrollTop;
169
+
170
+ setScrollTop(scrolledToHeader);
171
+ setScrollBottom(scrolledToFooter);
172
+
173
+ onScroll?.(event);
174
+ };
175
+
142
176
  const renderList = () =>
143
177
  rowVirtualizer.virtualItems.map((virtualRow) => {
144
178
  const option = flatOptions[virtualRow.index];
@@ -177,16 +211,7 @@ export const VirtualOptionsList = forwardRef<HTMLDivElement, OptionsListProps>(
177
211
  };
178
212
 
179
213
  return (
180
- <div
181
- key={virtualRow.index}
182
- ref={virtualRow.measureRef}
183
- className={cn(styles.virtualRow, {
184
- [styles.highlighted]: highlightedIndex === virtualRow.index,
185
- })}
186
- style={{
187
- transform: `translateY(${virtualRow.start}px)`,
188
- }}
189
- >
214
+ <div key={virtualRow.index} ref={virtualRow.measureRef}>
190
215
  {renderGroup()}
191
216
  {!isGroup(option) && (
192
217
  <Option {...getOptionProps(option, virtualRow.index)} />
@@ -195,72 +220,70 @@ export const VirtualOptionsList = forwardRef<HTMLDivElement, OptionsListProps>(
195
220
  );
196
221
  });
197
222
 
198
- const contentNodeProps = {
199
- className: styles.inner,
200
- style: { height: `${rowVirtualizer.totalSize}px` },
201
- ref: listRef,
202
- };
203
-
204
- const renderWithCustomScrollbar = () => (
205
- <Scrollbar
206
- className={cn(styles.scrollable, scrollbarClassName)}
207
- ref={scrollbarRef}
208
- horizontalAutoStretch={optionsListWidth === 'content'}
209
- scrollableNodeProps={{ onScroll, ref: parentRef }}
210
- contentNodeProps={contentNodeProps}
211
- >
212
- {renderList()}
213
- </Scrollbar>
214
- );
215
-
216
- const renderWithNativeScrollbar = () => {
217
- if (visibleOptions) {
218
- return (
219
- <div
220
- className={cn(styles.scrollable, scrollbarClassName)}
221
- ref={mergeRefs([parentRef, ref])}
222
- onScroll={onScroll}
223
- >
224
- <div {...contentNodeProps}>{renderList()}</div>
225
- </div>
226
- );
227
- }
228
-
229
- return <div {...contentNodeProps}>{renderList()}</div>;
230
- };
231
-
232
223
  const resetHighlightedIndex = () => setHighlightedIndex?.(-1);
233
224
 
234
225
  if (options.length === 0 && !emptyPlaceholder) {
235
226
  return null;
236
227
  }
237
228
 
229
+ const [firstVirualRow] = rowVirtualizer.virtualItems;
230
+
238
231
  return (
239
232
  <div
240
- className={cn(styles.virtualOptionsList, styles[`size-${size}`], className)}
241
- data-test-id={dataTestId}
233
+ {...(nativeScrollbar && { 'data-test-id': dataTestId })}
234
+ className={cn(styles.optionsList, styles[`size-${size}`], className)}
242
235
  >
243
236
  {header && (
244
237
  <div
245
- className={styles.virtualOptionsListHeader}
238
+ className={cn(styles.optionsListHeader, {
239
+ [styles.headerHighlighted]: !scrollTop,
240
+ })}
246
241
  onMouseEnter={resetHighlightedIndex}
247
242
  >
248
243
  {header}
249
244
  </div>
250
245
  )}
251
246
 
252
- {nativeScrollbar ? renderWithNativeScrollbar() : renderWithCustomScrollbar()}
247
+ {!noOptions && (
248
+ <ScrollbarPrivate
249
+ native={nativeScrollbar}
250
+ className={scrollbarClassName}
251
+ style={{ maxHeight }}
252
+ scrollableNodeProps={{
253
+ ref: scrollableNodeRef,
254
+ onScroll: handleScroll,
255
+ className: scrollableNodeClassName,
256
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
257
+ // @ts-expect-error
258
+ 'data-test-id': nativeScrollbar ? undefined : dataTestId,
259
+ }}
260
+ contentNodeProps={{
261
+ className: contentNodeClassName,
262
+ style: { minHeight: rowVirtualizer.totalSize },
263
+ }}
264
+ >
265
+ <div
266
+ className={cn(styles.list, listNodeClassName)}
267
+ ref={listRef}
268
+ style={{ transform: `translateY(${firstVirualRow?.start ?? 0}px)` }}
269
+ >
270
+ {renderList()}
271
+ </div>
272
+ </ScrollbarPrivate>
273
+ )}
253
274
 
254
- {emptyPlaceholder && options.length === 0 && (
275
+ {emptyPlaceholder && noOptions && (
255
276
  <div className={styles.emptyPlaceholder}>{emptyPlaceholder}</div>
256
277
  )}
257
278
 
258
279
  {showFooter && footer && (
259
280
  <div
260
281
  onMouseEnter={resetHighlightedIndex}
261
- className={cn(styles.virtualOptionsListFooter, {
282
+ className={cn(styles.optionsListFooter, footerClassName, {
262
283
  [styles.withBorder]:
263
- visibleOptions && flatOptions.length > visibleOptions,
284
+ visibleOptions &&
285
+ flatOptions.length > visibleOptions &&
286
+ !scrollBottom,
264
287
  })}
265
288
  >
266
289
  {footer}
@@ -6,10 +6,6 @@
6
6
  padding: var(--gap-12);
7
7
  outline: none;
8
8
 
9
- &.size-72 {
10
- padding: var(--gap-12) var(--gap-16);
11
- }
12
-
13
9
  & > .button + .button {
14
10
  margin-left: var(--select-option-list-footer-button-gap);
15
11
  }
@@ -1,10 +1,10 @@
1
1
  @import '@alfalab/core-components-vars/src/index.css';
2
+ @import '../../../../vars.css';
2
3
 
3
4
  .desktop {
4
- padding: var(--gap-12);
5
- &.size-72 {
6
- padding-left: var(--gap-16);
7
- }
5
+ padding: calc(var(--gap-12) + var(--select-options-list-top-padding))
6
+ calc(var(--gap-12) + var(--select-options-list-right-padding)) var(--gap-12)
7
+ calc(var(--gap-12) + var(--select-options-list-left-padding));
8
8
  }
9
9
 
10
10
  .mobile {
package/src/typings.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  type MouseEvent,
7
7
  type ReactElement,
8
8
  type ReactNode,
9
+ type Ref,
9
10
  type RefAttributes,
10
11
  type SVGProps,
11
12
  } from 'react';
@@ -155,6 +156,7 @@ export interface BaseSelectProps {
155
156
 
156
157
  /**
157
158
  * Размер пунктов меню
159
+ * @deprecated Управление размером пунктов меню через отдельный проп не поддерживается дизайн-контрактом. Размер пункта должен определяться его контентом.
158
160
  */
159
161
  optionsSize?: 40 | 48 | 56 | 64 | 72;
160
162
 
@@ -691,6 +693,26 @@ export type OptionsListProps = Pick<SelectProps, 'client'> & {
691
693
  * Ограничение динамического размера группы вариантов выбора
692
694
  */
693
695
  limitDynamicOptionGroupSize?: BaseSelectProps['limitDynamicOptionGroupSize'];
696
+
697
+ /**
698
+ *
699
+ */
700
+ scrollableNodeClassName?: string;
701
+
702
+ /**
703
+ *
704
+ */
705
+ contentNodeClassName?: string;
706
+
707
+ /**
708
+ *
709
+ */
710
+ listNodeClassName?: string;
711
+
712
+ /**
713
+ *
714
+ */
715
+ ctrlRef?: Ref<OptionsListController>;
694
716
  };
695
717
 
696
718
  export type OptgroupProps = {
@@ -1003,3 +1025,7 @@ export type ClearButtonProps = {
1003
1025
  */
1004
1026
  size?: FormControlProps['size'];
1005
1027
  };
1028
+
1029
+ export interface OptionsListController {
1030
+ scrollToIndex(index: number): void;
1031
+ }