@astryxdesign/core 0.7.8 → 0.7.9-canary.0a64fdb

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 (276) hide show
  1. package/dist/AppShell/observeMainOverflow.d.ts.map +1 -1
  2. package/dist/AppShell/observeMainOverflow.js +9 -6
  3. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  4. package/dist/BottomSheet/BottomSheet.js +5 -0
  5. package/dist/Calendar/styles.d.ts +1 -1
  6. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  7. package/dist/CheckboxInput/CheckboxInput.js +54 -11
  8. package/dist/ClickableCard/ClickableCard.d.ts +1 -1
  9. package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
  10. package/dist/ClickableCard/ClickableCard.js +5 -1
  11. package/dist/DateInput/PickerWheel.d.ts.map +1 -1
  12. package/dist/DateInput/PickerWheel.js +7 -1
  13. package/dist/DateRangeInput/DateRangeInput.js +1 -1
  14. package/dist/Dialog/Dialog.d.ts +2 -1
  15. package/dist/Dialog/Dialog.d.ts.map +1 -1
  16. package/dist/Dialog/Dialog.js +12 -1
  17. package/dist/Drawer/useDrawerDialogPresence.d.ts +2 -1
  18. package/dist/Drawer/useDrawerDialogPresence.d.ts.map +1 -1
  19. package/dist/Drawer/useDrawerDialogPresence.js +11 -2
  20. package/dist/DropdownMenu/DropdownMenu.d.ts +9 -1
  21. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  22. package/dist/DropdownMenu/DropdownMenu.js +9 -4
  23. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -1
  24. package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +3 -2
  25. package/dist/DropdownMenu/DropdownMenuItem.d.ts.map +1 -1
  26. package/dist/DropdownMenu/DropdownMenuItem.js +3 -2
  27. package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts.map +1 -1
  28. package/dist/DropdownMenu/DropdownMenuRadioItem.js +3 -2
  29. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  30. package/dist/DropdownMenu/DropdownMenuSubMenu.js +3 -2
  31. package/dist/Field/inputDisabledFocus.d.ts +4 -4
  32. package/dist/Field/inputStyles.stylex.d.ts +1 -1
  33. package/dist/FileInput/FileInput.d.ts.map +1 -1
  34. package/dist/FileInput/FileInput.js +16 -2
  35. package/dist/Icon/Icon.d.ts +1 -1
  36. package/dist/InfoTip/InfoTip.d.ts +1 -0
  37. package/dist/InfoTip/InfoTip.d.ts.map +1 -1
  38. package/dist/InfoTip/InfoTip.js +1 -0
  39. package/dist/InputGroup/InputGroup.d.ts +4 -3
  40. package/dist/InputGroup/InputGroup.d.ts.map +1 -1
  41. package/dist/InputGroup/InputGroup.js +2 -2
  42. package/dist/InputGroup/groupStyles.d.ts +1 -1
  43. package/dist/InputGroup/groupStyles.d.ts.map +1 -1
  44. package/dist/InputGroup/groupStyles.js +1 -1
  45. package/dist/Item/Item.d.ts +2 -1
  46. package/dist/Item/Item.d.ts.map +1 -1
  47. package/dist/Item/Item.js +29 -6
  48. package/dist/Layer/holdEntranceInput.d.ts +24 -0
  49. package/dist/Layer/holdEntranceInput.d.ts.map +1 -0
  50. package/dist/Layer/holdEntranceInput.js +65 -0
  51. package/dist/Layer/inputHold.d.ts +49 -0
  52. package/dist/Layer/inputHold.d.ts.map +1 -0
  53. package/dist/Layer/inputHold.js +636 -0
  54. package/dist/Layer/preserveAnimations.d.ts +1 -5
  55. package/dist/Layer/preserveAnimations.d.ts.map +1 -1
  56. package/dist/Layer/preserveAnimations.js +12 -214
  57. package/dist/Layout/LayoutContent.d.ts +3 -1
  58. package/dist/Layout/LayoutContent.d.ts.map +1 -1
  59. package/dist/Layout/LayoutContent.js +12 -4
  60. package/dist/Layout/LayoutPanel.d.ts +4 -1
  61. package/dist/Layout/LayoutPanel.d.ts.map +1 -1
  62. package/dist/Layout/LayoutPanel.js +13 -4
  63. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  64. package/dist/Lightbox/Lightbox.js +4 -0
  65. package/dist/MetadataList/MetadataList.js +1 -1
  66. package/dist/MobileNav/MobileNav.d.ts +11 -1
  67. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  68. package/dist/MobileNav/MobileNav.js +39 -1
  69. package/dist/MoreMenu/MoreMenu.d.ts +5 -3
  70. package/dist/MoreMenu/MoreMenu.d.ts.map +1 -1
  71. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  72. package/dist/RadioList/RadioListItem.js +22 -4
  73. package/dist/SelectableCard/SelectableCard.d.ts +1 -1
  74. package/dist/SelectableCard/SelectableCard.d.ts.map +1 -1
  75. package/dist/SelectableCard/SelectableCard.js +5 -1
  76. package/dist/Spinner/Spinner.d.ts.map +1 -1
  77. package/dist/Spinner/Spinner.js +12 -2
  78. package/dist/Stack/StackItem.d.ts +8 -4
  79. package/dist/Stack/StackItem.d.ts.map +1 -1
  80. package/dist/Stack/StackItem.js +17 -4
  81. package/dist/Table/plugins/sortable/useTableSortable.js +1 -1
  82. package/dist/Token/Token.d.ts.map +1 -1
  83. package/dist/Token/Token.js +9 -0
  84. package/dist/Token/TokenLink.d.ts +1 -1
  85. package/dist/Token/TokenLink.d.ts.map +1 -1
  86. package/dist/Token/TokenLink.js +4 -1
  87. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  88. package/dist/Tooltip/useTooltip.js +162 -59
  89. package/dist/TreeList/TreeListItem.d.ts +2 -1
  90. package/dist/TreeList/TreeListItem.d.ts.map +1 -1
  91. package/dist/TreeList/TreeListItem.js +27 -3
  92. package/dist/astryx.css +35 -8
  93. package/dist/gestures/GestureHost.d.ts +2 -2
  94. package/dist/gestures/GestureHost.d.ts.map +1 -1
  95. package/dist/gestures/GestureHost.js +21 -6
  96. package/dist/gestures/PanGestureRecognizer.d.ts +11 -1
  97. package/dist/gestures/PanGestureRecognizer.d.ts.map +1 -1
  98. package/dist/gestures/PanGestureRecognizer.js +15 -1
  99. package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts +6 -1
  100. package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts.map +1 -1
  101. package/dist/gestures/nativeScroll.d.ts +3 -6
  102. package/dist/gestures/nativeScroll.d.ts.map +1 -1
  103. package/dist/gestures/nativeScroll.js +7 -52
  104. package/dist/gestures/wheelSegmenter.d.ts +68 -1
  105. package/dist/gestures/wheelSegmenter.d.ts.map +1 -1
  106. package/dist/gestures/wheelSegmenter.js +199 -10
  107. package/dist/hooks/StackItemScrollRegion.d.ts +31 -0
  108. package/dist/hooks/StackItemScrollRegion.d.ts.map +1 -0
  109. package/dist/hooks/StackItemScrollRegion.js +40 -0
  110. package/dist/hooks/menuPressGesture.d.ts +133 -21
  111. package/dist/hooks/menuPressGesture.d.ts.map +1 -1
  112. package/dist/hooks/menuPressGesture.js +155 -30
  113. package/dist/hooks/menuPressValues.d.ts +16 -0
  114. package/dist/hooks/menuPressValues.d.ts.map +1 -0
  115. package/dist/hooks/menuPressValues.js +18 -0
  116. package/dist/hooks/useClickableContainer.d.ts +27 -5
  117. package/dist/hooks/useClickableContainer.d.ts.map +1 -1
  118. package/dist/hooks/useClickableContainer.js +41 -5
  119. package/dist/hooks/useFocusTrap.d.ts +10 -0
  120. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  121. package/dist/hooks/useFocusTrap.js +225 -132
  122. package/dist/hooks/useMenuPress.d.ts +34 -1
  123. package/dist/hooks/useMenuPress.d.ts.map +1 -1
  124. package/dist/hooks/useMenuPress.js +93 -5
  125. package/dist/hooks/usePressFeedback.d.ts +6 -6
  126. package/dist/hooks/usePressFeedback.js +6 -6
  127. package/dist/hooks/useScrollRegionProps.d.ts +35 -0
  128. package/dist/hooks/useScrollRegionProps.d.ts.map +1 -0
  129. package/dist/hooks/useScrollRegionProps.js +51 -0
  130. package/dist/i18n/generated-english/hooks/useScrollRegionProps.generated.d.ts +8 -0
  131. package/dist/i18n/generated-english/hooks/useScrollRegionProps.generated.d.ts.map +1 -0
  132. package/dist/i18n/generated-english/hooks/useScrollRegionProps.generated.js +12 -0
  133. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  134. package/dist/i18n/generated-locales/en.generated.js +1 -0
  135. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  136. package/dist/i18n/generated-locales/pseudo.generated.js +1 -0
  137. package/dist/motion/carryAnimations.d.ts +58 -0
  138. package/dist/motion/carryAnimations.d.ts.map +1 -0
  139. package/dist/motion/carryAnimations.js +305 -0
  140. package/dist/motion/transientCopy.d.ts +4 -2
  141. package/dist/motion/transientCopy.d.ts.map +1 -1
  142. package/dist/motion/transientCopy.js +34 -5
  143. package/dist/motion/uikit/gestures.d.ts +41 -0
  144. package/dist/motion/uikit/gestures.d.ts.map +1 -1
  145. package/dist/motion/uikit/gestures.js +25 -0
  146. package/dist/motion/uikit/index.d.ts +1 -1
  147. package/dist/motion/uikit/index.d.ts.map +1 -1
  148. package/dist/motion/uikit/index.js +1 -1
  149. package/dist/privateAttributes/menuPressLit.stylex.d.ts +52 -0
  150. package/dist/privateAttributes/menuPressLit.stylex.d.ts.map +1 -0
  151. package/dist/privateAttributes/menuPressLit.stylex.js +99 -0
  152. package/dist/privateAttributes/press.stylex.d.ts +139 -0
  153. package/dist/privateAttributes/press.stylex.d.ts.map +1 -0
  154. package/dist/privateAttributes/press.stylex.js +98 -0
  155. package/dist/theme/tokens.stylex.d.ts +2 -2
  156. package/dist/theme/tokens.stylex.js +1 -1
  157. package/dist/utils/flatTree.d.ts +6 -3
  158. package/dist/utils/flatTree.d.ts.map +1 -1
  159. package/dist/utils/flatTree.js +13 -3
  160. package/dist/utils/hitArea.d.ts.map +1 -1
  161. package/dist/utils/hitArea.js +82 -5
  162. package/dist/utils/inputStream.d.ts +14 -1
  163. package/dist/utils/inputStream.d.ts.map +1 -1
  164. package/dist/utils/inputStream.js +40 -9
  165. package/dist/utils/interactionOverlay.stylex.d.ts +2 -1
  166. package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
  167. package/dist/utils/interactionOverlay.stylex.js +7 -2
  168. package/dist/utils/pressFeedback.d.ts +7 -0
  169. package/dist/utils/pressFeedback.d.ts.map +1 -1
  170. package/dist/utils/pressFeedback.js +32 -7
  171. package/dist/utils/pressGesture.d.ts +6 -0
  172. package/dist/utils/pressGesture.d.ts.map +1 -1
  173. package/dist/utils/pressGesture.js +17 -0
  174. package/dist/utils/rowPressLayer.stylex.d.ts +14 -0
  175. package/dist/utils/rowPressLayer.stylex.d.ts.map +1 -0
  176. package/dist/utils/rowPressLayer.stylex.js +25 -0
  177. package/dist/utils/scrollView.d.ts +25 -0
  178. package/dist/utils/scrollView.d.ts.map +1 -0
  179. package/dist/utils/scrollView.js +108 -0
  180. package/dist/utils/selectionLock.d.ts.map +1 -1
  181. package/dist/utils/selectionLock.js +1 -10
  182. package/dist/utils/touchHitArea.stylex.d.ts +35 -0
  183. package/dist/utils/touchHitArea.stylex.d.ts.map +1 -0
  184. package/dist/utils/touchHitArea.stylex.js +32 -0
  185. package/dist/utils/triggerFocus.d.ts +5 -0
  186. package/dist/utils/triggerFocus.d.ts.map +1 -1
  187. package/dist/utils/triggerFocus.js +9 -2
  188. package/locales/en.json +4 -0
  189. package/locales/pseudo.json +3 -0
  190. package/package.json +5 -5
  191. package/src/AppShell/observeMainOverflow.ts +9 -6
  192. package/src/BottomSheet/BottomSheet.tsx +5 -0
  193. package/src/CheckboxInput/CheckboxInput.doc.mjs +3 -2
  194. package/src/CheckboxInput/CheckboxInput.tsx +71 -9
  195. package/src/ClickableCard/ClickableCard.tsx +8 -1
  196. package/src/DateInput/PickerWheel.tsx +7 -2
  197. package/src/DateRangeInput/DateRangeInput.tsx +1 -1
  198. package/src/Dialog/Dialog.doc.mjs +1 -1
  199. package/src/Dialog/Dialog.tsx +12 -1
  200. package/src/Drawer/Drawer.doc.mjs +1 -1
  201. package/src/Drawer/useDrawerDialogPresence.ts +9 -2
  202. package/src/DropdownMenu/DropdownMenu.doc.mjs +1 -2
  203. package/src/DropdownMenu/DropdownMenu.tsx +29 -5
  204. package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +5 -1
  205. package/src/DropdownMenu/DropdownMenuItem.tsx +8 -4
  206. package/src/DropdownMenu/DropdownMenuRadioItem.tsx +5 -1
  207. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +5 -1
  208. package/src/FileInput/FileInput.tsx +16 -2
  209. package/src/InfoTip/InfoTip.doc.mjs +26 -2
  210. package/src/InfoTip/InfoTip.tsx +1 -0
  211. package/src/InputGroup/InputGroup.doc.mjs +4 -3
  212. package/src/InputGroup/InputGroup.tsx +4 -3
  213. package/src/InputGroup/groupStyles.ts +3 -1
  214. package/src/Item/Item.tsx +51 -9
  215. package/src/Layer/holdEntranceInput.ts +84 -0
  216. package/src/Layer/inputHold.ts +734 -0
  217. package/src/Layer/preserveAnimations.ts +12 -275
  218. package/src/Layout/LayoutContent.doc.mjs +6 -3
  219. package/src/Layout/LayoutContent.tsx +10 -4
  220. package/src/Layout/LayoutPanel.doc.mjs +6 -3
  221. package/src/Layout/LayoutPanel.tsx +11 -4
  222. package/src/Lightbox/Lightbox.tsx +4 -0
  223. package/src/MetadataList/MetadataList.tsx +1 -1
  224. package/src/MobileNav/MobileNav.doc.mjs +3 -3
  225. package/src/MobileNav/MobileNav.tsx +39 -1
  226. package/src/MoreMenu/MoreMenu.doc.mjs +2 -4
  227. package/src/MoreMenu/MoreMenu.tsx +5 -3
  228. package/src/RadioList/RadioListItem.doc.mjs +3 -2
  229. package/src/RadioList/RadioListItem.tsx +25 -7
  230. package/src/SelectableCard/SelectableCard.tsx +8 -1
  231. package/src/Spinner/Spinner.tsx +13 -3
  232. package/src/Stack/Stack.doc.mjs +1 -1
  233. package/src/Stack/StackItem.tsx +27 -13
  234. package/src/Table/plugins/sortable/useTableSortable.tsx +1 -1
  235. package/src/Token/Token.tsx +18 -1
  236. package/src/Token/TokenLink.tsx +7 -1
  237. package/src/Tooltip/useTooltip.tsx +175 -79
  238. package/src/TreeList/TreeListItem.tsx +36 -2
  239. package/src/gestures/GestureHost.ts +21 -5
  240. package/src/gestures/PanGestureRecognizer.ts +19 -2
  241. package/src/gestures/README.md +14 -5
  242. package/src/gestures/ScreenEdgePanGestureRecognizer.ts +6 -1
  243. package/src/gestures/nativeScroll.ts +12 -68
  244. package/src/gestures/wheelSegmenter.ts +243 -10
  245. package/src/hooks/StackItemScrollRegion.tsx +57 -0
  246. package/src/hooks/menuPressGesture.ts +244 -38
  247. package/src/hooks/menuPressValues.ts +18 -0
  248. package/src/hooks/useClickableContainer.doc.mjs +8 -2
  249. package/src/hooks/useClickableContainer.ts +72 -6
  250. package/src/hooks/useFocusTrap.ts +268 -151
  251. package/src/hooks/useMenuPress.ts +118 -11
  252. package/src/hooks/usePressFeedback.doc.mjs +3 -3
  253. package/src/hooks/usePressFeedback.ts +6 -6
  254. package/src/hooks/useScrollRegionProps.ts +76 -0
  255. package/src/i18n/generated-english/hooks/useScrollRegionProps.generated.ts +13 -0
  256. package/src/i18n/generated-locales/en.generated.ts +1 -0
  257. package/src/i18n/generated-locales/pseudo.generated.ts +1 -0
  258. package/src/motion/MEASUREMENTS.md +11 -0
  259. package/src/motion/carryAnimations.ts +389 -0
  260. package/src/motion/transientCopy.ts +45 -5
  261. package/src/motion/uikit/gestures.ts +22 -0
  262. package/src/motion/uikit/index.ts +1 -0
  263. package/src/privateAttributes/menuPressLit.stylex.ts +104 -0
  264. package/src/privateAttributes/press.stylex.ts +244 -0
  265. package/src/theme/tokens.stylex.ts +1 -1
  266. package/src/utils/flatTree.ts +15 -3
  267. package/src/utils/hitArea.ts +107 -5
  268. package/src/utils/inputStream.ts +47 -10
  269. package/src/utils/interactionOverlay.stylex.ts +7 -2
  270. package/src/utils/pressFeedback.ts +31 -6
  271. package/src/utils/pressGesture.ts +24 -1
  272. package/src/utils/rowPressLayer.stylex.ts +27 -0
  273. package/src/utils/scrollView.ts +135 -0
  274. package/src/utils/selectionLock.ts +5 -10
  275. package/src/utils/touchHitArea.stylex.ts +41 -0
  276. package/src/utils/triggerFocus.ts +17 -4
