@astryxdesign/core 0.7.7-canary.f9ac85f → 0.7.7

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 (198) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/dist/AppShell/useCloseMobileDrawerOnActivate.d.ts +13 -0
  3. package/dist/AppShell/useCloseMobileDrawerOnActivate.d.ts.map +1 -0
  4. package/dist/AppShell/useCloseMobileDrawerOnActivate.js +45 -0
  5. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  6. package/dist/BottomSheet/BottomSheet.js +79 -9
  7. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  8. package/dist/BottomSheet/BottomSheetPanel.js +1 -1
  9. package/dist/BottomSheet/bottomSheetStack.d.ts.map +1 -1
  10. package/dist/BottomSheet/bottomSheetStack.js +5 -4
  11. package/dist/Breadcrumbs/BreadcrumbItem.d.ts +2 -1
  12. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  13. package/dist/Breadcrumbs/BreadcrumbItem.js +7 -3
  14. package/dist/Button/Button.d.ts.map +1 -1
  15. package/dist/Button/Button.js +30 -2
  16. package/dist/CheckboxList/CheckboxList.d.ts.map +1 -1
  17. package/dist/CheckboxList/CheckboxList.js +7 -1
  18. package/dist/CheckboxList/CheckboxListContext.d.ts +6 -0
  19. package/dist/CheckboxList/CheckboxListContext.d.ts.map +1 -1
  20. package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
  21. package/dist/CheckboxList/CheckboxListItem.js +10 -2
  22. package/dist/DateInput/DateInput.d.ts.map +1 -1
  23. package/dist/DateInput/DateInput.js +5 -2
  24. package/dist/DateInput/MonthScroller.d.ts.map +1 -1
  25. package/dist/DateInput/MonthScroller.js +13 -27
  26. package/dist/DateInput/Wheel.d.ts.map +1 -1
  27. package/dist/DateInput/Wheel.js +22 -41
  28. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  29. package/dist/DateTimeInput/DateTimeInput.js +5 -2
  30. package/dist/Dialog/Dialog.d.ts.map +1 -1
  31. package/dist/Dialog/Dialog.js +4 -3
  32. package/dist/Drawer/Drawer.js +0 -1
  33. package/dist/Drawer/useDrawerDialogPresence.d.ts.map +1 -1
  34. package/dist/Drawer/useDrawerDialogPresence.js +5 -1
  35. package/dist/Field/PanelSearchInput.d.ts +9 -1
  36. package/dist/Field/PanelSearchInput.d.ts.map +1 -1
  37. package/dist/Field/PanelSearchInput.js +3 -1
  38. package/dist/InfoTip/InfoTip.d.ts +2 -2
  39. package/dist/InfoTip/InfoTip.js +4 -4
  40. package/dist/Layer/layerAnimations.stylex.d.ts +7 -8
  41. package/dist/Layer/layerAnimations.stylex.d.ts.map +1 -1
  42. package/dist/Layer/layerAnimations.stylex.js +25 -20
  43. package/dist/Layer/usePressPin.d.ts +3 -5
  44. package/dist/Layer/usePressPin.d.ts.map +1 -1
  45. package/dist/Layer/usePressPin.js +4 -6
  46. package/dist/MobileNav/MobileNav.js +0 -1
  47. package/dist/MultiSelector/hooks.d.ts.map +1 -1
  48. package/dist/MultiSelector/hooks.js +2 -4
  49. package/dist/Outline/Outline.d.ts +2 -4
  50. package/dist/Outline/Outline.d.ts.map +1 -1
  51. package/dist/Outline/Outline.js +4 -52
  52. package/dist/Outline/useOutlineFromMarkdown.d.ts.map +1 -1
  53. package/dist/Outline/useOutlineFromMarkdown.js +1 -0
  54. package/dist/Outline/useScrollSpy.js +3 -3
  55. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  56. package/dist/PowerSearch/PowerSearch.js +134 -179
  57. package/dist/RadioList/RadioList.d.ts.map +1 -1
  58. package/dist/RadioList/RadioList.js +5 -0
  59. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  60. package/dist/Resizable/ResizeHandle.js +118 -260
  61. package/dist/Resizable/resizeGesture.d.ts +91 -0
  62. package/dist/Resizable/resizeGesture.d.ts.map +1 -0
  63. package/dist/Resizable/resizeGesture.js +251 -0
  64. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  65. package/dist/SideNav/SideNavItem.js +3 -9
  66. package/dist/Token/Token.d.ts +2 -2
  67. package/dist/Token/Token.d.ts.map +1 -1
  68. package/dist/Token/Token.js +12 -4
  69. package/dist/Token/TokenNamingContext.d.ts +14 -0
  70. package/dist/Token/TokenNamingContext.d.ts.map +1 -0
  71. package/dist/Token/TokenNamingContext.js +23 -0
  72. package/dist/Tokenizer/Tokenizer.d.ts +46 -59
  73. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  74. package/dist/Tokenizer/Tokenizer.js +35 -58
  75. package/dist/Tokenizer/TokenizerBottomSheet.d.ts +25 -28
  76. package/dist/Tokenizer/TokenizerBottomSheet.d.ts.map +1 -1
  77. package/dist/Tokenizer/TokenizerBottomSheet.js +157 -54
  78. package/dist/Tokenizer/TokenizerItemRemovableContext.d.ts.map +1 -1
  79. package/dist/Tokenizer/TokenizerItemRemovableContext.js +5 -3
  80. package/dist/Tooltip/Tooltip.d.ts +25 -30
  81. package/dist/Tooltip/Tooltip.d.ts.map +1 -1
  82. package/dist/Tooltip/Tooltip.js +24 -5
  83. package/dist/Tooltip/index.d.ts +1 -1
  84. package/dist/Tooltip/index.d.ts.map +1 -1
  85. package/dist/Tooltip/useTooltip.d.ts +28 -29
  86. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  87. package/dist/Tooltip/useTooltip.js +20 -8
  88. package/dist/TopNav/TopNavItem.d.ts.map +1 -1
  89. package/dist/TopNav/TopNavItem.js +4 -7
  90. package/dist/TopNav/TopNavMegaMenuItem.d.ts.map +1 -1
  91. package/dist/TopNav/TopNavMegaMenuItem.js +4 -7
  92. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  93. package/dist/TopNav/TopNavMenu.js +4 -7
  94. package/dist/astryx.css +5 -5
  95. package/dist/gestures/GestureHost.d.ts +27 -1
  96. package/dist/gestures/GestureHost.d.ts.map +1 -1
  97. package/dist/gestures/GestureHost.js +90 -8
  98. package/dist/hooks/useHighlightedOptionScroll.d.ts +11 -1
  99. package/dist/hooks/useHighlightedOptionScroll.d.ts.map +1 -1
  100. package/dist/hooks/useHighlightedOptionScroll.js +103 -5
  101. package/dist/hooks/useMenuPress.d.ts.map +1 -1
  102. package/dist/hooks/useMenuPress.js +4 -2
  103. package/dist/i18n/generated-english/PowerSearch/PowerSearch.generated.d.ts.map +1 -1
  104. package/dist/i18n/generated-english/PowerSearch/PowerSearch.generated.js +1 -0
  105. package/dist/i18n/generated-english/Tokenizer/TokenizerBottomSheet.generated.d.ts.map +1 -1
  106. package/dist/i18n/generated-english/Tokenizer/TokenizerBottomSheet.generated.js +7 -1
  107. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  108. package/dist/i18n/generated-locales/en.generated.js +1 -0
  109. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  110. package/dist/i18n/generated-locales/pseudo.generated.js +1 -0
  111. package/dist/internal/SheetConfirmation.d.ts +11 -0
  112. package/dist/internal/SheetConfirmation.d.ts.map +1 -0
  113. package/dist/internal/SheetConfirmation.js +92 -0
  114. package/dist/internal/sheetDraft.d.ts +35 -0
  115. package/dist/internal/sheetDraft.d.ts.map +1 -0
  116. package/dist/internal/sheetDraft.js +65 -0
  117. package/dist/internal/touch-sheet.d.ts +21 -0
  118. package/dist/internal/touch-sheet.d.ts.map +1 -0
  119. package/dist/internal/touch-sheet.js +19 -0
  120. package/dist/internal/useDismissSearchOnTouchDrag.d.ts +24 -0
  121. package/dist/internal/useDismissSearchOnTouchDrag.d.ts.map +1 -0
  122. package/dist/internal/useDismissSearchOnTouchDrag.js +56 -0
  123. package/dist/motion/reducedMotion.d.ts +24 -0
  124. package/dist/motion/reducedMotion.d.ts.map +1 -1
  125. package/dist/motion/reducedMotion.js +78 -1
  126. package/dist/utils/clickGuard.d.ts +8 -1
  127. package/dist/utils/clickGuard.d.ts.map +1 -1
  128. package/dist/utils/clickGuard.js +24 -1
  129. package/locales/en.json +4 -0
  130. package/locales/pseudo.json +3 -0
  131. package/package.json +9 -9
  132. package/src/AppShell/useCloseMobileDrawerOnActivate.ts +52 -0
  133. package/src/BottomSheet/BottomSheet.tsx +95 -10
  134. package/src/BottomSheet/BottomSheetPanel.tsx +7 -2
  135. package/src/BottomSheet/bottomSheetStack.ts +8 -4
  136. package/src/Breadcrumbs/BreadcrumbItem.tsx +13 -3
  137. package/src/Button/Button.tsx +37 -1
  138. package/src/CheckboxList/CheckboxList.doc.mjs +4 -2
  139. package/src/CheckboxList/CheckboxList.tsx +6 -0
  140. package/src/CheckboxList/CheckboxListContext.tsx +6 -0
  141. package/src/CheckboxList/CheckboxListItem.tsx +11 -2
  142. package/src/DateInput/DateInput.tsx +8 -2
  143. package/src/DateInput/MonthScroller.tsx +18 -22
  144. package/src/DateInput/Wheel.tsx +30 -32
  145. package/src/DateTimeInput/DateTimeInput.tsx +8 -2
  146. package/src/Dialog/Dialog.tsx +19 -4
  147. package/src/Drawer/Drawer.tsx +1 -1
  148. package/src/Drawer/useDrawerDialogPresence.ts +5 -1
  149. package/src/Field/PanelSearchInput.tsx +11 -0
  150. package/src/InfoTip/InfoTip.doc.mjs +3 -3
  151. package/src/InfoTip/InfoTip.tsx +5 -5
  152. package/src/Layer/layerAnimations.stylex.ts +24 -16
  153. package/src/Layer/usePressPin.ts +5 -11
  154. package/src/MobileNav/MobileNav.tsx +1 -1
  155. package/src/MultiSelector/hooks.ts +2 -4
  156. package/src/Outline/Outline.doc.mjs +177 -51
  157. package/src/Outline/Outline.tsx +5 -57
  158. package/src/Outline/useOutlineFromMarkdown.doc.mjs +157 -0
  159. package/src/Outline/useOutlineFromMarkdown.ts +1 -0
  160. package/src/Outline/useScrollSpy.ts +3 -3
  161. package/src/PowerSearch/PowerSearch.doc.mjs +3 -3
  162. package/src/PowerSearch/PowerSearch.tsx +196 -183
  163. package/src/RadioList/RadioList.doc.mjs +4 -2
  164. package/src/RadioList/RadioList.tsx +4 -0
  165. package/src/Resizable/ResizeHandle.tsx +133 -328
  166. package/src/Resizable/resizeGesture.ts +368 -0
  167. package/src/SideNav/SideNavItem.tsx +3 -7
  168. package/src/Token/Token.tsx +13 -4
  169. package/src/Token/TokenNamingContext.ts +28 -0
  170. package/src/Tokenizer/Tokenizer.doc.mjs +16 -0
  171. package/src/Tokenizer/Tokenizer.tsx +81 -86
  172. package/src/Tokenizer/TokenizerBottomSheet.tsx +219 -93
  173. package/src/Tokenizer/TokenizerItemRemovableContext.ts +5 -3
  174. package/src/Tooltip/Tooltip.doc.mjs +123 -54
  175. package/src/Tooltip/Tooltip.tsx +36 -39
  176. package/src/Tooltip/index.ts +0 -1
  177. package/src/Tooltip/useTooltip.doc.mjs +147 -33
  178. package/src/Tooltip/useTooltip.tsx +39 -32
  179. package/src/TopNav/TopNavItem.tsx +6 -5
  180. package/src/TopNav/TopNavMegaMenuItem.tsx +6 -5
  181. package/src/TopNav/TopNavMenu.tsx +6 -5
  182. package/src/gestures/GestureHost.ts +105 -10
  183. package/src/hooks/useHighlightedOptionScroll.ts +113 -9
  184. package/src/hooks/useMenuPress.ts +8 -2
  185. package/src/i18n/generated-english/PowerSearch/PowerSearch.generated.ts +1 -0
  186. package/src/i18n/generated-english/Tokenizer/TokenizerBottomSheet.generated.ts +7 -1
  187. package/src/i18n/generated-locales/en.generated.ts +1 -0
  188. package/src/i18n/generated-locales/pseudo.generated.ts +1 -0
  189. package/src/internal/SheetConfirmation.tsx +101 -0
  190. package/src/internal/sheetDraft.ts +80 -0
  191. package/src/internal/touch-sheet.ts +21 -0
  192. package/src/internal/useDismissSearchOnTouchDrag.ts +69 -0
  193. package/src/motion/reducedMotion.ts +79 -1
  194. package/src/utils/clickGuard.ts +37 -1
  195. package/dist/internal/tokenizer-presentation.d.ts +0 -21
  196. package/dist/internal/tokenizer-presentation.d.ts.map +0 -1
  197. package/dist/internal/tokenizer-presentation.js +0 -21
  198. package/src/internal/tokenizer-presentation.ts +0 -25
