@astryxdesign/core 0.7.6 → 0.7.7-canary.0eb44a2

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 (272) hide show
  1. package/dist/AppShell/AppShell.d.ts.map +1 -1
  2. package/dist/AppShell/AppShell.js +23 -3
  3. package/dist/Avatar/Avatar.d.ts.map +1 -1
  4. package/dist/Avatar/Avatar.js +1 -1
  5. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  6. package/dist/BottomSheet/BottomSheetPanel.js +16 -9
  7. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  8. package/dist/Breadcrumbs/BreadcrumbItem.js +2 -1
  9. package/dist/Button/Button.d.ts.map +1 -1
  10. package/dist/Button/Button.js +3 -2
  11. package/dist/Chat/ChatComposerInput.d.ts +9 -0
  12. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  13. package/dist/Chat/ChatComposerInput.js +2 -0
  14. package/dist/Chat/ChatMessage.js +2 -2
  15. package/dist/Chat/chatMessage.vars.stylex.d.ts +8 -8
  16. package/dist/Chat/chatMessage.vars.stylex.js +8 -8
  17. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  18. package/dist/CommandPalette/CommandPaletteInput.js +8 -1
  19. package/dist/DateInput/MonthScroller.d.ts.map +1 -1
  20. package/dist/DateInput/MonthScroller.js +72 -2
  21. package/dist/DateInput/Wheel.d.ts +17 -1
  22. package/dist/DateInput/Wheel.d.ts.map +1 -1
  23. package/dist/DateInput/Wheel.js +179 -19
  24. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  25. package/dist/DateRangeInput/DateRangeInput.js +63 -49
  26. package/dist/Dialog/Dialog.d.ts.map +1 -1
  27. package/dist/Dialog/Dialog.js +37 -1
  28. package/dist/Drawer/Drawer.d.ts.map +1 -1
  29. package/dist/Drawer/Drawer.js +6 -0
  30. package/dist/Drawer/useDrawerDialogPresence.d.ts.map +1 -1
  31. package/dist/Drawer/useDrawerDialogPresence.js +4 -0
  32. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  33. package/dist/DropdownMenu/DropdownMenu.js +20 -4
  34. package/dist/DropdownMenu/useMenuOverflow.d.ts +13 -3
  35. package/dist/DropdownMenu/useMenuOverflow.d.ts.map +1 -1
  36. package/dist/DropdownMenu/useMenuOverflow.js +31 -8
  37. package/dist/Field/PanelSearchInput.d.ts.map +1 -1
  38. package/dist/Field/PanelSearchInput.js +8 -1
  39. package/dist/Field/textEntryTraits.d.ts +6 -6
  40. package/dist/Field/textEntryTraits.js +6 -6
  41. package/dist/Layer/layerClickBoundary.d.ts +7 -1
  42. package/dist/Layer/layerClickBoundary.d.ts.map +1 -1
  43. package/dist/Layer/layerClickBoundary.js +2 -2
  44. package/dist/Layer/usePressPin.d.ts +95 -0
  45. package/dist/Layer/usePressPin.d.ts.map +1 -0
  46. package/dist/Layer/usePressPin.js +365 -0
  47. package/dist/Layer/useTouchTrigger.d.ts +23 -1
  48. package/dist/Layer/useTouchTrigger.d.ts.map +1 -1
  49. package/dist/Layer/useTouchTrigger.js +45 -7
  50. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  51. package/dist/Lightbox/Lightbox.js +2 -1
  52. package/dist/Link/Link.d.ts.map +1 -1
  53. package/dist/Link/Link.js +1 -0
  54. package/dist/Popover/usePopover.d.ts.map +1 -1
  55. package/dist/Popover/usePopover.js +56 -7
  56. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  57. package/dist/PowerSearch/PowerSearch.js +38 -30
  58. package/dist/ProgressBar/ProgressBar.d.ts +1 -1
  59. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  60. package/dist/ProgressBar/ProgressBar.js +52 -14
  61. package/dist/ProgressBar/ProgressBarTouch.d.ts +29 -0
  62. package/dist/ProgressBar/ProgressBarTouch.d.ts.map +1 -0
  63. package/dist/ProgressBar/ProgressBarTouch.js +91 -0
  64. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  65. package/dist/SegmentedControl/SegmentedControlItem.js +2 -1
  66. package/dist/Selector/Selector.d.ts.map +1 -1
  67. package/dist/Selector/Selector.js +25 -13
  68. package/dist/Slider/Slider.d.ts.map +1 -1
  69. package/dist/Slider/Slider.js +404 -131
  70. package/dist/Slider/sliderTouchController.d.ts +189 -0
  71. package/dist/Slider/sliderTouchController.d.ts.map +1 -0
  72. package/dist/Slider/sliderTouchController.js +1017 -0
  73. package/dist/Slider/useSliderTouch.d.ts +22 -0
  74. package/dist/Slider/useSliderTouch.d.ts.map +1 -0
  75. package/dist/Slider/useSliderTouch.js +56 -0
  76. package/dist/Switch/Switch.d.ts +1 -1
  77. package/dist/Switch/Switch.d.ts.map +1 -1
  78. package/dist/Switch/Switch.js +200 -46
  79. package/dist/Switch/switchFill.d.ts +31 -0
  80. package/dist/Switch/switchFill.d.ts.map +1 -0
  81. package/dist/Switch/switchFill.js +88 -0
  82. package/dist/Switch/switchTouch.d.ts +81 -0
  83. package/dist/Switch/switchTouch.d.ts.map +1 -0
  84. package/dist/Switch/switchTouch.js +133 -0
  85. package/dist/Switch/useSwitchTouch.d.ts +285 -0
  86. package/dist/Switch/useSwitchTouch.d.ts.map +1 -0
  87. package/dist/Switch/useSwitchTouch.js +998 -0
  88. package/dist/TabList/Tab.d.ts.map +1 -1
  89. package/dist/TabList/Tab.js +4 -3
  90. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  91. package/dist/Tokenizer/Tokenizer.js +12 -5
  92. package/dist/Tokenizer/TokenizerItemRemovableContext.d.ts +3 -0
  93. package/dist/Tokenizer/TokenizerItemRemovableContext.d.ts.map +1 -0
  94. package/dist/Tokenizer/TokenizerItemRemovableContext.js +17 -0
  95. package/dist/Tooltip/Tooltip.d.ts +30 -6
  96. package/dist/Tooltip/Tooltip.d.ts.map +1 -1
  97. package/dist/Tooltip/Tooltip.js +5 -0
  98. package/dist/Tooltip/index.d.ts +1 -1
  99. package/dist/Tooltip/index.d.ts.map +1 -1
  100. package/dist/Tooltip/useTooltip.d.ts +31 -5
  101. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  102. package/dist/Tooltip/useTooltip.js +59 -18
  103. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  104. package/dist/Typeahead/BaseTypeahead.js +19 -9
  105. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  106. package/dist/Typeahead/Typeahead.js +26 -15
  107. package/dist/astryx.css +91 -8
  108. package/dist/gestures/GestureArena.d.ts +21 -2
  109. package/dist/gestures/GestureArena.d.ts.map +1 -1
  110. package/dist/gestures/GestureArena.js +23 -3
  111. package/dist/gestures/GestureHost.d.ts +123 -3
  112. package/dist/gestures/GestureHost.d.ts.map +1 -1
  113. package/dist/gestures/GestureHost.js +797 -15
  114. package/dist/gestures/GestureRecognizer.d.ts +15 -1
  115. package/dist/gestures/GestureRecognizer.d.ts.map +1 -1
  116. package/dist/gestures/GestureRecognizer.js +12 -0
  117. package/dist/gestures/PanGestureRecognizer.d.ts +8 -0
  118. package/dist/gestures/PanGestureRecognizer.d.ts.map +1 -1
  119. package/dist/gestures/PinchGestureRecognizer.d.ts +5 -1
  120. package/dist/gestures/PinchGestureRecognizer.d.ts.map +1 -1
  121. package/dist/gestures/PinchGestureRecognizer.js +10 -0
  122. package/dist/gestures/PressTracker.d.ts +6 -0
  123. package/dist/gestures/PressTracker.d.ts.map +1 -1
  124. package/dist/gestures/PressTracker.js +3 -0
  125. package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts +3 -3
  126. package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts.map +1 -1
  127. package/dist/gestures/ScreenEdgePanGestureRecognizer.js +7 -5
  128. package/dist/gestures/nativeScroll.d.ts +16 -2
  129. package/dist/gestures/nativeScroll.d.ts.map +1 -1
  130. package/dist/gestures/nativeScroll.js +39 -8
  131. package/dist/gestures/recognizerInternals.d.ts +4 -2
  132. package/dist/gestures/recognizerInternals.d.ts.map +1 -1
  133. package/dist/gestures/types.d.ts +17 -0
  134. package/dist/gestures/types.d.ts.map +1 -1
  135. package/dist/gestures/types.js +7 -0
  136. package/dist/gestures/usePressTracker.d.ts.map +1 -1
  137. package/dist/gestures/usePressTracker.js +1 -0
  138. package/dist/gestures/wheelSegmenter.d.ts +56 -0
  139. package/dist/gestures/wheelSegmenter.d.ts.map +1 -0
  140. package/dist/gestures/wheelSegmenter.js +122 -0
  141. package/dist/hooks/useAttachment.d.ts +16 -8
  142. package/dist/hooks/useAttachment.d.ts.map +1 -1
  143. package/dist/hooks/useAttachment.js +35 -15
  144. package/dist/hooks/useFocusTrap.d.ts +15 -0
  145. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  146. package/dist/hooks/useFocusTrap.js +13 -2
  147. package/dist/hooks/usePager.d.ts +261 -0
  148. package/dist/hooks/usePager.d.ts.map +1 -0
  149. package/dist/hooks/usePager.js +1169 -0
  150. package/dist/motion/animator.d.ts.map +1 -1
  151. package/dist/motion/animator.js +140 -19
  152. package/dist/motion/paging.d.ts +69 -0
  153. package/dist/motion/paging.d.ts.map +1 -0
  154. package/dist/motion/paging.js +148 -0
  155. package/dist/motion/uikit/index.d.ts +8 -4
  156. package/dist/motion/uikit/index.d.ts.map +1 -1
  157. package/dist/motion/uikit/index.js +8 -4
  158. package/dist/motion/uikit/paging.d.ts +171 -0
  159. package/dist/motion/uikit/paging.d.ts.map +1 -0
  160. package/dist/motion/uikit/paging.js +192 -0
  161. package/dist/motion/uikit/progressBar.d.ts +79 -0
  162. package/dist/motion/uikit/progressBar.d.ts.map +1 -0
  163. package/dist/motion/uikit/progressBar.js +102 -0
  164. package/dist/motion/uikit/scroll.d.ts +0 -19
  165. package/dist/motion/uikit/scroll.d.ts.map +1 -1
  166. package/dist/motion/uikit/scroll.js +2 -21
  167. package/dist/motion/uikit/slider.d.ts +97 -0
  168. package/dist/motion/uikit/slider.d.ts.map +1 -0
  169. package/dist/motion/uikit/slider.js +111 -0
  170. package/dist/motion/uikit/springs.d.ts +59 -30
  171. package/dist/motion/uikit/springs.d.ts.map +1 -1
  172. package/dist/motion/uikit/springs.js +64 -34
  173. package/dist/motion/uikit/switch.d.ts +166 -0
  174. package/dist/motion/uikit/switch.d.ts.map +1 -0
  175. package/dist/motion/uikit/switch.js +186 -0
  176. package/dist/privateAttributes/pressHoldsDrag.stylex.d.ts +18 -0
  177. package/dist/privateAttributes/pressHoldsDrag.stylex.d.ts.map +1 -0
  178. package/dist/privateAttributes/pressHoldsDrag.stylex.js +28 -0
  179. package/dist/utils/pressFeedback.d.ts.map +1 -1
  180. package/dist/utils/pressFeedback.js +5 -0
  181. package/dist/utils/selectionLock.d.ts +43 -0
  182. package/dist/utils/selectionLock.d.ts.map +1 -1
  183. package/dist/utils/selectionLock.js +394 -6
  184. package/package.json +5 -5
  185. package/src/AppShell/AppShell.doc.mjs +10 -0
  186. package/src/AppShell/AppShell.tsx +48 -2
  187. package/src/Avatar/Avatar.tsx +3 -1
  188. package/src/BottomSheet/BottomSheetPanel.tsx +16 -9
  189. package/src/Breadcrumbs/BreadcrumbItem.tsx +4 -1
  190. package/src/Button/Button.tsx +3 -2
  191. package/src/Chat/Chat.doc.mjs +1 -0
  192. package/src/Chat/ChatComposerInput.doc.mjs +11 -0
  193. package/src/Chat/ChatComposerInput.tsx +11 -0
  194. package/src/Chat/ChatMessage.tsx +2 -2
  195. package/src/Chat/chatMessage.vars.stylex.ts +8 -8
  196. package/src/CommandPalette/CommandPaletteInput.tsx +6 -0
  197. package/src/DateInput/MonthScroller.tsx +63 -5
  198. package/src/DateInput/Wheel.tsx +174 -14
  199. package/src/DateRangeInput/DateRangeInput.doc.mjs +3 -2
  200. package/src/DateRangeInput/DateRangeInput.tsx +117 -71
  201. package/src/Dialog/Dialog.tsx +37 -1
  202. package/src/Drawer/Drawer.tsx +6 -0
  203. package/src/Drawer/useDrawerDialogPresence.ts +4 -0
  204. package/src/DropdownMenu/DropdownMenu.tsx +28 -6
  205. package/src/DropdownMenu/useMenuOverflow.ts +44 -8
  206. package/src/Field/PanelSearchInput.tsx +6 -0
  207. package/src/Field/textEntryTraits.ts +6 -6
  208. package/src/Layer/layerClickBoundary.ts +2 -2
  209. package/src/Layer/usePressPin.ts +516 -0
  210. package/src/Layer/useTouchTrigger.ts +56 -6
  211. package/src/Lightbox/Lightbox.tsx +2 -1
  212. package/src/Link/Link.tsx +3 -0
  213. package/src/Popover/usePopover.tsx +71 -9
  214. package/src/PowerSearch/PowerSearch.tsx +38 -31
  215. package/src/ProgressBar/ProgressBar.doc.mjs +7 -5
  216. package/src/ProgressBar/ProgressBar.tsx +147 -16
  217. package/src/ProgressBar/ProgressBarTouch.ts +109 -0
  218. package/src/SegmentedControl/SegmentedControlItem.tsx +2 -1
  219. package/src/Selector/Selector.tsx +29 -2
  220. package/src/Slider/Slider.doc.mjs +11 -7
  221. package/src/Slider/Slider.tsx +539 -132
  222. package/src/Slider/sliderTouchController.ts +1209 -0
  223. package/src/Slider/useSliderTouch.ts +78 -0
  224. package/src/Switch/Switch.doc.mjs +3 -3
  225. package/src/Switch/Switch.tsx +412 -86
  226. package/src/Switch/switchFill.ts +106 -0
  227. package/src/Switch/switchTouch.ts +194 -0
  228. package/src/Switch/useSwitchTouch.ts +1121 -0
  229. package/src/TabList/Tab.tsx +5 -3
  230. package/src/Tokenizer/Tokenizer.tsx +15 -5
  231. package/src/Tokenizer/TokenizerItemRemovableContext.ts +22 -0
  232. package/src/Tooltip/Tooltip.doc.mjs +21 -6
  233. package/src/Tooltip/Tooltip.tsx +42 -5
  234. package/src/Tooltip/index.ts +1 -0
  235. package/src/Tooltip/useTooltip.doc.mjs +4 -2
  236. package/src/Tooltip/useTooltip.tsx +84 -21
  237. package/src/Typeahead/BaseTypeahead.tsx +16 -8
  238. package/src/Typeahead/Typeahead.tsx +29 -15
  239. package/src/gestures/GestureArena.ts +45 -4
  240. package/src/gestures/GestureHost.ts +958 -15
  241. package/src/gestures/GestureRecognizer.ts +28 -0
  242. package/src/gestures/PanGestureRecognizer.ts +8 -0
  243. package/src/gestures/PinchGestureRecognizer.ts +16 -1
  244. package/src/gestures/PressTracker.ts +10 -0
  245. package/src/gestures/README.md +82 -3
  246. package/src/gestures/ScreenEdgePanGestureRecognizer.ts +7 -5
  247. package/src/gestures/nativeScroll.ts +49 -9
  248. package/src/gestures/recognizerInternals.ts +9 -2
  249. package/src/gestures/types.ts +18 -0
  250. package/src/gestures/usePressTracker.ts +2 -0
  251. package/src/gestures/wheelSegmenter.ts +143 -0
  252. package/src/hooks/useAttachment.ts +56 -14
  253. package/src/hooks/useFocusTrap.ts +26 -2
  254. package/src/hooks/usePager.ts +1446 -0
  255. package/src/motion/MEASUREMENTS.md +36 -27
  256. package/src/motion/README.md +7 -3
  257. package/src/motion/animator.ts +193 -25
  258. package/src/motion/paging.ts +161 -0
  259. package/src/motion/uikit/index.ts +7 -4
  260. package/src/motion/uikit/paging.ts +193 -0
  261. package/src/motion/uikit/progressBar.ts +102 -0
  262. package/src/motion/uikit/scroll.ts +2 -18
  263. package/src/motion/uikit/slider.ts +105 -0
  264. package/src/motion/uikit/springs.ts +53 -32
  265. package/src/motion/uikit/switch.ts +174 -0
  266. package/src/privateAttributes/pressHoldsDrag.stylex.ts +28 -0
  267. package/src/utils/pressFeedback.ts +5 -0
  268. package/src/utils/selectionLock.ts +501 -6
  269. package/dist/motion/selectPage.d.ts +0 -26
  270. package/dist/motion/selectPage.d.ts.map +0 -1
  271. package/dist/motion/selectPage.js +0 -55
  272. package/src/motion/selectPage.ts +0 -55
