@astryxdesign/core 0.7.7-canary.4a8a76e → 0.7.7-canary.4e495ec
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.
- package/dist/AppShell/useCloseMobileDrawerOnActivate.d.ts +13 -0
- package/dist/AppShell/useCloseMobileDrawerOnActivate.d.ts.map +1 -0
- package/dist/AppShell/useCloseMobileDrawerOnActivate.js +45 -0
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +79 -9
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +1 -1
- package/dist/BottomSheet/bottomSheetStack.d.ts.map +1 -1
- package/dist/BottomSheet/bottomSheetStack.js +5 -4
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts +2 -1
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.js +7 -3
- package/dist/CheckboxList/CheckboxList.d.ts.map +1 -1
- package/dist/CheckboxList/CheckboxList.js +7 -1
- package/dist/CheckboxList/CheckboxListContext.d.ts +6 -0
- package/dist/CheckboxList/CheckboxListContext.d.ts.map +1 -1
- package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
- package/dist/CheckboxList/CheckboxListItem.js +10 -2
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +5 -2
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +5 -2
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +16 -14
- package/dist/Drawer/Drawer.d.ts.map +1 -1
- package/dist/Drawer/Drawer.js +15 -11
- package/dist/Drawer/useDrawerDialogPresence.d.ts.map +1 -1
- package/dist/Drawer/useDrawerDialogPresence.js +5 -1
- package/dist/Field/PanelSearchInput.d.ts +9 -1
- package/dist/Field/PanelSearchInput.d.ts.map +1 -1
- package/dist/Field/PanelSearchInput.js +3 -1
- package/dist/InfoTip/InfoTip.d.ts +2 -2
- package/dist/InfoTip/InfoTip.js +4 -4
- package/dist/Layer/layerAnimations.stylex.d.ts +7 -8
- package/dist/Layer/layerAnimations.stylex.d.ts.map +1 -1
- package/dist/Layer/layerAnimations.stylex.js +25 -20
- package/dist/Layer/useBackdropPress.d.ts +27 -0
- package/dist/Layer/useBackdropPress.d.ts.map +1 -0
- package/dist/Layer/useBackdropPress.js +53 -0
- package/dist/Layer/usePressPin.d.ts +3 -5
- package/dist/Layer/usePressPin.d.ts.map +1 -1
- package/dist/Layer/usePressPin.js +4 -6
- package/dist/MobileNav/MobileNav.js +0 -1
- package/dist/MultiSelector/hooks.d.ts.map +1 -1
- package/dist/MultiSelector/hooks.js +2 -4
- package/dist/Outline/Outline.d.ts +2 -4
- package/dist/Outline/Outline.d.ts.map +1 -1
- package/dist/Outline/Outline.js +4 -52
- package/dist/Outline/useOutlineFromMarkdown.d.ts.map +1 -1
- package/dist/Outline/useOutlineFromMarkdown.js +1 -0
- package/dist/Outline/useScrollSpy.js +3 -3
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +134 -179
- package/dist/RadioList/RadioList.d.ts.map +1 -1
- package/dist/RadioList/RadioList.js +5 -0
- package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
- package/dist/Resizable/ResizeHandle.js +118 -260
- package/dist/Resizable/resizeGesture.d.ts +91 -0
- package/dist/Resizable/resizeGesture.d.ts.map +1 -0
- package/dist/Resizable/resizeGesture.js +251 -0
- package/dist/SegmentedControl/SegmentedControl.d.ts +3 -1
- package/dist/SegmentedControl/SegmentedControl.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControl.js +115 -8
- package/dist/SegmentedControl/SegmentedControlContext.d.ts +6 -0
- package/dist/SegmentedControl/SegmentedControlContext.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControlItem.d.ts +1 -0
- package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControlItem.js +8 -1
- package/dist/SegmentedControl/useSegmentedSelectionMotion.d.ts +357 -0
- package/dist/SegmentedControl/useSegmentedSelectionMotion.d.ts.map +1 -0
- package/dist/SegmentedControl/useSegmentedSelectionMotion.js +1349 -0
- package/dist/SegmentedControl/useSegmentedTouchTracking.d.ts +70 -0
- package/dist/SegmentedControl/useSegmentedTouchTracking.d.ts.map +1 -0
- package/dist/SegmentedControl/useSegmentedTouchTracking.js +264 -0
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +96 -67
- package/dist/Selector/optionShape.d.ts +24 -0
- package/dist/Selector/optionShape.d.ts.map +1 -0
- package/dist/Selector/optionShape.js +48 -0
- package/dist/Selector/selectableOptionEntries.d.ts +20 -0
- package/dist/Selector/selectableOptionEntries.d.ts.map +1 -0
- package/dist/Selector/selectableOptionEntries.js +38 -0
- package/dist/Selector/utils.d.ts +2 -17
- package/dist/Selector/utils.d.ts.map +1 -1
- package/dist/Selector/utils.js +3 -51
- package/dist/SideNav/SideNavItem.d.ts.map +1 -1
- package/dist/SideNav/SideNavItem.js +3 -9
- package/dist/Switch/switchTouch.d.ts +0 -7
- package/dist/Switch/switchTouch.d.ts.map +1 -1
- package/dist/Switch/switchTouch.js +2 -10
- package/dist/Switch/useSwitchTouch.d.ts +2 -11
- package/dist/Switch/useSwitchTouch.d.ts.map +1 -1
- package/dist/Switch/useSwitchTouch.js +26 -85
- package/dist/Token/Token.d.ts +2 -2
- package/dist/Token/Token.d.ts.map +1 -1
- package/dist/Token/Token.js +12 -4
- package/dist/Token/TokenNamingContext.d.ts +14 -0
- package/dist/Token/TokenNamingContext.d.ts.map +1 -0
- package/dist/Token/TokenNamingContext.js +23 -0
- package/dist/Tokenizer/Tokenizer.d.ts +46 -59
- package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.js +35 -58
- package/dist/Tokenizer/TokenizerBottomSheet.d.ts +25 -28
- package/dist/Tokenizer/TokenizerBottomSheet.d.ts.map +1 -1
- package/dist/Tokenizer/TokenizerBottomSheet.js +157 -54
- package/dist/Tokenizer/TokenizerItemRemovableContext.d.ts.map +1 -1
- package/dist/Tokenizer/TokenizerItemRemovableContext.js +5 -3
- package/dist/Tooltip/Tooltip.d.ts +25 -30
- package/dist/Tooltip/Tooltip.d.ts.map +1 -1
- package/dist/Tooltip/Tooltip.js +24 -5
- package/dist/Tooltip/index.d.ts +1 -1
- package/dist/Tooltip/index.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.d.ts +28 -29
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +20 -8
- package/dist/TopNav/TopNavItem.d.ts.map +1 -1
- package/dist/TopNav/TopNavItem.js +4 -7
- package/dist/TopNav/TopNavMegaMenuItem.d.ts.map +1 -1
- package/dist/TopNav/TopNavMegaMenuItem.js +4 -7
- package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMenu.js +4 -7
- package/dist/astryx.css +76 -5
- package/dist/gestures/GestureHost.d.ts.map +1 -1
- package/dist/gestures/GestureHost.js +53 -5
- package/dist/hooks/actionGroupTracker.d.ts +3 -3
- package/dist/hooks/actionGroupTracker.d.ts.map +1 -1
- package/dist/hooks/actionGroupTracker.js +6 -53
- package/dist/hooks/useHighlightedOptionScroll.d.ts +11 -1
- package/dist/hooks/useHighlightedOptionScroll.d.ts.map +1 -1
- package/dist/hooks/useHighlightedOptionScroll.js +112 -11
- package/dist/hooks/useMediaQuery.d.ts +2 -1
- package/dist/hooks/useMediaQuery.d.ts.map +1 -1
- package/dist/hooks/useMediaQuery.js +7 -4
- package/dist/hooks/useMenuPress.d.ts.map +1 -1
- package/dist/hooks/useMenuPress.js +4 -2
- package/dist/i18n/generated-english/PowerSearch/PowerSearch.generated.d.ts.map +1 -1
- package/dist/i18n/generated-english/PowerSearch/PowerSearch.generated.js +1 -0
- package/dist/i18n/generated-english/Tokenizer/TokenizerBottomSheet.generated.d.ts.map +1 -1
- package/dist/i18n/generated-english/Tokenizer/TokenizerBottomSheet.generated.js +7 -1
- package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/en.generated.js +1 -0
- package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/pseudo.generated.js +1 -0
- package/dist/internal/SheetConfirmation.d.ts +11 -0
- package/dist/internal/SheetConfirmation.d.ts.map +1 -0
- package/dist/internal/SheetConfirmation.js +92 -0
- package/dist/internal/sheetDraft.d.ts +35 -0
- package/dist/internal/sheetDraft.d.ts.map +1 -0
- package/dist/internal/sheetDraft.js +65 -0
- package/dist/internal/touch-sheet.d.ts +21 -0
- package/dist/internal/touch-sheet.d.ts.map +1 -0
- package/dist/internal/touch-sheet.js +19 -0
- package/dist/internal/useDismissSearchOnTouchDrag.d.ts +24 -0
- package/dist/internal/useDismissSearchOnTouchDrag.d.ts.map +1 -0
- package/dist/internal/useDismissSearchOnTouchDrag.js +56 -0
- package/dist/motion/roles.d.ts +7 -1
- package/dist/motion/roles.d.ts.map +1 -1
- package/dist/motion/roles.js +20 -1
- package/dist/motion/uikit/index.d.ts +6 -5
- package/dist/motion/uikit/index.d.ts.map +1 -1
- package/dist/motion/uikit/index.js +6 -5
- package/dist/motion/uikit/segmented.d.ts +55 -0
- package/dist/motion/uikit/segmented.d.ts.map +1 -0
- package/dist/motion/uikit/segmented.js +77 -0
- package/dist/motion/uikit/springs.d.ts +0 -18
- package/dist/motion/uikit/springs.d.ts.map +1 -1
- package/dist/motion/uikit/springs.js +5 -21
- package/dist/privateAttributes/segmentTitle.stylex.d.ts +5 -0
- package/dist/privateAttributes/segmentTitle.stylex.d.ts.map +1 -0
- package/dist/privateAttributes/segmentTitle.stylex.js +22 -0
- package/dist/utils/browserClickWait.d.ts +35 -0
- package/dist/utils/browserClickWait.d.ts.map +1 -0
- package/dist/utils/browserClickWait.js +123 -0
- package/dist/utils/clickGuard.d.ts +8 -1
- package/dist/utils/clickGuard.d.ts.map +1 -1
- package/dist/utils/clickGuard.js +24 -1
- package/locales/en.json +4 -0
- package/locales/pseudo.json +3 -0
- package/package.json +8 -8
- package/src/AppShell/useCloseMobileDrawerOnActivate.ts +52 -0
- package/src/BottomSheet/BottomSheet.tsx +95 -10
- package/src/BottomSheet/BottomSheetPanel.tsx +7 -2
- package/src/BottomSheet/bottomSheetStack.ts +8 -4
- package/src/Breadcrumbs/BreadcrumbItem.tsx +13 -3
- package/src/CheckboxList/CheckboxList.doc.mjs +4 -2
- package/src/CheckboxList/CheckboxList.tsx +6 -0
- package/src/CheckboxList/CheckboxListContext.tsx +6 -0
- package/src/CheckboxList/CheckboxListItem.tsx +11 -2
- package/src/DateInput/DateInput.tsx +8 -2
- package/src/DateTimeInput/DateTimeInput.tsx +8 -2
- package/src/Dialog/Dialog.tsx +44 -14
- package/src/Drawer/Drawer.tsx +28 -11
- package/src/Drawer/useDrawerDialogPresence.ts +5 -1
- package/src/Field/PanelSearchInput.tsx +11 -0
- package/src/InfoTip/InfoTip.doc.mjs +3 -3
- package/src/InfoTip/InfoTip.tsx +5 -5
- package/src/Layer/layerAnimations.stylex.ts +24 -16
- package/src/Layer/useBackdropPress.ts +81 -0
- package/src/Layer/usePressPin.ts +5 -11
- package/src/MobileNav/MobileNav.tsx +1 -1
- package/src/MultiSelector/hooks.ts +2 -4
- package/src/Outline/Outline.doc.mjs +177 -51
- package/src/Outline/Outline.tsx +5 -57
- package/src/Outline/useOutlineFromMarkdown.doc.mjs +157 -0
- package/src/Outline/useOutlineFromMarkdown.ts +1 -0
- package/src/Outline/useScrollSpy.ts +3 -3
- package/src/PowerSearch/PowerSearch.doc.mjs +3 -3
- package/src/PowerSearch/PowerSearch.tsx +196 -183
- package/src/RadioList/RadioList.doc.mjs +4 -2
- package/src/RadioList/RadioList.tsx +4 -0
- package/src/Resizable/ResizeHandle.tsx +133 -328
- package/src/Resizable/resizeGesture.ts +368 -0
- package/src/SegmentedControl/SegmentedControl.doc.mjs +3 -3
- package/src/SegmentedControl/SegmentedControl.tsx +179 -5
- package/src/SegmentedControl/SegmentedControlContext.ts +6 -0
- package/src/SegmentedControl/SegmentedControlItem.tsx +12 -1
- package/src/SegmentedControl/useSegmentedSelectionMotion.ts +1637 -0
- package/src/SegmentedControl/useSegmentedTouchTracking.ts +308 -0
- package/src/Selector/Selector.tsx +136 -89
- package/src/Selector/optionShape.ts +64 -0
- package/src/Selector/selectableOptionEntries.ts +39 -0
- package/src/Selector/utils.ts +9 -68
- package/src/SideNav/SideNavItem.tsx +3 -7
- package/src/Switch/switchTouch.ts +2 -10
- package/src/Switch/useSwitchTouch.ts +26 -96
- package/src/Token/Token.tsx +13 -4
- package/src/Token/TokenNamingContext.ts +28 -0
- package/src/Tokenizer/Tokenizer.doc.mjs +16 -0
- package/src/Tokenizer/Tokenizer.tsx +81 -86
- package/src/Tokenizer/TokenizerBottomSheet.tsx +219 -93
- package/src/Tokenizer/TokenizerItemRemovableContext.ts +5 -3
- package/src/Tooltip/Tooltip.doc.mjs +123 -54
- package/src/Tooltip/Tooltip.tsx +36 -39
- package/src/Tooltip/index.ts +0 -1
- package/src/Tooltip/useTooltip.doc.mjs +147 -33
- package/src/Tooltip/useTooltip.tsx +39 -32
- package/src/TopNav/TopNavItem.tsx +6 -5
- package/src/TopNav/TopNavMegaMenuItem.tsx +6 -5
- package/src/TopNav/TopNavMenu.tsx +6 -5
- package/src/gestures/GestureHost.ts +73 -4
- package/src/hooks/actionGroupTracker.ts +12 -56
- package/src/hooks/useHighlightedOptionScroll.ts +123 -15
- package/src/hooks/useMediaQuery.doc.mjs +2 -2
- package/src/hooks/useMediaQuery.ts +13 -4
- package/src/hooks/useMenuPress.ts +8 -2
- package/src/i18n/generated-english/PowerSearch/PowerSearch.generated.ts +1 -0
- package/src/i18n/generated-english/Tokenizer/TokenizerBottomSheet.generated.ts +7 -1
- package/src/i18n/generated-locales/en.generated.ts +1 -0
- package/src/i18n/generated-locales/pseudo.generated.ts +1 -0
- package/src/internal/SheetConfirmation.tsx +101 -0
- package/src/internal/sheetDraft.ts +80 -0
- package/src/internal/touch-sheet.ts +21 -0
- package/src/internal/useDismissSearchOnTouchDrag.ts +69 -0
- package/src/motion/MEASUREMENTS.md +7 -0
- package/src/motion/README.md +1 -0
- package/src/motion/roles.ts +20 -1
- package/src/motion/uikit/index.ts +5 -5
- package/src/motion/uikit/segmented.ts +74 -0
- package/src/motion/uikit/springs.ts +5 -21
- package/src/privateAttributes/segmentTitle.stylex.ts +22 -0
- package/src/utils/browserClickWait.ts +133 -0
- package/src/utils/clickGuard.ts +37 -1
- package/dist/internal/tokenizer-presentation.d.ts +0 -21
- package/dist/internal/tokenizer-presentation.d.ts.map +0 -1
- package/dist/internal/tokenizer-presentation.js +0 -21
- 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 {
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
530
|
-
}}
|
|
531
|
+
})}
|
|
531
532
|
{...focusOutlineProps.focusVisible(
|
|
532
533
|
navItemStyles.item,
|
|
533
534
|
interactionOverlayStyles.backgroundColor,
|
|
@@ -475,6 +475,16 @@ interface WheelFollow {
|
|
|
475
475
|
/** The fingers' speed at that move, content px per ms. */
|
|
476
476
|
speedX: number;
|
|
477
477
|
speedY: number;
|
|
478
|
+
/**
|
|
479
|
+
* The reading behind that speed: the fingers' travel where it began, and
|
|
480
|
+
* its interval in ms.
|
|
481
|
+
*/
|
|
482
|
+
readFromX: number;
|
|
483
|
+
readFromY: number;
|
|
484
|
+
readSpan: number;
|
|
485
|
+
/** The fingers' speed before that reading, content px per ms. */
|
|
486
|
+
priorSpeedX: number;
|
|
487
|
+
priorSpeedY: number;
|
|
478
488
|
}
|
|
479
489
|
|
|
480
490
|
/**
|
|
@@ -503,6 +513,39 @@ function absorbedDebt(debt: number, moved: number): number {
|
|
|
503
513
|
return Math.sign(debt) * Math.max(0, Math.abs(debt) - Math.abs(moved) / 2);
|
|
504
514
|
}
|
|
505
515
|
|
|
516
|
+
/**
|
|
517
|
+
* A stall held a bunched wheel's frames back, and the move just before it took
|
|
518
|
+
* their time. So the wheel joins that move's reading. It can restore what the
|
|
519
|
+
* late move lowered, but never passes the speed before it.
|
|
520
|
+
*/
|
|
521
|
+
function joinReading(f: WheelFollow, trueX: number, trueY: number): void {
|
|
522
|
+
if (f.readSpan <= 0) {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
f.speedX = joinedSpeed(
|
|
526
|
+
f.speedX,
|
|
527
|
+
(trueX - f.readFromX) / f.readSpan,
|
|
528
|
+
f.priorSpeedX,
|
|
529
|
+
);
|
|
530
|
+
f.speedY = joinedSpeed(
|
|
531
|
+
f.speedY,
|
|
532
|
+
(trueY - f.readFromY) / f.readSpan,
|
|
533
|
+
f.priorSpeedY,
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* A joined reading, held to the speed before the late move. It only ever
|
|
539
|
+
* raises the current speed, in the current direction.
|
|
540
|
+
*/
|
|
541
|
+
function joinedSpeed(current: number, reading: number, prior: number): number {
|
|
542
|
+
const held =
|
|
543
|
+
Math.sign(reading) * Math.min(Math.abs(reading), Math.abs(prior));
|
|
544
|
+
return held * current >= 0 && Math.abs(held) > Math.abs(current)
|
|
545
|
+
? held
|
|
546
|
+
: current;
|
|
547
|
+
}
|
|
548
|
+
|
|
506
549
|
/**
|
|
507
550
|
* `target` where it is ahead of `shown` in the direction of `speed`, else
|
|
508
551
|
* `shown`: a reconcile that never steps back.
|
|
@@ -2276,9 +2319,22 @@ export class GestureHost {
|
|
|
2276
2319
|
}
|
|
2277
2320
|
if (this.wheelSegment?.decaying === true) {
|
|
2278
2321
|
// Undecided: the real deltas wait for the splitter while the
|
|
2279
|
-
// fingers' last speed carries the pan on.
|
|
2322
|
+
// fingers' last speed carries the pan on. A held wheel with its own
|
|
2323
|
+
// frame ends the reading a stall's bunch joins.
|
|
2280
2324
|
f.heldX += dx;
|
|
2281
2325
|
f.heldY += dy;
|
|
2326
|
+
if (this.wheelSegment.bunched) {
|
|
2327
|
+
// A raised speed moves on from where the pan is shown, so the
|
|
2328
|
+
// position never jumps inside the bunch.
|
|
2329
|
+
const since = time - f.lastMove;
|
|
2330
|
+
const speedX = f.speedX;
|
|
2331
|
+
const speedY = f.speedY;
|
|
2332
|
+
joinReading(f, f.trueX - f.heldX, f.trueY - f.heldY);
|
|
2333
|
+
f.baseX += (speedX - f.speedX) * since;
|
|
2334
|
+
f.baseY += (speedY - f.speedY) * since;
|
|
2335
|
+
} else {
|
|
2336
|
+
f.readSpan = 0;
|
|
2337
|
+
}
|
|
2282
2338
|
this.followProvisionally(gesture, f, time);
|
|
2283
2339
|
return;
|
|
2284
2340
|
}
|
|
@@ -2291,10 +2347,18 @@ export class GestureHost {
|
|
|
2291
2347
|
const debtY = absorbedDebt(gesture.y - beforeY, dy);
|
|
2292
2348
|
const trueX = beforeX - dx;
|
|
2293
2349
|
const trueY = beforeY - dy;
|
|
2294
|
-
// A wheel the splitter reads as one stall's merge
|
|
2295
|
-
//
|
|
2350
|
+
// A wheel the splitter reads as one stall's merge has no speed of its
|
|
2351
|
+
// own. It joins the reading of the move before its bunch, whose
|
|
2352
|
+
// interval holds its time.
|
|
2296
2353
|
const elapsed = time - f.lastMove;
|
|
2297
|
-
if (
|
|
2354
|
+
if (this.wheelSegment?.bunched === true) {
|
|
2355
|
+
joinReading(f, trueX, trueY);
|
|
2356
|
+
} else if (elapsed > 0) {
|
|
2357
|
+
f.priorSpeedX = f.speedX;
|
|
2358
|
+
f.priorSpeedY = f.speedY;
|
|
2359
|
+
f.readFromX = f.trueX;
|
|
2360
|
+
f.readFromY = f.trueY;
|
|
2361
|
+
f.readSpan = elapsed;
|
|
2298
2362
|
f.speedX = (trueX - f.trueX) / elapsed;
|
|
2299
2363
|
f.speedY = (trueY - f.trueY) / elapsed;
|
|
2300
2364
|
}
|
|
@@ -2445,6 +2509,11 @@ export class GestureHost {
|
|
|
2445
2509
|
baseY: kept.y,
|
|
2446
2510
|
speedX: 0,
|
|
2447
2511
|
speedY: 0,
|
|
2512
|
+
readFromX: kept.x,
|
|
2513
|
+
readFromY: kept.y,
|
|
2514
|
+
readSpan: 0,
|
|
2515
|
+
priorSpeedX: 0,
|
|
2516
|
+
priorSpeedY: 0,
|
|
2448
2517
|
};
|
|
2449
2518
|
}
|
|
2450
2519
|
this.arena.began(
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* SYNC: When modified, update actionGroupTracker.test.ts
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import {clickAndReport, clickForLift} from '../utils/browserClickWait';
|
|
16
|
+
import {armClickGuard} from '../utils/clickGuard';
|
|
15
17
|
import {PRESSED_ATTRIBUTE} from '../utils/pressFeedback';
|
|
16
18
|
import {PRESS_FADE_MS} from '../utils/pressGesture';
|
|
17
19
|
|
|
@@ -70,9 +72,9 @@ export interface ActionGroupTrackerOptions {
|
|
|
70
72
|
*/
|
|
71
73
|
onActivated?: (item: HTMLElement, cancelled: boolean) => void;
|
|
72
74
|
/**
|
|
73
|
-
* After the lift ran an action,
|
|
74
|
-
*
|
|
75
|
-
*
|
|
75
|
+
* After the lift ran an action, the shared click guard takes the browser's
|
|
76
|
+
* own click for that touch, so the action runs once. A recognizer that did
|
|
77
|
+
* not claim the touch leaves that click to the browser. @default false
|
|
76
78
|
*/
|
|
77
79
|
swallowsNativeClick?: boolean;
|
|
78
80
|
}
|
|
@@ -126,54 +128,6 @@ interface TrackedAction {
|
|
|
126
128
|
bottom: number;
|
|
127
129
|
}
|
|
128
130
|
|
|
129
|
-
/**
|
|
130
|
-
* Swallow the browser's own click for a touch that the lift already acted
|
|
131
|
-
* on. The listeners exist only from that lift until the click, the next
|
|
132
|
-
* press, or the next key. An activation with `detail === 0` (a key or
|
|
133
|
-
* assistive technology) always passes, and the swallower keeps waiting
|
|
134
|
-
* (`AST-094`).
|
|
135
|
-
*/
|
|
136
|
-
function swallowNextClick(view: Window): void {
|
|
137
|
-
const disarm = () => {
|
|
138
|
-
view.removeEventListener('click', onClick, true);
|
|
139
|
-
view.removeEventListener('pointerdown', disarm, true);
|
|
140
|
-
view.removeEventListener('keydown', disarm, true);
|
|
141
|
-
};
|
|
142
|
-
const onClick = (event: MouseEvent) => {
|
|
143
|
-
if (!event.isTrusted || event.detail === 0) {
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
disarm();
|
|
147
|
-
event.preventDefault();
|
|
148
|
-
event.stopImmediatePropagation();
|
|
149
|
-
};
|
|
150
|
-
view.addEventListener('click', onClick, true);
|
|
151
|
-
view.addEventListener('pointerdown', disarm, true);
|
|
152
|
-
view.addEventListener('keydown', disarm, true);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Click `element` (its own `click()`, so its activation behavior and every
|
|
157
|
-
* handler run as for any click) and report whether a handler cancelled it.
|
|
158
|
-
* The event is read from a capture listener that exists only for the call.
|
|
159
|
-
*/
|
|
160
|
-
function clickAndReport(element: HTMLElement): boolean {
|
|
161
|
-
const view = element.ownerDocument.defaultView;
|
|
162
|
-
let clicked: Event | null = null;
|
|
163
|
-
const grab = (event: Event) => {
|
|
164
|
-
if (clicked == null && event.target === element) {
|
|
165
|
-
clicked = event;
|
|
166
|
-
}
|
|
167
|
-
};
|
|
168
|
-
view?.addEventListener('click', grab, true);
|
|
169
|
-
try {
|
|
170
|
-
element.click();
|
|
171
|
-
} finally {
|
|
172
|
-
view?.removeEventListener('click', grab, true);
|
|
173
|
-
}
|
|
174
|
-
return (clicked as Event | null)?.defaultPrevented === true;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
131
|
/**
|
|
178
132
|
* Create a tracker. `getOptions` is read on every call, so a hook can pass
|
|
179
133
|
* the latest props without recreating it.
|
|
@@ -358,14 +312,16 @@ export function createActionGroupTracker(
|
|
|
358
312
|
const {onActivate, onActivated, swallowsNativeClick} = getOptions();
|
|
359
313
|
if (onActivate != null) {
|
|
360
314
|
onActivate(lifted);
|
|
315
|
+
if (swallowsNativeClick === true) {
|
|
316
|
+
armClickGuard(lifted.ownerDocument);
|
|
317
|
+
}
|
|
361
318
|
} else {
|
|
362
|
-
const cancelled =
|
|
319
|
+
const cancelled =
|
|
320
|
+
swallowsNativeClick === true
|
|
321
|
+
? clickForLift(lifted)
|
|
322
|
+
: clickAndReport(lifted);
|
|
363
323
|
onActivated?.(lifted, cancelled);
|
|
364
324
|
}
|
|
365
|
-
const view = lifted.ownerDocument.defaultView;
|
|
366
|
-
if (swallowsNativeClick === true && view != null) {
|
|
367
|
-
swallowNextClick(view);
|
|
368
|
-
}
|
|
369
325
|
return lifted;
|
|
370
326
|
},
|
|
371
327
|
cancel() {
|
|
@@ -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
|
|
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,31 +61,119 @@ export function useHighlightedOptionScroll({
|
|
|
41
61
|
setHighlightedIndex,
|
|
42
62
|
getOptionId,
|
|
43
63
|
itemCount,
|
|
44
|
-
}: UseHighlightedOptionScrollOptions):
|
|
45
|
-
|
|
64
|
+
}: UseHighlightedOptionScrollOptions): HighlightFromHover {
|
|
65
|
+
// The callback reads no render state, so memoized rows keep one function.
|
|
66
|
+
const hoveredIndexRef = useRef<number | null>(null);
|
|
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]);
|
|
95
|
+
|
|
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]);
|
|
46
127
|
|
|
47
|
-
const highlightFromHover = useCallback(
|
|
48
|
-
(index
|
|
49
|
-
if (
|
|
50
|
-
|
|
128
|
+
const highlightFromHover = useCallback<HighlightFromHover>(
|
|
129
|
+
(index, onHighlight) => {
|
|
130
|
+
if (removeHoldListenersRef.current != null) {
|
|
131
|
+
waitingHoverRef.current = {index, onHighlight};
|
|
132
|
+
return;
|
|
51
133
|
}
|
|
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
|
-
|
|
59
|
-
|
|
152
|
+
// Only a move to the hovered row came from the hover. A hover on the row
|
|
153
|
+
// already highlighted changes nothing, so a later run for the same row
|
|
154
|
+
// (a new list) still scrolls it into view.
|
|
155
|
+
const hasMoved = shownIndexRef.current !== highlightedIndex;
|
|
156
|
+
shownIndexRef.current = highlightedIndex;
|
|
157
|
+
const isFromHover =
|
|
158
|
+
hasMoved && hoveredIndexRef.current === highlightedIndex;
|
|
159
|
+
hoveredIndexRef.current = null;
|
|
160
|
+
if (isFromHover) {
|
|
60
161
|
return;
|
|
61
162
|
}
|
|
62
|
-
|
|
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();
|
|
63
172
|
return;
|
|
64
173
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}, [isOpen, highlightedIndex, getOptionId, itemCount]);
|
|
174
|
+
startHold();
|
|
175
|
+
option.scrollIntoView?.({block: 'nearest'});
|
|
176
|
+
}, [isOpen, highlightedIndex, getOptionId, itemCount, endHold, startHold]);
|
|
69
177
|
|
|
70
178
|
return highlightFromHover;
|
|
71
179
|
}
|
|
@@ -21,7 +21,7 @@ export const docs = {
|
|
|
21
21
|
},
|
|
22
22
|
],
|
|
23
23
|
usage: {
|
|
24
|
-
description: 'SSR-safe media query hook that subscribes to window.matchMedia changes. Returns whether the given media query matches. Always returns false on first render for SSR compatibility.',
|
|
24
|
+
description: 'SSR-safe media query hook that subscribes to window.matchMedia changes. Returns whether the given media query matches. Always returns false on first render for SSR compatibility. Where `window.matchMedia` is missing, it returns its server default and never throws.',
|
|
25
25
|
bestPractices: [
|
|
26
26
|
{ guidance: true, description: 'Use for responsive layout switching based on viewport width, color scheme, or motion preferences.' },
|
|
27
27
|
{ guidance: true, description: 'Prefer Astryx responsive tokens and component props over manual breakpoint logic when possible.' },
|
|
@@ -44,7 +44,7 @@ export const docsDense = {
|
|
|
44
44
|
matches: 'whether media query currently matches. Always false on first render (SSR-safe).',
|
|
45
45
|
},
|
|
46
46
|
usage: {
|
|
47
|
-
description: 'SSR-safe media query hook subscribing to window.matchMedia changes. Returns whether given media query matches. Always returns false on first render for SSR compatibility.',
|
|
47
|
+
description: 'SSR-safe media query hook subscribing to window.matchMedia changes. Returns whether given media query matches. Always returns false on first render for SSR compatibility. Where `window.matchMedia` is missing, returns its server default and never throws.',
|
|
48
48
|
bestPractices: [
|
|
49
49
|
{ guidance: true, description: 'Use for responsive layout switching based on viewport width, color scheme, or motion preferences.' },
|
|
50
50
|
{ guidance: true, description: 'Prefer Astryx responsive tokens + component props over manual breakpoint logic when possible.' },
|
|
@@ -20,7 +20,8 @@ import {useCallback, useSyncExternalStore} from 'react';
|
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* SSR-safe media query hook.
|
|
23
|
-
* Returns whether the given media query matches.
|
|
23
|
+
* Returns whether the given media query matches. Where the window has no
|
|
24
|
+
* `matchMedia`, as in a test DOM, it returns `serverDefault`.
|
|
24
25
|
*
|
|
25
26
|
* @param query - CSS media query string
|
|
26
27
|
* @param serverDefault - Value to return during SSR. Pass a server-side hint
|
|
@@ -35,6 +36,10 @@ import {useCallback, useSyncExternalStore} from 'react';
|
|
|
35
36
|
export function useMediaQuery(query: string, serverDefault = false): boolean {
|
|
36
37
|
const subscribe = useCallback(
|
|
37
38
|
(onStoreChange: () => void) => {
|
|
39
|
+
// A DOM without `matchMedia`, such as a test DOM, has nothing to watch.
|
|
40
|
+
if (typeof window.matchMedia !== 'function') {
|
|
41
|
+
return () => {};
|
|
42
|
+
}
|
|
38
43
|
const mql = window.matchMedia(query);
|
|
39
44
|
mql.addEventListener('change', onStoreChange);
|
|
40
45
|
return () => mql.removeEventListener('change', onStoreChange);
|
|
@@ -42,9 +47,13 @@ export function useMediaQuery(query: string, serverDefault = false): boolean {
|
|
|
42
47
|
[query],
|
|
43
48
|
);
|
|
44
49
|
|
|
45
|
-
const getSnapshot = useCallback(
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
const getSnapshot = useCallback(
|
|
51
|
+
() =>
|
|
52
|
+
typeof window.matchMedia === 'function'
|
|
53
|
+
? window.matchMedia(query).matches
|
|
54
|
+
: serverDefault,
|
|
55
|
+
[query, serverDefault],
|
|
56
|
+
);
|
|
48
57
|
|
|
49
58
|
const getServerSnapshot = useCallback(() => serverDefault, [serverDefault]);
|
|
50
59
|
|