@@ -5,7 +5,8 @@
5
5
  /**
6
6
  * @file useTooltip.tsx
7
7
  * @input Uses useLayer, usePressPin, React hooks
8
- * @output Exports useTooltip hook with stable trigger refs for hover/focus/tap tooltips
8
+ * @output Exports useTooltip hook with stable trigger refs for hover/focus/tap tooltips,
9
+ * and the module-only useTooltipWithTriggerBehavior that InfoTip's press-to-pin uses
9
10
  * @position Layer hook; builds on useLayer for tooltip behavior
10
11
  *
11
12
  * SYNC: When modified, update:
@@ -31,7 +32,7 @@ import {useAnnounceRenderedText} from '../hooks/useAnnounceRenderedText';
31
32
  import {useMergedRefs} from '../hooks/useMergedRefs';
32
33
  import {layerAnimations} from '../Layer/layerAnimations.stylex';
33
34
  import {registerLayerSurface} from '../Layer/layerSurfacePress';
34
- import {usePressPin} from '../Layer/usePressPin';
35
+ import {usePressPin, type LayerTriggerBehavior} from '../Layer/usePressPin';
35
36
  import {useLayerDismissal} from '../Layer/useLayerDismissal';
36
37
  import {themeProps} from '../utils/themeProps';
37
38
  import {
@@ -83,14 +84,6 @@ export type TooltipFocusTrigger = 'auto' | 'always' | 'never';
83
84
  */