@@ -42,6 +42,7 @@ import {useDevWarning} from '../hooks/useDevWarning';
42
42
  import {EDGE_COMP_ATTR} from '../Layout/edgeCompensation.stylex';
43
43
  import {themeProps} from '../utils/themeProps';
44
44
  import {focusOutlineProps} from '../utils/focusOutline.stylex';
45
+ import {isRenderable} from '../utils/isRenderable';
45
46
 
46
47
  export interface TabProps extends BaseProps<HTMLButtonElement> {
47
48
  /**
@@ -304,7 +305,8 @@ export function Tab({
304
305
  const isTabsPattern = tabListCtx.pattern === 'tabs';
305
306
  const isLink = href != null && !isTabsPattern;
306
307
  const isTabRole = isTabsPattern && !isLink;
307
- const displayIcon = isSelected && selectedIcon ? selectedIcon : icon;
308
+ const displayIcon =
309
+ isSelected && isRenderable(selectedIcon) ? selectedIcon : icon;
308
310
  const hasVisibleLabel = !isLabelHidden && label !== '';
309
311
 
310
312
  const isDisabled =
@@ -353,7 +355,7 @@ export function Tab({
353
355
  !isTabsPattern && panelId != null,
354
356
  );
355
357
 
356
- const iconElement = displayIcon ? (
358
+ const iconElement = isRenderable(displayIcon) ? (
357
359
  <span {...stylex.props(styles.icon, iconSizeStyles[size])}>
358
360
  {displayIcon}
359
361
  </span>
@@ -439,7 +441,7 @@ export function Tab({
439
441
  </span>
440
442
  ) : null;
441
443
 
442
- const endContentElement = endContent ? (
444
+ const endContentElement = isRenderable(endContent) ? (
443
445
  <span {...stylex.props(styles.endContentWrapper)}>{endContent}</span>
444
446
  ) : null;
445
447
 
@@ -55,6 +55,7 @@ import {
55
55
  import {inputDisabledFocusStyles} from '../Field/inputDisabledFocus';
56
56
  import {useMergedRefs} from '../hooks/useMergedRefs';
57
57
  import {TokenizerClearAllMessageContext} from './TokenizerClearAllMessageContext';
58
+ import {TokenizerItemRemovableContext} from './TokenizerItemRemovableContext';
58
59
  import {useBusyAnnouncement} from '../hooks/useBusyAnnouncement';
59
60
  import {useAdaptivePresentation} from '../hooks/useAdaptivePresentation';
60
61
  import {useIsHydrating} from '../hooks/useIsHydrating';
@@ -103,6 +104,7 @@ import {
103
104
  useComponentTranslator,
104
105
  } from '../i18n/useComponentTranslator';
105
106
  import english from '../i18n/generated-english/Tokenizer/Tokenizer.generated';
107
+ import {isRenderable} from '../utils/isRenderable';
106
108
 
107
109
  registerEnglishFallback(english);
108
110
 
@@ -1007,6 +1009,14 @@ export function Tokenizer<T extends SearchableItem>({
1007
1009
  // `endContent`, a clear button, or all three — so its width is measured
1008
1010
  // rather than assumed, and the input reserves it.
1009
1011
  const [laneRef, laneReserve] = useEndLaneReserve(END_LANE_INSET);
1012
+ // A composing component can lock tokens (PowerSearch's read-only field).
1013
+ // Backspace and the token remove controls skip a locked token. Clear all
1014
+ // does not check it.
1015
+ const isItemRemovable = use(TokenizerItemRemovableContext);
1016
+ const canRemoveItem = useCallback(
1017
+ (item: T) => isItemRemovable?.(item) ?? true,
1018
+ [isItemRemovable],
1019
+ );
1010
1020
  // The half of the lane's contents this component knows about. The busy half
1011
1021
  // is the leaf's own business — folding it in here would mean reading the
1012
1022
  // busy state during this render, which is exactly the re-render the store
@@ -1263,7 +1273,7 @@ export function Tokenizer<T extends SearchableItem>({
1263
1273
  // announcement covers both.
1264
1274
  const handleRemove = useCallback(
1265
1275
  (item: T, options?: {silent?: boolean}): boolean => {
1266
- if (isDisabled) {
1276
+ if (isDisabled || !canRemoveItem(item)) {
1267
1277
  return false;
1268
1278
  }
1269
1279
  const newItems = value.filter(v => v.id !== item.id);
@@ -1274,7 +1284,7 @@ export function Tokenizer<T extends SearchableItem>({
1274
1284
  inputRef.current?.focus();
1275
1285
  return true;
1276
1286
  },
1277
- [value, onChange, isDisabled, announce, t],
1287
+ [value, onChange, isDisabled, canRemoveItem, announce, t],
1278
1288
  );
1279
1289
 
1280
1290
  // Handle clearing all items
@@ -1414,7 +1424,7 @@ export function Tokenizer<T extends SearchableItem>({
1414
1424
  controlClassName,
1415
1425
  controlStyle,
1416
1426
  )}>
1417
- {startIcon && (
1427
+ {isRenderable(startIcon as ReactNode) && (
1418
1428
  <span {...stylex.props(value.length > 0 && styles.startIconWithTokens)}>
1419
1429
  {renderIconSlot(startIcon, {size: 'sm', color: 'secondary'})}
1420
1430
  </span>
@@ -1647,7 +1657,7 @@ export function Tokenizer<T extends SearchableItem>({
1647
1657
  controlClassName,
1648
1658
  controlStyle,
1649
1659
  )}>
1650
- {startIcon && (
1660
+ {isRenderable(startIcon as ReactNode) && (
1651
1661
  <span
1652
1662
  {...stylex.props(value.length > 0 && styles.startIconWithTokens)}>
1653
1663
  {renderIconSlot(startIcon, {size: 'sm', color: 'secondary'})}
@@ -1751,7 +1761,7 @@ export function Tokenizer<T extends SearchableItem>({
1751
1761
  )}>
1752
1762
  {isTruncated && (
1753
1763
  <>
1754
- {startIcon &&
1764
+ {isRenderable(startIcon as ReactNode) &&
1755
1765
  renderIconSlot(startIcon, {
1756
1766
  size: 'sm',
1757
1767
  color: 'secondary',
@@ -0,0 +1,22 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file TokenizerItemRemovableContext.ts
7
+ * @input None
8
+ * @output A context carrying a predicate that tells which tokens the user can
9
+ * remove
10
+ * @position Internal to Core: not exported from the Tokenizer barrel. A
11
+ * component that composes Tokenizer and locks some tokens (PowerSearch: every
12
+ * filter of a read-only field) provides it. Backspace then removes nothing
13
+ * while the last token is locked. Without it, every token is removable.
14
+ */
15
+
16
+ import {createContext} from 'react';
17
+ import type {SearchableItem} from '../Typeahead/types';
18
+
19
+ export const TokenizerItemRemovableContext = createContext<
20
+ ((item: SearchableItem) => boolean) | null
21
+ >(null);
22
+ TokenizerItemRemovableContext.displayName = 'TokenizerItemRemovableContext';
@@ -84,9 +84,16 @@ export const docs = {
84
84
  name: 'touchTrigger',
85
85
  type: "'auto' | 'tap' | 'none'",
86
86
  description:
87
- 'What a tap does where there is no hover. auto opens on tap unless the trigger performs an action of its own (a button, link, or form control), whose tap belongs to the control; an aria-disabled control performs none, so a tap on it opens its reason. tap always opens; use it for an info icon rendered as a button, whose only job is to reveal the tooltip. none never opens on touch.',
87
+ 'What a tap does where there is no hover. auto opens on tap unless the trigger performs an action of its own (a button, link, or form control), whose tap belongs to the control; an aria-disabled control performs none, so a tap on it opens its reason. tap always opens. none never opens on touch. It decides only under triggerBehavior="adaptive", the default.',
88
88
  default: "'auto'",
89
89
  },
90
+ {
91
+ name: 'triggerBehavior',
92
+ type: "'adaptive' | 'hover' | 'toggle'",
93
+ description:
94
+ 'How the trigger opens the tooltip. adaptive (the default): hover and focus preview it, and a tap opens it unless the trigger performs an action of its own, as touchTrigger decides while it remains. hover: hover and focus preview it, and no touch or pen tap opens it. toggle, on a button trigger: a click, a tap, Enter, or Space pins the tooltip and the same press closes it, while hover and keyboard focus still preview it. A pinned tooltip stays until Escape, a second press, a press outside, or focus leaving, and the press stops at the trigger, so a clickable row or card around it does not activate (with anchorRef, the anchored trigger still runs its own onClick, Astryx clickable rows and cards ignore the press except an Item row when the anchor is not a native button, and a plain onClick element around the anchor receives it unless that onClick stops it). The trigger reports aria-expanded and aria-controls, and the text is announced once when it pins. Use it for an info icon or any button whose only job is revealing the tooltip.',
95
+ default: "'adaptive'",
96
+ },
90
97
  {
91
98
  name: 'isEnabled',
92
99
  type: 'boolean',
@@ -130,7 +137,7 @@ export const docs = {
130
137
  bestPractices: [
131
138
  {guidance: true, description: 'Keep tooltip content concise: aim for under 140 characters of plain text.'},
132
139
  {guidance: true, description: 'Add a tooltip to icon-only buttons and controls that lack a visible label.'},
133
- {guidance: true, description: 'Set touchTrigger to tap when the trigger is a button whose only job is revealing the tooltip, such as an info icon: touch has no hover, and auto keeps the tap for triggers that perform an action.'},
140
+ {guidance: true, description: 'Set triggerBehavior to toggle when the trigger is a button whose only job is revealing the tooltip, such as an info icon: a click, a tap, Enter, or Space pins it, and the press stops at the trigger.'},
134
141
  {guidance: false, description: 'Place interactive elements like links or buttons inside a tooltip; use HoverCard or Popover instead.'},
135
142
  {guidance: false, description: 'Use tooltips for essential information that users must see to complete a task.'},
136
143
  ],
@@ -197,9 +204,16 @@ export const docsZh = {
197
204
  name: 'touchTrigger',
198
205
  type: "'auto' | 'tap' | 'none'",
199
206
  description:
200
- '在没有悬停的触摸设备上,轻点的行为。auto:轻点即打开,除非触发元素本身会执行操作(按钮、链接、表单控件),此时轻点归该控件所有;设置了 aria-disabled 的控件不执行操作,因此轻点它会显示其原因。tap:始终轻点打开——适用于以按钮形式呈现、唯一作用就是显示工具提示的信息图标。none:触摸永不打开。',
207
+ '在没有悬停的触摸设备上,轻点的行为。auto:轻点即打开,除非触发元素本身会执行操作(按钮、链接、表单控件),此时轻点归该控件所有;设置了 aria-disabled 的控件不执行操作,因此轻点它会显示其原因。tap:始终轻点打开。none:触摸永不打开。仅在 triggerBehavior 为 adaptive(默认)时生效。',
201
208
  default: "'auto'",
202
209
  },
210
+ {
211
+ name: 'triggerBehavior',
212
+ type: "'adaptive' | 'hover' | 'toggle'",
213
+ description:
214
+ '触发元素如何打开工具提示。adaptive(默认):悬停和聚焦会预览;轻点即打开,除非触发元素本身会执行操作(在 touchTrigger 仍存在期间由它决定)。hover:悬停和聚焦会预览,触摸或触控笔轻点都不会打开。toggle(仅限按钮触发元素):单击、轻点、Enter 或空格键固定打开,再按一次关闭;悬停和键盘聚焦仍会预览。固定后,直到按 Escape、再次按下、在外部按下或焦点移出才关闭;这次按下止于触发元素,外层可点击的行或卡片不会被激活(使用 anchorRef 时,锚定元素自身的 onClick 仍会运行,Astryx 的可点击行和卡片会忽略这次按下,但锚定元素不是原生按钮时,Item 行仍会响应,锚定元素外层普通的 onClick 元素仍会收到,除非锚定元素的 onClick 阻止它)。触发元素会报告 aria-expanded 和 aria-controls,固定时朗读一次文本。适用于信息图标等唯一作用就是显示工具提示的按钮。',
215
+ default: "'adaptive'",
216
+ },
203
217
  {
204
218
  name: 'isEnabled',
205
219
  type: 'boolean',
@@ -238,7 +252,7 @@ export const docsZh = {
238
252
  bestPractices: [
239
253
  {guidance: true, description: 'Keep tooltip content concise: aim for under 140 characters of plain text.'},
240
254
  {guidance: true, description: 'Add a tooltip to icon-only buttons and controls that lack a visible label.'},
241
- {guidance: true, description: 'Set touchTrigger to tap when the trigger is a button whose only job is revealing the tooltip, such as an info icon: touch has no hover, and auto keeps the tap for triggers that perform an action.'},
255
+ {guidance: true, description: 'Set triggerBehavior to toggle when the trigger is a button whose only job is revealing the tooltip, such as an info icon: a click, a tap, Enter, or Space pins it, and the press stops at the trigger.'},
242
256
  {guidance: false, description: 'Place interactive elements like links or buttons inside a tooltip; use HoverCard or Popover instead.'},
243
257
  {guidance: false, description: 'Use tooltips for essential information that users must see to complete a task.'},
244
258
  ],
@@ -255,7 +269,7 @@ export const docsDense = {
255
269
  bestPractices: [
256
270
  {guidance: true, description: 'Keep tooltip content concise: aim for under 140 characters of plain text.'},
257
271
  {guidance: true, description: 'Add a tooltip to icon-only buttons and controls that lack a visible label.'},
258
- {guidance: true, description: 'Set touchTrigger to tap when the trigger is a button whose only job is revealing the tooltip, such as an info icon: touch has no hover, and auto keeps the tap for triggers that perform an action.'},
272
+ {guidance: true, description: 'Set triggerBehavior to toggle when the trigger is a button whose only job is revealing the tooltip, such as an info icon: a click, a tap, Enter, or Space pins it, and the press stops at the trigger.'},
259
273
  {guidance: false, description: 'Place interactive elements like links or buttons inside a tooltip; use HoverCard or Popover instead.'},
260
274
  {guidance: false, description: 'Use tooltips for essential information that users must see to complete a task.'},
261
275
  ],
@@ -274,7 +288,8 @@ export const docsDense = {
274
288
  delay: 'Show delay in ms.',
275
289
  hideDelay: 'Hide delay in ms.',
276
290
  focusTrigger: 'Controls when focus events trigger tooltip.',
277
- touchTrigger: 'Tap behavior where there is no hover. auto = tap opens unless the trigger acts (button/link/control; an aria-disabled control does not act); tap = always opens (info icon rendered as a button); none = never on touch.',
291
+ touchTrigger: 'Tap behavior where there is no hover, under triggerBehavior="adaptive" only. auto = tap opens unless the trigger acts (button/link/control; an aria-disabled control does not act); tap = always opens; none = never on touch.',
292
+ triggerBehavior: 'adaptive (default) = hover/focus preview, a tap opens unless the trigger acts; hover = hover/focus preview, no touch/pen tap opens; toggle = a click/tap/Enter/Space on a button trigger pins the tooltip (aria-expanded, announced once), the same press closes it, and the press stops at the trigger.',
278
293
  isEnabled: 'Enables/disables tooltip triggers.',
279
294
  onOpenChange: 'Callback when visibility changes; true=shown, false=hidden.',
280
295
  hasHoverIndication: 'Dashed underline on trigger element.',
@@ -25,12 +25,18 @@ import * as stylex from '@stylexjs/stylex';
25
25
  import {
26
26
  useTooltip,
27
27
  type TooltipFocusTrigger,
28
+ type TooltipTriggerBehavior,
28
29
  type TooltipTouchTrigger,
29
30
  } from './useTooltip';
31
+ import {stopPressPinClick} from '../Layer/usePressPin';
30
32
  import type {LayerAlignment, LayerPlacement} from '../Layer/useLayer';
31
33
  import {colorVars} from '../theme/tokens.stylex';
32
34
 
33
- export type {TooltipFocusTrigger, TooltipTouchTrigger} from './useTooltip';
35
+ export type {
36
+ TooltipFocusTrigger,
37
+ TooltipTriggerBehavior,
38
+ TooltipTouchTrigger,
39
+ } from './useTooltip';
34
40
 
35
41
  const styles = stylex.create({
36
42
  wrapperContents: {
@@ -109,15 +115,40 @@ export interface TooltipProps {
109
115
  * - `auto`: tap opens the tooltip, unless the trigger performs an action of
110
116
  * its own (a button, a link, a form control) — that tap belongs to the
111
117
  * control, and a hint about a control the user just operated is noise
112
- * - `tap`: tap always opens the tooltip. This is what an info icon rendered
113
- * as a button wants: it looks like an action to the DOM, but revealing the
114
- * tooltip is the only thing it does
118
+ * - `tap`: tap always opens the tooltip, even on a trigger that acts
115
119
  * - `none`: touch never opens the tooltip
116
120
  *
121
+ * It decides only under `triggerBehavior="adaptive"`, the default.
122
+ *
117
123
  * @default 'auto'
118
124
  */
119
125
  touchTrigger?: TooltipTouchTrigger;
120
126
 
127
+ /**
128
+ * How the trigger opens the tooltip:
129
+ * - `adaptive` (the default): hover and focus preview it. A tap opens it
130
+ * unless the trigger performs an action of its own. While `touchTrigger`
131
+ * remains, it decides those taps.
132
+ * - `hover`: hover and focus preview it; no touch or pen tap opens it.
133
+ * - `toggle`: on a button trigger, a click, a tap, Enter, or Space pins the
134
+ * tooltip open, and the same press closes it. Hover and keyboard focus
135
+ * still preview it. A pinned tooltip stays until Escape, a second press,
136
+ * a press outside, or focus leaving. The press stops at the trigger. Its
137
+ * default action is prevented, and the trigger's own handlers still run.
138
+ * No clickable row or card around the trigger activates. With
139
+ * `anchorRef`, the anchor's own `onClick` runs, and a plain `onClick`
140
+ * element around it receives the press unless that `onClick` stops it.
141
+ * Astryx clickable rows, cards, and containers ignore it, except an Item
142
+ * row when the anchor is not a native button. The trigger reports
143
+ * `aria-expanded` and `aria-controls`, and the tooltip's text is
144
+ * announced once when it pins. Use it for a trigger whose information
145
+ * lives only in the tooltip, such as an info icon. `touchTrigger` is
146
+ * ignored.
147
+ *
148
+ * @default 'adaptive'
149
+ */
150
+ triggerBehavior?: TooltipTriggerBehavior;
151
+
121
152
  /**
122
153
  * Whether the tooltip is enabled.
123
154
  * When false, hover/focus triggers are disabled.
@@ -201,6 +232,7 @@ export function Tooltip({
201
232
  hideDelay = 0,
202
233
  focusTrigger = 'auto',
203
234
  touchTrigger = 'auto',
235
+ triggerBehavior = 'adaptive',
204
236
  isEnabled = true,
205
237
  onOpenChange,
206
238
  hasHoverIndication = 'auto',
@@ -230,6 +262,7 @@ export function Tooltip({
230
262
  hideDelay,
231
263
  focusTrigger,
232
264
  touchTrigger,
265
+ triggerBehavior,
233
266
  isEnabled,
234
267
  isOpen,
235
268
  isDefaultOpen,
@@ -343,6 +376,7 @@ export function Tooltip({
343
376
  ref={tooltip.ref}
344
377
  tabIndex={0}
345
378
  aria-describedby={tooltip.describedBy}
379
+ onClick={stopPressPinClick}
346
380
  {...stylex.props(
347
381
  styles.wrapperInline,
348
382
  showHoverIndication && styles.hoverIndication,
@@ -357,7 +391,10 @@ export function Tooltip({
357
391
  // For element children: use display:contents, ref on first child
358
392
  return (
359
393
  <>
360
- <div ref={wrapperRef} {...stylex.props(styles.wrapperContents)}>
394
+ <div
395
+ ref={wrapperRef}
396
+ onClick={stopPressPinClick}
397
+ {...stylex.props(styles.wrapperContents)}>
361
398
  {children}
362
399
  </div>
363
400
  {tooltip.renderTooltip(content)}
@@ -15,6 +15,7 @@
15
15
  export {useTooltip} from './useTooltip';
16
16
  export type {
17
17
  TooltipFocusTrigger,
18
+ TooltipTriggerBehavior,
18
19
  TooltipTouchTrigger,
19
20
  TooltipOptions,
20
21
  TooltipReturn,
@@ -12,7 +12,8 @@ export const docs = {
12
12
  {name: 'delay', type: 'number', description: 'Delay before showing on hover, in milliseconds.', default: '200'},
13
13
  {name: 'hideDelay', type: 'number', description: 'Delay before hiding after mouse or focus leaves, in milliseconds.', default: '0'},
14
14
  {name: 'focusTrigger', type: "'auto' | 'always' | 'never'", description: 'When focus should open the tooltip. auto opens when the trigger itself takes focus, or focus lands inside a trigger that is focusable at that moment, so a roving-tabindex member opens when the arrow keys reach it.', default: "'auto'"},
15
- {name: 'touchTrigger', type: "'auto' | 'tap' | 'none'", description: 'What a tap does where there is no hover. auto opens on tap unless the trigger performs an action of its own (an aria-disabled control performs none); tap always opens (an info icon rendered as a button); none never opens on touch.', default: "'auto'"},
15
+ {name: 'touchTrigger', type: "'auto' | 'tap' | 'none'", description: 'What a tap does where there is no hover. auto opens on tap unless the trigger performs an action of its own (an aria-disabled control performs none); tap always opens; none never opens on touch. It decides only under triggerBehavior="adaptive", the default.', default: "'auto'"},
16
+ {name: 'triggerBehavior', type: "'adaptive' | 'hover' | 'toggle'", description: 'How the trigger opens the tooltip. adaptive (the default) previews on hover and focus and opens on a tap unless the trigger acts (touchTrigger decides while it remains); hover previews on hover and focus, and no touch or pen tap opens it; toggle, on a button trigger, pins on a click, a tap, Enter, or Space and closes on the same press, Escape, a press outside, or focus leaving. The trigger carries aria-expanded and aria-controls, the tooltip is announced once when it pins. Astryx clickable rows and cards ignore the pinning press, except an Item row on a trigger that is not a native button; a plain onClick ancestor still receives it from the hook (the Tooltip component stops it at a child trigger).', default: "'adaptive'"},
16
17
  {name: 'isEnabled', type: 'boolean', description: 'Whether hover and focus triggers are enabled.', default: 'true'},
17
18
  {name: 'isOpen', type: 'boolean', description: 'Controlled open state. true force-shows, false force-hides, undefined lets hover/focus manage visibility.'},
18
19
  {name: 'isDefaultOpen', type: 'boolean', description: 'Whether the tooltip should be shown on mount.', default: 'false'},
@@ -50,7 +51,8 @@ export const docsDense = {
50
51
  delay: 'show delay in ms.',
51
52
  hideDelay: 'hide delay after mouse/focus leave in ms.',
52
53
  focusTrigger: 'when focus opens tooltip; auto = focus on the trigger, or inside a trigger focusable when focus arrives.',
53
- touchTrigger: 'tap behavior with no hover; auto = tap unless trigger acts, tap = always, none = never.',
54
+ touchTrigger: 'tap behavior with no hover, under triggerBehavior adaptive only; auto = tap unless trigger acts, tap = always, none = never.',
55
+ triggerBehavior: 'adaptive (default) = hover/focus preview, tap opens unless the trigger acts; hover = no touch/pen tap opens; toggle = a press on a button trigger pins and closes; clickable rows and cards ignore it (an Item row only on a native-button trigger), and the Tooltip component also stops it at a child trigger.',
54
56
  isEnabled: 'whether hover/focus triggers are enabled.',
55
57
  isOpen: 'controlled open state: true force-show, false force-hide, undefined unmanaged.',
56
58
  isDefaultOpen: 'show on mount.',
@@ -4,7 +4,7 @@
4
4
 
5
5
  /**
6
6
  * @file useTooltip.tsx
7
- * @input Uses useLayer, useTouchTrigger, React hooks
7
+ * @input Uses useLayer, usePressPin, React hooks
8
8
  * @output Exports useTooltip hook with stable trigger refs for hover/focus/tap tooltips
9
9
  * @position Layer hook; builds on useLayer for tooltip behavior
10
10
  *
@@ -26,12 +26,12 @@ import {
26
26
  type LayerAlignment,
27
27
  type LayerPlacement,
28
28
  } from '../Layer/useLayer';
29
- import {
30
- useTouchTrigger,
31
- type LayerTouchTrigger,
32
- } from '../Layer/useTouchTrigger';
29
+ import type {LayerTouchTrigger} from '../Layer/useTouchTrigger';
30
+ import {useAnnounceRenderedText} from '../hooks/useAnnounceRenderedText';
31
+ import {useMergedRefs} from '../hooks/useMergedRefs';
33
32
  import {layerAnimations} from '../Layer/layerAnimations.stylex';
34
33
  import {registerLayerSurface} from '../Layer/layerSurfacePress';
34
+ import {usePressPin} from '../Layer/usePressPin';
35
35
  import {useLayerDismissal} from '../Layer/useLayerDismissal';
36
36
  import {themeProps} from '../utils/themeProps';
37
37
  import {
@@ -83,6 +83,14 @@ export type TooltipFocusTrigger = 'auto' | 'always' | 'never';
83
83
  */
84
84
  export type TooltipTouchTrigger = LayerTouchTrigger;
85
85
 
86
+ /**
87
+ * How the trigger opens the tooltip. `adaptive`, the default, previews it on
88
+ * hover and focus, and a tap opens it unless the trigger acts. `hover`
89
+ * previews it on hover and focus, and no touch or pen tap opens it. `toggle`
90
+ * pins it on a click, tap, Enter, or Space on a button trigger.
91
+ */
92
+ export type TooltipTriggerBehavior = 'adaptive' | 'hover' | 'toggle';
93
+
86
94
  export interface TooltipOptions {
87
95
  /**
88
96
  * Position placement relative to anchor
@@ -124,15 +132,35 @@ export interface TooltipOptions {
124
132
  * - `auto`: tap opens the tooltip, unless the trigger performs an action of
125
133
  * its own (a button, a link, a form control) — that tap belongs to the
126
134
  * control, and a hint about a control the user just operated is noise
127
- * - `tap`: tap always opens the tooltip. This is what an info icon rendered
128
- * as a button wants: it looks like an action to the DOM, but revealing the
129
- * tooltip is the only thing it does
135
+ * - `tap`: tap always opens the tooltip, even on a trigger that acts
130
136
  * - `none`: touch never opens the tooltip
131
137
  *
138
+ * It decides only under `triggerBehavior="adaptive"`, the default.
139
+ *
132
140
  * @default 'auto'
133
141
  */
134
142
  touchTrigger?: TooltipTouchTrigger;
135
143
 
144
+ /**
145
+ * How the trigger opens the tooltip:
146
+ * - `adaptive` (the default): hover and focus preview it. A tap opens it
147
+ * unless the trigger performs an action of its own. While `touchTrigger`
148
+ * remains, it decides those taps.
149
+ * - `hover`: hover and focus preview it; no touch or pen tap opens it.
150
+ * - `toggle`: on a button trigger, a click, a tap, Enter, or Space pins the
151
+ * tooltip open, and the same press closes it. Hover and keyboard focus
152
+ * still preview it. A pinned tooltip stays until Escape, a second press,
153
+ * a press outside, or focus leaving. Astryx clickable rows and cards
154
+ * ignore the pinning press, except an Item row on a trigger that is not
155
+ * a native button. The `Tooltip` component also stops it at a child
156
+ * trigger. A hook caller stops a plain `onClick` ancestor itself. The
157
+ * trigger reports `aria-expanded` and `aria-controls`, and the tooltip's
158
+ * text is announced once when it pins. `touchTrigger` is ignored.
159
+ *
160
+ * @default 'adaptive'
161
+ */
162
+ triggerBehavior?: TooltipTriggerBehavior;
163
+
136
164
  /**
137
165
  * Whether the tooltip is enabled.
138
166
  * When false, hover/focus triggers are disabled.
@@ -275,6 +303,7 @@ export function useTooltip(options: TooltipOptions = {}): TooltipReturn {
275
303
  hideDelay = 0,
276
304
  focusTrigger = 'auto',
277
305
  touchTrigger = 'auto',
306
+ triggerBehavior = 'adaptive',
278
307
  isEnabled = true,
279
308
  isOpen,
280
309
  isDefaultOpen = false,
@@ -332,23 +361,44 @@ export function useTooltip(options: TooltipOptions = {}): TooltipReturn {
332
361
  hideLayer();
333
362
  }, [clearTimeouts, hideLayer]);
334
363
 
364
+ // A press pins the tooltip under `triggerBehavior="toggle"`. The pin also
365
+ // owns the tooltip's touch trigger, so this one call wires both.
335
366
  const {
336
- isTouchPointerRef,
337
- isTouchInteraction,
338
- handlePointerEnter,
339
- handlePointerDown: handleTouchPointerDown,
340
- clearTapOpen,
341
- } = useTouchTrigger({
367
+ isToggleTrigger,
368
+ isHeldOpen: isPressHeldOpen,
369
+ attach: attachPressPin,
370
+ unpin: unpinPress,
371
+ isPinShown,
372
+ touch: {
373
+ isTouchPointerRef,
374
+ isTouchInteraction,
375
+ handlePointerEnter,
376
+ handlePointerDown: handleTouchPointerDown,
377
+ clearTapOpen,
378
+ },
379
+ } = usePressPin({
380
+ triggerBehavior,
342
381
  touchTrigger,
343
382
  isEnabled,
344
- isControlled: isOpen !== undefined,
345
- isOpen: isLayerOpen,
346
- layerId: layerId,
383
+ isOpen,
384
+ isLayerOpen,
385
+ layerId,
347
386
  triggerRef,
387
+ component: 'Tooltip',
348
388
  show: showNow,
349
389
  hide: hideNow,
390
+ report: next => (next ? onShow?.() : onHide?.()),
350
391
  });
351
392
 
393
+ // Only a pin announces the text: a preview is already the trigger's
394
+ // description.
395
+ const contentRef = useRef<HTMLDivElement | null>(null);
396
+ useAnnounceRenderedText(contentRef, isPinShown, 'pinned');
397
+ const contentRefs = useMergedRefs<HTMLDivElement>(
398
+ contentRef,
399
+ registerLayerSurface,
400
+ );
401
+
352
402
  // Schedule show with delay (suppressed when isOpen is false)
353
403
  const scheduleShow = useCallback(() => {
354
404
  if (!isEnabled || isOpen === false) {
@@ -376,7 +426,7 @@ export function useTooltip(options: TooltipOptions = {}): TooltipReturn {
376
426
  // trigger onto the tooltip surface without the tooltip vanishing — required
377
427
  // for WCAG 1.4.13 (Content on Hover or Focus: hoverable).
378
428
  const scheduleHide = useCallback(() => {
379
- if (isOpen === true) {
429
+ if (isOpen === true || isPressHeldOpen()) {
380
430
  return;
381
431
  }
382
432
  clearTimeouts();
@@ -384,7 +434,7 @@ export function useTooltip(options: TooltipOptions = {}): TooltipReturn {
384
434
  hideTimeoutRef.current = setTimeout(() => {
385
435
  hideLayer();
386
436
  }, effectiveHideDelay);
387
- }, [isOpen, clearTimeouts, hideLayer, hideDelay]);
437
+ }, [isOpen, isPressHeldOpen, clearTimeouts, hideLayer, hideDelay]);
388
438
 
389
439
  // Cancel a pending hide (e.g. the pointer entered the tooltip surface).
390
440
  const cancelHide = useCallback(() => {
@@ -482,6 +532,9 @@ export function useTooltip(options: TooltipOptions = {}): TooltipReturn {
482
532
  ],
483
533
  );
484
534
 
535
+ // A pinned tooltip's focus-leave close is the pin's own: it listens on the
536
+ // trigger whatever `focusTrigger` is. This schedules the unpinned hide,
537
+ // which a held tooltip skips.
485
538
  const handleFocusOut = useCallback(
486
539
  (e: Event) => {
487
540
  if (!focusCountedRef.current && !isFocusForTrigger(e)) {
@@ -502,6 +555,11 @@ export function useTooltip(options: TooltipOptions = {}): TooltipReturn {
502
555
  // tooltip at all), so the touch path answers it first.
503
556
  const handlePointerDown = useCallback(
504
557
  (event: PointerEvent) => {
558
+ // A press on a toggle trigger pins or closes through its click. It
559
+ // neither hides the preview here nor waits for a tap verdict.
560
+ if (isToggleTrigger(triggerRef.current)) {
561
+ return;
562
+ }
505
563
  if (handleTouchPointerDown(event)) {
506
564
  return;
507
565
  }
@@ -511,7 +569,7 @@ export function useTooltip(options: TooltipOptions = {}): TooltipReturn {
511
569
  clearTimeouts();
512
570
  hideLayer();
513
571
  },
514
- [handleTouchPointerDown, isOpen, clearTimeouts, hideLayer],
572
+ [isToggleTrigger, handleTouchPointerDown, isOpen, clearTimeouts, hideLayer],
515
573
  );
516
574
 
517
575
  // Interaction ref that handles event listeners only
@@ -552,8 +610,10 @@ export function useTooltip(options: TooltipOptions = {}): TooltipReturn {
552
610
  }
553
611
 
554
612
  triggerRef.current = el;
613
+ attachPressPin(el);
555
614
  },
556
615
  [
616
+ attachPressPin,
557
617
  focusTrigger,
558
618
  handleMouseEnter,
559
619
  handleMouseLeave,
@@ -641,6 +701,7 @@ export function useTooltip(options: TooltipOptions = {}): TooltipReturn {
641
701
  onDismiss: () => {
642
702
  clearTimeouts();
643
703
  clearTapOpen();
704
+ unpinPress();
644
705
  if (isOpen !== undefined) {
645
706
  onHide?.();
646
707
  return;
@@ -677,7 +738,8 @@ export function useTooltip(options: TooltipOptions = {}): TooltipReturn {
677
738
  // something else in the shared interactive-ancestor walk, so a
678
739
  // clickable container around the trigger leaves the press alone while
679
740
  // document listeners still see it.
680
- <div ref={registerLayerSurface} {...stylex.props(styles.content)}>
741
+ // The pin's announcement reads this element's text.
742
+ <div ref={contentRefs} {...stylex.props(styles.content)}>
681
743
  {children}
682
744
  </div>,
683
745
  renderProps,
@@ -690,6 +752,7 @@ export function useTooltip(options: TooltipOptions = {}): TooltipReturn {
690
752
  popoverXstyle,
691
753
  cancelHide,
692
754
  scheduleHide,
755
+ contentRefs,
693
756
  ],
694
757
  );
695
758
 
@@ -79,6 +79,7 @@ import {
79
79
  useComponentTranslator,
80
80
  } from '../i18n/useComponentTranslator';
81
81
  import english from '../i18n/generated-english/Typeahead/BaseTypeahead.generated';
82
+ import {textEntryTraitAttributes} from '../Field/textEntryTraits';
82
83
 
83
84
  registerEnglishFallback(english);
84
85
  // =============================================================================
@@ -607,8 +608,9 @@ export const BaseTypeahead = function BaseTypeahead<T extends SearchableItem>({
607
608
  );
608
609
  // Committed values (Tokenizer's tokens) leave the shown rows in the same
609
610
  // render that commits them, before any reload answers, so a list never
610
- // offers a token (component:Tokenizer FR13). The highlight stays on its item
611
- // when that item remains. The previous key is remembered in state, like
611
+ // offers a token (component:Tokenizer FR13). Until the reload answers, the
612
+ // highlight stays on its item when that item remains; the reloaded list
613
+ // highlights its first row. The previous key is remembered in state, like
612
614
  // `sourceOfState` below.
613
615
  const committedChoices = useCommittedChoices();
614
616
  const committedChoicesKey = committedChoices?.key;
@@ -1119,11 +1121,16 @@ export const BaseTypeahead = function BaseTypeahead<T extends SearchableItem>({
1119
1121
  resultsGenRef.current = gen;
1120
1122
  const shown = bootstrapResults.slice(0, maxMenuItems);
1121
1123
  const nextHighlightedIndex = shown.length > 0 ? 0 : -1;
1124
+ // A focus load that yields no choice, or fails (it arrives here empty),
1125
+ // closes the list rather than leaving it open and empty. A typed
1126
+ // query's empty search is not a focus load and keeps its empty-search
1127
+ // message.
1122
1128
  if (
1123
1129
  results.length === 0 &&
1124
1130
  shown.length === 0 &&
1125
1131
  highlightedIndex === -1
1126
1132
  ) {
1133
+ popover.hide();
1127
1134
  return;
1128
1135
  }
1129
1136
  setHeldHeight(null);
@@ -1131,11 +1138,7 @@ export const BaseTypeahead = function BaseTypeahead<T extends SearchableItem>({
1131
1138
  setHighlightedIndex(nextHighlightedIndex);
1132
1139
  if (bootstrapResults.length > 0) {
1133
1140
  showLayer();
1134
- } else if (committedChoices != null) {
1135
- // A Tokenizer list left with no eligible choice (its focus entries
1136
- // all committed, or the request failed) closes rather than staying
1137
- // open and empty (component:Tokenizer FR15). A plain Typeahead
1138
- // provides no committed choices and keeps its current behavior.
1141
+ } else {
1139
1142
  popover.hide();
1140
1143
  }
1141
1144
  },
@@ -1146,7 +1149,6 @@ export const BaseTypeahead = function BaseTypeahead<T extends SearchableItem>({
1146
1149
  showLayer,
1147
1150
  replaceResults,
1148
1151
  popover,
1149
- committedChoices,
1150
1152
  ],
1151
1153
  );
1152
1154
 
@@ -1722,6 +1724,12 @@ export const BaseTypeahead = function BaseTypeahead<T extends SearchableItem>({
1722
1724
  autoFocus={hasAutoFocus}
1723
1725
  data-autofocus={hasAutoFocus || undefined}
1724
1726
  autoComplete="off"
1727
+ // Autocorrect would rewrite names and identifiers. The key is only a
1728
+ // label: Enter still commits the highlighted option.
1729
+ {...textEntryTraitAttributes({
1730
+ enterKeyHint: 'search',
1731
+ hasAutoCorrect: false,
1732
+ })}
1725
1733
  {...mergeProps(
1726
1734
  stylex.props(
1727
1735
  styles.input,