@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,998 @@
1
+ import { helpBubble, type HelpBubbleVariantProps } from "@seed-design/lynx-css/recipes/help-bubble";
2
+ import { getRectByRef } from "@lynx-js/lynx-ui-common";
3
+ import type { NodesRef } from "@lynx-js/types";
4
+ import * as React from "@lynx-js/react";
5
+ import clsx from "clsx";
6
+
7
+ import { useControllableState } from "../../hooks/useControllableState";
8
+ import { usePressTap } from "../../hooks/usePressTap";
9
+ import type {
10
+ LynxAccessibilityProps,
11
+ LynxPressableProps,
12
+ LynxStyledElementProps,
13
+ LynxTextRef,
14
+ LynxViewProps,
15
+ LynxViewRef,
16
+ } from "../../types";
17
+ import { createSlotRecipeContext } from "../../utils/create-slot-recipe-context";
18
+ import { mergeProps } from "../../utils/merge-props";
19
+ import { useStyleProps, type StyleProps } from "../../utils/styled";
20
+ import {
21
+ computePosition,
22
+ type Placement,
23
+ type Position,
24
+ type Rect,
25
+ type Side,
26
+ } from "../private/Positioning";
27
+
28
+ const { ClassNamesProvider, useClassNames } = createSlotRecipeContext(helpBubble);
29
+
30
+ type HelpBubblePublicVariantProps = Omit<
31
+ HelpBubbleVariantProps,
32
+ "open" | "positioned" | "side" | "pressed"
33
+ >;
34
+ type NativeTapHandler = NonNullable<LynxViewProps["bindtap"]>;
35
+ type NativeLayoutHandler = NonNullable<LynxViewProps["bindlayoutchange"]>;
36
+ type NativeTransitionHandler = NonNullable<LynxViewProps["bindtransitionend"]>;
37
+
38
+ interface HelpBubbleContextValue {
39
+ open: boolean;
40
+ mounted: boolean;
41
+ positioned: boolean;
42
+ side: Side;
43
+ placement: Placement;
44
+ gutter: number;
45
+ overflowPadding: number;
46
+ arrowPadding: number;
47
+ flip: HelpBubbleRootProps["flip"];
48
+ closeOnInteractOutside: boolean;
49
+ openEpoch: number;
50
+ isOpenRef: React.MutableRefObject<boolean>;
51
+ openEpochRef: React.MutableRefObject<number>;
52
+ anchorRef: React.MutableRefObject<NodesRef | null>;
53
+ triggerRef: React.MutableRefObject<NodesRef | null>;
54
+ position: Position | null;
55
+ referenceRect: Rect | null;
56
+ positionRevision: number;
57
+ positionRevisionRef: React.MutableRefObject<number>;
58
+ requestOpen: (nextOpen: boolean) => void;
59
+ finishClose: () => void;
60
+ setPosition: (position: Position | null, referenceRect: Rect | null) => void;
61
+ resetPosition: () => void;
62
+ requestPositionUpdate: () => void;
63
+ }
64
+
65
+ const HelpBubbleContext = React.createContext<HelpBubbleContextValue | null>(null);
66
+
67
+ function useHelpBubbleContext(consumer: string): HelpBubbleContextValue {
68
+ const context = React.useContext(HelpBubbleContext);
69
+ if (!context) throw new Error(`<${consumer}/> must be rendered inside <HelpBubbleRoot/>.`);
70
+ return context;
71
+ }
72
+
73
+ function mergeNodeRef(forwardedRef: React.ForwardedRef<unknown>, node: NodesRef | null) {
74
+ if (typeof forwardedRef === "function") forwardedRef(node);
75
+ else if (forwardedRef) forwardedRef.current = node;
76
+ }
77
+
78
+ function toPixel(value: number) {
79
+ return `${value}px`;
80
+ }
81
+
82
+ function getLayoutSize(
83
+ event: Parameters<NativeLayoutHandler>[0],
84
+ ): { width: number; height: number } | null {
85
+ const width = event.detail?.width ?? event.params?.width;
86
+ const height = event.detail?.height ?? event.params?.height;
87
+ if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
88
+ return { width: Math.max(0, width), height: Math.max(0, height) };
89
+ }
90
+
91
+ function hasExitTransition(event: Parameters<NativeTransitionHandler>[0]): boolean {
92
+ if (event.target.uid !== event.currentTarget.uid) return false;
93
+ return (
94
+ event.params.animation_type === "transition-opacity" ||
95
+ event.params.animation_name === "opacity"
96
+ );
97
+ }
98
+ const EXIT_TRANSITION_FALLBACK_MS = 200;
99
+
100
+ function getRootRect() {
101
+ "background only";
102
+ return getRectByRef({ current: lynx.createSelectorQuery().selectRoot() }, true);
103
+ }
104
+
105
+ function areStylesEqual(left: object | undefined, right: object | undefined) {
106
+ if (left === right) return true;
107
+ if (!left || !right) return false;
108
+ const leftEntries = Object.entries(left);
109
+ if (leftEntries.length !== Object.keys(right).length) return false;
110
+ return leftEntries.every(([key, value]) =>
111
+ Object.is(value, (right as Record<string, unknown>)[key]),
112
+ );
113
+ }
114
+
115
+ ////////////////////////////////////////////////////////////////////////////////////
116
+
117
+ export interface HelpBubbleRootProps extends HelpBubblePublicVariantProps, LynxStyledElementProps {
118
+ open?: boolean;
119
+ defaultOpen?: boolean;
120
+ onOpenChange?: (open: boolean) => void;
121
+ /** @default "top" */
122
+ placement?: Placement;
123
+ /** @default 4 */
124
+ gutter?: number;
125
+ /** @default 16 */
126
+ overflowPadding?: number;
127
+ /** @default 14 */
128
+ arrowPadding?: number;
129
+ /** @default true */
130
+ flip?:
131
+ | boolean
132
+ | {
133
+ fallbackStrategy?: "bestFit" | "initialPlacement";
134
+ fallbackPlacements?: Placement[];
135
+ };
136
+ /** @default true */
137
+ closeOnInteractOutside?: boolean;
138
+ }
139
+
140
+ /**
141
+ * @platform Lynx
142
+ *
143
+ * The native implementation uses fixed positioned views rather than DOM portals.
144
+ * `closeOnInteractOutside={false}` has no full-screen hit surface, so other native
145
+ * controls and independently-open HelpBubbles remain interactive.
146
+ */
147
+ export const HelpBubbleRoot = React.forwardRef<unknown, HelpBubbleRootProps>((props, ref) => {
148
+ const {
149
+ children,
150
+ className,
151
+ style,
152
+ open: openProp,
153
+ defaultOpen = false,
154
+ onOpenChange,
155
+ placement = "top",
156
+ gutter = 4,
157
+ overflowPadding = 16,
158
+ arrowPadding = 14,
159
+ flip = true,
160
+ closeOnInteractOutside = true,
161
+ ...nativeProps
162
+ } = props;
163
+ const [open, setOpen] = useControllableState({
164
+ value: openProp,
165
+ defaultValue: defaultOpen,
166
+ });
167
+ const [mounted, setMounted] = React.useState(open);
168
+ const [position, setPositionState] = React.useState<Position | null>(null);
169
+ const [positionedEpoch, setPositionedEpoch] = React.useState<number | null>(null);
170
+ const [referenceRect, setReferenceRect] = React.useState<Rect | null>(null);
171
+ const [positionRevision, setPositionRevision] = React.useState(0);
172
+ const positionRevisionRef = React.useRef(0);
173
+ const isOpenRef = React.useRef(open);
174
+ const openEpochRef = React.useRef(0);
175
+ const anchorRef = React.useRef<NodesRef | null>(null);
176
+ const triggerRef = React.useRef<NodesRef | null>(null);
177
+
178
+ if (open && !isOpenRef.current) openEpochRef.current += 1;
179
+ isOpenRef.current = open;
180
+ const openEpoch = openEpochRef.current;
181
+ const positioned = positionedEpoch === openEpoch;
182
+
183
+ React.useEffect(() => {
184
+ "background only";
185
+ if (open) {
186
+ setMounted(true);
187
+ return;
188
+ }
189
+ if (!positioned) setMounted(false);
190
+ }, [open, positioned]);
191
+
192
+ const requestOpen = React.useCallback(
193
+ (nextOpen: boolean) => {
194
+ "background only";
195
+ if (nextOpen === open) return;
196
+ setOpen(nextOpen);
197
+ onOpenChange?.(nextOpen);
198
+ },
199
+ [onOpenChange, open, setOpen],
200
+ );
201
+ const finishClose = React.useCallback(() => {
202
+ "background only";
203
+ if (!isOpenRef.current) {
204
+ setMounted(false);
205
+ setPositionedEpoch(null);
206
+ setPositionState(null);
207
+ setReferenceRect(null);
208
+ }
209
+ }, []);
210
+ const setPosition = React.useCallback(
211
+ (nextPosition: Position | null, nextReferenceRect: Rect | null) => {
212
+ setPositionState((previous) => {
213
+ if (
214
+ previous !== null &&
215
+ nextPosition !== null &&
216
+ previous.left === nextPosition.left &&
217
+ previous.top === nextPosition.top &&
218
+ previous.width === nextPosition.width &&
219
+ previous.height === nextPosition.height &&
220
+ previous.placement === nextPosition.placement &&
221
+ previous.transformOrigin === nextPosition.transformOrigin &&
222
+ previous.availableWidth === nextPosition.availableWidth &&
223
+ previous.availableHeight === nextPosition.availableHeight &&
224
+ previous.arrow?.left === nextPosition.arrow?.left &&
225
+ previous.arrow?.top === nextPosition.arrow?.top &&
226
+ previous.arrow?.centerOffset === nextPosition.arrow?.centerOffset
227
+ ) {
228
+ return previous;
229
+ }
230
+ return nextPosition;
231
+ });
232
+ setReferenceRect((previous) => {
233
+ if (
234
+ previous !== null &&
235
+ nextReferenceRect !== null &&
236
+ previous.left === nextReferenceRect.left &&
237
+ previous.top === nextReferenceRect.top &&
238
+ previous.right === nextReferenceRect.right &&
239
+ previous.bottom === nextReferenceRect.bottom &&
240
+ previous.width === nextReferenceRect.width &&
241
+ previous.height === nextReferenceRect.height
242
+ ) {
243
+ return previous;
244
+ }
245
+ return nextReferenceRect;
246
+ });
247
+ setPositionedEpoch(nextPosition ? openEpochRef.current : null);
248
+ },
249
+ [],
250
+ );
251
+ const resetPosition = React.useCallback(() => {
252
+ setPositionState(null);
253
+ setReferenceRect(null);
254
+ setPositionedEpoch(null);
255
+ }, []);
256
+ const requestPositionUpdate = React.useCallback(() => {
257
+ positionRevisionRef.current += 1;
258
+ setPositionRevision(positionRevisionRef.current);
259
+ }, []);
260
+ const classes = helpBubble({
261
+ open,
262
+ positioned,
263
+ side: position?.placement.split("-")[0] as Side | undefined,
264
+ pressed: false,
265
+ });
266
+ const contextValue = React.useMemo<HelpBubbleContextValue>(
267
+ () => ({
268
+ open,
269
+ mounted,
270
+ positioned,
271
+ side: (position?.placement.split("-")[0] as Side | undefined) ?? "top",
272
+ placement,
273
+ gutter,
274
+ overflowPadding,
275
+ arrowPadding,
276
+ flip,
277
+ closeOnInteractOutside,
278
+ openEpoch,
279
+ isOpenRef,
280
+ openEpochRef,
281
+ anchorRef,
282
+ triggerRef,
283
+ position,
284
+ referenceRect,
285
+ positionRevision,
286
+ positionRevisionRef,
287
+ requestOpen,
288
+ finishClose,
289
+ setPosition,
290
+ resetPosition,
291
+ requestPositionUpdate,
292
+ }),
293
+ [
294
+ arrowPadding,
295
+ closeOnInteractOutside,
296
+ finishClose,
297
+ flip,
298
+ gutter,
299
+ mounted,
300
+ open,
301
+ openEpoch,
302
+ overflowPadding,
303
+ placement,
304
+ position,
305
+ positioned,
306
+ requestOpen,
307
+ resetPosition,
308
+ setPosition,
309
+ referenceRect,
310
+ positionRevisionRef,
311
+ positionRevision,
312
+ requestPositionUpdate,
313
+ ],
314
+ );
315
+
316
+ return (
317
+ <HelpBubbleContext.Provider value={contextValue}>
318
+ <ClassNamesProvider value={classes}>
319
+ <view
320
+ {...mergeProps(ref ? { ref: ref as LynxViewRef } : {}, nativeProps)}
321
+ className={className}
322
+ style={style}
323
+ >
324
+ {children}
325
+ </view>
326
+ </ClassNamesProvider>
327
+ </HelpBubbleContext.Provider>
328
+ );
329
+ });
330
+ HelpBubbleRoot.displayName = "HelpBubbleRoot";
331
+
332
+ ////////////////////////////////////////////////////////////////////////////////////
333
+
334
+ export interface HelpBubbleAnchorProps
335
+ extends LynxStyledElementProps,
336
+ Pick<LynxViewProps, "bindlayoutchange"> {}
337
+
338
+ export const HelpBubbleAnchor = React.forwardRef<unknown, HelpBubbleAnchorProps>((props, ref) => {
339
+ const { children, className, style, bindlayoutchange, ...nativeProps } = props;
340
+ const context = useHelpBubbleContext("HelpBubbleAnchor");
341
+ const handleRef = React.useCallback(
342
+ (node: NodesRef | null) => {
343
+ context.anchorRef.current = node;
344
+ mergeNodeRef(ref, node);
345
+ },
346
+ [context.anchorRef, ref],
347
+ );
348
+ const handleLayoutChange = React.useCallback<NativeLayoutHandler>(
349
+ (...args) => {
350
+ "background only";
351
+ context.requestPositionUpdate();
352
+ bindlayoutchange?.(...args);
353
+ },
354
+ [bindlayoutchange, context.requestPositionUpdate],
355
+ );
356
+
357
+ return (
358
+ <view
359
+ ref={handleRef as LynxViewRef}
360
+ className={className}
361
+ style={style}
362
+ bindlayoutchange={handleLayoutChange}
363
+ {...nativeProps}
364
+ >
365
+ {children}
366
+ </view>
367
+ );
368
+ });
369
+ HelpBubbleAnchor.displayName = "HelpBubbleAnchor";
370
+
371
+ ////////////////////////////////////////////////////////////////////////////////////
372
+
373
+ export interface HelpBubbleTriggerProps
374
+ extends LynxStyledElementProps,
375
+ LynxPressableProps,
376
+ LynxAccessibilityProps,
377
+ Pick<LynxViewProps, "bindlayoutchange"> {}
378
+
379
+ export const HelpBubbleTrigger = React.forwardRef<unknown, HelpBubbleTriggerProps>((props, ref) => {
380
+ const {
381
+ children,
382
+ className,
383
+ style,
384
+ bindtap,
385
+ bindlayoutchange,
386
+ "main-thread:bindtap": mainThreadBindtap,
387
+ "accessibility-element": accessibilityElement = true,
388
+ "accessibility-label": accessibilityLabel,
389
+ "accessibility-traits": accessibilityTraits,
390
+ ...nativeProps
391
+ } = props;
392
+ const context = useHelpBubbleContext("HelpBubbleTrigger");
393
+ const handleTap = React.useCallback<NativeTapHandler>(
394
+ (event, instance) => {
395
+ "background only";
396
+ context.requestOpen(!context.open);
397
+ bindtap?.(event, instance);
398
+ },
399
+ [bindtap, context],
400
+ );
401
+ const {
402
+ pressed: _pressed,
403
+ bindtap: triggerTap,
404
+ ...pressHandlers
405
+ } = usePressTap({
406
+ onTap: handleTap,
407
+ mainThreadOnTap: mainThreadBindtap,
408
+ });
409
+ const handleRef = React.useCallback(
410
+ (node: NodesRef | null) => {
411
+ context.triggerRef.current = node;
412
+ mergeNodeRef(ref, node);
413
+ },
414
+ [context.triggerRef, ref],
415
+ );
416
+ const handleLayoutChange = React.useCallback<NativeLayoutHandler>(
417
+ (...args) => {
418
+ "background only";
419
+ context.requestPositionUpdate();
420
+ bindlayoutchange?.(...args);
421
+ },
422
+ [bindlayoutchange, context.requestPositionUpdate],
423
+ );
424
+
425
+ return (
426
+ <view
427
+ ref={handleRef as LynxViewRef}
428
+ className={className}
429
+ style={style}
430
+ accessibility-element={accessibilityElement}
431
+ accessibility-label={accessibilityLabel}
432
+ accessibility-role-description="button"
433
+ accessibility-value={context.open ? "expanded" : "collapsed"}
434
+ accessibility-traits={accessibilityTraits ?? "button"}
435
+ {...nativeProps}
436
+ {...pressHandlers}
437
+ bindtap={triggerTap}
438
+ bindlayoutchange={handleLayoutChange}
439
+ >
440
+ {children}
441
+ </view>
442
+ );
443
+ });
444
+ HelpBubbleTrigger.displayName = "HelpBubbleTrigger";
445
+
446
+ ////////////////////////////////////////////////////////////////////////////////////
447
+
448
+ export interface HelpBubblePositionerProps extends LynxStyledElementProps {
449
+ zIndexOffset?: number;
450
+ }
451
+
452
+ function OutsidePressCatcher({
453
+ referenceRect,
454
+ onTap,
455
+ }: {
456
+ referenceRect: Rect;
457
+ onTap: NativeTapHandler;
458
+ }) {
459
+ const screen = typeof SystemInfo === "undefined" ? undefined : SystemInfo;
460
+ const pixelRatio = screen?.pixelRatio;
461
+ const width = screen?.pixelWidth;
462
+ const height = screen?.pixelHeight;
463
+ if (
464
+ typeof pixelRatio !== "number" ||
465
+ pixelRatio <= 0 ||
466
+ typeof width !== "number" ||
467
+ typeof height !== "number"
468
+ ) {
469
+ return null;
470
+ }
471
+
472
+ const screenWidth = width / pixelRatio;
473
+ const screenHeight = height / pixelRatio;
474
+ const left = Math.max(0, referenceRect.left);
475
+ const top = Math.max(0, referenceRect.top);
476
+ const right = Math.min(screenWidth, referenceRect.right);
477
+ const bottom = Math.min(screenHeight, referenceRect.bottom);
478
+ const areas = [
479
+ { top: 0, left: 0, width: screenWidth, height: top },
480
+ { top: bottom, left: 0, width: screenWidth, height: Math.max(0, screenHeight - bottom) },
481
+ { top, left: 0, width: left, height: Math.max(0, bottom - top) },
482
+ {
483
+ top,
484
+ left: right,
485
+ width: Math.max(0, screenWidth - right),
486
+ height: Math.max(0, bottom - top),
487
+ },
488
+ ];
489
+
490
+ return (
491
+ <>
492
+ {areas.map((area, index) =>
493
+ area.width > 0 && area.height > 0 ? (
494
+ <view
495
+ key={index}
496
+ style={{
497
+ position: "fixed",
498
+ top: toPixel(area.top),
499
+ left: toPixel(area.left),
500
+ width: toPixel(area.width),
501
+ height: toPixel(area.height),
502
+ zIndex: 0,
503
+ }}
504
+ bindtap={onTap}
505
+ />
506
+ ) : null,
507
+ )}
508
+ </>
509
+ );
510
+ }
511
+
512
+ export const HelpBubblePositioner = React.forwardRef<unknown, HelpBubblePositionerProps>(
513
+ (props, ref) => {
514
+ const { children, className, style, zIndexOffset = 0, ...nativeProps } = props;
515
+ const context = useHelpBubbleContext("HelpBubblePositioner");
516
+ const classNames = useClassNames();
517
+ const handleOutsideTap = React.useCallback<NativeTapHandler>(() => {
518
+ "background only";
519
+ context.requestOpen(false);
520
+ }, [context.requestOpen]);
521
+
522
+ if (!context.mounted) return null;
523
+
524
+ return (
525
+ <view
526
+ {...mergeProps(ref ? { ref: ref as LynxViewRef } : {}, nativeProps)}
527
+ className={clsx(classNames.positioner, className)}
528
+ style={{
529
+ left: toPixel(context.position?.left ?? 0),
530
+ top: toPixel(context.position?.top ?? 0),
531
+ width: context.position ? toPixel(context.position.width) : undefined,
532
+ zIndex: 99 + zIndexOffset,
533
+ ...style,
534
+ }}
535
+ >
536
+ {context.closeOnInteractOutside && context.open && context.referenceRect ? (
537
+ <OutsidePressCatcher referenceRect={context.referenceRect} onTap={handleOutsideTap} />
538
+ ) : null}
539
+ {children}
540
+ </view>
541
+ );
542
+ },
543
+ );
544
+ HelpBubblePositioner.displayName = "HelpBubblePositioner";
545
+
546
+ ////////////////////////////////////////////////////////////////////////////////////
547
+
548
+ export interface HelpBubbleContentProps
549
+ extends Pick<StyleProps, "maxWidth">,
550
+ LynxStyledElementProps {}
551
+
552
+ export const HelpBubbleContent = React.forwardRef<unknown, HelpBubbleContentProps>((props, ref) => {
553
+ const { style, restProps } = useStyleProps(props);
554
+ const { children, className, ...nativeProps } = restProps;
555
+ const context = useHelpBubbleContext("HelpBubbleContent");
556
+ const measurementVersionRef = React.useRef(0);
557
+ const intrinsicWidthRef = React.useRef<number | null>(null);
558
+ const measurementConfigRef = React.useRef(0);
559
+ const configuredClassNameRef = React.useRef(className);
560
+ const configuredStyleRef = React.useRef(style);
561
+ const [contentNode, setContentNode] = React.useState<NodesRef | null>(null);
562
+ const [measurementRevision, setMeasurementRevision] = React.useState(0);
563
+ const [intrinsicSize, setIntrinsicSize] = React.useState<{
564
+ width: number;
565
+ height: number;
566
+ epoch: number;
567
+ config: number;
568
+ } | null>(null);
569
+ const [widthConstraint, setWidthConstraint] = React.useState<number | null>(null);
570
+ const closeFinishedRef = React.useRef(false);
571
+ const finishClose = React.useCallback(() => {
572
+ "background only";
573
+ if (closeFinishedRef.current) return;
574
+ closeFinishedRef.current = true;
575
+ context.finishClose();
576
+ }, [context.finishClose]);
577
+
578
+ const measureIntrinsicSize = React.useCallback(async () => {
579
+ "background only";
580
+ const openEpoch = context.openEpoch;
581
+ const config = measurementConfigRef.current;
582
+ if (!context.open || !contentNode) return;
583
+ const version = ++measurementVersionRef.current;
584
+ try {
585
+ const rect = await getRectByRef({ current: contentNode }, true);
586
+ if (
587
+ version !== measurementVersionRef.current ||
588
+ !context.isOpenRef.current ||
589
+ openEpoch !== context.openEpochRef.current ||
590
+ config !== measurementConfigRef.current
591
+ ) {
592
+ return;
593
+ }
594
+ if (intrinsicWidthRef.current === null) intrinsicWidthRef.current = rect.width;
595
+ setIntrinsicSize((current) => {
596
+ const width = intrinsicWidthRef.current ?? rect.width;
597
+ if (
598
+ current?.width === width &&
599
+ current.height === rect.height &&
600
+ current.epoch === openEpoch &&
601
+ current.config === config
602
+ ) {
603
+ return current;
604
+ }
605
+ return { width, height: rect.height, epoch: openEpoch, config };
606
+ });
607
+ } catch {
608
+ // A native ref can disappear while an async selector query is in flight.
609
+ }
610
+ }, [contentNode, context.isOpenRef, context.open, context.openEpoch, context.openEpochRef]);
611
+
612
+ const measurePosition = React.useCallback(async () => {
613
+ "background only";
614
+ const referenceNode = context.anchorRef.current ?? context.triggerRef.current;
615
+ const openEpoch = context.openEpoch;
616
+ const positionRevision = context.positionRevisionRef.current;
617
+ if (
618
+ !context.open ||
619
+ !referenceNode ||
620
+ !intrinsicSize ||
621
+ intrinsicSize.epoch !== openEpoch ||
622
+ intrinsicSize.config !== measurementConfigRef.current
623
+ ) {
624
+ return;
625
+ }
626
+ const version = ++measurementVersionRef.current;
627
+ try {
628
+ const [reference, boundary] = await Promise.all([
629
+ getRectByRef({ current: referenceNode }, true),
630
+ getRootRect(),
631
+ ]);
632
+ if (
633
+ version !== measurementVersionRef.current ||
634
+ !context.isOpenRef.current ||
635
+ openEpoch !== context.openEpochRef.current ||
636
+ positionRevision !== context.positionRevisionRef.current
637
+ ) {
638
+ return;
639
+ }
640
+ const width = widthConstraint ?? intrinsicWidthRef.current ?? intrinsicSize.width;
641
+ const nextPosition = await computePosition({
642
+ reference,
643
+ boundary,
644
+ width,
645
+ height: intrinsicSize.height,
646
+ placement: context.placement,
647
+ gutter: context.gutter,
648
+ overflowPadding: context.overflowPadding,
649
+ flip:
650
+ context.flip === false
651
+ ? false
652
+ : context.flip === true
653
+ ? { fallbackStrategy: "bestFit" }
654
+ : context.flip,
655
+ shift: { crossAxis: true, limit: true },
656
+ arrow: { size: 12, tipHeight: 8, padding: context.arrowPadding },
657
+ });
658
+ if (
659
+ version !== measurementVersionRef.current ||
660
+ !context.isOpenRef.current ||
661
+ openEpoch !== context.openEpochRef.current ||
662
+ positionRevision !== context.positionRevisionRef.current
663
+ ) {
664
+ return;
665
+ }
666
+ if (nextPosition.availableWidth < width) {
667
+ const constrainedWidth = nextPosition.availableWidth;
668
+ if (widthConstraint !== constrainedWidth) {
669
+ setWidthConstraint(constrainedWidth);
670
+ setIntrinsicSize(null);
671
+ context.resetPosition();
672
+ setMeasurementRevision((current) => current + 1);
673
+ }
674
+ return;
675
+ }
676
+ context.setPosition(nextPosition, reference);
677
+ } catch {
678
+ // A native ref can disappear while an async selector query is in flight.
679
+ // The current epoch stays unpositioned and therefore hidden.
680
+ }
681
+ }, [
682
+ context.anchorRef,
683
+ context.arrowPadding,
684
+ context.flip,
685
+ context.gutter,
686
+ context.isOpenRef,
687
+ context.open,
688
+ context.openEpoch,
689
+ context.openEpochRef,
690
+ context.overflowPadding,
691
+ context.placement,
692
+ context.resetPosition,
693
+ context.setPosition,
694
+ context.triggerRef,
695
+ intrinsicSize,
696
+ widthConstraint,
697
+ ]);
698
+
699
+ React.useEffect(() => {
700
+ "background only";
701
+ measurementVersionRef.current += 1;
702
+ intrinsicWidthRef.current = null;
703
+ setWidthConstraint(null);
704
+ setIntrinsicSize(null);
705
+ context.resetPosition();
706
+ }, [context.openEpoch, context.resetPosition]);
707
+ React.useEffect(() => {
708
+ "background only";
709
+ if (
710
+ configuredClassNameRef.current === className &&
711
+ areStylesEqual(configuredStyleRef.current, style)
712
+ ) {
713
+ return;
714
+ }
715
+ measurementVersionRef.current += 1;
716
+ intrinsicWidthRef.current = null;
717
+ measurementConfigRef.current += 1;
718
+ configuredClassNameRef.current = className;
719
+ configuredStyleRef.current = style;
720
+ setWidthConstraint(null);
721
+ setIntrinsicSize(null);
722
+ context.resetPosition();
723
+ setMeasurementRevision((current) => current + 1);
724
+ }, [className, context.resetPosition, style]);
725
+ React.useEffect(() => {
726
+ "background only";
727
+ if (!context.open) return;
728
+ const frame = requestAnimationFrame(() => {
729
+ "background only";
730
+ void measureIntrinsicSize();
731
+ });
732
+ return () => cancelAnimationFrame(frame);
733
+ }, [context.open, context.openEpoch, measureIntrinsicSize, measurementRevision]);
734
+ React.useEffect(() => {
735
+ "background only";
736
+ if (!context.open) return;
737
+ let disposed = false;
738
+ let frame: number | undefined;
739
+ const trackPosition = () => {
740
+ void measurePosition().finally(() => {
741
+ if (!disposed && context.isOpenRef.current) {
742
+ frame = requestAnimationFrame(trackPosition);
743
+ }
744
+ });
745
+ };
746
+ trackPosition();
747
+ return () => {
748
+ disposed = true;
749
+ measurementVersionRef.current += 1;
750
+ if (frame !== undefined) cancelAnimationFrame(frame);
751
+ };
752
+ }, [
753
+ context.isOpenRef,
754
+ context.open,
755
+ context.openEpoch,
756
+ context.positionRevision,
757
+ measurePosition,
758
+ ]);
759
+ React.useEffect(() => {
760
+ "background only";
761
+ if (context.open) {
762
+ closeFinishedRef.current = false;
763
+ return;
764
+ }
765
+ if (!context.mounted) return;
766
+ const timer = setTimeout(() => {
767
+ "background only";
768
+ finishClose();
769
+ }, EXIT_TRANSITION_FALLBACK_MS);
770
+ return () => clearTimeout(timer);
771
+ }, [context.mounted, context.open, finishClose]);
772
+
773
+ const handleRef = React.useCallback(
774
+ (node: NodesRef | null) => {
775
+ measurementVersionRef.current += 1;
776
+ setContentNode(node);
777
+ mergeNodeRef(ref, node);
778
+ },
779
+ [ref],
780
+ );
781
+ const handleLayoutChange = React.useCallback<NativeLayoutHandler>(
782
+ (event) => {
783
+ "background only";
784
+ const nextSize = getLayoutSize(event);
785
+ if (!nextSize || !context.open) return;
786
+ measurementVersionRef.current += 1;
787
+ if (intrinsicWidthRef.current === null) intrinsicWidthRef.current = nextSize.width;
788
+ setIntrinsicSize((current) => {
789
+ const width = intrinsicWidthRef.current ?? nextSize.width;
790
+ if (
791
+ current?.width === width &&
792
+ current.height === nextSize.height &&
793
+ current.epoch === context.openEpoch &&
794
+ current.config === measurementConfigRef.current
795
+ ) {
796
+ return current;
797
+ }
798
+ return {
799
+ width,
800
+ height: nextSize.height,
801
+ epoch: context.openEpoch,
802
+ config: measurementConfigRef.current,
803
+ };
804
+ });
805
+ },
806
+ [context.open, context.openEpoch],
807
+ );
808
+ const handleTransitionEnd = React.useCallback<NativeTransitionHandler>(
809
+ (event) => {
810
+ "background only";
811
+ if (!context.open && hasExitTransition(event)) finishClose();
812
+ },
813
+ [context.open, finishClose],
814
+ );
815
+
816
+ const motionClassName = useClassNames().motion;
817
+ return (
818
+ <view className={motionClassName} bindtransitionend={handleTransitionEnd}>
819
+ <view
820
+ ref={handleRef as LynxViewRef}
821
+ className={clsx(useClassNames().content, className)}
822
+ style={{
823
+ ...style,
824
+ ...(widthConstraint != null ? { width: toPixel(widthConstraint) } : {}),
825
+ }}
826
+ bindlayoutchange={handleLayoutChange}
827
+ {...nativeProps}
828
+ >
829
+ {children}
830
+ </view>
831
+ </view>
832
+ );
833
+ });
834
+ HelpBubbleContent.displayName = "HelpBubbleContent";
835
+
836
+ ////////////////////////////////////////////////////////////////////////////////////
837
+
838
+ export interface HelpBubbleArrowProps extends LynxStyledElementProps {}
839
+
840
+ export const HelpBubbleArrow = React.forwardRef<unknown, HelpBubbleArrowProps>((props, ref) => {
841
+ const { children, className, style, ...nativeProps } = props;
842
+ const context = useHelpBubbleContext("HelpBubbleArrow");
843
+ const arrow = context.position?.arrow;
844
+ const side = context.side;
845
+ const geometryStyle =
846
+ side === "top"
847
+ ? { left: toPixel(arrow?.left ?? 0), top: "100%" }
848
+ : side === "bottom"
849
+ ? { left: toPixel(arrow?.left ?? 0), bottom: "100%" }
850
+ : side === "right"
851
+ ? { top: toPixel(arrow?.top ?? 0), right: "100%" }
852
+ : { top: toPixel(arrow?.top ?? 0), left: "100%" };
853
+
854
+ return (
855
+ <view
856
+ {...mergeProps(ref ? { ref: ref as LynxViewRef } : {}, nativeProps)}
857
+ className={clsx(useClassNames().arrow, className)}
858
+ style={{ ...geometryStyle, ...style }}
859
+ >
860
+ {children}
861
+ </view>
862
+ );
863
+ });
864
+ HelpBubbleArrow.displayName = "HelpBubbleArrow";
865
+
866
+ export interface HelpBubbleArrowTipProps extends LynxStyledElementProps {}
867
+
868
+ export const HelpBubbleArrowTip = React.forwardRef<unknown, HelpBubbleArrowTipProps>(
869
+ (props, ref) => {
870
+ const { children, className, style, ...nativeProps } = props;
871
+ return (
872
+ <view
873
+ {...mergeProps(ref ? { ref: ref as LynxViewRef } : {}, nativeProps)}
874
+ className={clsx(useClassNames().arrowTip, className)}
875
+ style={style}
876
+ accessibility-elements-hidden={true}
877
+ >
878
+ {children}
879
+ </view>
880
+ );
881
+ },
882
+ );
883
+ HelpBubbleArrowTip.displayName = "HelpBubbleArrowTip";
884
+
885
+ ////////////////////////////////////////////////////////////////////////////////////
886
+
887
+ export interface HelpBubbleBodyProps extends LynxStyledElementProps {}
888
+
889
+ export const HelpBubbleBody = React.forwardRef<unknown, HelpBubbleBodyProps>((props, ref) => {
890
+ const { children, className, style, ...nativeProps } = props;
891
+ return (
892
+ <view
893
+ {...mergeProps(ref ? { ref: ref as LynxViewRef } : {}, nativeProps)}
894
+ className={clsx(useClassNames().body, className)}
895
+ style={style}
896
+ >
897
+ {children}
898
+ </view>
899
+ );
900
+ });
901
+ HelpBubbleBody.displayName = "HelpBubbleBody";
902
+
903
+ export interface HelpBubbleTitleProps extends LynxStyledElementProps, LynxAccessibilityProps {}
904
+
905
+ export const HelpBubbleTitle = React.forwardRef<unknown, HelpBubbleTitleProps>((props, ref) => {
906
+ const { children, className, style, ...nativeProps } = props;
907
+ return (
908
+ <text
909
+ {...mergeProps(ref ? { ref: ref as LynxTextRef } : {}, nativeProps)}
910
+ className={clsx(useClassNames().title, className)}
911
+ style={style}
912
+ >
913
+ {children}
914
+ </text>
915
+ );
916
+ });
917
+ HelpBubbleTitle.displayName = "HelpBubbleTitle";
918
+
919
+ export interface HelpBubbleDescriptionProps
920
+ extends LynxStyledElementProps,
921
+ LynxAccessibilityProps {}
922
+
923
+ export const HelpBubbleDescription = React.forwardRef<unknown, HelpBubbleDescriptionProps>(
924
+ (props, ref) => {
925
+ const { children, className, style, ...nativeProps } = props;
926
+ return (
927
+ <text
928
+ {...mergeProps(ref ? { ref: ref as LynxTextRef } : {}, nativeProps)}
929
+ className={clsx(useClassNames().description, className)}
930
+ style={style}
931
+ >
932
+ {children}
933
+ </text>
934
+ );
935
+ },
936
+ );
937
+ HelpBubbleDescription.displayName = "HelpBubbleDescription";
938
+
939
+ ////////////////////////////////////////////////////////////////////////////////////
940
+
941
+ export interface HelpBubbleCloseButtonProps
942
+ extends LynxStyledElementProps,
943
+ LynxPressableProps,
944
+ LynxAccessibilityProps {}
945
+
946
+ export const HelpBubbleCloseButton = React.forwardRef<unknown, HelpBubbleCloseButtonProps>(
947
+ (props, ref) => {
948
+ const {
949
+ children,
950
+ className,
951
+ style,
952
+ bindtap,
953
+ "main-thread:bindtap": mainThreadBindtap,
954
+ "accessibility-element": accessibilityElement = true,
955
+ "accessibility-label": accessibilityLabel,
956
+ "accessibility-traits": accessibilityTraits = "button",
957
+ ...nativeProps
958
+ } = props;
959
+ const context = useHelpBubbleContext("HelpBubbleCloseButton");
960
+ const handleTap = React.useCallback<NativeTapHandler>(
961
+ (event, instance) => {
962
+ "background only";
963
+ bindtap?.(event, instance);
964
+ context.requestOpen(false);
965
+ },
966
+ [bindtap, context],
967
+ );
968
+ const { pressed, ...pressHandlers } = usePressTap({
969
+ onTap: handleTap,
970
+ mainThreadOnTap: mainThreadBindtap,
971
+ });
972
+ const closeButtonClassNames = helpBubble({
973
+ open: context.open,
974
+ positioned: context.positioned,
975
+ side: context.side,
976
+ pressed,
977
+ });
978
+
979
+ if (process.env.NODE_ENV !== "production" && accessibilityElement && !accessibilityLabel) {
980
+ console.warn("HelpBubbleCloseButton requires `accessibility-label` for accessibility.");
981
+ }
982
+
983
+ return (
984
+ <view
985
+ {...mergeProps(ref ? { ref: ref as LynxViewRef } : {}, pressHandlers, nativeProps)}
986
+ accessibility-element={accessibilityElement}
987
+ accessibility-label={accessibilityLabel}
988
+ accessibility-traits={accessibilityTraits}
989
+ className={clsx(closeButtonClassNames.closeButton, className)}
990
+ style={style}
991
+ flatten={false}
992
+ >
993
+ {children}
994
+ </view>
995
+ );
996
+ },
997
+ );
998
+ HelpBubbleCloseButton.displayName = "HelpBubbleCloseButton";