84
85
  export type TooltipTouchTrigger = LayerTouchTrigger;
85
86
 
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
-
94
87
  export interface TooltipOptions {
95
88
  /**
96
89
  * Position placement relative to anchor
@@ -135,32 +128,10 @@ export interface TooltipOptions {
135
128
  * - `tap`: tap always opens the tooltip, even on a trigger that acts
136
129
  * - `none`: touch never opens the tooltip
137
130
  *
138
- * It decides only under `triggerBehavior="adaptive"`, the default.
139
- *
140
131
  * @default 'auto'
141
132
  */
142
133
  touchTrigger?: TooltipTouchTrigger;
143
134
 
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
-
164
135
  /**
165
136
  * Whether the tooltip is enabled.
166
137
  * When false, hover/focus triggers are disabled.
@@ -296,6 +267,42 @@ function isFocusable(element: HTMLElement): boolean {
296
267
  */
297
268
 
298
269
  export function useTooltip(options: TooltipOptions = {}): TooltipReturn {
270
+ // The option is fixed to `adaptive`, so an options object that carries it at
271
+ // runtime cannot reach the press-to-pin path.
272
+ return useTooltipWithTriggerBehavior({
273
+ ...options,
274
+ triggerBehavior: 'adaptive',
275
+ });
276
+ }
277
+
278
+ /**
279
+ * Module-only options for `useTooltipWithTriggerBehavior`. Not exported from
280
+ * the package.
281
+ */
282
+ export interface TooltipWithTriggerBehaviorOptions extends TooltipOptions {
283
+ /**
284
+ * How the trigger opens the tooltip:
285
+ * - `adaptive` (the default): hover and focus preview it. `touchTrigger`
286
+ * decides taps.
287
+ * - `toggle`: on a button trigger, a click, a tap, Enter, or Space pins the
288
+ * tooltip open, and the same press closes it. Hover and keyboard focus
289
+ * still preview it. A pinned tooltip stays until Escape, a second press,
290
+ * a press outside, or focus leaving. The trigger reports `aria-expanded`
291
+ * and `aria-controls`, and the tooltip's text is announced once when it
292
+ * pins. `touchTrigger` is ignored. InfoTip uses it.
293
+ *
294
+ * @default 'adaptive'
295
+ */
296
+ triggerBehavior?: LayerTriggerBehavior;
297
+ }
298
+
299
+ /**
300
+ * `useTooltip` with a trigger behavior. Module-only: InfoTip's press-to-pin
301
+ * goes through it, and the package does not export it.
302
+ */
303
+ export function useTooltipWithTriggerBehavior(
304
+ options: TooltipWithTriggerBehaviorOptions = {},
305
+ ): TooltipReturn {
299
306
  const {
300
307
  placement = 'above',
301
308
  alignment = 'center',
@@ -33,7 +33,7 @@ import type {LinkComponentType} from '../Link/types';
33
33
  import {useTopNavRenderMode} from './TopNavRenderContext';
34
34
  import {navItemStyles, type NavItemSize} from '../NavItem/navItemStyles.stylex';
35
35
  import {mergeProps} from '../utils';
36
- import {useAppShellMobile} from '../AppShell/AppShellMobileContext';
36
+ import {useCloseMobileDrawerOnActivate} from '../AppShell/useCloseMobileDrawerOnActivate';
37
37
  import {themeProps} from '../utils/themeProps';
38
38
  import {focusOutlineProps} from '../utils/focusOutline.stylex';
39
39
  import {interactionOverlayStyles} from '../utils/interactionOverlay.stylex';
@@ -199,7 +199,9 @@ export function TopNavItem({
199
199
  const pressable = usePressFeedback();
200
200
  const LinkComponent = useLinkComponent(as);
201
201
  const renderMode = useTopNavRenderMode();
202
- const {closeMobileNav} = useAppShellMobile();
202
+ const closeDrawerAfter = useCloseMobileDrawerOnActivate(
203
+ renderMode === 'drawer',
204
+ );
203
205
 
204
206
  // A disabled item renders as a plain <a> with no href: an href-less anchor
205
207
  // is not focusable and exposes no link affordance, so programmatic focus +
@@ -214,14 +216,13 @@ export function TopNavItem({
214
216
  // Drawer mode — render as a SideNavItem-style vertical list element
215
217
  // =========================================================================
216
218
  if (renderMode === 'drawer') {
219
+ const activate = closeDrawerAfter(onClick);
217
220
  const handleDrawerClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
218
221
  if (isDisabled) {
219
222
  preventDefaultClick(e);
220
223
  return;
221
224
  }
222
- // Forward the original onClick if present
223
- onClick?.(e);
224
- closeMobileNav();
225
+ activate?.(e);
225
226
  };
226
227
 
227
228
  return (
@@ -32,7 +32,7 @@ import type {LinkComponentType} from '../Link/types';
32
32
  import {useTopNavRenderMode} from './TopNavRenderContext';
33
33
  import {mergeProps} from '../utils';
34
34
  import type {BaseProps} from '../BaseProps';
35
- import {useAppShellMobile} from '../AppShell/AppShellMobileContext';
35
+ import {useCloseMobileDrawerOnActivate} from '../AppShell/useCloseMobileDrawerOnActivate';
36
36
  import {themeProps} from '../utils/themeProps';
37
37
  import {focusOutlineProps} from '../utils/focusOutline.stylex';
38
38
  import {interactionOverlayStyles} from '../utils/interactionOverlay.stylex';
@@ -200,7 +200,9 @@ export function TopNavMegaMenuItem({
200
200
  const pressable = usePressFeedback();
201
201
  const renderMode = useTopNavRenderMode();
202
202
  const LinkComponent = useLinkComponent(as);
203
- const {closeMobileNav} = useAppShellMobile();
203
+ const closeDrawerAfter = useCloseMobileDrawerOnActivate(
204
+ renderMode === 'drawer',
205
+ );
204
206
 
205
207
  // =========================================================================
206
208
  // Drawer mode — matches SideNavItem / TopNavMenu drawer appearance
@@ -208,10 +210,9 @@ export function TopNavMegaMenuItem({
208
210
  if (renderMode === 'drawer') {
209
211
  const Element = href ? LinkComponent : 'button';
210
212
  const elementProps = Element === 'button' ? {type: 'button' as const} : {};
211
- const handleDrawerClick = () => {
213
+ const handleDrawerClick = closeDrawerAfter(() => {
212
214
  onClick?.();
213
- closeMobileNav();
214
- };
215
+ });
215
216
  return (
216
217
  <Element
217
218
  {...restProps}
@@ -39,7 +39,7 @@ import type {BaseProps} from '../BaseProps';
39
39
  import {navItemStyles} from '../NavItem/navItemStyles.stylex';
40
40
  import {useTopNavSlot} from './TopNavContext';
41
41
  import {useTopNavRenderMode} from './TopNavRenderContext';
42
- import {useAppShellMobile} from '../AppShell/AppShellMobileContext';
42
+ import {useCloseMobileDrawerOnActivate} from '../AppShell/useCloseMobileDrawerOnActivate';
43
43
  import {useLinkComponent} from '../Link/useLinkComponent';
44
44
  import {isSafeUrl} from '../utils/safeUrl';
45
45
  import {themeProps} from '../utils/themeProps';
@@ -363,7 +363,9 @@ export function TopNavMenu({
363
363
  }: TopNavMenuProps) {
364
364
  const pressable = usePressFeedback();
365
365
  const renderMode = useTopNavRenderMode();
366
- const {closeMobileNav} = useAppShellMobile();
366
+ const closeDrawerAfter = useCloseMobileDrawerOnActivate(
367
+ renderMode === 'drawer',
368
+ );
367
369
  const LinkComponent = useLinkComponent();
368
370
  const [drawerExpanded, setDrawerExpanded] = useState(false);
369
371
  const menuId = useId();
@@ -524,10 +526,9 @@ export function TopNavMenu({
524
526
  key={getMenuItemKey(item)}
525
527
  href={item.href}
526
528
  {...pressable}
527
- onClick={(_e: React.MouseEvent) => {
529
+ onClick={closeDrawerAfter((_e: React.MouseEvent) => {
528
530
  item.onClick?.();
529
- closeMobileNav();
530
- }}
531
+ })}
531
532
  {...focusOutlineProps.focusVisible(
532
533
  navItemStyles.item,
533
534
  interactionOverlayStyles.backgroundColor,
@@ -731,6 +731,12 @@ export class GestureHost {
731
731
  * lift click (`spec:AST-094` FR28: nothing acts).
732
732
  */
733
733
  private readonly cancelledFingers = new Set<number>();
734
+ /**
735
+ * The pens and pointer-read fingers that the app's lost focus cancelled,
736
+ * by pointer id. Each is kept until its lift, which must not click either,
737
+ * or until a new press.
738
+ */
739
+ private readonly cancelledPointers = new Map<number, PointerKind>();
734
740
  /** Touch.identifier → the element the finger landed on. */
735
741
  private readonly touchTargets = new Map<number, EventTarget>();
736
742
  /**
@@ -755,6 +761,8 @@ export class GestureHost {
755
761
  private readonly processed = new WeakSet<Event>();
756
762
  /** pointerId → arena id, for live mouse and pen contacts. */
757
763
  private readonly pointerIds = new Map<number, number>();
764
+ /** Each contact's kind: a blur refuses a pen's or a pointer-read finger's lift, never a mouse's. */
765
+ private readonly pointerKinds = new Map<number, PointerKind>();
758
766
  /**
759
767
  * The boxes the host watches for screen-edge pans (syncLayoutWatch): each
760
768
  * element with an enabled one, and the root for one whose band moves with
@@ -1820,13 +1828,36 @@ export class GestureHost {
1820
1828
  }
1821
1829
  }
1822
1830
 
1823
- /** After a contact ends: an armed press that is over stops listening. */
1831
+ /**
1832
+ * A new press is a new gesture, as the click guard reads one, so the lifts
1833
+ * the blur cancelled owe nothing after it. A second finger keeps the other
1834
+ * fingers' refusals, since those fingers may still be down.
1835
+ */
1836
+ private dropCancelledLifts(event: PointerEvent): void {
1837
+ const isSecond = event.pointerType === 'touch' && event.isPrimary === false;
1838
+ for (const [pointerId, kind] of [...this.cancelledPointers]) {
1839
+ if (pointerId === event.pointerId || !(isSecond && kind === 'touch')) {
1840
+ this.cancelledPointers.delete(pointerId);
1841
+ }
1842
+ }
1843
+ }
1844
+
1845
+ /**
1846
+ * After a contact ends: an armed press that is over stops listening. A
1847
+ * contact the lost focus cancelled still owes its lift, so the press is
1848
+ * not over until that lift comes. A lift the page never hears ends it at
1849
+ * the next press, or at a cancel.
1850
+ */
1824
1851
  private releaseArmedPress(): void {
1825
1852
  if (!this.isArmedPress) {
1826
1853
  return;
1827
1854
  }
1828
1855
  queueMicrotask(() => {
1829
- if (this.isArmedPress && this.arena.touches.length === 0) {
1856
+ if (
1857
+ this.isArmedPress &&
1858
+ this.arena.touches.length === 0 &&
1859
+ this.cancelledPointers.size === 0
1860
+ ) {
1830
1861
  this.isArmedPress = false;
1831
1862
  this.syncKeptPresses();
1832
1863
  this.syncListeners();
@@ -1857,6 +1888,7 @@ export class GestureHost {
1857
1888
  return;
1858
1889
  }
1859
1890
  this.cancelledFingers.clear();
1891
+ this.cancelledPointers.clear();
1860
1892
  this.clearClaimedLifts();
1861
1893
  this.cancelCallerRead?.();
1862
1894
  this.cancelCallerRead = null;
@@ -2934,13 +2966,19 @@ export class GestureHost {
2934
2966
  if (locksSelectionFor('touch')) {
2935
2967
  toLock.push({id, target, consumerElement});
2936
2968
  }
2937
- // The finger's events keep going to what the engine hit.
2969
+ // The finger's events keep going to what the engine hit. A disabled
2970
+ // participant can never claim, so a scroll never waits on it.
2938
2971
  this.watchTarget(
2939
2972
  touch.identifier,
2940
2973
  native,
2941
- recognizers.some(claimsWhileMoving) || consumer?.keepsTouch != null,
2942
- consumer != null ||
2943
- recognizers.some(recognizer => recognizer.claimsTouches),
2974
+ recognizers.some(
2975
+ recognizer => recognizer.isEnabled && claimsWhileMoving(recognizer),
2976
+ ) ||
2977
+ (consumer?.keepsTouch != null && consumer.isEnabled?.() !== false),
2978
+ (consumer != null && consumer.isEnabled?.() !== false) ||
2979
+ recognizers.some(
2980
+ recognizer => recognizer.isEnabled && recognizer.claimsTouches,
2981
+ ),
2944
2982
  );
2945
2983
  this.lastFed.set(id, {x: touch.clientX, y: touch.clientY, time});
2946
2984
  this.pairTouch(id, {x: touch.clientX, y: touch.clientY, target: native});
@@ -2975,6 +3013,11 @@ export class GestureHost {
2975
3013
  for (const input of untracked) {
2976
3014
  this.arena.untrackedBegan(input);
2977
3015
  }
3016
+ // Again once the new fingers are delivered: a pinch's second finger may
3017
+ // have landed on another one-finger pan, the next row.
3018
+ if (this.fingerTargets.length > 1 && !this.isFingerClaimed()) {
3019
+ this.failUnclaimedLoneFingerPans();
3020
+ }
2978
3021
  // Stamped after the consumers judged the touch: the brake asks whether
2979
3022
  // an EARLIER finger is still driving a scroll.
2980
3023
  const landed = landedOn(event);
@@ -3570,6 +3613,7 @@ export class GestureHost {
3570
3613
  if (!isPointerKind(event.pointerType)) {
3571
3614
  return;
3572
3615
  }
3616
+ this.dropCancelledLifts(event);
3573
3617
  if (event.pointerType === 'touch' && this.usesTouchEvents) {
3574
3618
  this.pairPointer(event.pointerId, {
3575
3619
  x: event.clientX,
@@ -3578,6 +3622,13 @@ export class GestureHost {
3578
3622
  });
3579
3623
  return;
3580
3624
  }
3625
+ if (event.pointerType === 'touch' && event.isPrimary === false) {
3626
+ // Fingers from pointer events: a second one, wherever either lands,
3627
+ // leaves the two to the page's pinch, as onTouchStart does. A finger
3628
+ // that is not the primary pointer is a second finger, even where the
3629
+ // host never heard the first.
3630
+ this.failUnclaimedLoneFingerPans();
3631
+ }
3581
3632
  // A right click, a pen's barrel button, or a ctrl-click on macOS (its
3582
3633
  // context click) starts no press (FR43).
3583
3634
  if (event.pointerType !== 'touch' && !isPrimaryClick(event)) {
@@ -3599,6 +3650,7 @@ export class GestureHost {
3599
3650
  }
3600
3651
  const id = this.nextId++;
3601
3652
  this.pointerIds.set(event.pointerId, id);
3653
+ this.pointerKinds.set(event.pointerId, event.pointerType);
3602
3654
  if (target !== landed && target != null) {
3603
3655
  this.retargeted.set(id, {x: event.clientX, y: event.clientY, target});
3604
3656
  }
@@ -3620,6 +3672,15 @@ export class GestureHost {
3620
3672
  consumer,
3621
3673
  },
3622
3674
  ]);
3675
+ if (
3676
+ event.pointerType === 'touch' &&
3677
+ event.isPrimary === false &&
3678
+ !this.isFingerClaimed()
3679
+ ) {
3680
+ // Again once this finger is delivered: it may have landed on another
3681
+ // one-finger pan.
3682
+ this.failUnclaimedLoneFingerPans();
3683
+ }
3623
3684
  if (locksSelectionFor(event.pointerType)) {
3624
3685
  // The presented target: what the pen is on, where the engine's own
3625
3686
  // hit test may still see the motion's start.
@@ -3678,9 +3739,14 @@ export class GestureHost {
3678
3739
  }
3679
3740
  const id = this.pointerIds.get(event.pointerId);
3680
3741
  if (id == null) {
3742
+ // A contact the lost focus cancelled: its lift clicks nothing.
3743
+ if (this.cancelledPointers.delete(event.pointerId)) {
3744
+ this.refuseCancelledLift(event);
3745
+ }
3681
3746
  return;
3682
3747
  }
3683
3748
  this.pointerIds.delete(event.pointerId);
3749
+ this.pointerKinds.delete(event.pointerId);
3684
3750
  this.unlockSelection(id);
3685
3751
  const dispositions = this.arena.ended([
3686
3752
  {id, x: event.clientX, y: event.clientY, time: eventTime(event)},
@@ -3784,9 +3850,12 @@ export class GestureHost {
3784
3850
  }
3785
3851
  return;
3786
3852
  }
3853
+ // A contact that ends in a cancel owes no refused lift.
3854
+ this.cancelledPointers.delete(event.pointerId);
3787
3855
  const id = this.pointerIds.get(event.pointerId);
3788
3856
  if (id != null) {
3789
3857
  this.pointerIds.delete(event.pointerId);
3858
+ this.pointerKinds.delete(event.pointerId);
3790
3859
  this.unlockSelection(id, true);
3791
3860
  this.retargeted.delete(id);
3792
3861
  this.arena.cancelled([id], eventTime(event));
@@ -3991,20 +4060,26 @@ export class GestureHost {
3991
4060
  }
3992
4061
  }
3993
4062
  }
4063
+ /** Whether a finger's drag is claimed: then two fingers make no pinch. */
4064
+ private isFingerClaimed(): boolean {
4065
+ return this.arena.touches.some(
4066
+ touch => touch.pointerType === 'touch' && this.arena.isClaimed(touch.id),
4067
+ );
4068
+ }
3994
4069
 
3995
4070
  private readonly onVisibilityChange = (): void => {
3996
4071
  if (this.doc.visibilityState === 'hidden') {
3997
4072
  this.cancelEverything(this.arena.now());
3998
- // As at a blur: the press sends no further pointer event, so an armed
3999
- // press's page listeners go now (FR24).
4073
+ // As at a blur: an armed press's page listeners go once nothing is left
4074
+ // to hear.
4000
4075
  this.releaseArmedPress();
4001
4076
  }
4002
4077
  };
4003
4078
 
4004
4079
  private readonly onBlur = (): void => {
4005
4080
  this.cancelEverything(this.arena.now());
4006
- // The press a blur ends sends no further pointer event: an armed press's
4007
- // page listeners go now, as at its lift (FR24).
4081
+ // An armed press's page listeners go once nothing is left to hear. A pen
4082
+ // or a pointer-read finger the blur cancelled still owes its lift.
4008
4083
  this.releaseArmedPress();
4009
4084
  };
4010
4085
 
@@ -4027,8 +4102,27 @@ export class GestureHost {
4027
4102
  );
4028
4103
  }
4029
4104
 
4105
+ /**
4106
+ * The lift of a contact the lost focus cancelled: its click is refused,
4107
+ * and a finger's touchend after it is cancelled.
4108
+ */
4109
+ private refuseCancelledLift(event: PointerEvent): void {
4110
+ armClickGuard(this.doc, {takesLateMouse: event.pointerType === 'touch'});
4111
+ if (event.pointerType === 'touch') {
4112
+ // Noted as a claimed lift is: capped, and gone after its time.
4113
+ this.noteClaimedLift(event);
4114
+ }
4115
+ }
4116
+
4030
4117
  /** The app lost focus: UIKit cancels every touch in flight. */
4031
4118
  private cancelEverything(time: number): void {
4119
+ // The browser may keep a pen or a pointer-read finger down: its lift must
4120
+ // not click. A mouse button held through the blur keeps its click.
4121
+ for (const [pointerId, kind] of this.pointerKinds) {
4122
+ if (kind !== 'mouse') {
4123
+ this.cancelledPointers.set(pointerId, kind);
4124
+ }
4125
+ }
4032
4126
  const ids = [...this.pointerIds.values()];
4033
4127
  for (const id of ids) {
4034
4128
  this.unlockSelection(id, true);
@@ -4042,6 +4136,7 @@ export class GestureHost {
4042
4136
  }
4043
4137
  }
4044
4138
  this.pointerIds.clear();
4139
+ this.pointerKinds.clear();
4045
4140
  this.retargeted.clear();
4046
4141
  this.fingerPointers.clear();
4047
4142
  this.pendingPointers.clear();
@@ -8,7 +8,8 @@
8
8
  * option-id resolver for the owning listbox
9
9
  * @output Exports useHighlightedOptionScroll: keeps the highlighted option
10
10
  * scrolled into view for keyboard navigation while hover-driven highlights
11
- * never scroll; the hover callback is stable while `setHighlightedIndex` is
11
+ * never scroll, and a hover after a keyboard move waits for the pointer to
12
+ * move or press; the hover callback is stable while `setHighlightedIndex` is
12
13
  * @position Internal hook; used by useCombobox, useMultiCombobox,
13
14
  * BaseTypeahead, and DateTimeInput — the owners of combobox highlight state
14
15
  */
@@ -27,6 +28,21 @@ interface UseHighlightedOptionScrollOptions {
27
28
  itemCount?: number;
28
29
  }
29
30
 
31
+ /**
32
+ * Highlights the hovered option at `index`. `onHighlight` runs when the hover
33
+ * takes effect: at once, or after the pointer moves or presses when a
34
+ * keyboard move holds the highlight.
35
+ */
36
+ export type HighlightFromHover = (
37
+ index: number,
38
+ onHighlight?: () => void,
39
+ ) => void;
40
+
41
+ interface WaitingHover {
42
+ index: number;
43
+ onHighlight?: () => void;
44
+ }
45
+
30
46
  /**
31
47
  * Keep the highlighted option visible during keyboard navigation — but not for
32
48
  * hover highlights. Hover must highlight only: scrollIntoView moves the next
@@ -34,6 +50,10 @@ interface UseHighlightedOptionScrollOptions {
34
50
  * scrolls again — a runaway auto-scroll loop with no user input.
35
51
  * Keyboard paths call the raw setter directly and keep their scrolling; hover
36
52
  * paths call the returned callback.
53
+ *
54
+ * A keyboard move holds the highlight. A key that scrolls the list moves rows
55
+ * under a still mouse, and the browser reports the row under it as entered;
56
+ * that row takes the highlight only once the pointer moves or presses.
37
57
  */
38
58
  export function useHighlightedOptionScroll({
39
59
  isOpen,
@@ -41,19 +61,93 @@ export function useHighlightedOptionScroll({
41
61
  setHighlightedIndex,
42
62
  getOptionId,
43
63
  itemCount,
44
- }: UseHighlightedOptionScrollOptions): (index: number) => void {
64
+ }: UseHighlightedOptionScrollOptions): HighlightFromHover {
45
65
  // The callback reads no render state, so memoized rows keep one function.
46
66
  const hoveredIndexRef = useRef<number | null>(null);
47
67
  const shownIndexRef = useRef(highlightedIndex);
68
+ // The hover that came while a keyboard move held the highlight.
69
+ const waitingHoverRef = useRef<WaitingHover | null>(null);
70
+ // Set only while a keyboard move holds the highlight.
71
+ const removeHoldListenersRef = useRef<(() => void) | null>(null);
72
+ const setHighlightedIndexRef = useRef(setHighlightedIndex);
73
+ // The pointer's last screen position a hold saw, kept across holds.
74
+ const lastPointerRef = useRef<{x: number; y: number} | null>(null);
75
+
76
+ const applyHover = useCallback((hover: WaitingHover) => {
77
+ hoveredIndexRef.current = hover.index;
78
+ setHighlightedIndexRef.current(hover.index);
79
+ hover.onHighlight?.();
80
+ }, []);
81
+
82
+ const endHold = useCallback(() => {
83
+ removeHoldListenersRef.current?.();
84
+ removeHoldListenersRef.current = null;
85
+ }, []);
86
+
87
+ const releaseHold = useCallback(() => {
88
+ endHold();
89
+ const waiting = waitingHoverRef.current;
90
+ waitingHoverRef.current = null;
91
+ if (waiting != null) {
92
+ applyHover(waiting);
93
+ }
94
+ }, [applyHover, endHold]);
48
95
 
49
- const highlightFromHover = useCallback(
50
- (index: number) => {
96
+ // Holds the highlight until the pointer moves or presses. The listeners
97
+ // live only while the hold does. Movement is read by screen coordinates: a
98
+ // scroll moves the rows, not the pointer. The browser reports a row as
99
+ // entered before the move that reaches it, so the first real move gives
100
+ // that row the highlight. A press releases at once, so a held press lights
101
+ // the pressed row.
102
+ const startHold = useCallback(() => {
103
+ waitingHoverRef.current = null;
104
+ if (removeHoldListenersRef.current != null) {
105
+ return;
106
+ }
107
+ const onPointerMove = (event: PointerEvent) => {
108
+ const last = lastPointerRef.current;
109
+ lastPointerRef.current = {x: event.screenX, y: event.screenY};
110
+ if (
111
+ last != null &&
112
+ last.x === event.screenX &&
113
+ last.y === event.screenY
114
+ ) {
115
+ return;
116
+ }
117
+ releaseHold();
118
+ };
119
+ const options = {capture: true, passive: true} as const;
120
+ document.addEventListener('pointermove', onPointerMove, options);
121
+ document.addEventListener('pointerdown', releaseHold, options);
122
+ removeHoldListenersRef.current = () => {
123
+ document.removeEventListener('pointermove', onPointerMove, options);
124
+ document.removeEventListener('pointerdown', releaseHold, options);
125
+ };
126
+ }, [releaseHold]);
127
+
128
+ const highlightFromHover = useCallback<HighlightFromHover>(
129
+ (index, onHighlight) => {
130
+ if (removeHoldListenersRef.current != null) {
131
+ waitingHoverRef.current = {index, onHighlight};
132
+ return;
133
+ }
51
134
  hoveredIndexRef.current = index;
52
135
  setHighlightedIndex(index);
136
+ onHighlight?.();
53
137
  },
54
138
  [setHighlightedIndex],
55
139
  );
56
140
 
141
+ // Effect: keeps the hold's document listeners calling the current setter.
142
+ useEffect(() => {
143
+ setHighlightedIndexRef.current = setHighlightedIndex;
144
+ }, [setHighlightedIndex]);
145
+
146
+ // Effect: removes the hold's document listeners on unmount.
147
+ useEffect(() => endHold, [endHold]);
148
+
149
+ // Effect: scrolls the highlighted option into view in the document, and
150
+ // starts or ends the hold on its document listeners.
57
151
  useEffect(() => {
58
152
  // Only a move to the hovered row came from the hover. A hover on the row
59
153
  // already highlighted changes nothing, so a later run for the same row
@@ -63,13 +157,23 @@ export function useHighlightedOptionScroll({
63
157
  const isFromHover =
64
158
  hasMoved && hoveredIndexRef.current === highlightedIndex;
65
159
  hoveredIndexRef.current = null;
66
- if (isFromHover || !isOpen || highlightedIndex < 0) {
160
+ if (isFromHover) {
161
+ return;
162
+ }
163
+ // A cleared highlight, a closed list, or a list that is not shown (a
164
+ // closed CommandPalette reports itself as open) has nothing to hold.
165
+ const option =
166
+ isOpen && highlightedIndex >= 0
167
+ ? document.getElementById(getOptionId(highlightedIndex))
168
+ : null;
169
+ if (option == null) {
170
+ waitingHoverRef.current = null;
171
+ endHold();
67
172
  return;
68
173
  }
69
- document
70
- .getElementById(getOptionId(highlightedIndex))
71
- ?.scrollIntoView?.({block: 'nearest'});
72
- }, [isOpen, highlightedIndex, getOptionId, itemCount]);
174
+ startHold();
175
+ option.scrollIntoView?.({block: 'nearest'});
176
+ }, [isOpen, highlightedIndex, getOptionId, itemCount, endHold, startHold]);
73
177
 
74
178
  return highlightFromHover;
75
179
  }
@@ -483,7 +483,10 @@ export function useMenuPress(options: UseMenuPressOptions): UseMenuPressReturn {
483
483
  // Detach before acting: the activation usually closes the menu.
484
484
  endGesture();
485
485
  // Once armed, the one swallower stays installed.
486
- armClickGuard(doc, {staysInstalled: true});
486
+ armClickGuard(doc, {
487
+ staysInstalled: true,
488
+ cancelsTouchEnd: native?.pointerType === 'touch',
489
+ });
487
490
  if (native != null) {
488
491
  activate(effect.row, native);
489
492
  }
@@ -492,7 +495,10 @@ export function useMenuPress(options: UseMenuPressOptions): UseMenuPressReturn {
492
495
  case 'settle': {
493
496
  endGesture();
494
497
  if (effect.stray) {
495
- armClickGuard(doc, {staysInstalled: true});
498
+ armClickGuard(doc, {
499
+ staysInstalled: true,
500
+ cancelsTouchEnd: native?.pointerType === 'touch',
501
+ });
496
502
  }
497
503
  if (effect.dismiss) {
498
504
  optionsRef.current.onDismiss?.();
@@ -8,6 +8,7 @@
8
8
 
9
9
  const english: Readonly<Record<string, string>> = {
10
10
  "@astryx.powersearch.allFiltersRemoved": "All filters removed",
11
+ "@astryx.powersearch.editableFiltersRemoved": "Editable filters removed",
11
12
  "@astryx.powersearch.editor.apply": "Apply",
12
13
  "@astryx.powersearch.label": "Search",
13
14
  "@astryx.powersearch.placeholder": "Search…",