@musecat/uikit 0.1.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 (180) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +51 -0
  3. package/index.ts +78 -0
  4. package/package.json +103 -0
  5. package/packages/Components/Box/Box.tsx +173 -0
  6. package/packages/Components/Box/Box.types.ts +48 -0
  7. package/packages/Components/Button/Button.tsx +275 -0
  8. package/packages/Components/Button/Button.types.ts +44 -0
  9. package/packages/Components/Card/Card.default.tsx +180 -0
  10. package/packages/Components/Card/Card.foldable.tsx +383 -0
  11. package/packages/Components/Card/Card.module.scss +23 -0
  12. package/packages/Components/Card/Card.tsx +8 -0
  13. package/packages/Components/Card/Card.types.ts +91 -0
  14. package/packages/Components/Card/Card.utils.ts +66 -0
  15. package/packages/Components/Checkbox/Checkbox.module.scss +11 -0
  16. package/packages/Components/Checkbox/Checkbox.tsx +104 -0
  17. package/packages/Components/Checkbox/Checkbox.types.ts +24 -0
  18. package/packages/Components/CodeBox/CodeBox.copy.tsx +43 -0
  19. package/packages/Components/CodeBox/CodeBox.module.scss +72 -0
  20. package/packages/Components/CodeBox/CodeBox.tsx +91 -0
  21. package/packages/Components/CodeBox/CodeBox.types.ts +17 -0
  22. package/packages/Components/ContextMenu/ContextMenu.module.scss +8 -0
  23. package/packages/Components/ContextMenu/ContextMenu.options.tsx +126 -0
  24. package/packages/Components/ContextMenu/ContextMenu.tsx +381 -0
  25. package/packages/Components/ContextMenu/ContextMenu.types.ts +46 -0
  26. package/packages/Components/ContributionGraph/ContributionGraph.module.scss +42 -0
  27. package/packages/Components/ContributionGraph/ContributionGraph.tsx +454 -0
  28. package/packages/Components/ContributionGraph/ContributionGraph.types.ts +27 -0
  29. package/packages/Components/DatePicker/DatePicker.calendar.tsx +161 -0
  30. package/packages/Components/DatePicker/DatePicker.core.tsx +582 -0
  31. package/packages/Components/DatePicker/DatePicker.module.scss +26 -0
  32. package/packages/Components/DatePicker/DatePicker.tsx +786 -0
  33. package/packages/Components/DatePicker/DatePicker.types.ts +36 -0
  34. package/packages/Components/DatePicker/DatePicker.utils.ts +80 -0
  35. package/packages/Components/DatePicker/hooks/useCalendar.ts +61 -0
  36. package/packages/Components/DatePicker/hooks/useSelection.ts +208 -0
  37. package/packages/Components/Divider/Divider.tsx +46 -0
  38. package/packages/Components/Divider/Divider.types.ts +12 -0
  39. package/packages/Components/Header/Header.module.scss +74 -0
  40. package/packages/Components/Header/Header.tsx +47 -0
  41. package/packages/Components/Header/Header.types.ts +8 -0
  42. package/packages/Components/Icon/Icon.group.tsx +122 -0
  43. package/packages/Components/Icon/Icon.group.types.ts +20 -0
  44. package/packages/Components/Icon/Icon.tsx +341 -0
  45. package/packages/Components/Icon/Icon.types.ts +59 -0
  46. package/packages/Components/Icon/SVG/data.tsx +232 -0
  47. package/packages/Components/Input/Input.module.scss +5 -0
  48. package/packages/Components/Input/Input.tsx +87 -0
  49. package/packages/Components/Input/Input.types.ts +14 -0
  50. package/packages/Components/Label/Label.module.scss +19 -0
  51. package/packages/Components/Label/Label.tsx +115 -0
  52. package/packages/Components/Label/Label.types.ts +28 -0
  53. package/packages/Components/Layout/Layout.docs.module.scss +42 -0
  54. package/packages/Components/Layout/Layout.docs.tsx +150 -0
  55. package/packages/Components/Layout/Layout.docs.types.ts +18 -0
  56. package/packages/Components/Layout/Layout.module.scss +91 -0
  57. package/packages/Components/Layout/Layout.tsx +306 -0
  58. package/packages/Components/Layout/Layout.types.ts +71 -0
  59. package/packages/Components/Maps/OSM/MapOSM.module.scss +11 -0
  60. package/packages/Components/Maps/OSM/MapOSM.shared.ts +121 -0
  61. package/packages/Components/Maps/OSM/MapOSM.tsx +279 -0
  62. package/packages/Components/Maps/OSM/MapOSM.types.ts +18 -0
  63. package/packages/Components/Nav/Nav.module.scss +137 -0
  64. package/packages/Components/Nav/Nav.tsx +304 -0
  65. package/packages/Components/Nav/Nav.types.ts +32 -0
  66. package/packages/Components/Nav/hooks/useNavDrag.ts +374 -0
  67. package/packages/Components/Nav/hooks/useNavIndicator.ts +137 -0
  68. package/packages/Components/Pagination/Pagination.tsx +254 -0
  69. package/packages/Components/Pagination/Pagination.types.ts +18 -0
  70. package/packages/Components/Pill/Pill.tsx +127 -0
  71. package/packages/Components/Pill/Pill.types.ts +33 -0
  72. package/packages/Components/Profile/Profile.shared.ts +74 -0
  73. package/packages/Components/Profile/Profile.tsx +382 -0
  74. package/packages/Components/Profile/Profile.types.ts +55 -0
  75. package/packages/Components/Progress/Progress.module.scss +15 -0
  76. package/packages/Components/Progress/Progress.tsx +88 -0
  77. package/packages/Components/Progress/Progress.types.ts +22 -0
  78. package/packages/Components/Radio/Radio.module.scss +11 -0
  79. package/packages/Components/Radio/Radio.tsx +121 -0
  80. package/packages/Components/Radio/Radio.types.ts +20 -0
  81. package/packages/Components/Select/Select.keyboard.tsx +76 -0
  82. package/packages/Components/Select/Select.trigger.tsx +88 -0
  83. package/packages/Components/Select/Select.tsx +11 -0
  84. package/packages/Components/Select/Select.types.ts +38 -0
  85. package/packages/Components/Select/Select.utils.ts +84 -0
  86. package/packages/Components/Select/hooks/useSelectNavigation.ts +461 -0
  87. package/packages/Components/Select/hooks/useSelectSelection.ts +142 -0
  88. package/packages/Components/Select/hooks/useSelectState.ts +314 -0
  89. package/packages/Components/Select/select.control.tsx +228 -0
  90. package/packages/Components/Select/select.inner.tsx +312 -0
  91. package/packages/Components/Skeleton/Skeleton.tsx +30 -0
  92. package/packages/Components/Skeleton/Skeleton.types.ts +12 -0
  93. package/packages/Components/Spinner/Spinner.module.scss +78 -0
  94. package/packages/Components/Spinner/Spinner.tsx +125 -0
  95. package/packages/Components/Spinner/Spinner.types.ts +19 -0
  96. package/packages/Components/Text/Text.module.scss +7 -0
  97. package/packages/Components/Text/Text.tsx +49 -0
  98. package/packages/Components/Text/Text.types.ts +33 -0
  99. package/packages/Components/TimePicker/TimePicker.core.tsx +448 -0
  100. package/packages/Components/TimePicker/TimePicker.module.scss +30 -0
  101. package/packages/Components/TimePicker/TimePicker.tsx +511 -0
  102. package/packages/Components/TimePicker/TimePicker.types.ts +22 -0
  103. package/packages/Components/Timeline/Timeline.tsx +111 -0
  104. package/packages/Components/Timeline/Timeline.types.ts +23 -0
  105. package/packages/Components/Title/Title.tsx +305 -0
  106. package/packages/Components/Title/Title.types.ts +64 -0
  107. package/packages/Components/Toggle/Toggle.module.scss +32 -0
  108. package/packages/Components/Toggle/Toggle.tsx +272 -0
  109. package/packages/Components/Toggle/Toggle.types.ts +17 -0
  110. package/packages/Components/Tooltip/Tooltip.module.scss +44 -0
  111. package/packages/Components/Tooltip/Tooltip.tsx +340 -0
  112. package/packages/Components/Tooltip/Tooltip.types.ts +38 -0
  113. package/packages/Frameworks/Dialog/Dialog.background.tsx +88 -0
  114. package/packages/Frameworks/Dialog/Dialog.boot.tsx +32 -0
  115. package/packages/Frameworks/Dialog/Dialog.store.ts +199 -0
  116. package/packages/Frameworks/Dialog/Dialog.tsx +185 -0
  117. package/packages/Frameworks/Dialog/Dialog.types.ts +264 -0
  118. package/packages/Frameworks/Dialog/Dialog.utils.ts +22 -0
  119. package/packages/Frameworks/Dialog/contents/Dialog.footer.tsx +79 -0
  120. package/packages/Frameworks/Dialog/contents/Dialog.funnel.tsx +260 -0
  121. package/packages/Frameworks/Dialog/contents/Dialog.header.tsx +137 -0
  122. package/packages/Frameworks/Dialog/hooks/useDialogPosition.ts +298 -0
  123. package/packages/Frameworks/Dialog/hooks/useSheetDrag.ts +325 -0
  124. package/packages/Frameworks/Dialog/hooks/useSheetGeometry.ts +201 -0
  125. package/packages/Frameworks/Dialog/hooks/useSheetProgressive.ts +98 -0
  126. package/packages/Frameworks/Dialog/renderers/renderModal.tsx +228 -0
  127. package/packages/Frameworks/Dialog/renderers/renderPopover.tsx +220 -0
  128. package/packages/Frameworks/Dialog/renderers/renderSheet.tsx +571 -0
  129. package/packages/Frameworks/EdgeEffect/EdgeEffect.tsx +52 -0
  130. package/packages/Frameworks/EdgeEffect/EdgeEffect.types.ts +5 -0
  131. package/packages/Frameworks/Motion/Motion.presets.ts +49 -0
  132. package/packages/Frameworks/Pressable/Pressable.module.scss +19 -0
  133. package/packages/Frameworks/Pressable/Pressable.tsx +651 -0
  134. package/packages/Frameworks/Pressable/Pressable.types.ts +73 -0
  135. package/packages/Frameworks/Squircle/Squircle.tsx +263 -0
  136. package/packages/Frameworks/Squircle/Squircle.types.ts +15 -0
  137. package/packages/Frameworks/Theme/Radius.types.ts +55 -0
  138. package/packages/Frameworks/Theme/Theme.types.ts +325 -0
  139. package/packages/Frameworks/Toaster/Toaster.boot.tsx +47 -0
  140. package/packages/Frameworks/Toaster/Toaster.module.scss +168 -0
  141. package/packages/Frameworks/Toaster/Toaster.types.ts +13 -0
  142. package/packages/Frameworks/View/DNDView/DNDView.module.scss +8 -0
  143. package/packages/Frameworks/View/DNDView/DNDView.tsx +470 -0
  144. package/packages/Frameworks/View/DNDView/DNDView.types.ts +61 -0
  145. package/packages/Frameworks/View/HScrollView/HScrollView.module.scss +59 -0
  146. package/packages/Frameworks/View/HScrollView/HScrollView.tsx +386 -0
  147. package/packages/Frameworks/View/HScrollView/HScrollView.types.ts +46 -0
  148. package/packages/Frameworks/View/ImageView/Dialog/ImageDialog.counter.tsx +27 -0
  149. package/packages/Frameworks/View/ImageView/Dialog/ImageDialog.footer.tsx +76 -0
  150. package/packages/Frameworks/View/ImageView/Dialog/ImageDialog.header.tsx +49 -0
  151. package/packages/Frameworks/View/ImageView/Dialog/ImageDialog.list.tsx +74 -0
  152. package/packages/Frameworks/View/ImageView/Dialog/ImageDialog.slide.tsx +71 -0
  153. package/packages/Frameworks/View/ImageView/Dialog/ImageDialog.tsx +175 -0
  154. package/packages/Frameworks/View/ImageView/Image.controls.tsx +39 -0
  155. package/packages/Frameworks/View/ImageView/Image.hooks.ts +82 -0
  156. package/packages/Frameworks/View/ImageView/Image.module.scss +7 -0
  157. package/packages/Frameworks/View/ImageView/Image.tsx +375 -0
  158. package/packages/Frameworks/View/ImageView/Image.types.ts +71 -0
  159. package/packages/Frameworks/View/ImageView/Image.utils.ts +79 -0
  160. package/packages/Frameworks/View/View.module.scss +34 -0
  161. package/packages/Frameworks/View/View.tsx +200 -0
  162. package/packages/Frameworks/View/View.types.ts +36 -0
  163. package/packages/Frameworks/_shared/Padding.types.ts +52 -0
  164. package/packages/Frameworks/_shared/StopParentInteraction.tsx +45 -0
  165. package/packages/Frameworks/_shared/Wind.types.ts +17 -0
  166. package/packages/Frameworks/_shared/bodyScrollLock.ts +127 -0
  167. package/packages/Frameworks/_shared/layer.constants.ts +8 -0
  168. package/packages/Frameworks/_shared/normalize.ts +39 -0
  169. package/packages/Frameworks/_shared/sizing.ts +55 -0
  170. package/packages/Frameworks/_shared/useControllableState.ts +29 -0
  171. package/packages/Styles/_animation.scss +42 -0
  172. package/packages/Styles/_color.scss +131 -0
  173. package/packages/Styles/_font.scss +30 -0
  174. package/packages/Styles/_icon.scss +782 -0
  175. package/packages/Styles/_importer.scss +9 -0
  176. package/packages/Styles/_system.scss +69 -0
  177. package/packages/Styles/_textstyle.scss +111 -0
  178. package/packages/Styles/_theme.scss +452 -0
  179. package/packages/Styles/_viewport-global.scss +99 -0
  180. package/packages/Styles/_viewport.scss +108 -0
