@seed-design/lynx-react 0.7.1 → 0.8.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 (241) hide show
  1. package/lib/components/Accordion/Accordion.jsx +2 -2
  2. package/lib/components/ActionButton/ActionButton.jsx +1 -1
  3. package/lib/components/AlertDialog/AlertDialog.d.ts +64 -0
  4. package/lib/components/AlertDialog/AlertDialog.d.ts.map +1 -0
  5. package/lib/components/AlertDialog/AlertDialog.jsx +138 -0
  6. package/lib/components/AlertDialog/AlertDialog.namespace.d.ts +2 -0
  7. package/lib/components/AlertDialog/AlertDialog.namespace.d.ts.map +1 -0
  8. package/lib/components/AlertDialog/AlertDialog.namespace.js +17 -0
  9. package/lib/components/AlertDialog/index.d.ts +3 -0
  10. package/lib/components/AlertDialog/index.d.ts.map +1 -0
  11. package/lib/components/AppBar/AppBar.d.ts +14 -1
  12. package/lib/components/AppBar/AppBar.d.ts.map +1 -1
  13. package/lib/components/AppBar/AppBar.jsx +39 -8
  14. package/lib/components/AppBar/AppBar.namespace.d.ts +1 -1
  15. package/lib/components/AppBar/AppBar.namespace.d.ts.map +1 -1
  16. package/lib/components/AppBar/index.d.ts +1 -1
  17. package/lib/components/AppBar/index.d.ts.map +1 -1
  18. package/lib/components/AttachmentDisplay/AttachmentDisplay.d.ts +182 -0
  19. package/lib/components/AttachmentDisplay/AttachmentDisplay.d.ts.map +1 -0
  20. package/lib/components/AttachmentDisplay/AttachmentDisplay.jsx +444 -0
  21. package/lib/components/AttachmentDisplay/index.d.ts +2 -0
  22. package/lib/components/AttachmentDisplay/index.d.ts.map +1 -0
  23. package/lib/components/AttachmentInput/AttachmentInput.d.ts +179 -0
  24. package/lib/components/AttachmentInput/AttachmentInput.d.ts.map +1 -0
  25. package/lib/components/AttachmentInput/AttachmentInput.jsx +520 -0
  26. package/lib/components/AttachmentInput/index.d.ts +2 -0
  27. package/lib/components/AttachmentInput/index.d.ts.map +1 -0
  28. package/lib/components/BottomSheet/BottomSheet.d.ts +1 -1
  29. package/lib/components/BottomSheet/BottomSheet.d.ts.map +1 -1
  30. package/lib/components/BottomSheet/BottomSheet.jsx +1 -7
  31. package/lib/components/Callout/Callout.jsx +2 -2
  32. package/lib/components/Checkbox/Checkbox.jsx +2 -2
  33. package/lib/components/Chip/Chip.jsx +2 -2
  34. package/lib/components/ChipTabs/ChipTabs.d.ts +41 -0
  35. package/lib/components/ChipTabs/ChipTabs.d.ts.map +1 -0
  36. package/lib/components/ChipTabs/ChipTabs.jsx +39 -0
  37. package/lib/components/ChipTabs/ChipTabs.namespace.d.ts +2 -0
  38. package/lib/components/ChipTabs/ChipTabs.namespace.d.ts.map +1 -0
  39. package/lib/components/ChipTabs/ChipTabs.namespace.js +13 -0
  40. package/lib/components/ChipTabs/index.d.ts +3 -0
  41. package/lib/components/ChipTabs/index.d.ts.map +1 -0
  42. package/lib/components/ContextualFloatingButton/ContextualFloatingButton.d.ts +18 -0
  43. package/lib/components/ContextualFloatingButton/ContextualFloatingButton.d.ts.map +1 -0
  44. package/lib/components/ContextualFloatingButton/ContextualFloatingButton.jsx +113 -0
  45. package/lib/components/ContextualFloatingButton/index.d.ts +2 -0
  46. package/lib/components/ContextualFloatingButton/index.d.ts.map +1 -0
  47. package/lib/components/Dialog/Dialog.d.ts +87 -0
  48. package/lib/components/Dialog/Dialog.d.ts.map +1 -0
  49. package/lib/components/Dialog/Dialog.jsx +147 -0
  50. package/lib/components/Dialog/Dialog.namespace.d.ts +2 -0
  51. package/lib/components/Dialog/Dialog.namespace.d.ts.map +1 -0
  52. package/lib/components/Dialog/Dialog.namespace.js +18 -0
  53. package/lib/components/Dialog/index.d.ts +3 -0
  54. package/lib/components/Dialog/index.d.ts.map +1 -0
  55. package/lib/components/FloatingActionButton/FloatingActionButton.d.ts +23 -0
  56. package/lib/components/FloatingActionButton/FloatingActionButton.d.ts.map +1 -0
  57. package/lib/components/FloatingActionButton/FloatingActionButton.jsx +76 -0
  58. package/lib/components/FloatingActionButton/FloatingActionButton.namespace.d.ts +2 -0
  59. package/lib/components/FloatingActionButton/FloatingActionButton.namespace.d.ts.map +1 -0
  60. package/lib/components/FloatingActionButton/FloatingActionButton.namespace.js +10 -0
  61. package/lib/components/FloatingActionButton/index.d.ts +3 -0
  62. package/lib/components/FloatingActionButton/index.d.ts.map +1 -0
  63. package/lib/components/HelpBubble/HelpBubble.d.ts +67 -0
  64. package/lib/components/HelpBubble/HelpBubble.d.ts.map +1 -0
  65. package/lib/components/HelpBubble/HelpBubble.jsx +600 -0
  66. package/lib/components/HelpBubble/HelpBubble.namespace.d.ts +2 -0
  67. package/lib/components/HelpBubble/HelpBubble.namespace.d.ts.map +1 -0
  68. package/lib/components/HelpBubble/HelpBubble.namespace.js +18 -0
  69. package/lib/components/HelpBubble/index.d.ts +3 -0
  70. package/lib/components/HelpBubble/index.d.ts.map +1 -0
  71. package/lib/components/Icon/Icon.d.ts +1 -0
  72. package/lib/components/Icon/Icon.d.ts.map +1 -1
  73. package/lib/components/Icon/Icon.jsx +2 -1
  74. package/lib/components/IdentityPlaceholder/IdentityPlaceholder.d.ts +28 -0
  75. package/lib/components/IdentityPlaceholder/IdentityPlaceholder.d.ts.map +1 -0
  76. package/lib/components/IdentityPlaceholder/IdentityPlaceholder.jsx +37 -0
  77. package/lib/components/IdentityPlaceholder/IdentityPlaceholder.namespace.d.ts +2 -0
  78. package/lib/components/IdentityPlaceholder/IdentityPlaceholder.namespace.d.ts.map +1 -0
  79. package/lib/components/IdentityPlaceholder/IdentityPlaceholder.namespace.js +9 -0
  80. package/lib/components/IdentityPlaceholder/identity-placeholder-business.js +4 -0
  81. package/lib/components/IdentityPlaceholder/identity-placeholder-person.js +4 -0
  82. package/lib/components/IdentityPlaceholder/index.d.ts +3 -0
  83. package/lib/components/IdentityPlaceholder/index.d.ts.map +1 -0
  84. package/lib/components/InputButton/InputButton.d.ts +2 -0
  85. package/lib/components/InputButton/InputButton.d.ts.map +1 -1
  86. package/lib/components/InputButton/InputButton.jsx +40 -8
  87. package/lib/components/List/List.d.ts +1 -1
  88. package/lib/components/List/List.d.ts.map +1 -1
  89. package/lib/components/List/List.jsx +26 -12
  90. package/lib/components/Menu/Menu.d.ts.map +1 -1
  91. package/lib/components/Menu/Menu.jsx +15 -5
  92. package/lib/components/PageBanner/PageBanner.d.ts +3 -0
  93. package/lib/components/PageBanner/PageBanner.d.ts.map +1 -1
  94. package/lib/components/PageBanner/PageBanner.jsx +49 -11
  95. package/lib/components/QuantityPicker/QuantityPicker.d.ts +53 -0
  96. package/lib/components/QuantityPicker/QuantityPicker.d.ts.map +1 -0
  97. package/lib/components/QuantityPicker/QuantityPicker.jsx +282 -0
  98. package/lib/components/QuantityPicker/QuantityPicker.namespace.d.ts +2 -0
  99. package/lib/components/QuantityPicker/QuantityPicker.namespace.d.ts.map +1 -0
  100. package/lib/components/QuantityPicker/QuantityPicker.namespace.js +11 -0
  101. package/lib/components/QuantityPicker/index.d.ts +3 -0
  102. package/lib/components/QuantityPicker/index.d.ts.map +1 -0
  103. package/lib/components/RadioGroup/RadioGroup.jsx +2 -2
  104. package/lib/components/ReactionButton/ReactionButton.d.ts +1 -1
  105. package/lib/components/ReactionButton/ReactionButton.d.ts.map +1 -1
  106. package/lib/components/ReactionButton/ReactionButton.jsx +22 -13
  107. package/lib/components/ScrollFog/ScrollFog.d.ts +41 -0
  108. package/lib/components/ScrollFog/ScrollFog.d.ts.map +1 -0
  109. package/lib/components/ScrollFog/ScrollFog.jsx +65 -0
  110. package/lib/components/ScrollFog/index.d.ts +2 -0
  111. package/lib/components/ScrollFog/index.d.ts.map +1 -0
  112. package/lib/components/SegmentedControl/SegmentedControl.jsx +2 -2
  113. package/lib/components/Select/Select.d.ts +112 -0
  114. package/lib/components/Select/Select.d.ts.map +1 -0
  115. package/lib/components/Select/Select.jsx +885 -0
  116. package/lib/components/Select/Select.namespace.d.ts +2 -0
  117. package/lib/components/Select/Select.namespace.d.ts.map +1 -0
  118. package/lib/components/Select/Select.namespace.js +23 -0
  119. package/lib/components/Select/index.d.ts +3 -0
  120. package/lib/components/Select/index.d.ts.map +1 -0
  121. package/lib/components/Skeleton/Skeleton.d.ts +15 -0
  122. package/lib/components/Skeleton/Skeleton.d.ts.map +1 -0
  123. package/lib/components/Skeleton/Skeleton.jsx +18 -0
  124. package/lib/components/Skeleton/index.d.ts +2 -0
  125. package/lib/components/Skeleton/index.d.ts.map +1 -0
  126. package/lib/components/Slider/Slider.d.ts +101 -0
  127. package/lib/components/Slider/Slider.d.ts.map +1 -0
  128. package/lib/components/Slider/Slider.jsx +573 -0
  129. package/lib/components/Slider/Slider.namespace.d.ts +2 -0
  130. package/lib/components/Slider/Slider.namespace.d.ts.map +1 -0
  131. package/lib/components/Slider/Slider.namespace.js +19 -0
  132. package/lib/components/Slider/Slider.utils.d.ts +19 -0
  133. package/lib/components/Slider/Slider.utils.d.ts.map +1 -0
  134. package/lib/components/Slider/Slider.utils.js +106 -0
  135. package/lib/components/Slider/index.d.ts +3 -0
  136. package/lib/components/Slider/index.d.ts.map +1 -0
  137. package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.d.ts.map +1 -1
  138. package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.jsx +22 -8
  139. package/lib/components/Switch/Switch.d.ts.map +1 -1
  140. package/lib/components/Switch/Switch.jsx +5 -3
  141. package/lib/components/Tabs/Tabs.d.ts +34 -2
  142. package/lib/components/Tabs/Tabs.d.ts.map +1 -1
  143. package/lib/components/Tabs/Tabs.jsx +186 -56
  144. package/lib/components/Tabs/Tabs.utils.d.ts +14 -1
  145. package/lib/components/Tabs/Tabs.utils.d.ts.map +1 -1
  146. package/lib/components/Tabs/Tabs.utils.js +24 -3
  147. package/lib/components/Tabs/index.d.ts +1 -1
  148. package/lib/components/Tabs/index.d.ts.map +1 -1
  149. package/lib/components/index.d.ts +14 -0
  150. package/lib/components/index.d.ts.map +1 -1
  151. package/lib/hooks/useControllableState.d.ts +1 -14
  152. package/lib/hooks/useControllableState.d.ts.map +1 -1
  153. package/lib/hooks/usePressTap.d.ts +1 -27
  154. package/lib/hooks/usePressTap.d.ts.map +1 -1
  155. package/lib/index.js +29 -6
  156. package/lib/{hooks/useControllableState.js → packages/lynx-react-headless/use-controllable-state/lib/index.js} +2 -3
  157. package/lib/{hooks/usePressTap.js → packages/lynx-react-headless/use-press-tap/lib/index.js} +13 -10
  158. package/lib/types.d.ts +1 -0
  159. package/lib/types.d.ts.map +1 -1
  160. package/package.json +9 -5
  161. package/src/components/AlertDialog/AlertDialog.namespace.ts +22 -0
  162. package/src/components/AlertDialog/AlertDialog.test.tsx +226 -0
  163. package/src/components/AlertDialog/AlertDialog.tsx +374 -0
  164. package/src/components/AlertDialog/index.ts +24 -0
  165. package/src/components/AppBar/AppBar.namespace.ts +1 -0
  166. package/src/components/AppBar/AppBar.test.tsx +231 -0
  167. package/src/components/AppBar/AppBar.tsx +105 -13
  168. package/src/components/AppBar/index.ts +1 -0
  169. package/src/components/AttachmentDisplay/AttachmentDisplay.tsx +886 -0
  170. package/src/components/AttachmentDisplay/index.ts +48 -0
  171. package/src/components/AttachmentInput/AttachmentInput.tsx +1001 -0
  172. package/src/components/AttachmentInput/index.ts +49 -0
  173. package/src/components/BottomSheet/BottomSheet.test.tsx +22 -7
  174. package/src/components/BottomSheet/BottomSheet.tsx +1 -19
  175. package/src/components/ChipTabs/ChipTabs.namespace.ts +14 -0
  176. package/src/components/ChipTabs/ChipTabs.tsx +119 -0
  177. package/src/components/ChipTabs/index.ts +16 -0
  178. package/src/components/ContextualFloatingButton/ContextualFloatingButton.test.tsx +63 -0
  179. package/src/components/ContextualFloatingButton/ContextualFloatingButton.tsx +232 -0
  180. package/src/components/ContextualFloatingButton/index.ts +4 -0
  181. package/src/components/Dialog/Dialog.namespace.ts +24 -0
  182. package/src/components/Dialog/Dialog.test.tsx +172 -0
  183. package/src/components/Dialog/Dialog.tsx +373 -0
  184. package/src/components/Dialog/index.ts +26 -0
  185. package/src/components/FloatingActionButton/FloatingActionButton.namespace.ts +8 -0
  186. package/src/components/FloatingActionButton/FloatingActionButton.test.tsx +59 -0
  187. package/src/components/FloatingActionButton/FloatingActionButton.tsx +207 -0
  188. package/src/components/FloatingActionButton/index.ts +10 -0
  189. package/src/components/HelpBubble/HelpBubble.namespace.ts +24 -0
  190. package/src/components/HelpBubble/HelpBubble.tsx +998 -0
  191. package/src/components/HelpBubble/index.ts +26 -0
  192. package/src/components/Icon/Icon.test.tsx +0 -3
  193. package/src/components/Icon/Icon.tsx +11 -1
  194. package/src/components/IdentityPlaceholder/IdentityPlaceholder.namespace.ts +6 -0
  195. package/src/components/IdentityPlaceholder/IdentityPlaceholder.tsx +103 -0
  196. package/src/components/IdentityPlaceholder/assets.d.ts +4 -0
  197. package/src/components/IdentityPlaceholder/identity-placeholder-business.webp +0 -0
  198. package/src/components/IdentityPlaceholder/identity-placeholder-person.webp +0 -0
  199. package/src/components/IdentityPlaceholder/index.ts +8 -0
  200. package/src/components/InputButton/InputButton.test.tsx +128 -1
  201. package/src/components/InputButton/InputButton.tsx +60 -9
  202. package/src/components/List/List.test.tsx +54 -2
  203. package/src/components/List/List.tsx +43 -10
  204. package/src/components/Menu/Menu.tsx +14 -4
  205. package/src/components/Menu/__tests__/Menu.test.tsx +34 -0
  206. package/src/components/PageBanner/PageBanner.test.tsx +110 -3
  207. package/src/components/PageBanner/PageBanner.tsx +83 -19
  208. package/src/components/QuantityPicker/QuantityPicker.namespace.ts +10 -0
  209. package/src/components/QuantityPicker/QuantityPicker.test.tsx +235 -0
  210. package/src/components/QuantityPicker/QuantityPicker.tsx +605 -0
  211. package/src/components/QuantityPicker/index.ts +14 -0
  212. package/src/components/ReactionButton/ReactionButton.feedback.test.tsx +50 -0
  213. package/src/components/ReactionButton/ReactionButton.test.tsx +5 -3
  214. package/src/components/ReactionButton/ReactionButton.tsx +20 -12
  215. package/src/components/ScrollFog/ScrollFog.test.tsx +222 -0
  216. package/src/components/ScrollFog/ScrollFog.tsx +172 -0
  217. package/src/components/ScrollFog/index.ts +1 -0
  218. package/src/components/Select/Select.namespace.ts +34 -0
  219. package/src/components/Select/Select.test.tsx +297 -0
  220. package/src/components/Select/Select.tsx +1483 -0
  221. package/src/components/Select/index.ts +39 -0
  222. package/src/components/Skeleton/Skeleton.test.tsx +64 -0
  223. package/src/components/Skeleton/Skeleton.tsx +45 -0
  224. package/src/components/Skeleton/index.ts +1 -0
  225. package/src/components/Slider/Slider.namespace.ts +26 -0
  226. package/src/components/Slider/Slider.test.tsx +260 -0
  227. package/src/components/Slider/Slider.tsx +897 -0
  228. package/src/components/Slider/Slider.utils.ts +167 -0
  229. package/src/components/Slider/index.ts +30 -0
  230. package/src/components/SwipeableMenuSheet/SwipeableMenuSheet.test.tsx +21 -1
  231. package/src/components/SwipeableMenuSheet/SwipeableMenuSheet.tsx +30 -7
  232. package/src/components/Switch/Switch.tsx +14 -13
  233. package/src/components/Tabs/Tabs.test.tsx +218 -1
  234. package/src/components/Tabs/Tabs.tsx +346 -75
  235. package/src/components/Tabs/Tabs.utils.ts +51 -1
  236. package/src/components/Tabs/index.ts +17 -1
  237. package/src/components/index.ts +14 -0
  238. package/src/hooks/useControllableState.ts +1 -43
  239. package/src/hooks/usePressTap.test.ts +23 -11
  240. package/src/hooks/usePressTap.ts +1 -60
  241. package/src/types.ts +1 -0