@@ -8,7 +8,7 @@ export const docs = {
8
8
  displayName: 'Radio List Item',
9
9
  isHiddenFromOverview: true,
10
10
  description:
11
- 'Individual radio item with label, description, and content slots.',
11
+ 'Individual radio item with label, description, and content slots. Under a coarse pointer an enabled option’s target grows toward 44 px through an invisible hit area. It never shrinks below the row and stops at half the gap to a closer neighbour; the row keeps its size. That hit area is the row’s `::after`, so decorate a row with a child.',
12
12
  // RadioListItem requires RadioList context; wrap it so the preview doesn't throw.
13
13
  playground: {
14
14
  defaults: {value: 'option-1', label: 'Option'},
@@ -97,7 +97,8 @@ export const docsZh = {
97
97
  name: 'RadioListItem',
98
98
  isHiddenFromOverview: true,
99
99
  displayName: 'Radio List Item',
100
- description: '单个单选选项,包含标签、描述和内容插槽。',
100
+ description:
101
+ '单个单选选项,包含标签、描述和内容插槽。在粗指针(触摸)下,已启用选项的目标通过不可见的点击区域向 44 px 扩展。它不小于所在行,与相邻控件较近时止于间距的一半;行尺寸不变。该点击区域是行的 `::after`,因此请用子元素装饰行。',
101
102
  props: [
102
103
  {
103
104
  name: 'label',
@@ -18,7 +18,7 @@
18
18
  * - /packages/cli/assets/templates/blocks/components/RadioList/ (showcase blocks)
19
19
  */
20
20
 
21
- import React, {use, useId, useRef, type ReactNode} from 'react';
21
+ import React, {use, useCallback, useId, useRef, type ReactNode} from 'react';
22
22
  import * as stylex from '@stylexjs/stylex';
23
23
  import type {StyleXStyles} from '@stylexjs/stylex';
24
24
  import type {BaseProps} from '../BaseProps';
@@ -32,9 +32,13 @@ import {rtlStyles} from '../utils/rtlStyles';
32
32
  import {indicatorScope} from '../Indicator/indicator.markers.stylex';
33
33
  import {useIndicatorFocusRing} from '../hooks/useIndicatorFocusRing';
34
34
  import {usePressFeedback} from '../hooks/usePressFeedback';
35
+ import {useMergedRefs} from '../hooks/useMergedRefs';
36
+ import {watchHitArea, writeHitInsets} from '../utils/hitArea';
37
+ import {touchHitAreaStyles} from '../utils/touchHitArea.stylex';
35
38
  import {useIndicator} from '../Indicator/useIndicator';
36
39
  import {Item, type ItemAlign} from '../Item/Item';
37
40
  import {themeProps} from '../utils/themeProps';
41
+ import {rowPressLayerStyles} from '../privateAttributes/press.stylex';
38
42
 
39
43
  // The touch press's paint, declared by the shared overlay styles on the
40
44
  // element the controller writes to (`pressedAlpha`) at the press's strength,
@@ -147,11 +151,6 @@ const rowStyles = stylex.create({
147
151
  // Suppress Item's interactive hover/press background so the resting and
148
152
  // hovered row look identical by default (a theme can restyle either).
149
153
  backgroundColor: 'transparent',
150
- // ...and the touch press's overlay image, for the same reason: under a
151
- // finger only the radio circle paints, as under a mouse. The press's
152
- // strength still lands on the row, which is where the layer over the
153
- // circle reads it.
154
- backgroundImage: 'none',
155
154
  },
156
155
  // The mouse press (a finger's lives on `data-astryx-press`): the row is
157
156
  // pressed, but not through a nested link or button, which keep their own
@@ -309,6 +308,21 @@ export function RadioListItem({
309
308
  // `<label htmlFor>` would double-fire under delegation.
310
309
  const radioRef = useRef<HTMLInputElement>(null);
311
310
 
311
+ // Under a coarse pointer an option's row grows toward 44 px through an
312
+ // invisible band, and the row keeps its size. A tap on the band lands on
313
+ // the row, which hands it to the radio. The watch lives as long as the row
314
+ // is attached: the ref's cleanup stops it.
315
+ const watchRowHitArea = useCallback(
316
+ (row: HTMLDivElement | null) => {
317
+ if (row == null || isDisabled) {
318
+ return;
319
+ }
320
+ return watchHitArea(row, insets => writeHitInsets(row, insets));
321
+ },
322
+ [isDisabled],
323
+ );
324
+ const mergedRowRef = useMergedRefs(ref, watchRowHitArea);
325
+
312
326
  const radioCircle = (
313
327
  <div
314
328
  {...stylex.props(
@@ -391,7 +405,7 @@ export function RadioListItem({
391
405
 
392
406
  return (
393
407
  <Item
394
- ref={ref}
408
+ ref={mergedRowRef}
395
409
  {...(isDisabled ? undefined : pressable)}
396
410
  startContent={mediaContent}
397
411
  // Delegate row-surface clicks (label text, description, and the empty
@@ -414,12 +428,16 @@ export function RadioListItem({
414
428
  // the painting row element (Item), the same element that carries the
415
429
  // theme target, so a theme's hover styling stays in step with the tint.
416
430
  !isDisabled && indicatorScope,
431
+ // The row keeps no press paint of its own: the circle paints, and
432
+ // reads the strength whose release the row keeps.
433
+ rowPressLayerStyles.cellWithoutLayer,
417
434
  // Restore the bare default look: zero Item's padding/radius and its
418
435
  // interactive hover/press background. Applied after Item's own base
419
436
  // styles so it wins within the base layer; a `radio-list-item` theme
420
437
  // still overrides it from the higher `astryx-theme` layer.
421
438
  rowStyles.root,
422
439
  !isDisabled && rowStyles.press,
440
+ !isDisabled && touchHitAreaStyles.layer,
423
441
  xstyle,
424
442
  ] as StyleXStyles
425
443
  }
@@ -44,6 +44,7 @@ import type {StyleXStyles} from '@stylexjs/stylex';
44
44
  import {colorVars, durationVars, easeVars} from '../theme/tokens.stylex';
45
45
  import type {Elevation, SizeValue, SpacingStep} from '../utils/types';
46
46
  import {isImeKeyEvent, mergeProps} from '../utils';
47
+ import {chainEventHandlers} from '../utils/chainEventHandlers';
47
48
  import {Card} from '../Card/Card';
48
49
  import type {CardVariant} from '../Card/Card';
49
50
  import {useClickableContainer} from '../hooks/useClickableContainer';
@@ -398,6 +399,7 @@ export function SelectableCard({
398
399
  onChange,
399
400
  onClick: onClickProp,
400
401
  onMouseUp: onMouseUpProp,
402
+ onPointerDown: onPointerDownProp,
401
403
  isDisabled = false,
402
404
  children,
403
405
  padding,
@@ -458,7 +460,7 @@ export function SelectableCard({
458
460
  [isDisabled, requestChange],
459
461
  );
460
462
 
461
- const {onClick, onMouseUp} = useClickableContainer({
463
+ const {onClick, onMouseUp, onPointerDown} = useClickableContainer({
462
464
  containerRef,
463
465
  interactiveRef,
464
466
  onClick: handleClick,
@@ -530,6 +532,11 @@ export function SelectableCard({
530
532
  }
531
533
  onClick={!isDisabled ? composedOnClick : undefined}
532
534
  onMouseUp={!isDisabled ? composedOnMouseUp : undefined}
535
+ onPointerDown={
536
+ !isDisabled
537
+ ? chainEventHandlers(onPointerDown, onPointerDownProp)
538
+ : onPointerDownProp
539
+ }
533
540
  {...props}
534
541
  onClickCapture={
535
542
  !isDisabled && handleLayerClickCapture
@@ -211,6 +211,16 @@ function syncRotationPhase(
211
211
  // Animation
212
212
  // =============================================================================
213
213
 
214
+ /**
215
+ * Full turns in one iteration of the rotation.
216
+ *
217
+ * Chromium wakes the main thread at each iteration boundary while the
218
+ * document listens for `animationiteration`. React listens for it on every
219
+ * root. One iteration holds 100 turns, so the wake comes once in 100 turns.
220
+ * Each turn is 360 degrees, so the ring looks the same.
221
+ */
222
+ const TURNS_PER_ITERATION = 100;
223
+
214
224
  /**
215
225
  * A `transform` animation runs on the compositor, so the ring keeps turning
216
226
  * while a script runs. A `stroke-dashoffset` animation would repaint the arc
@@ -218,7 +228,7 @@ function syncRotationPhase(
218
228
  */
219
229
  const rotation = stylex.keyframes({
220
230
  '0%': {transform: 'rotate(0deg)'},
221
- '100%': {transform: 'rotate(360deg)'},
231
+ '100%': {transform: `rotate(${360 * TURNS_PER_ITERATION}deg)`},
222
232
  });
223
233
 
224
234
  // =============================================================================
@@ -307,8 +317,8 @@ const styles = stylex.create({
307
317
  // broken. The status role and its name still say that work is busy.
308
318
  animationDuration: {
309
319
  // The loop reads the duration before a theme's tempo scales it.
310
- default: `var(--_astryx-pre-tempo-duration-slow-min, ${durationVars['--duration-slow-min']})`,
311
- '@media (prefers-reduced-motion: reduce)': '3s',
320
+ default: `calc(var(--_astryx-pre-tempo-duration-slow-min, ${durationVars['--duration-slow-min']}) * ${TURNS_PER_ITERATION})`,
321
+ '@media (prefers-reduced-motion: reduce)': `calc(3s * ${TURNS_PER_ITERATION})`,
312
322
  },
313
323
  animationIterationCount: 'infinite',
314
324
  animationName: rotation,
@@ -524,7 +524,7 @@ export const docs = {
524
524
  name: 'isScrollable',
525
525
  type: 'boolean',
526
526
  description:
527
- 'Enables scrollable overflow (overflow: auto). StackItem already applies the flex min-height/min-width reset, so <StackItem size="fill" isScrollable> is a complete scroll region. Matches isScrollable on LayoutContent and LayoutPanel.',
527
+ 'Enables scrollable overflow (overflow: auto). StackItem already applies the flex min-height/min-width reset, so <StackItem size="fill" isScrollable> is a complete scroll region. Matches isScrollable on LayoutContent and LayoutPanel. While its content overflows, a div or span item is a group named by aria-label, or by a translated "Content"; another element keeps its own role.',
528
528
  default: 'false',
529
529
  },
530
530
  {
@@ -2,9 +2,11 @@
2
2
 
3
3
  /**
4
4
  * @file StackItem.tsx
5
- * @input Uses React, ElementType, stackItem utility
5
+ * @input Uses React, ElementType, stackItem utility, StackItemScrollRegion
6
6
  * @output Exports StackItem polymorphic component and StackItemProps
7
- * @position Layout/Stack component; uses stackItem.stylex.ts
7
+ * @position Layout/Stack component; uses stackItem.stylex.ts. A scrollable
8
+ * `div` or `span` item renders through StackItemScrollRegion, which names
9
+ * it while it scrolls.
8
10
  *
9
11
  * SYNC: When modified, update these files to stay in sync:
10
12
  * - /packages/core/src/Stack/Stack.doc.mjs
@@ -21,6 +23,7 @@ import {
21
23
  } from './stackItem.stylex';
22
24
  import {mergeProps} from '../utils/mergeProps';
23
25
  import {themeProps} from '../utils/themeProps';
26
+ import {StackItemScrollRegion} from '../hooks/StackItemScrollRegion';
24
27
 
25
28
  const overflowStyles = stylex.create({
26
29
  scrollable: {
@@ -53,7 +56,9 @@ export interface StackItemProps extends BaseProps<HTMLElement> {
53
56
  * reset, so `<StackItem size="fill" isScrollable>` is a complete scroll
54
57
  * region — it grows to fill the stack and scrolls its own overflow with
55
58
  * no extra style plumbing. Matches `isScrollable` on `LayoutContent`
56
- * and `LayoutPanel`.
59
+ * and `LayoutPanel`. While its content overflows, a `div` or `span` item
60
+ * is a group named by the caller's `aria-label`, or by a translated
61
+ * "Content". Another element keeps its own role.
57
62
  * @default false
58
63
  */
59
64
  isScrollable?: boolean;
@@ -102,18 +107,27 @@ export function StackItem({
102
107
  xstyle,
103
108
  );
104
109
 
110
+ const elementProps = {
111
+ ...mergeProps(
112
+ themeProps('stack-item', {size}),
113
+ stylexProps,
114
+ className,
115
+ style,
116
+ ),
117
+ ...props,
118
+ };
119
+
120
+ if (isScrollable && (element === 'div' || element === 'span')) {
121
+ return createElement(
122
+ StackItemScrollRegion,
123
+ {as: element, ref, elementProps},
124
+ children,
125
+ );
126
+ }
127
+
105
128
  return createElement(
106
129
  element,
107
- {
108
- ref: ref as Ref<Element>,
109
- ...mergeProps(
110
- themeProps('stack-item', {size}),
111
- stylexProps,
112
- className,
113
- style,
114
- ),
115
- ...props,
116
- },
130
+ {ref: ref as Ref<Element>, ...elementProps},
117
131
  children,
118
132
  );
119
133
  }
@@ -168,7 +168,7 @@ const sortStyles = stylex.create({
168
168
  fontSize: 10,
169
169
  // eslint-disable-next-line @astryx/no-hardcoded-styles -- no token for lineHeight:1 (tight badge)
170
170
  lineHeight: '1',
171
- color: colorVars['--color-accent'],
171
+ color: colorVars['--color-text-accent'],
172
172
  },
173
173
  });
174
174
 
@@ -39,6 +39,7 @@ import {themeProps} from '../utils/themeProps';
39
39
  import {focusOutlineProps} from '../utils/focusOutline.stylex';
40
40
  import {interactionOverlayStyles} from '../utils/interactionOverlay.stylex';
41
41
  import {usePressFeedback} from '../hooks/usePressFeedback';
42
+ import {cancelNestedTouchPress} from '../hooks/useClickableContainer';
42
43
  import type {TokenColorMap} from './index';
43
44
  import {disabledInteractionProps} from '../utils/disabledInteraction';
44
45
  import {TokenNamingContext} from './TokenNamingContext';
@@ -510,6 +511,19 @@ export function Token({
510
511
  }
511
512
  effectiveOnClick(e);
512
513
  };
514
+ // A finger press on Remove or a control in end content does not run the
515
+ // token's action, so the token shows no press for it. The inner button,
516
+ // the first button child, is the token's own action.
517
+ const handleContainerPointerDown = (
518
+ e: React.PointerEvent<HTMLSpanElement>,
519
+ ) => {
520
+ rest.onPointerDown?.(e);
521
+ cancelNestedTouchPress(
522
+ e,
523
+ e.currentTarget,
524
+ e.currentTarget.querySelector(':scope > button'),
525
+ );
526
+ };
513
527
 
514
528
  return (
515
529
  <span
@@ -535,7 +549,10 @@ export function Token({
535
549
  style,
536
550
  )}
537
551
  {...rest}
538
- {...sharedProps}>
552
+ {...sharedProps}
553
+ onPointerDown={
554
+ isDisabled ? rest.onPointerDown : handleContainerPointerDown
555
+ }>
539
556
  {icon}
540
557
  <button
541
558
  type="button"
@@ -90,12 +90,13 @@ export function TokenLink({
90
90
  removeButton,
91
91
  onClick: onClickProp,
92
92
  onMouseUp: onMouseUpProp,
93
+ onPointerDown: onPointerDownProp,
93
94
  ...containerProps
94
95
  }: TokenLinkProps) {
95
96
  const containerRef = useRef<HTMLElement | null>(null);
96
97
  const linkRef = useRef<HTMLElement | null>(null);
97
98
 
98
- const {onClick, onMouseUp} = useClickableContainer({
99
+ const {onClick, onMouseUp, onPointerDown} = useClickableContainer({
99
100
  containerRef,
100
101
  interactiveRef: linkRef,
101
102
  href,
@@ -113,6 +114,11 @@ export function TokenLink({
113
114
  isDisabled
114
115
  ? onMouseUpProp
115
116
  : composeEventHandlers(onMouseUpProp, onMouseUp)
117
+ }
118
+ onPointerDown={
119
+ isDisabled
120
+ ? onPointerDownProp
121
+ : composeEventHandlers(onPointerDownProp, onPointerDown)
116
122
  }>
117
123
  {icon}
118
124
  {isDisabled ? (
@@ -21,7 +21,7 @@ import {
21
21
  type ReactNode,
22
22
  type RefCallback,
23
23
  } from 'react';
24
- import {createTriggerFocusGate} from '../utils/triggerFocus';
24
+ import {createTriggerFocusGate, isFocusOnTrigger} from '../utils/triggerFocus';
25
25
  import * as stylex from '@stylexjs/stylex';
26
26
  import {
27
27
  useLayerInternal,
@@ -308,6 +308,17 @@ export function useTooltip(options: TooltipOptions = {}): TooltipReturn {
308
308
  const showTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
309
309
  const hideTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
310
310
  const triggerRef = useRef<HTMLElement | null>(null);
311
+ // Whether the tooltip is enabled now, for a show that runs after a render
312
+ // (a pending hover show) and for the trigger ref callback.
313
+ const isEnabledRef = useRef(isEnabled);
314
+ isEnabledRef.current = isEnabled;
315
+ // Counts the renders in which the tooltip is disabled. A pending show
316
+ // records it, so a show scheduled before a disable is dropped even when
317
+ // the tooltip is enabled again before the timer fires.
318
+ const disabledRendersRef = useRef(0);
319
+ if (!isEnabled) {
320
+ disabledRendersRef.current += 1;
321
+ }
311
322
 
312
323
  // Clear all timeouts
313
324
  const clearTimeouts = useCallback(() => {
@@ -460,22 +471,16 @@ export function useTooltip(options: TooltipOptions = {}): TooltipReturn {
460
471
  return;
461
472
  }
462
473
  clearTimeouts();
474
+ const disabledRenders = disabledRendersRef.current;
463
475
  showTimeoutRef.current = setTimeout(() => {
464
- showLayer();
476
+ // A show runs only if the tooltip stayed enabled since it was
477
+ // scheduled: nothing would close a tooltip that opens after a disable.
478
+ if (disabledRendersRef.current === disabledRenders) {
479
+ showLayer();
480
+ }
465
481
  }, delay);
466
482
  }, [isEnabled, isOpen, clearTimeouts, showLayer, delay]);
467
483
 
468
- // A show scheduled while the triggers were enabled must not outlive them:
469
- // disabling also detaches the leave listener that would have cancelled it,
470
- // so the timer would open a tooltip that nothing ever closes. Only the
471
- // pending show is dropped; a pending hide still runs.
472
- useEffect(() => {
473
- if (!isEnabled && showTimeoutRef.current) {
474
- clearTimeout(showTimeoutRef.current);
475
- showTimeoutRef.current = null;
476
- }
477
- }, [isEnabled]);
478
-
479
484
  // Schedule hide with delay (suppressed when isOpen is true).
480
485
  // A small hover bridge (when hideDelay is 0) lets the pointer travel from the
481
486
  // trigger onto the tooltip surface without the tooltip vanishing — required
@@ -537,23 +542,25 @@ export function useTooltip(options: TooltipOptions = {}): TooltipReturn {
537
542
  // focus that counted still hides on blur (utils/triggerFocus.ts).
538
543
  const [focusGate] = useState(createTriggerFocusGate);
539
544
 
545
+ // Whether focus on `target` opens the tooltip. With a `trigger`, its
546
+ // `focusTrigger` rule applies too. Only `:focus-visible` focus opens it, and
547
+ // a tap never does: a tapped `<input>` also matches `:focus-visible`.
548
+ const opensOnFocus = useCallback(
549
+ (target: HTMLElement, trigger: HTMLElement | null): boolean => {
550
+ if (trigger != null && !isFocusOnTrigger(target, trigger, focusTrigger)) {
551
+ return false;
552
+ }
553
+ return !isTouchInteraction() && target.matches(':focus-visible');
554
+ },
555
+ [focusTrigger, isTouchInteraction],
556
+ );
557
+
540
558
  const handleFocusIn = useCallback(
541
559
  (e: Event) => {
542
560
  if (!focusGate.focusIn(e, focusTrigger) || !isEnabled) {
543
561
  return;
544
562
  }
545
- // A tap focuses the trigger it activates, and `:focus-visible` does not
546
- // filter that out on every element: a tapped `<input>` or contenteditable
547
- // matches it. Those are exactly the action triggers `auto` just decided
548
- // to keep shut, so without this the focus reopens what the tap
549
- // suppressed, over the field the user is trying to type into.
550
- if (isTouchInteraction()) {
551
- return;
552
- }
553
- // Only show tooltip for keyboard focus (:focus-visible),
554
- // not programmatic focus (e.g. dialog auto-focus, touch tap)
555
- const target = e.target as HTMLElement;
556
- if (!target.matches(':focus-visible')) {
563
+ if (!opensOnFocus(e.target as HTMLElement, null)) {
557
564
  return;
558
565
  }
559
566
  clearTimeouts();
@@ -563,7 +570,7 @@ export function useTooltip(options: TooltipOptions = {}): TooltipReturn {
563
570
  focusGate,
564
571
  focusTrigger,
565
572
  isEnabled,
566
- isTouchInteraction,
573
+ opensOnFocus,
567
574
  clearTimeouts,
568
575
  showLayer,
569
576
  ],
@@ -656,66 +663,155 @@ export function useTooltip(options: TooltipOptions = {}): TooltipReturn {
656
663
  ],
657
664
  );
658
665
 
659
- // Interaction ref that handles event listeners only
660
- const interactionRef: RefCallback<HTMLElement> = useCallback(
661
- (el: HTMLElement | null) => {
662
- // Cleanup previous element
663
- if (triggerRef.current) {
664
- triggerRef.current.removeEventListener('mouseenter', handleMouseEnter);
665
- triggerRef.current.removeEventListener('mouseleave', handleMouseLeave);
666
- triggerRef.current.removeEventListener('focusin', handleFocusIn);
667
- triggerRef.current.removeEventListener('focusout', handleFocusOut);
668
- triggerRef.current.removeEventListener('focusout', handleHeldFocusOut);
669
- stopPressTrackingRef.current?.();
670
- triggerRef.current.removeEventListener(
671
- 'pointerenter',
672
- handlePointerEnter,
673
- );
674
- triggerRef.current.removeEventListener(
675
- 'pointerdown',
676
- handlePointerDown,
677
- );
678
- }
666
+ // The trigger's listeners call the latest handlers through this ref, so the
667
+ // trigger ref callback keeps one identity.
668
+ const handlersRef = useRef({
669
+ handleMouseEnter,
670
+ handleMouseLeave,
671
+ handleFocusIn,
672
+ handleFocusOut,
673
+ handleHeldFocusOut,
674
+ handlePointerEnter,
675
+ handlePointerDown,
676
+ focusTrigger,
677
+ isOpen,
678
+ dropHold,
679
+ hideNow,
680
+ showNow,
681
+ opensOnFocus,
682
+ });
683
+ handlersRef.current = {
684
+ handleMouseEnter,
685
+ handleMouseLeave,
686
+ handleFocusIn,
687
+ handleFocusOut,
688
+ handleHeldFocusOut,
689
+ handlePointerEnter,
690
+ handlePointerDown,
691
+ focusTrigger,
692
+ isOpen,
693
+ dropHold,
694
+ hideNow,
695
+ showNow,
696
+ opensOnFocus,
697
+ };
679
698
 
680
- if (el) {
681
- // Attach hover listeners. `pointerenter` runs before the synthesized
682
- // `mouseenter` a tap produces, which is what lets the hover path know
683
- // it is looking at a finger.
684
- el.addEventListener('pointerenter', handlePointerEnter);
685
- el.addEventListener('mouseenter', handleMouseEnter);
686
- el.addEventListener('mouseleave', handleMouseLeave);
687
- // Press-to-dismiss on a mouse; tap-to-open on a touch pointer.
688
- el.addEventListener('pointerdown', handlePointerDown);
689
-
690
- // Focus listeners unless focus never opens the tooltip; `auto` decides
691
- // per event (utils/triggerFocus.ts).
692
- if (focusTrigger !== 'never') {
693
- el.addEventListener('focusin', handleFocusIn);
694
- el.addEventListener('focusout', handleFocusOut);
695
- }
699
+ // Removes the attached trigger's listeners (the trigger ref callback's
700
+ // cleanup).
701
+ const detachTriggerRef = useRef<(() => void) | null>(null);
702
+ // Set when the trigger detached while the tooltip was disabled: the
703
+ // caller removed the wiring (DropdownMenu's Button while its menu is
704
+ // open), so the next enabled attach reads the focus that arrived meanwhile.
705
+ const wasSuppressedRef = useRef(false);
706
+
707
+ // The trigger's ref callback owns every trigger listener, and reads the
708
+ // trigger's state when it attaches and detaches.
709
+ const interactionRef = useCallback((el: HTMLElement | null) => {
710
+ // A caller that reports `null` by hand detaches the trigger, as the
711
+ // returned cleanup does.
712
+ if (el == null) {
713
+ detachTriggerRef.current?.();
714
+ return;
715
+ }
716
+ if (el === triggerRef.current) {
717
+ return;
718
+ }
719
+ detachTriggerRef.current?.();
720
+ const latest = () => handlersRef.current;
721
+ // `pointerenter` runs before the synthesized `mouseenter` a tap produces,
722
+ // which is what lets the hover path know it is looking at a finger.
723
+ const onPointerEnter = (e: PointerEvent) => latest().handlePointerEnter(e);
724
+ const onMouseEnter = () => latest().handleMouseEnter();
725
+ const onMouseLeave = (e: Event) => latest().handleMouseLeave(e);
726
+ // Press-to-dismiss on a mouse; tap-to-open on a touch pointer.
727
+ const onPointerDown = (e: PointerEvent) => latest().handlePointerDown(e);
728
+ // Focus counts unless focus never opens the tooltip; `auto` decides per
729
+ // event (isFocusForTrigger).
730
+ const onFocusIn = (e: Event) => {
731
+ if (latest().focusTrigger !== 'never') {
732
+ latest().handleFocusIn(e);
733
+ }
734
+ };
735
+ const onFocusOut = (e: Event) => {
736
+ if (latest().focusTrigger !== 'never') {
737
+ latest().handleFocusOut(e);
696
738
  }
739
+ };
740
+ // A held tooltip's own focus-leave close, whatever `focusTrigger` is.
741
+ const onHeldFocusOut = (e: FocusEvent) => latest().handleHeldFocusOut(e);
742
+ el.addEventListener('pointerenter', onPointerEnter);
743
+ el.addEventListener('mouseenter', onMouseEnter);
744
+ el.addEventListener('mouseleave', onMouseLeave);
745
+ el.addEventListener('pointerdown', onPointerDown);
746
+ el.addEventListener('focusin', onFocusIn);
747
+ el.addEventListener('focusout', onFocusOut);
748
+ el.addEventListener('focusout', onHeldFocusOut);
749
+ triggerRef.current = el;
750
+
751
+ // A trigger that attaches again after its wiring was removed
752
+ // (DropdownMenu's Button as its menu closes) missed the focus that
753
+ // arrived meanwhile. Show for it only if it would open the tooltip:
754
+ // returned keyboard focus does, focus returned after a pointer pick does
755
+ // not. Any other attach leaves the tooltip as it is, so a tooltip
756
+ // dismissed with Escape stays closed.
757
+ const focused = el.ownerDocument.activeElement;
758
+ const wasSuppressed = wasSuppressedRef.current;
759
+ if (isEnabledRef.current) {
760
+ wasSuppressedRef.current = false;
761
+ }
762
+ if (
763
+ wasSuppressed &&
764
+ isEnabledRef.current &&
765
+ latest().isOpen === undefined &&
766
+ latest().focusTrigger !== 'never' &&
767
+ focused instanceof HTMLElement &&
768
+ el.contains(focused) &&
769
+ latest().opensOnFocus(focused, el)
770
+ ) {
771
+ latest().showNow();
772
+ }
697
773
 
698
- // Its own focus-leave listener: `focusTrigger="never"` attaches none.
699
- el?.addEventListener('focusout', handleHeldFocusOut);
700
- triggerRef.current = el;
701
- },
702
- [
703
- handleHeldFocusOut,
704
- focusTrigger,
705
- handleMouseEnter,
706
- handleMouseLeave,
707
- handleFocusIn,
708
- handleFocusOut,
709
- handlePointerEnter,
710
- handlePointerDown,
711
- ],
712
- );
774
+ const detach = () => {
775
+ if (detachTriggerRef.current === detach) {
776
+ detachTriggerRef.current = null;
777
+ }
778
+ el.removeEventListener('pointerenter', onPointerEnter);
779
+ el.removeEventListener('mouseenter', onMouseEnter);
780
+ el.removeEventListener('mouseleave', onMouseLeave);
781
+ el.removeEventListener('pointerdown', onPointerDown);
782
+ el.removeEventListener('focusin', onFocusIn);
783
+ el.removeEventListener('focusout', onFocusOut);
784
+ el.removeEventListener('focusout', onHeldFocusOut);
785
+ stopPressTrackingRef.current?.();
786
+ if (triggerRef.current === el) {
787
+ triggerRef.current = null;
788
+ }
789
+ // A caller removes the wiring as it disables the tooltip. Close an
790
+ // uncontrolled tooltip then, or the next attach shows a stale tooltip.
791
+ // A detach while enabled, such as a merged ref that changes identity
792
+ // on a re-render, keeps a hovered or held tooltip open.
793
+ if (!isEnabledRef.current && latest().isOpen === undefined) {
794
+ wasSuppressedRef.current = true;
795
+ latest().dropHold();
796
+ latest().hideNow();
797
+ }
798
+ };
799
+ detachTriggerRef.current = detach;
800
+ return detach;
801
+ }, []);
713
802
 
714
803
  // Combined ref - shorthand for calling both positionRef and interactionRef
715
- const ref: RefCallback<HTMLElement> = useCallback(
804
+ const ref = useCallback(
716
805
  (el: HTMLElement | null) => {
717
806
  layerRef(el);
718
- interactionRef(el);
807
+ const detachInteraction = interactionRef(el);
808
+ if (el == null) {
809
+ return;
810
+ }
811
+ return () => {
812
+ detachInteraction?.();
813
+ layerRef(null);
814
+ };
719
815
  },
720
816
  [layerRef, interactionRef],
721
817
  );