@@ -0,0 +1,314 @@
1
+ "use client";
2
+ import { useViewportMatch } from "@musecat/functionkit";
3
+ import {
4
+ useCallback,
5
+ useEffect,
6
+ useId,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ } from "react";
11
+ import { Word } from "@/i18n/shared";
12
+ import type {
13
+ Option,
14
+ SelectProps,
15
+ } from "@/packages/Components/Select/Select.types";
16
+ import {
17
+ filterOptionsByQuery,
18
+ findScrollableAncestor,
19
+ getStringValues,
20
+ isOptGroup,
21
+ } from "@/packages/Components/Select/Select.utils";
22
+
23
+ export function useSelectState(props: SelectProps<boolean>) {
24
+ const t = Word();
25
+ const {
26
+ value,
27
+ defaultValue,
28
+ options,
29
+ multiple = false,
30
+ combobox = false,
31
+ disabled = false,
32
+ readOnly = false,
33
+ lockAncestorScrollOnOpen = false,
34
+ placeholder,
35
+ } = props;
36
+ const isMultiple = multiple;
37
+ const isInlineCombobox = combobox === true || combobox === "inline";
38
+ const isPopoverSearch = combobox === "search";
39
+ const isMobileSheet = useViewportMatch("(max-width: 549.98px)");
40
+
41
+ const isInlineComboboxActive = isInlineCombobox && !isMobileSheet;
42
+ const isPopoverSearchActive =
43
+ isPopoverSearch || (isInlineCombobox && isMobileSheet);
44
+ const isTypeableControl = isInlineComboboxActive;
45
+ const isInteractionDisabled = disabled || readOnly;
46
+
47
+ const groupedOptions = useMemo(
48
+ () =>
49
+ (options ?? []).map((item) =>
50
+ isOptGroup(item) ? item : { label: undefined, options: [item] },
51
+ ),
52
+ [options],
53
+ );
54
+ const flatOptions = useMemo(
55
+ () => groupedOptions.flatMap((g) => g.options),
56
+ [groupedOptions],
57
+ );
58
+ const initialValues = getStringValues(value, defaultValue);
59
+ const instanceId = useId();
60
+ const selectId = props.id ?? `${instanceId}-control`;
61
+ const listId = `${instanceId}-listbox`;
62
+ const popoverOwnerId = `${instanceId}-popover`;
63
+
64
+ const [open, setOpen] = useState(false);
65
+ const [activeIndex, setActiveIndex] = useState(-1);
66
+ const [inputValue, setInputValue] = useState("");
67
+ const [query, setQuery] = useState("");
68
+ const [selectedValues, setSelectedValues] = useState<string[]>(initialValues);
69
+ const [openSource, setOpenSource] = useState<"mouse" | "keyboard">("mouse");
70
+
71
+ const selectedValue = selectedValues[0] ?? "";
72
+ const selectedOptionLabel = useMemo(() => {
73
+ const selectedOption = flatOptions.find(
74
+ (option) => String(option.value) === selectedValue,
75
+ );
76
+ return selectedOption?.label ?? "";
77
+ }, [flatOptions, selectedValue]);
78
+ const selectedOptions = useMemo(
79
+ () =>
80
+ selectedValues
81
+ .map((value) => flatOptions.find((o) => String(o.value) === value))
82
+ .filter((option): option is Option => option !== undefined),
83
+ [flatOptions, selectedValues],
84
+ );
85
+
86
+ const rootRef = useRef<HTMLDivElement>(null);
87
+ const controlRef = useRef<HTMLDivElement>(null);
88
+ const suppressOpenUntilRef = useRef(0);
89
+ const suppressClickUntilRef = useRef(0);
90
+ const ensureVisibleRafRef = useRef<number | null>(null);
91
+ const pointerDownInsidePopoverRef = useRef(false);
92
+ const lastClosedByTabRef = useRef(false);
93
+ const popoverInputRef = useRef<HTMLInputElement>(null);
94
+ const isFirstRenderRef = useRef(true);
95
+
96
+ const ensureControlVisible = useCallback(() => {
97
+ const controlEl = controlRef.current;
98
+ if (!controlEl) return;
99
+ const scrollAncestor = findScrollableAncestor(controlEl);
100
+ controlEl.scrollIntoView({ block: "nearest", inline: "nearest" });
101
+ void scrollAncestor;
102
+ }, []);
103
+ const scheduleEnsureControlVisible = useCallback(() => {
104
+ if (ensureVisibleRafRef.current !== null) {
105
+ cancelAnimationFrame(ensureVisibleRafRef.current);
106
+ }
107
+ const outerRafId = requestAnimationFrame(() => {
108
+ ensureVisibleRafRef.current = requestAnimationFrame(() => {
109
+ ensureControlVisible();
110
+ ensureVisibleRafRef.current = null;
111
+ });
112
+ });
113
+ ensureVisibleRafRef.current = outerRafId;
114
+ }, [ensureControlVisible]);
115
+
116
+ useEffect(() => {
117
+ return () => {
118
+ if (ensureVisibleRafRef.current !== null) {
119
+ cancelAnimationFrame(ensureVisibleRafRef.current);
120
+ }
121
+ };
122
+ }, []);
123
+ useEffect(() => {
124
+ if (value === undefined) return;
125
+ const nextValues = getStringValues(value);
126
+ setSelectedValues(nextValues);
127
+ }, [value]);
128
+ useEffect(() => {
129
+ if (isMultiple) return;
130
+ if (open && query) return;
131
+
132
+ const nextSelectedValue = selectedValues[0] ?? "";
133
+ const nextSelectedOption = flatOptions.find(
134
+ (option) => String(option.value) === nextSelectedValue,
135
+ );
136
+ if (!nextSelectedOption && isInlineComboboxActive && query) {
137
+ setInputValue(query);
138
+ return;
139
+ }
140
+ setInputValue(nextSelectedOption?.label ?? "");
141
+ }, [
142
+ flatOptions,
143
+ isMultiple,
144
+ isInlineComboboxActive,
145
+ open,
146
+ query,
147
+ selectedValues,
148
+ ]);
149
+
150
+ const isInOwnPopover = useCallback(
151
+ (target: EventTarget | null) => {
152
+ const element = target as HTMLElement | null;
153
+ if (!element) return false;
154
+ return !!element.closest(`[data-popover-owner="${popoverOwnerId}"]`);
155
+ },
156
+ [popoverOwnerId],
157
+ );
158
+
159
+ const visibleOptions = useMemo(
160
+ () =>
161
+ !isTypeableControl && !isPopoverSearchActive
162
+ ? flatOptions
163
+ : filterOptionsByQuery(flatOptions, query),
164
+ [flatOptions, isTypeableControl, isPopoverSearchActive, query],
165
+ );
166
+ const visibleIndexMap = useMemo(() => {
167
+ const map = new Map<Option, number>();
168
+ visibleOptions.forEach((option, index) => {
169
+ map.set(option, index);
170
+ });
171
+ return map;
172
+ }, [visibleOptions]);
173
+ const visibleGroupedOptions = useMemo(() => {
174
+ return groupedOptions
175
+ .map((group) => ({
176
+ label: group.label,
177
+ options: group.options.filter((option) => visibleIndexMap.has(option)),
178
+ }))
179
+ .filter((group) => group.options.length > 0);
180
+ }, [groupedOptions, visibleIndexMap]);
181
+
182
+ useEffect(() => {
183
+ setActiveIndex((prev) => {
184
+ if (!visibleOptions.length) return -1;
185
+ if (prev < 0) return prev;
186
+ return Math.min(prev, visibleOptions.length - 1);
187
+ });
188
+ }, [visibleOptions.length]);
189
+
190
+ const isOptionSelectionDisabled = useCallback(
191
+ (option: Option) =>
192
+ !!option.disabled ||
193
+ (!isMultiple &&
194
+ (isTypeableControl || isPopoverSearchActive) &&
195
+ selectedValues.includes(String(option.value))),
196
+ [isMultiple, isTypeableControl, isPopoverSearchActive, selectedValues],
197
+ );
198
+
199
+ const getFirstSelectableIndex = useCallback(
200
+ (items: Option[], blockedValues: string[] = selectedValues) =>
201
+ items.findIndex(
202
+ (item) =>
203
+ !item.disabled &&
204
+ (!(isTypeableControl || isPopoverSearchActive) ||
205
+ !blockedValues.includes(String(item.value))),
206
+ ),
207
+ [isTypeableControl, isPopoverSearchActive, selectedValues],
208
+ );
209
+ const getFirstEnabledIndex = useCallback(
210
+ (items: Option[]) => getFirstSelectableIndex(items),
211
+ [getFirstSelectableIndex],
212
+ );
213
+ const getNextEnabledIndex = useCallback(
214
+ (items: Option[], start: number, direction: 1 | -1) => {
215
+ if (!items.length) return -1;
216
+ let idx = start;
217
+ for (let i = 0; i < items.length; i += 1) {
218
+ idx += direction;
219
+ if (idx < 0) idx = items.length - 1;
220
+ if (idx >= items.length) idx = 0;
221
+ if (items[idx] && !isOptionSelectionDisabled(items[idx])) return idx;
222
+ }
223
+ return -1;
224
+ },
225
+ [isOptionSelectionDisabled],
226
+ );
227
+ const getSelectedEnabledIndex = useCallback(
228
+ (items: Option[]) =>
229
+ items.findIndex(
230
+ (item) =>
231
+ selectedValues.includes(String(item.value)) &&
232
+ !isOptionSelectionDisabled(item),
233
+ ),
234
+ [isOptionSelectionDisabled, selectedValues],
235
+ );
236
+ const getInitialActiveIndex = useCallback(
237
+ (items: Option[]) => {
238
+ const selectedIdx = getSelectedEnabledIndex(items);
239
+ return selectedIdx >= 0 ? selectedIdx : getFirstEnabledIndex(items);
240
+ },
241
+ [getFirstEnabledIndex, getSelectedEnabledIndex],
242
+ );
243
+
244
+ const selectedLabel = useMemo(() => {
245
+ const picked = flatOptions.find((o) => String(o.value) === selectedValue);
246
+ return picked?.label ?? placeholder ?? t.UIKit.ui.select;
247
+ }, [flatOptions, placeholder, selectedValue, t]);
248
+
249
+ return {
250
+ t,
251
+ props,
252
+
253
+ isMultiple,
254
+ isInlineCombobox,
255
+ isPopoverSearch,
256
+ isInlineComboboxActive,
257
+ isPopoverSearchActive,
258
+ isTypeableControl,
259
+ isInteractionDisabled,
260
+ lockAncestorScrollOnOpen,
261
+ isMobileSheet,
262
+
263
+ instanceId,
264
+ selectId,
265
+ listId,
266
+ popoverOwnerId,
267
+
268
+ open,
269
+ setOpen,
270
+ activeIndex,
271
+ setActiveIndex,
272
+ inputValue,
273
+ setInputValue,
274
+ query,
275
+ setQuery,
276
+ selectedValues,
277
+ setSelectedValues,
278
+ openSource,
279
+ setOpenSource,
280
+
281
+ rootRef,
282
+ controlRef,
283
+ suppressOpenUntilRef,
284
+ suppressClickUntilRef,
285
+ popoverInputRef,
286
+ pointerDownInsidePopoverRef,
287
+ lastClosedByTabRef,
288
+ isFirstRenderRef,
289
+ ensureVisibleRafRef,
290
+
291
+ flatOptions,
292
+ groupedOptions,
293
+ visibleOptions,
294
+ visibleIndexMap,
295
+ visibleGroupedOptions,
296
+ selectedOptions,
297
+ selectedLabel,
298
+ selectedValue,
299
+ selectedOptionLabel,
300
+
301
+ isInOwnPopover,
302
+ ensureControlVisible,
303
+ scheduleEnsureControlVisible,
304
+
305
+ getFirstEnabledIndex,
306
+ getNextEnabledIndex,
307
+ getSelectedEnabledIndex,
308
+ getFirstSelectableIndex,
309
+ getInitialActiveIndex,
310
+ isOptionSelectionDisabled,
311
+ };
312
+ }
313
+
314
+ export type SelectState = ReturnType<typeof useSelectState>;
@@ -0,0 +1,228 @@
1
+ "use client";
2
+ import type {
3
+ KeyboardEvent,
4
+ ReactElement,
5
+ MouseEvent as ReactMouseEvent,
6
+ } from "react";
7
+ import Pill from "@/packages/Components/Pill/Pill";
8
+ import SelectTrigger from "@/packages/Components/Select/Select.trigger";
9
+ import type { SelectNavigation } from "@/packages/Components/Select/hooks/useSelectNavigation";
10
+ import type { SelectState } from "@/packages/Components/Select/hooks/useSelectState";
11
+ import type { SelectValue } from "@/packages/Components/Select/Select.types";
12
+ import { filterOptionsByQuery } from "@/packages/Components/Select/Select.utils";
13
+ import Text from "@/packages/Components/Text/Text";
14
+ import View from "@/packages/Frameworks/View/View";
15
+
16
+ interface SelectControlProps {
17
+ state: SelectState;
18
+ emitNativeChange: (next: SelectValue<boolean>) => void;
19
+ removeSelectedOption: (value: string) => void;
20
+ navigation: SelectNavigation;
21
+ }
22
+
23
+ export default function SelectControl({
24
+ state,
25
+ emitNativeChange,
26
+ removeSelectedOption,
27
+ navigation,
28
+ }: SelectControlProps): ReactElement {
29
+ const { props, t } = state;
30
+ const {
31
+ isMultiple,
32
+ isInlineComboboxActive,
33
+ isInteractionDisabled,
34
+ open,
35
+ activeIndex,
36
+ query,
37
+ inputValue,
38
+ selectedValues,
39
+ selectedOptions,
40
+ selectedLabel,
41
+ selectedOptionLabel,
42
+ flatOptions,
43
+ getFirstEnabledIndex,
44
+ getInitialActiveIndex,
45
+ scheduleEnsureControlVisible,
46
+ setOpen,
47
+ setOpenSource,
48
+ setActiveIndex,
49
+ setSelectedValues,
50
+ setQuery,
51
+ setInputValue,
52
+ listId,
53
+ selectId,
54
+ visibleOptions,
55
+ } = state;
56
+
57
+ const {
58
+ placeholder,
59
+ title,
60
+ disabled,
61
+ readOnly,
62
+ required,
63
+ } = props;
64
+
65
+ const {
66
+ handleControlKeyDown,
67
+ handleTriggerClick,
68
+ handleTriggerPointerUp,
69
+ } = navigation;
70
+
71
+ const selectedPills =
72
+ isMultiple && selectedOptions.length > 0 ? (
73
+ <View wrap="wrap" gap={6} data-select-pills>
74
+ {selectedOptions.map((option, index) => (
75
+ <Pill
76
+ key={String(option.value)}
77
+ text={option.label}
78
+ disabled={isInteractionDisabled}
79
+ paddingVertical={6}
80
+ paddingHorizontal={8}
81
+ themePreset="BaseFull"
82
+ background={props.background}
83
+ color={props.color}
84
+ rightIcon={
85
+ isInteractionDisabled
86
+ ? undefined
87
+ : {
88
+ icon: "iClose",
89
+ pressable: {
90
+ tabIndex: -1,
91
+ onMouseDown: (e) => {
92
+ e.preventDefault();
93
+ e.stopPropagation();
94
+ },
95
+ onKeyDown: (e: KeyboardEvent<Element>) => {
96
+ if (e.key === "ArrowLeft") {
97
+ e.preventDefault();
98
+ e.stopPropagation();
99
+ if (index > 0) {
100
+ navigation.focusPillButton(index - 1);
101
+ } else {
102
+ navigation.focusInputField();
103
+ }
104
+ return;
105
+ }
106
+ if (e.key === "ArrowRight") {
107
+ e.preventDefault();
108
+ e.stopPropagation();
109
+ if (index < selectedOptions.length - 1) {
110
+ navigation.focusPillButton(index + 1);
111
+ } else {
112
+ navigation.focusInputField();
113
+ }
114
+ return;
115
+ }
116
+ if (e.key !== "Backspace" && e.key !== "Delete") return;
117
+ e.preventDefault();
118
+ e.stopPropagation();
119
+ navigation.focusInputField();
120
+ removeSelectedOption(String(option.value));
121
+ },
122
+ onClick: (e: ReactMouseEvent<Element>) => {
123
+ e.stopPropagation();
124
+ removeSelectedOption(String(option.value));
125
+ },
126
+ },
127
+ }
128
+ }
129
+ />
130
+ ))}
131
+ </View>
132
+ ) : null;
133
+
134
+ const placeholderText = placeholder ?? t.UIKit.ui.select;
135
+
136
+ const renderComboboxControl = () => (
137
+ <View
138
+ style={{
139
+ width: "calc(100% + 2.8rem)",
140
+ textAlign: "left",
141
+ cursor: "default",
142
+ }}
143
+ column
144
+ gap={4}
145
+ >
146
+ {selectedPills}
147
+ <input
148
+ id={selectId}
149
+ data-select-input="true"
150
+ title={title}
151
+ role="combobox"
152
+ aria-expanded={open}
153
+ aria-haspopup="listbox"
154
+ aria-controls={listId}
155
+ aria-activedescendant={
156
+ open && activeIndex >= 0 && activeIndex < visibleOptions.length
157
+ ? `${listId}-${activeIndex}`
158
+ : undefined
159
+ }
160
+ value={isMultiple ? query : inputValue}
161
+ placeholder={
162
+ isMultiple && selectedOptions.length > 0
163
+ ? undefined
164
+ : placeholderText
165
+ }
166
+ className="Subheadline"
167
+ onFocus={() => {
168
+ if (isInteractionDisabled) return;
169
+ if (document.activeElement?.matches(":focus-visible")) {
170
+ setOpenSource("keyboard");
171
+ }
172
+ setOpen(true);
173
+ scheduleEnsureControlVisible();
174
+ setActiveIndex((prev) => {
175
+ if (prev >= 0 && !visibleOptions[prev]?.disabled) return prev;
176
+ return getInitialActiveIndex(visibleOptions);
177
+ });
178
+ }}
179
+ onClick={() => {
180
+ if (isInteractionDisabled) return;
181
+ setOpen(true);
182
+ scheduleEnsureControlVisible();
183
+ }}
184
+ onChange={(e) => {
185
+ if (isInteractionDisabled) return;
186
+ const nextQuery = e.target.value;
187
+ setOpenSource("keyboard");
188
+ if (!isMultiple) {
189
+ if (selectedValues.length > 0 && nextQuery !== selectedOptionLabel) {
190
+ setSelectedValues([]);
191
+ emitNativeChange("" as SelectValue<boolean>);
192
+ }
193
+ setInputValue(nextQuery);
194
+ }
195
+ setQuery(nextQuery);
196
+ props.onInputChange?.(nextQuery);
197
+ setOpen(true);
198
+ const nextVisible = filterOptionsByQuery(flatOptions, nextQuery);
199
+ setActiveIndex(getFirstEnabledIndex(nextVisible));
200
+ }}
201
+ onKeyDown={(e) => handleControlKeyDown(e, false)}
202
+ required={required}
203
+ disabled={disabled}
204
+ readOnly={readOnly}
205
+ />
206
+ </View>
207
+ );
208
+
209
+ if (isInlineComboboxActive) return renderComboboxControl();
210
+
211
+ return (
212
+ <SelectTrigger
213
+ selectId={selectId}
214
+ title={title}
215
+ open={open}
216
+ listId={listId}
217
+ isMultiple={isMultiple}
218
+ isInteractionDisabled={isInteractionDisabled}
219
+ selectedPills={selectedPills}
220
+ selectedOptionsLength={selectedOptions.length}
221
+ selectedLabel={selectedLabel}
222
+ placeholderText={placeholderText}
223
+ handleTriggerClick={handleTriggerClick}
224
+ handleTriggerPointerUp={handleTriggerPointerUp}
225
+ handleControlKeyDown={handleControlKeyDown}
226
+ />
227
+ );
228
+ }