@@ -0,0 +1,1483 @@
1
+ import * as React from "@lynx-js/react";
2
+ import { isValidElement } from "@lynx-js/react";
3
+ import { getRectByRef } from "@lynx-js/lynx-ui-common";
4
+ import type { BaseEvent, NodesRef } from "@lynx-js/types";
5
+ import clsx from "clsx";
6
+
7
+ import { select, type SelectVariantProps } from "@seed-design/lynx-css/recipes/select";
8
+ import {
9
+ selectTrigger,
10
+ type SelectTriggerVariantProps,
11
+ } from "@seed-design/lynx-css/recipes/select-trigger";
12
+ import { selectItem, type SelectItemVariantProps } from "@seed-design/lynx-css/recipes/select-item";
13
+ import { select as selectVars } from "@seed-design/lynx-css/vars/component";
14
+
15
+ import { useControllableState } from "../../hooks/useControllableState";
16
+ import { usePressTap } from "../../hooks/usePressTap";
17
+ import type {
18
+ LynxAccessibilityProps,
19
+ LynxPressableProps,
20
+ LynxIconElementProps,
21
+ LynxStyledElementProps,
22
+ LynxTextRef,
23
+ LynxViewProps,
24
+ LynxViewRef,
25
+ } from "../../types";
26
+ import { toArray } from "../../utils/children";
27
+ import { useFieldContext } from "../Field/context";
28
+ import { InternalIcon, type InternalIconProps } from "../Icon/Icon";
29
+ import {
30
+ computePosition,
31
+ type Placement as SelectPlacement,
32
+ type Position as SelectPosition,
33
+ type Rect as SelectRect,
34
+ } from "../private/Positioning";
35
+
36
+ const EMPTY_VALUE: string[] = [];
37
+ // `$dimension.x2` is the Rootage Select spacing token. Generated component vars retain
38
+ // it as CSS `var()` for styling, but floating-point collision math needs its numeric 8px value.
39
+ const selectGutter = 8;
40
+ const selectOverflowPadding = 8;
41
+ const selectMaxHeight = Number.parseFloat(selectVars.base.enabled.root.maxHeight);
42
+ // This protects placement near the screen edge; it does not impose a minimum list viewport.
43
+ const selectMinimumAvailableHeight = 200;
44
+ let nextSelectScrollAreaId = 0;
45
+
46
+ interface SelectClassNames {
47
+ positioner: string;
48
+ backdrop: string;
49
+ content: string;
50
+ scrollArea: string;
51
+ scrollContent: string;
52
+ group: string;
53
+ groupLabel: string;
54
+ separator: string;
55
+ }
56
+
57
+ interface SelectItemClassNames {
58
+ root: string;
59
+ pressedOverlay: string;
60
+ body: string;
61
+ label: string;
62
+ description: string;
63
+ prefixIcon: string;
64
+ indicator: string;
65
+ }
66
+ type NativeTapHandler = NonNullable<LynxViewProps["bindtap"]>;
67
+ type NativeTransitionHandler = NonNullable<LynxViewProps["bindtransitionend"]>;
68
+ type NativeLayoutHandler = NonNullable<LynxViewProps["bindlayoutchange"]>;
69
+ type SelectTriggerHandlers = Pick<LynxViewProps, "bindtap" | "main-thread:bindtap">;
70
+
71
+ type SelectPublicVariantProps = Omit<SelectVariantProps, "size" | "open" | "positioned"> & {
72
+ size?: "large" | "medium" | "responsive";
73
+ };
74
+ type SelectTriggerPublicVariantProps = Omit<
75
+ SelectTriggerVariantProps,
76
+ "size" | "open" | "pressed" | "disabled" | "readOnly" | "invalid"
77
+ >;
78
+ type SelectItemPublicVariantProps = Omit<
79
+ SelectItemVariantProps,
80
+ "size" | "selected" | "pressed" | "disabled"
81
+ >;
82
+
83
+ export type SelectOpenChangeReason = "trigger" | "interactOutside" | "itemSelect" | "dismiss";
84
+
85
+ export interface SelectOpenChangeDetails {
86
+ reason: SelectOpenChangeReason;
87
+ event: BaseEvent;
88
+ }
89
+
90
+ export interface SelectSelectedItem {
91
+ value: string;
92
+ label: React.ReactNode;
93
+ textValue: string;
94
+ prefixIcon?: React.ReactNode;
95
+ resolved: boolean;
96
+ }
97
+
98
+ interface SelectOptionEntry {
99
+ label: React.ReactNode;
100
+ textValue: string;
101
+ prefixIcon?: React.ReactNode;
102
+ node: NodesRef | null;
103
+ }
104
+
105
+ interface RegisteredSelectItem extends SelectSelectedItem {
106
+ node: NodesRef | null;
107
+ }
108
+
109
+ interface SelectContextValue {
110
+ value: string[];
111
+ selectedItems: SelectSelectedItem[];
112
+ selectedItem: RegisteredSelectItem | undefined;
113
+ displayValue: React.ReactNode;
114
+ showPlaceholder: boolean;
115
+ multiple: boolean;
116
+ open: boolean;
117
+ mounted: boolean;
118
+ openEpoch: number;
119
+ disabled: boolean;
120
+ readOnly: boolean;
121
+ invalid: boolean;
122
+ required: boolean;
123
+ size: "large" | "medium";
124
+ placement: SelectPlacement;
125
+ gutter: number;
126
+ overflowPadding: number;
127
+ isOpenRef: React.MutableRefObject<boolean>;
128
+ openEpochRef: React.MutableRefObject<number>;
129
+ triggerRef: React.MutableRefObject<NodesRef | null>;
130
+ triggerHandlers: SelectTriggerHandlers;
131
+ setTriggerHandlers: (handlers: SelectTriggerHandlers) => void;
132
+ classes: SelectClassNames;
133
+ positioned: boolean;
134
+ setPositioned: (positioned: boolean) => void;
135
+ requestOpen: (open: boolean, details: SelectOpenChangeDetails) => void;
136
+ finishClose: (immediate?: boolean) => void;
137
+ selectValue: (value: string, event: BaseEvent) => void;
138
+ registerOption: (value: string, entry: SelectOptionEntry) => void;
139
+ unregisterOption: (value: string) => void;
140
+ }
141
+
142
+ interface SelectItemContextValue {
143
+ label: React.ReactNode;
144
+ prefixIcon?: React.ReactNode;
145
+ selected: boolean;
146
+ disabled: boolean;
147
+ pressed: boolean;
148
+ classes: SelectItemClassNames;
149
+ }
150
+
151
+ const SelectContext = React.createContext<SelectContextValue | null>(null);
152
+ const SelectItemContext = React.createContext<SelectItemContextValue | null>(null);
153
+ const SelectGroupPositionContext = React.createContext({ isFirst: true });
154
+
155
+ function useSelectContext(consumer: string): SelectContextValue {
156
+ const context = React.useContext(SelectContext);
157
+ if (!context) throw new Error(`<${consumer}/> must be rendered inside <SelectRoot/>.`);
158
+ return context;
159
+ }
160
+
161
+ function useSelectItemContext(consumer: string): SelectItemContextValue {
162
+ const context = React.useContext(SelectItemContext);
163
+ if (!context) throw new Error(`<${consumer}/> must be rendered inside <SelectItem/>.`);
164
+ return context;
165
+ }
166
+
167
+ function mergeNodeRef(forwardedRef: React.ForwardedRef<unknown>, node: NodesRef | null) {
168
+ if (typeof forwardedRef === "function") {
169
+ forwardedRef(node);
170
+ } else if (forwardedRef) {
171
+ forwardedRef.current = node;
172
+ }
173
+ }
174
+
175
+ function getScreenRect(): SelectRect | null {
176
+ const systemInfo = typeof SystemInfo === "undefined" ? undefined : SystemInfo;
177
+ const pixelWidth = systemInfo?.pixelWidth;
178
+ const pixelHeight = systemInfo?.pixelHeight;
179
+ const pixelRatio = systemInfo?.pixelRatio;
180
+ if (
181
+ typeof pixelWidth !== "number" ||
182
+ typeof pixelHeight !== "number" ||
183
+ typeof pixelRatio !== "number" ||
184
+ pixelWidth <= 0 ||
185
+ pixelHeight <= 0 ||
186
+ pixelRatio <= 0
187
+ ) {
188
+ return null;
189
+ }
190
+
191
+ const width = pixelWidth / pixelRatio;
192
+ const height = pixelHeight / pixelRatio;
193
+ return { left: 0, top: 0, right: width, bottom: height, width, height };
194
+ }
195
+
196
+ function getRootRect() {
197
+ "background only";
198
+ return getRectByRef({ current: lynx.createSelectorQuery().selectRoot() }, true);
199
+ }
200
+
201
+ function toPixel(value: number) {
202
+ return `${value}px`;
203
+ }
204
+
205
+ function areStylesEqual(left: object | undefined, right: object | undefined) {
206
+ if (left === right) return true;
207
+ if (!left || !right) return false;
208
+ const leftEntries = Object.entries(left);
209
+ if (leftEntries.length !== Object.keys(right).length) return false;
210
+ return leftEntries.every(([key, value]) =>
211
+ Object.is(value, (right as Record<string, unknown>)[key]),
212
+ );
213
+ }
214
+
215
+ function hasExitTransition(event: Parameters<NativeTransitionHandler>[0]): boolean {
216
+ if (event.target.uid !== event.currentTarget.uid) return false;
217
+ return (
218
+ event.params.animation_type === "transition-opacity" ||
219
+ event.params.animation_name === "opacity"
220
+ );
221
+ }
222
+
223
+ function isSameValue(a: string[], b: string[]) {
224
+ return a.length === b.length && a.every((entry, index) => entry === b[index]);
225
+ }
226
+
227
+ function renderTextContent(
228
+ className: string,
229
+ value: React.ReactNode,
230
+ ref: React.ForwardedRef<unknown>,
231
+ nativeProps: Omit<LynxStyledElementProps, "children" | "className">,
232
+ ) {
233
+ if (typeof value === "string" || typeof value === "number") {
234
+ return (
235
+ <text {...(ref ? { ref: ref as LynxTextRef } : {})} className={className} {...nativeProps}>
236
+ {value}
237
+ </text>
238
+ );
239
+ }
240
+
241
+ return (
242
+ <view {...(ref ? { ref: ref as LynxViewRef } : {})} className={className} {...nativeProps}>
243
+ {value}
244
+ </view>
245
+ );
246
+ }
247
+
248
+ ////////////////////////////////////////////////////////////////////////////////////
249
+
250
+ /**
251
+ * @platform Lynx
252
+ *
253
+ * Web-only DOM APIs are intentionally omitted: `asChild`, hidden native select/name/form
254
+ * submission, DOM focus/typeahead/keyboard navigation, and ARIA ids all rely on a browser DOM.
255
+ * Lynx uses native tap, overlay dismissal, `accessibility-*` attributes, and app-owned state.
256
+ */
257
+ export interface SelectRootProps
258
+ extends SelectPublicVariantProps,
259
+ SelectTriggerPublicVariantProps,
260
+ SelectItemPublicVariantProps,
261
+ LynxStyledElementProps {
262
+ value?: string[];
263
+ defaultValue?: string[];
264
+ onValueChange?: (value: string[]) => void;
265
+ multiple?: boolean;
266
+ open?: boolean;
267
+ defaultOpen?: boolean;
268
+ onOpenChange?: (open: boolean, details: SelectOpenChangeDetails) => void;
269
+ disabled?: boolean;
270
+ readOnly?: boolean;
271
+ invalid?: boolean;
272
+ required?: boolean;
273
+ placement?: SelectPlacement;
274
+ gutter?: number;
275
+ overflowPadding?: number;
276
+ formatValue?: (items: SelectSelectedItem[]) => React.ReactNode;
277
+ }
278
+
279
+ export const SelectRoot = React.forwardRef<unknown, SelectRootProps>((props, ref) => {
280
+ const screenRect = getScreenRect();
281
+ const [variantProps, otherProps] = select.splitVariantProps({
282
+ ...props,
283
+ size:
284
+ props.size === "responsive"
285
+ ? screenRect?.width != null && screenRect.width >= 1280
286
+ ? "medium"
287
+ : "large"
288
+ : props.size,
289
+ });
290
+ const { size = "large", open: openProp } = variantProps;
291
+ const {
292
+ children,
293
+ className,
294
+ value: valueProp,
295
+ defaultValue = EMPTY_VALUE,
296
+ onValueChange,
297
+ multiple = false,
298
+ defaultOpen = false,
299
+ onOpenChange,
300
+ disabled: disabledProp,
301
+ readOnly: readOnlyProp,
302
+ invalid: invalidProp,
303
+ required: requiredProp,
304
+ placement = "bottom",
305
+ gutter = selectGutter,
306
+ overflowPadding = selectOverflowPadding,
307
+ formatValue,
308
+ ...nativeProps
309
+ } = otherProps;
310
+ const fieldContext = useFieldContext({ strict: false });
311
+ const disabled = disabledProp ?? fieldContext?.disabled ?? false;
312
+ const readOnly = readOnlyProp ?? fieldContext?.readOnly ?? false;
313
+ const invalid = invalidProp ?? fieldContext?.invalid ?? false;
314
+ const required = requiredProp ?? fieldContext?.required ?? false;
315
+ const [value, setValueState] = useControllableState({
316
+ value: valueProp,
317
+ defaultValue,
318
+ onChange: onValueChange,
319
+ });
320
+ const [open, setOpenState] = useControllableState({
321
+ value: openProp,
322
+ defaultValue: defaultOpen,
323
+ });
324
+ const [optionRegistry, setOptionRegistry] = React.useState<
325
+ ReadonlyMap<string, SelectOptionEntry>
326
+ >(() => new Map());
327
+ const [mounted, setMounted] = React.useState(open);
328
+ const [positionedEpoch, setPositionedEpoch] = React.useState<number | null>(null);
329
+ const isOpenRef = React.useRef(open);
330
+ const openEpochRef = React.useRef(0);
331
+ if (open && !isOpenRef.current) openEpochRef.current++;
332
+ isOpenRef.current = open;
333
+ const openEpoch = openEpochRef.current;
334
+ const positioned = positionedEpoch === openEpoch;
335
+ const [triggerHandlers, setTriggerHandlers] = React.useState<SelectTriggerHandlers>({});
336
+ const triggerRef = React.useRef<NodesRef | null>(null);
337
+ const classes = select({ ...variantProps, open, positioned });
338
+
339
+ const selectedItems = React.useMemo(
340
+ () =>
341
+ value.map((optionValue): SelectSelectedItem => {
342
+ const entry = optionRegistry.get(optionValue);
343
+ return entry
344
+ ? { ...entry, value: optionValue, resolved: true }
345
+ : { value: optionValue, label: null, textValue: "", resolved: false };
346
+ }),
347
+ [optionRegistry, value],
348
+ );
349
+ const selectedOption = value.length === 1 ? optionRegistry.get(value[0] ?? "") : undefined;
350
+ const selectedItem = selectedOption
351
+ ? { ...selectedOption, value: value[0] ?? "", resolved: true }
352
+ : undefined;
353
+ const showPlaceholder =
354
+ value.length === 0 ||
355
+ (optionRegistry.size > 0 && selectedItems.every((item) => !item.resolved));
356
+ const displayValue = showPlaceholder
357
+ ? undefined
358
+ : formatValue
359
+ ? formatValue(selectedItems)
360
+ : selectedItems
361
+ .filter((item) => item.resolved)
362
+ .map((item) => item.textValue)
363
+ .join(", ");
364
+
365
+ React.useEffect(() => {
366
+ "background only";
367
+ if (open) {
368
+ setMounted(true);
369
+ return;
370
+ }
371
+ if (!positioned) setMounted(false);
372
+ }, [open, positioned]);
373
+
374
+ const setPositioned = React.useCallback(
375
+ (nextPositioned: boolean) => setPositionedEpoch(nextPositioned ? openEpochRef.current : null),
376
+ [],
377
+ );
378
+ const requestOpen = React.useCallback(
379
+ (nextOpen: boolean, details: SelectOpenChangeDetails) => {
380
+ "background only";
381
+ if (nextOpen && (disabled || readOnly)) return;
382
+ if (nextOpen === open) return;
383
+ setOpenState(nextOpen);
384
+ onOpenChange?.(nextOpen, details);
385
+ },
386
+ [disabled, onOpenChange, open, readOnly, setOpenState],
387
+ );
388
+ const finishClose = React.useCallback((immediate = false) => {
389
+ "background only";
390
+ if (immediate || !isOpenRef.current) {
391
+ setMounted(false);
392
+ setPositionedEpoch(null);
393
+ }
394
+ }, []);
395
+ const selectValue = React.useCallback(
396
+ (nextValue: string, event: BaseEvent) => {
397
+ "background only";
398
+ if (disabled || readOnly) return;
399
+ const next = multiple
400
+ ? value.includes(nextValue)
401
+ ? value.filter((entry) => entry !== nextValue)
402
+ : [...value, nextValue]
403
+ : [nextValue];
404
+ if (!isSameValue(value, next)) setValueState(next);
405
+ if (!multiple) requestOpen(false, { reason: "itemSelect", event });
406
+ },
407
+ [disabled, multiple, readOnly, requestOpen, setValueState, value],
408
+ );
409
+ const registerOption = React.useCallback((optionValue: string, entry: SelectOptionEntry) => {
410
+ setOptionRegistry((current) => {
411
+ const previous = current.get(optionValue);
412
+ if (
413
+ previous &&
414
+ previous.label === entry.label &&
415
+ previous.textValue === entry.textValue &&
416
+ previous.prefixIcon === entry.prefixIcon &&
417
+ previous.node === entry.node
418
+ ) {
419
+ return current;
420
+ }
421
+ return new Map(current).set(optionValue, entry);
422
+ });
423
+ }, []);
424
+ const unregisterOption = React.useCallback((optionValue: string) => {
425
+ setOptionRegistry((current) => {
426
+ if (!current.has(optionValue)) return current;
427
+ const next = new Map(current);
428
+ next.delete(optionValue);
429
+ return next;
430
+ });
431
+ }, []);
432
+
433
+ const contextValue = React.useMemo<SelectContextValue>(
434
+ () => ({
435
+ value,
436
+ selectedItems,
437
+ selectedItem,
438
+ displayValue,
439
+ showPlaceholder,
440
+ multiple,
441
+ open,
442
+ mounted,
443
+ openEpoch,
444
+ disabled,
445
+ readOnly,
446
+ invalid,
447
+ required,
448
+ size,
449
+ placement,
450
+ gutter,
451
+ overflowPadding,
452
+ isOpenRef,
453
+ openEpochRef,
454
+ triggerRef,
455
+ triggerHandlers,
456
+ setTriggerHandlers,
457
+ classes,
458
+ positioned,
459
+ setPositioned,
460
+ requestOpen,
461
+ finishClose,
462
+ selectValue,
463
+ registerOption,
464
+ unregisterOption,
465
+ }),
466
+ [
467
+ classes,
468
+ disabled,
469
+ displayValue,
470
+ finishClose,
471
+ gutter,
472
+ invalid,
473
+ mounted,
474
+ multiple,
475
+ open,
476
+ openEpoch,
477
+ overflowPadding,
478
+ placement,
479
+ positioned,
480
+ readOnly,
481
+ registerOption,
482
+ requestOpen,
483
+ required,
484
+ selectValue,
485
+ selectedItem,
486
+ selectedItems,
487
+ setPositioned,
488
+ showPlaceholder,
489
+ size,
490
+ triggerHandlers,
491
+ unregisterOption,
492
+ value,
493
+ ],
494
+ );
495
+
496
+ return (
497
+ <SelectContext.Provider value={contextValue}>
498
+ <view {...(ref ? { ref: ref as LynxViewRef } : {})} className={className} {...nativeProps}>
499
+ {children}
500
+ </view>
501
+ </SelectContext.Provider>
502
+ );
503
+ });
504
+ SelectRoot.displayName = "SelectRoot";
505
+
506
+ ////////////////////////////////////////////////////////////////////////////////////
507
+
508
+ export interface SelectTriggerProps
509
+ extends SelectTriggerPublicVariantProps,
510
+ LynxStyledElementProps,
511
+ LynxPressableProps,
512
+ LynxAccessibilityProps {
513
+ placeholder?: React.ReactNode;
514
+ prefixIcon?: React.ReactNode;
515
+ suffixIcon?: React.ReactNode;
516
+ }
517
+
518
+ export const SelectTrigger = React.forwardRef<unknown, SelectTriggerProps>((props, ref) => {
519
+ const {
520
+ children,
521
+ className,
522
+ placeholder,
523
+ prefixIcon,
524
+ suffixIcon,
525
+ bindtap,
526
+ "main-thread:bindtap": mainThreadBindtap,
527
+ "accessibility-element": accessibilityElement = true,
528
+ "accessibility-label": accessibilityLabel,
529
+ "accessibility-traits": accessibilityTraits,
530
+ ...nativeProps
531
+ } = props;
532
+ const context = useSelectContext("SelectTrigger");
533
+ const nonInteractive = context.disabled || context.readOnly;
534
+ const handleTap = React.useCallback<NativeTapHandler>(
535
+ (event, instance) => {
536
+ "background only";
537
+ context.requestOpen(!context.open, { reason: "trigger", event });
538
+ bindtap?.(event, instance);
539
+ },
540
+ [bindtap, context.open, context.requestOpen],
541
+ );
542
+ const {
543
+ pressed,
544
+ bindtap: proxyBindtap,
545
+ ...pressHandlers
546
+ } = usePressTap({
547
+ disabled: nonInteractive,
548
+ onTap: handleTap,
549
+ mainThreadOnTap: mainThreadBindtap,
550
+ });
551
+ const mainThreadProxyBindtap = pressHandlers["main-thread:bindtap"];
552
+ React.useEffect(() => {
553
+ "background only";
554
+ context.setTriggerHandlers({
555
+ bindtap: proxyBindtap,
556
+ "main-thread:bindtap": mainThreadProxyBindtap,
557
+ });
558
+ return () => {
559
+ context.setTriggerHandlers({});
560
+ };
561
+ }, [context.setTriggerHandlers, mainThreadProxyBindtap, proxyBindtap]);
562
+ const handleRef = React.useCallback(
563
+ (node: NodesRef | null) => {
564
+ context.triggerRef.current = node;
565
+ mergeNodeRef(ref, node);
566
+ },
567
+ [context.triggerRef, ref],
568
+ );
569
+ const classes = selectTrigger({
570
+ size: context.size,
571
+ open: context.open,
572
+ pressed,
573
+ disabled: context.disabled,
574
+ readOnly: context.readOnly,
575
+ invalid: context.invalid,
576
+ });
577
+
578
+ return (
579
+ <view
580
+ ref={handleRef as LynxViewRef}
581
+ className={clsx(classes.root, className)}
582
+ accessibility-element={accessibilityElement}
583
+ accessibility-label={accessibilityLabel}
584
+ accessibility-role-description="button"
585
+ accessibility-value={context.open ? "expanded" : "collapsed"}
586
+ accessibility-traits={nonInteractive ? "disabled" : (accessibilityTraits ?? "button")}
587
+ {...nativeProps}
588
+ {...pressHandlers}
589
+ bindtap={proxyBindtap}
590
+ >
591
+ <view className={classes.pressedOverlay} accessibility-elements-hidden={true} />
592
+ {children ?? (
593
+ <>
594
+ <SelectPrefixIcon fallback={prefixIcon} />
595
+ <SelectValue />
596
+ <SelectPlaceholder>{placeholder}</SelectPlaceholder>
597
+ <SelectSuffixIcon icon={suffixIcon} />
598
+ </>
599
+ )}
600
+ </view>
601
+ );
602
+ });
603
+ SelectTrigger.displayName = "SelectTrigger";
604
+
605
+ ////////////////////////////////////////////////////////////////////////////////////
606
+
607
+ export interface SelectValueProps extends LynxStyledElementProps {}
608
+
609
+ export const SelectValue = React.forwardRef<unknown, SelectValueProps>((props, ref) => {
610
+ const { children, className, ...nativeProps } = props;
611
+ const context = useSelectContext("SelectValue");
612
+ if (context.showPlaceholder) return null;
613
+ return renderTextContent(
614
+ clsx(
615
+ selectTrigger({
616
+ size: context.size,
617
+ open: context.open,
618
+ disabled: context.disabled,
619
+ readOnly: context.readOnly,
620
+ invalid: context.invalid,
621
+ }).value,
622
+ className,
623
+ ),
624
+ children ?? context.displayValue,
625
+ ref,
626
+ nativeProps,
627
+ );
628
+ });
629
+ SelectValue.displayName = "SelectValue";
630
+
631
+ export interface SelectPlaceholderProps extends LynxStyledElementProps {}
632
+
633
+ export const SelectPlaceholder = React.forwardRef<unknown, SelectPlaceholderProps>((props, ref) => {
634
+ const { children, className, ...nativeProps } = props;
635
+ const context = useSelectContext("SelectPlaceholder");
636
+ if (!context.showPlaceholder) return null;
637
+ return renderTextContent(
638
+ clsx(
639
+ selectTrigger({
640
+ size: context.size,
641
+ open: context.open,
642
+ disabled: context.disabled,
643
+ readOnly: context.readOnly,
644
+ invalid: context.invalid,
645
+ }).placeholder,
646
+ className,
647
+ ),
648
+ children,
649
+ ref,
650
+ nativeProps,
651
+ );
652
+ });
653
+ SelectPlaceholder.displayName = "SelectPlaceholder";
654
+
655
+ export interface SelectPrefixIconProps extends Omit<InternalIconProps, "icon" | "deps"> {
656
+ fallback?: React.ReactNode;
657
+ }
658
+
659
+ export const SelectPrefixIcon = React.forwardRef<unknown, SelectPrefixIconProps>((props, ref) => {
660
+ const { fallback, className, ...nativeProps } = props;
661
+ const context = useSelectContext("SelectPrefixIcon");
662
+ const icon = context.selectedItem?.prefixIcon ?? fallback;
663
+ if (!isValidElement<LynxIconElementProps>(icon)) return null;
664
+ const classes = selectTrigger({
665
+ size: context.size,
666
+ open: context.open,
667
+ disabled: context.disabled,
668
+ readOnly: context.readOnly,
669
+ invalid: context.invalid,
670
+ });
671
+ return (
672
+ <InternalIcon
673
+ ref={ref}
674
+ icon={icon}
675
+ className={clsx(classes.prefixIcon, className)}
676
+ accessibility-elements-hidden={true}
677
+ deps={[context.selectedItem?.prefixIcon, context.value]}
678
+ {...nativeProps}
679
+ />
680
+ );
681
+ });
682
+ SelectPrefixIcon.displayName = "SelectPrefixIcon";
683
+
684
+ export interface SelectSuffixIconProps extends Omit<InternalIconProps, "icon" | "deps"> {
685
+ icon?: React.ReactNode;
686
+ }
687
+
688
+ export const SelectSuffixIcon = React.forwardRef<unknown, SelectSuffixIconProps>((props, ref) => {
689
+ const { icon, className, ...nativeProps } = props;
690
+ const context = useSelectContext("SelectSuffixIcon");
691
+ const classes = selectTrigger({
692
+ size: context.size,
693
+ open: context.open,
694
+ disabled: context.disabled,
695
+ readOnly: context.readOnly,
696
+ invalid: context.invalid,
697
+ });
698
+ if (!isValidElement<LynxIconElementProps>(icon)) {
699
+ return (
700
+ <text
701
+ {...(ref ? { ref: ref as LynxTextRef } : {})}
702
+ className={clsx(classes.suffixIcon, className)}
703
+ accessibility-elements-hidden={true}
704
+ {...nativeProps}
705
+ >
706
+ ⌄
707
+ </text>
708
+ );
709
+ }
710
+ return (
711
+ <InternalIcon
712
+ ref={ref}
713
+ icon={icon}
714
+ className={clsx(classes.suffixIcon, className)}
715
+ accessibility-elements-hidden={true}
716
+ deps={[context.open]}
717
+ {...nativeProps}
718
+ />
719
+ );
720
+ });
721
+ SelectSuffixIcon.displayName = "SelectSuffixIcon";
722
+
723
+ ////////////////////////////////////////////////////////////////////////////////////
724
+
725
+ export interface SelectContentProps extends LynxStyledElementProps {}
726
+
727
+ export const SelectContent = React.forwardRef<unknown, SelectContentProps>((props, ref) => {
728
+ const { children, className, style, ...nativeProps } = props;
729
+ const context = useSelectContext("SelectContent");
730
+ const measurementVersionRef = React.useRef(0);
731
+ const intrinsicMeasurementVersionRef = React.useRef(0);
732
+ const scrollRequestVersionRef = React.useRef(0);
733
+ const scrolledEpochRef = React.useRef<number | null>(null);
734
+ // Snapshot refs can be recreated on each patch. Their current native node belongs in a ref;
735
+ // the one-time attachment state merely starts effects that require an attached node.
736
+ const overlayNodeRef = React.useRef<NodesRef | null>(null);
737
+ const scrollNodeRef = React.useRef<NodesRef | null>(null);
738
+ const scrollContentNodeRef = React.useRef<NodesRef | null>(null);
739
+ const overlayAttachedRef = React.useRef(false);
740
+ const scrollAttachedRef = React.useRef(false);
741
+ const scrollContentAttachedRef = React.useRef(false);
742
+ const [overlayAttached, setOverlayAttached] = React.useState(false);
743
+ const [scrollAttached, setScrollAttached] = React.useState(false);
744
+ const [scrollContentAttached, setScrollContentAttached] = React.useState(false);
745
+ const [scrollAreaId, setScrollAreaId] = React.useState<string | undefined>(undefined);
746
+ React.useEffect(() => {
747
+ "background only";
748
+ // Generate once on the background thread, then share the id through state.
749
+ setScrollAreaId(`seed-select-scroll-area-${nextSelectScrollAreaId++}`);
750
+ }, []);
751
+ const measurementConfigRef = React.useRef(0);
752
+ const configuredClassNameRef = React.useRef(className);
753
+ const configuredSizeRef = React.useRef(context.size);
754
+ const configuredStyleRef = React.useRef(style);
755
+ const [intrinsicSize, setIntrinsicSize] = React.useState<{
756
+ width: number;
757
+ height: number;
758
+ epoch: number;
759
+ config: number;
760
+ } | null>(null);
761
+ const [position, setPosition] = React.useState<SelectPosition | null>(null);
762
+ const [widthConstraint, setWidthConstraint] = React.useState<number | null>(null);
763
+ const [overlayRect, setOverlayRect] = React.useState<SelectRect | null>(null);
764
+ const [triggerRect, setTriggerRect] = React.useState<SelectRect | null>(null);
765
+ const selectedNode = context.selectedItem?.node;
766
+
767
+ const measurePosition = React.useCallback(async () => {
768
+ "background only";
769
+ const referenceNode = context.triggerRef.current;
770
+ const openEpoch = context.openEpoch;
771
+ const overlayNode = overlayNodeRef.current;
772
+ if (
773
+ !context.open ||
774
+ !referenceNode ||
775
+ !overlayNode ||
776
+ !intrinsicSize ||
777
+ intrinsicSize.epoch !== openEpoch ||
778
+ intrinsicSize.config !== measurementConfigRef.current
779
+ ) {
780
+ return;
781
+ }
782
+ const version = ++measurementVersionRef.current;
783
+ try {
784
+ const [reference, boundary, overlay] = await Promise.all([
785
+ getRectByRef({ current: referenceNode }, true),
786
+ getRootRect(),
787
+ getRectByRef({ current: overlayNode }, true),
788
+ ]);
789
+ if (
790
+ version !== measurementVersionRef.current ||
791
+ !context.isOpenRef.current ||
792
+ openEpoch !== context.openEpochRef.current
793
+ ) {
794
+ return;
795
+ }
796
+ const width = widthConstraint ?? reference.width;
797
+ const nextPosition = await computePosition({
798
+ reference,
799
+ boundary,
800
+ width,
801
+ height: Math.min(intrinsicSize.height, selectMaxHeight),
802
+ placement: context.placement,
803
+ gutter: context.gutter,
804
+ overflowPadding: context.overflowPadding,
805
+ flip: { fallbackStrategy: "bestFit" },
806
+ shift: { crossAxis: true },
807
+ size: { order: "beforeFlip", minimumHeight: selectMinimumAvailableHeight },
808
+ });
809
+ if (
810
+ version !== measurementVersionRef.current ||
811
+ !context.isOpenRef.current ||
812
+ openEpoch !== context.openEpochRef.current
813
+ ) {
814
+ return;
815
+ }
816
+ if (nextPosition.availableWidth < width) {
817
+ const constrainedWidth = nextPosition.availableWidth;
818
+ if (widthConstraint !== constrainedWidth) {
819
+ measurementVersionRef.current++;
820
+ measurementConfigRef.current++;
821
+ intrinsicMeasurementVersionRef.current++;
822
+ scrollRequestVersionRef.current++;
823
+ setWidthConstraint(constrainedWidth);
824
+ setIntrinsicSize(null);
825
+ setPosition(null);
826
+ setOverlayRect(null);
827
+ setTriggerRect(null);
828
+ context.setPositioned(false);
829
+ }
830
+ return;
831
+ }
832
+ setPosition((previous) => {
833
+ if (
834
+ previous?.left === nextPosition.left &&
835
+ previous.top === nextPosition.top &&
836
+ previous.width === nextPosition.width &&
837
+ previous.height === nextPosition.height &&
838
+ previous.placement === nextPosition.placement
839
+ ) {
840
+ return previous;
841
+ }
842
+ return nextPosition;
843
+ });
844
+ setOverlayRect(overlay);
845
+ setTriggerRect(reference);
846
+ context.setPositioned(true);
847
+ } catch {
848
+ // Native nodes can disappear while a selector query is in flight; keep the popup hidden.
849
+ }
850
+ }, [
851
+ context.gutter,
852
+ context.isOpenRef,
853
+ context.open,
854
+ context.openEpoch,
855
+ context.openEpochRef,
856
+ context.overflowPadding,
857
+ context.placement,
858
+ context.setPositioned,
859
+ context.triggerRef,
860
+ intrinsicSize,
861
+ overlayAttached,
862
+ widthConstraint,
863
+ ]);
864
+
865
+ const measureIntrinsicSize = React.useCallback(async () => {
866
+ "background only";
867
+ const scrollNode = scrollNodeRef.current;
868
+ const scrollContentNode = scrollContentNodeRef.current;
869
+ const openEpoch = context.openEpochRef.current;
870
+ const config = measurementConfigRef.current;
871
+ if (!context.isOpenRef.current || !scrollAreaId || !scrollNode || !scrollContentNode) {
872
+ return;
873
+ }
874
+ const version = ++intrinsicMeasurementVersionRef.current;
875
+ try {
876
+ const rect = await getRectByRef({ current: scrollContentNode }, false, scrollAreaId);
877
+ if (
878
+ version !== intrinsicMeasurementVersionRef.current ||
879
+ !context.isOpenRef.current ||
880
+ openEpoch !== context.openEpochRef.current ||
881
+ config !== measurementConfigRef.current
882
+ ) {
883
+ return;
884
+ }
885
+ setIntrinsicSize((current) => {
886
+ if (
887
+ current?.width === rect.width &&
888
+ current.height === rect.height &&
889
+ current.epoch === openEpoch &&
890
+ current.config === config
891
+ ) {
892
+ return current;
893
+ }
894
+ return { width: rect.width, height: rect.height, epoch: openEpoch, config };
895
+ });
896
+ } catch {
897
+ // The hidden overlay may release either node before the native query resolves.
898
+ }
899
+ }, [context.isOpenRef, context.openEpochRef, scrollAreaId]);
900
+
901
+ React.useEffect(() => {
902
+ "background only";
903
+ measurementVersionRef.current++;
904
+ intrinsicMeasurementVersionRef.current++;
905
+ scrollRequestVersionRef.current++;
906
+ setWidthConstraint(null);
907
+ setPosition(null);
908
+ setOverlayRect(null);
909
+ setTriggerRect(null);
910
+ setIntrinsicSize((current) => (current ? { ...current, epoch: context.openEpoch } : current));
911
+ scrolledEpochRef.current = null;
912
+ if (scrollNodeRef.current && scrollContentNodeRef.current) void measureIntrinsicSize();
913
+ }, [context.openEpoch, measureIntrinsicSize]);
914
+ React.useEffect(() => {
915
+ "background only";
916
+ if (context.open) return;
917
+ measurementVersionRef.current++;
918
+ intrinsicMeasurementVersionRef.current++;
919
+ scrollRequestVersionRef.current++;
920
+ }, [context.open]);
921
+ React.useEffect(() => {
922
+ "background only";
923
+ return () => {
924
+ measurementVersionRef.current++;
925
+ intrinsicMeasurementVersionRef.current++;
926
+ scrollRequestVersionRef.current++;
927
+ };
928
+ }, []);
929
+
930
+ React.useEffect(() => {
931
+ "background only";
932
+ if (
933
+ configuredClassNameRef.current === className &&
934
+ configuredSizeRef.current === context.size &&
935
+ areStylesEqual(configuredStyleRef.current, style)
936
+ ) {
937
+ return;
938
+ }
939
+ measurementVersionRef.current++;
940
+ intrinsicMeasurementVersionRef.current++;
941
+ scrollRequestVersionRef.current++;
942
+ measurementConfigRef.current++;
943
+ configuredClassNameRef.current = className;
944
+ configuredSizeRef.current = context.size;
945
+ configuredStyleRef.current = style;
946
+ setWidthConstraint(null);
947
+ setIntrinsicSize(null);
948
+ setPosition(null);
949
+ setOverlayRect(null);
950
+ setTriggerRect(null);
951
+ context.setPositioned(false);
952
+ if (scrollNodeRef.current && scrollContentNodeRef.current) void measureIntrinsicSize();
953
+ }, [className, context.setPositioned, context.size, measureIntrinsicSize, style]);
954
+
955
+ React.useEffect(() => {
956
+ "background only";
957
+ if (scrollAttached && scrollContentAttached) void measureIntrinsicSize();
958
+ }, [measureIntrinsicSize, scrollAttached, scrollContentAttached]);
959
+
960
+ React.useEffect(() => {
961
+ "background only";
962
+ if (context.open) void measurePosition();
963
+ }, [context.open, measurePosition]);
964
+
965
+ React.useEffect(() => {
966
+ "background only";
967
+ if (context.open && widthConstraint != null) void measureIntrinsicSize();
968
+ }, [context.open, measureIntrinsicSize, widthConstraint]);
969
+ React.useEffect(() => {
970
+ "background only";
971
+ const requestVersion = ++scrollRequestVersionRef.current;
972
+ const openEpoch = context.openEpoch;
973
+ const selected = context.selectedItem;
974
+ const scrollNode = scrollNodeRef.current;
975
+ if (
976
+ !context.open ||
977
+ !context.positioned ||
978
+ !position ||
979
+ !intrinsicSize ||
980
+ intrinsicSize.epoch !== openEpoch ||
981
+ intrinsicSize.config !== measurementConfigRef.current ||
982
+ !selected?.node ||
983
+ !scrollNode ||
984
+ !scrollAreaId ||
985
+ scrolledEpochRef.current === openEpoch
986
+ ) {
987
+ return;
988
+ }
989
+ if (intrinsicSize.height <= position.height) {
990
+ scrolledEpochRef.current = openEpoch;
991
+ return;
992
+ }
993
+ const entry = selected.node;
994
+ void getRectByRef({ current: entry }, false, scrollAreaId)
995
+ .then((item) => {
996
+ if (
997
+ requestVersion !== scrollRequestVersionRef.current ||
998
+ !context.isOpenRef.current ||
999
+ openEpoch !== context.openEpochRef.current ||
1000
+ scrolledEpochRef.current === openEpoch
1001
+ ) {
1002
+ return;
1003
+ }
1004
+ const offset =
1005
+ item.top < 0
1006
+ ? item.top
1007
+ : item.bottom > position.height
1008
+ ? item.bottom - position.height
1009
+ : 0;
1010
+ if (offset === 0) {
1011
+ scrolledEpochRef.current = openEpoch;
1012
+ return;
1013
+ }
1014
+ try {
1015
+ scrollNode.invoke({ method: "scrollBy", params: { offset } }).exec();
1016
+ scrolledEpochRef.current = openEpoch;
1017
+ } catch {
1018
+ // Test refs do not implement native UI methods.
1019
+ }
1020
+ })
1021
+ .catch(() => {
1022
+ // The entry can be replaced before its native bounds are available.
1023
+ });
1024
+ }, [
1025
+ context.isOpenRef,
1026
+ context.open,
1027
+ context.openEpoch,
1028
+ context.openEpochRef,
1029
+ context.positioned,
1030
+ context.selectedItem,
1031
+ intrinsicSize,
1032
+ position,
1033
+ scrollAreaId,
1034
+ scrollAttached,
1035
+ selectedNode,
1036
+ ]);
1037
+
1038
+ const handleShowOverlay = React.useCallback(() => {
1039
+ "background only";
1040
+ void measureIntrinsicSize();
1041
+ void measurePosition();
1042
+ }, [measureIntrinsicSize, measurePosition]);
1043
+ const handleRef = React.useCallback(
1044
+ (node: NodesRef | null) => {
1045
+ mergeNodeRef(ref, node);
1046
+ },
1047
+ [ref],
1048
+ );
1049
+ const handlePositionerRef = React.useCallback((node: NodesRef | null) => {
1050
+ overlayNodeRef.current = node;
1051
+ if (node && !overlayAttachedRef.current) {
1052
+ overlayAttachedRef.current = true;
1053
+ setOverlayAttached(true);
1054
+ }
1055
+ }, []);
1056
+ const handleScrollRef = React.useCallback((node: NodesRef | null) => {
1057
+ "background only";
1058
+ scrollNodeRef.current = node;
1059
+ if (node && !scrollAttachedRef.current) {
1060
+ scrollAttachedRef.current = true;
1061
+ setScrollAttached(true);
1062
+ }
1063
+ }, []);
1064
+ const handleScrollContentRef = React.useCallback((node: NodesRef | null) => {
1065
+ "background only";
1066
+ scrollContentNodeRef.current = node;
1067
+ if (node && !scrollContentAttachedRef.current) {
1068
+ scrollContentAttachedRef.current = true;
1069
+ setScrollContentAttached(true);
1070
+ }
1071
+ }, []);
1072
+ const handleIntrinsicLayoutChange = React.useCallback<NativeLayoutHandler>(() => {
1073
+ "background only";
1074
+ void measureIntrinsicSize();
1075
+ }, [measureIntrinsicSize]);
1076
+ const handleTransitionEnd = React.useCallback<NativeTransitionHandler>(
1077
+ (event) => {
1078
+ "background only";
1079
+ if (!context.open && hasExitTransition(event)) context.finishClose();
1080
+ },
1081
+ [context],
1082
+ );
1083
+ const handleBackdropTap = React.useCallback<NativeTapHandler>(
1084
+ (event) => {
1085
+ "background only";
1086
+ context.requestOpen(false, { reason: "interactOutside", event });
1087
+ },
1088
+ [context],
1089
+ );
1090
+ const handleNativeDismiss = React.useCallback(
1091
+ (event: BaseEvent) => {
1092
+ "background only";
1093
+ context.requestOpen(false, { reason: "dismiss", event });
1094
+ context.finishClose(true);
1095
+ },
1096
+ [context],
1097
+ );
1098
+ const handleRequestClose = React.useCallback(
1099
+ (event: BaseEvent) => {
1100
+ "background only";
1101
+ context.requestOpen(false, { reason: "dismiss", event });
1102
+ },
1103
+ [context],
1104
+ );
1105
+
1106
+ const geometryStyle =
1107
+ position && overlayRect
1108
+ ? {
1109
+ left: toPixel(position.left - overlayRect.left),
1110
+ top: toPixel(position.top - overlayRect.top),
1111
+ width: toPixel(position.width),
1112
+ transformOrigin: position.transformOrigin,
1113
+ }
1114
+ : undefined;
1115
+ const scrollStyle = position ? { height: toPixel(position.height) } : undefined;
1116
+ const triggerProxyStyle =
1117
+ triggerRect && overlayRect
1118
+ ? {
1119
+ left: toPixel(triggerRect.left - overlayRect.left),
1120
+ top: toPixel(triggerRect.top - overlayRect.top),
1121
+ width: toPixel(triggerRect.width),
1122
+ height: toPixel(triggerRect.height),
1123
+ }
1124
+ : undefined;
1125
+ const groups = toArray(children);
1126
+
1127
+ return (
1128
+ // Keep options mounted while closed so defaultValue can resolve label/icon metadata before open.
1129
+ <overlay
1130
+ visible={context.mounted}
1131
+ style={{ position: "fixed" }}
1132
+ binddismissoverlay={handleNativeDismiss}
1133
+ bindshowoverlay={handleShowOverlay}
1134
+ bindrequestclose={handleRequestClose}
1135
+ >
1136
+ <view ref={handlePositionerRef as LynxViewRef} className={context.classes.positioner}>
1137
+ <view className={context.classes.backdrop} bindtap={handleBackdropTap} />
1138
+ {triggerProxyStyle && (
1139
+ <view
1140
+ style={{ position: "absolute", ...triggerProxyStyle }}
1141
+ bindtap={context.triggerHandlers.bindtap}
1142
+ main-thread:bindtap={context.triggerHandlers["main-thread:bindtap"]}
1143
+ />
1144
+ )}
1145
+ <view
1146
+ ref={handleRef as LynxViewRef}
1147
+ className={clsx(context.classes.content, className)}
1148
+ style={{
1149
+ ...geometryStyle,
1150
+ ...style,
1151
+ ...(widthConstraint != null ? { width: toPixel(widthConstraint) } : {}),
1152
+ }}
1153
+ bindtransitionend={handleTransitionEnd}
1154
+ {...nativeProps}
1155
+ >
1156
+ <scroll-view
1157
+ ref={handleScrollRef as LynxViewRef}
1158
+ id={scrollAreaId}
1159
+ className={context.classes.scrollArea}
1160
+ scroll-orientation="vertical"
1161
+ enable-scroll={Boolean(
1162
+ position &&
1163
+ intrinsicSize &&
1164
+ intrinsicSize.epoch === context.openEpoch &&
1165
+ intrinsicSize.config === measurementConfigRef.current &&
1166
+ intrinsicSize.height > position.height,
1167
+ )}
1168
+ style={scrollStyle}
1169
+ >
1170
+ <view
1171
+ ref={handleScrollContentRef as LynxViewRef}
1172
+ className={context.classes.scrollContent}
1173
+ bindlayoutchange={handleIntrinsicLayoutChange}
1174
+ >
1175
+ {groups.map((group, index) => (
1176
+ <SelectGroupPositionContext.Provider
1177
+ key={React.isValidElement(group) ? (group.key ?? index) : index}
1178
+ value={{ isFirst: index === 0 }}
1179
+ >
1180
+ {group}
1181
+ </SelectGroupPositionContext.Provider>
1182
+ ))}
1183
+ </view>
1184
+ </scroll-view>
1185
+ </view>
1186
+ </view>
1187
+ </overlay>
1188
+ );
1189
+ });
1190
+ SelectContent.displayName = "SelectContent";
1191
+
1192
+ /** The viewport is normally owned by SelectContent; use this only for custom content composition. */
1193
+ export interface SelectScrollAreaProps extends LynxStyledElementProps {}
1194
+
1195
+ export const SelectScrollArea = React.forwardRef<unknown, SelectScrollAreaProps>((props, ref) => {
1196
+ const { children, className, ...nativeProps } = props;
1197
+ const context = useSelectContext("SelectScrollArea");
1198
+ return (
1199
+ <scroll-view
1200
+ {...(ref ? { ref: ref as LynxViewRef } : {})}
1201
+ className={clsx(context.classes.scrollArea, className)}
1202
+ scroll-orientation="vertical"
1203
+ {...nativeProps}
1204
+ >
1205
+ <view className={context.classes.scrollContent}>{children}</view>
1206
+ </scroll-view>
1207
+ );
1208
+ });
1209
+ SelectScrollArea.displayName = "SelectScrollArea";
1210
+
1211
+ ////////////////////////////////////////////////////////////////////////////////////
1212
+
1213
+ export interface SelectGroupProps extends LynxStyledElementProps {}
1214
+
1215
+ export const SelectGroup = React.forwardRef<unknown, SelectGroupProps>((props, ref) => {
1216
+ const { children, className, ...nativeProps } = props;
1217
+ const context = useSelectContext("SelectGroup");
1218
+ const position = React.useContext(SelectGroupPositionContext);
1219
+ return (
1220
+ <view
1221
+ {...(ref ? { ref: ref as LynxViewRef } : {})}
1222
+ className={clsx(context.classes.group, className)}
1223
+ {...nativeProps}
1224
+ >
1225
+ {!position.isFirst ? (
1226
+ <view className={context.classes.separator} accessibility-elements-hidden={true} />
1227
+ ) : null}
1228
+ {children}
1229
+ </view>
1230
+ );
1231
+ });
1232
+ SelectGroup.displayName = "SelectGroup";
1233
+
1234
+ export interface SelectGroupLabelProps extends LynxStyledElementProps, LynxAccessibilityProps {}
1235
+
1236
+ export const SelectGroupLabel = React.forwardRef<unknown, SelectGroupLabelProps>((props, ref) => {
1237
+ const {
1238
+ children,
1239
+ className,
1240
+ "accessibility-heading": accessibilityHeading = true,
1241
+ ...nativeProps
1242
+ } = props;
1243
+ const context = useSelectContext("SelectGroupLabel");
1244
+ return (
1245
+ <text
1246
+ {...(ref ? { ref: ref as LynxTextRef } : {})}
1247
+ className={clsx(context.classes.groupLabel, className)}
1248
+ accessibility-heading={accessibilityHeading}
1249
+ {...nativeProps}
1250
+ >
1251
+ {children}
1252
+ </text>
1253
+ );
1254
+ });
1255
+ SelectGroupLabel.displayName = "SelectGroupLabel";
1256
+
1257
+ ////////////////////////////////////////////////////////////////////////////////////
1258
+
1259
+ export interface SelectItemProps
1260
+ extends SelectItemPublicVariantProps,
1261
+ LynxStyledElementProps,
1262
+ LynxPressableProps,
1263
+ LynxAccessibilityProps {
1264
+ value: string;
1265
+ label?: React.ReactNode;
1266
+ textValue?: string;
1267
+ prefixIcon?: React.ReactNode;
1268
+ disabled?: boolean;
1269
+ readOnly?: boolean;
1270
+ }
1271
+
1272
+ export const SelectItem = React.forwardRef<unknown, SelectItemProps>((props, ref) => {
1273
+ const [variantProps, otherProps] = selectItem.splitVariantProps(props);
1274
+ const { disabled: disabledProp = false } = variantProps;
1275
+ const {
1276
+ value,
1277
+ label,
1278
+ textValue,
1279
+ prefixIcon,
1280
+ readOnly = false,
1281
+ children,
1282
+ className,
1283
+ bindtap,
1284
+ "main-thread:bindtap": mainThreadBindtap,
1285
+ "accessibility-element": accessibilityElement = true,
1286
+ "accessibility-label": accessibilityLabel,
1287
+ "accessibility-traits": accessibilityTraits,
1288
+ "accessibility-value": accessibilityValue,
1289
+ ...nativeProps
1290
+ } = otherProps;
1291
+ const context = useSelectContext("SelectItem");
1292
+ // A fresh snapshot ref is not a semantic option change. Register after its first attachment
1293
+ // without turning every native ref patch into a component state update.
1294
+ const nodeRef = React.useRef<NodesRef | null>(null);
1295
+ const nodeAttachedRef = React.useRef(false);
1296
+ const [nodeAttached, setNodeAttached] = React.useState(false);
1297
+ const disabled = context.disabled || context.readOnly || disabledProp || readOnly;
1298
+ const selected = context.value.includes(value);
1299
+ const resolvedTextValue = textValue ?? (typeof label === "string" ? label : value);
1300
+ const handleTap = React.useCallback<NativeTapHandler>(
1301
+ (event, instance) => {
1302
+ "background only";
1303
+ context.selectValue(value, event);
1304
+ bindtap?.(event, instance);
1305
+ },
1306
+ [bindtap, context, value],
1307
+ );
1308
+ const { pressed, ...pressHandlers } = usePressTap({
1309
+ disabled,
1310
+ onTap: handleTap,
1311
+ mainThreadOnTap: mainThreadBindtap,
1312
+ });
1313
+ const handleRef = React.useCallback(
1314
+ (nextNode: NodesRef | null) => {
1315
+ nodeRef.current = nextNode;
1316
+ if (nextNode && !nodeAttachedRef.current) {
1317
+ nodeAttachedRef.current = true;
1318
+ setNodeAttached(true);
1319
+ }
1320
+ mergeNodeRef(ref, nextNode);
1321
+ },
1322
+ [ref],
1323
+ );
1324
+ React.useEffect(() => {
1325
+ "background only";
1326
+ context.registerOption(value, {
1327
+ label,
1328
+ textValue: resolvedTextValue,
1329
+ prefixIcon,
1330
+ node: nodeRef.current,
1331
+ });
1332
+ return () => {
1333
+ context.unregisterOption(value);
1334
+ };
1335
+ }, [
1336
+ context.registerOption,
1337
+ context.unregisterOption,
1338
+ label,
1339
+ nodeAttached,
1340
+ prefixIcon,
1341
+ resolvedTextValue,
1342
+ value,
1343
+ ]);
1344
+ const classes = selectItem({
1345
+ ...variantProps,
1346
+ size: context.size,
1347
+ disabled,
1348
+ selected,
1349
+ pressed,
1350
+ });
1351
+ const itemContextValue = React.useMemo<SelectItemContextValue>(
1352
+ () => ({ label, prefixIcon, selected, disabled, pressed, classes }),
1353
+ [classes, disabled, label, prefixIcon, pressed, selected],
1354
+ );
1355
+
1356
+ return (
1357
+ <SelectItemContext.Provider value={itemContextValue}>
1358
+ <view
1359
+ ref={handleRef as LynxViewRef}
1360
+ className={clsx(classes.root, className)}
1361
+ accessibility-element={accessibilityElement}
1362
+ accessibility-label={accessibilityLabel ?? resolvedTextValue}
1363
+ accessibility-role-description="option"
1364
+ accessibility-value={accessibilityValue ?? (selected ? "selected" : "not selected")}
1365
+ accessibility-traits={disabled ? "disabled" : accessibilityTraits}
1366
+ {...nativeProps}
1367
+ {...pressHandlers}
1368
+ >
1369
+ <view className={classes.pressedOverlay} accessibility-elements-hidden={true} />
1370
+ {children}
1371
+ </view>
1372
+ </SelectItemContext.Provider>
1373
+ );
1374
+ });
1375
+ SelectItem.displayName = "SelectItem";
1376
+
1377
+ export interface SelectItemPrefixIconProps extends Omit<InternalIconProps, "icon" | "deps"> {
1378
+ icon?: React.ReactNode;
1379
+ }
1380
+
1381
+ export const SelectItemPrefixIcon = React.forwardRef<unknown, SelectItemPrefixIconProps>(
1382
+ (props, ref) => {
1383
+ const { icon: iconOverride, className, ...nativeProps } = props;
1384
+ const context = useSelectItemContext("SelectItemPrefixIcon");
1385
+ const icon = iconOverride ?? context.prefixIcon;
1386
+ if (!isValidElement<LynxIconElementProps>(icon)) return null;
1387
+ return (
1388
+ <InternalIcon
1389
+ ref={ref}
1390
+ icon={icon}
1391
+ className={clsx(context.classes.prefixIcon, className)}
1392
+ accessibility-elements-hidden={true}
1393
+ deps={[context.selected, context.disabled, context.pressed, icon]}
1394
+ {...nativeProps}
1395
+ />
1396
+ );
1397
+ },
1398
+ );
1399
+ SelectItemPrefixIcon.displayName = "SelectItemPrefixIcon";
1400
+
1401
+ export interface SelectItemBodyProps extends LynxStyledElementProps {}
1402
+
1403
+ export const SelectItemBody = React.forwardRef<unknown, SelectItemBodyProps>((props, ref) => {
1404
+ const { children, className, ...nativeProps } = props;
1405
+ const context = useSelectItemContext("SelectItemBody");
1406
+ return (
1407
+ <view
1408
+ {...(ref ? { ref: ref as LynxViewRef } : {})}
1409
+ className={clsx(context.classes.body, className)}
1410
+ {...nativeProps}
1411
+ >
1412
+ {children}
1413
+ </view>
1414
+ );
1415
+ });
1416
+ SelectItemBody.displayName = "SelectItemBody";
1417
+
1418
+ export interface SelectItemLabelProps extends LynxStyledElementProps {}
1419
+
1420
+ export const SelectItemLabel = React.forwardRef<unknown, SelectItemLabelProps>((props, ref) => {
1421
+ const { children, className, ...nativeProps } = props;
1422
+ const context = useSelectItemContext("SelectItemLabel");
1423
+ return renderTextContent(
1424
+ clsx(context.classes.label, className),
1425
+ children ?? context.label,
1426
+ ref,
1427
+ nativeProps,
1428
+ );
1429
+ });
1430
+ SelectItemLabel.displayName = "SelectItemLabel";
1431
+
1432
+ export interface SelectItemDescriptionProps extends LynxStyledElementProps {}
1433
+
1434
+ export const SelectItemDescription = React.forwardRef<unknown, SelectItemDescriptionProps>(
1435
+ (props, ref) => {
1436
+ const { children, className, ...nativeProps } = props;
1437
+ const context = useSelectItemContext("SelectItemDescription");
1438
+ return renderTextContent(
1439
+ clsx(context.classes.description, className),
1440
+ children,
1441
+ ref,
1442
+ nativeProps,
1443
+ );
1444
+ },
1445
+ );
1446
+ SelectItemDescription.displayName = "SelectItemDescription";
1447
+
1448
+ export interface SelectItemIndicatorProps extends Omit<InternalIconProps, "icon" | "deps"> {
1449
+ selected?: React.ReactNode;
1450
+ unselected?: React.ReactNode;
1451
+ }
1452
+
1453
+ export const SelectItemIndicator = React.forwardRef<unknown, SelectItemIndicatorProps>(
1454
+ (props, ref) => {
1455
+ const { selected: selectedIcon, unselected, className, ...nativeProps } = props;
1456
+ const context = useSelectItemContext("SelectItemIndicator");
1457
+ const icon = context.selected ? selectedIcon : unselected;
1458
+ if (!isValidElement<LynxIconElementProps>(icon)) {
1459
+ if (!context.selected) return null;
1460
+ return (
1461
+ <text
1462
+ {...(ref ? { ref: ref as LynxTextRef } : {})}
1463
+ className={clsx(context.classes.indicator, className)}
1464
+ accessibility-elements-hidden={true}
1465
+ {...nativeProps}
1466
+ >
1467
+ ✓
1468
+ </text>
1469
+ );
1470
+ }
1471
+ return (
1472
+ <InternalIcon
1473
+ ref={ref}
1474
+ icon={icon}
1475
+ className={clsx(context.classes.indicator, className)}
1476
+ accessibility-elements-hidden={true}
1477
+ deps={[context.selected, context.disabled, context.pressed, icon]}
1478
+ {...nativeProps}
1479
+ />
1480
+ );
1481
+ },
1482
+ );
1483
+ SelectItemIndicator.displayName = "SelectItemIndicator";