@astryxdesign/core 0.7.6 → 0.7.7-canary.1d04d40

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 (173) hide show
  1. package/dist/AppShell/AppShell.d.ts.map +1 -1
  2. package/dist/AppShell/AppShell.js +23 -3
  3. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  4. package/dist/BottomSheet/BottomSheetPanel.js +16 -9
  5. package/dist/Chat/ChatComposerInput.d.ts +9 -0
  6. package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
  7. package/dist/Chat/ChatComposerInput.js +2 -0
  8. package/dist/Chat/ChatMessage.js +2 -2
  9. package/dist/Chat/chatMessage.vars.stylex.d.ts +8 -8
  10. package/dist/Chat/chatMessage.vars.stylex.js +8 -8
  11. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  12. package/dist/CommandPalette/CommandPaletteInput.js +8 -1
  13. package/dist/DateInput/MonthScroller.d.ts.map +1 -1
  14. package/dist/DateInput/MonthScroller.js +72 -2
  15. package/dist/Drawer/Drawer.d.ts.map +1 -1
  16. package/dist/Drawer/Drawer.js +6 -0
  17. package/dist/DropdownMenu/useMenuOverflow.d.ts +13 -3
  18. package/dist/DropdownMenu/useMenuOverflow.d.ts.map +1 -1
  19. package/dist/DropdownMenu/useMenuOverflow.js +31 -8
  20. package/dist/Field/PanelSearchInput.d.ts.map +1 -1
  21. package/dist/Field/PanelSearchInput.js +8 -1
  22. package/dist/Field/textEntryTraits.d.ts +6 -6
  23. package/dist/Field/textEntryTraits.js +6 -6
  24. package/dist/Layer/layerClickBoundary.d.ts +7 -1
  25. package/dist/Layer/layerClickBoundary.d.ts.map +1 -1
  26. package/dist/Layer/layerClickBoundary.js +2 -2
  27. package/dist/Layer/usePressPin.d.ts +95 -0
  28. package/dist/Layer/usePressPin.d.ts.map +1 -0
  29. package/dist/Layer/usePressPin.js +335 -0
  30. package/dist/Layer/useTouchTrigger.d.ts +23 -1
  31. package/dist/Layer/useTouchTrigger.d.ts.map +1 -1
  32. package/dist/Layer/useTouchTrigger.js +29 -2
  33. package/dist/Link/Link.d.ts.map +1 -1
  34. package/dist/Link/Link.js +1 -0
  35. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
  36. package/dist/ProgressBar/ProgressBar.js +20 -9
  37. package/dist/ProgressBar/ProgressBarTouch.d.ts +29 -0
  38. package/dist/ProgressBar/ProgressBarTouch.d.ts.map +1 -0
  39. package/dist/ProgressBar/ProgressBarTouch.js +91 -0
  40. package/dist/Selector/Selector.d.ts.map +1 -1
  41. package/dist/Selector/Selector.js +25 -13
  42. package/dist/Slider/Slider.d.ts.map +1 -1
  43. package/dist/Slider/Slider.js +404 -131
  44. package/dist/Slider/sliderTouchController.d.ts +189 -0
  45. package/dist/Slider/sliderTouchController.d.ts.map +1 -0
  46. package/dist/Slider/sliderTouchController.js +1017 -0
  47. package/dist/Slider/useSliderTouch.d.ts +22 -0
  48. package/dist/Slider/useSliderTouch.d.ts.map +1 -0
  49. package/dist/Slider/useSliderTouch.js +56 -0
  50. package/dist/Tooltip/Tooltip.d.ts +30 -6
  51. package/dist/Tooltip/Tooltip.d.ts.map +1 -1
  52. package/dist/Tooltip/Tooltip.js +5 -0
  53. package/dist/Tooltip/index.d.ts +1 -1
  54. package/dist/Tooltip/index.d.ts.map +1 -1
  55. package/dist/Tooltip/useTooltip.d.ts +31 -5
  56. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  57. package/dist/Tooltip/useTooltip.js +59 -18
  58. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  59. package/dist/Typeahead/BaseTypeahead.js +19 -9
  60. package/dist/astryx.css +40 -6
  61. package/dist/gestures/GestureArena.d.ts +21 -2
  62. package/dist/gestures/GestureArena.d.ts.map +1 -1
  63. package/dist/gestures/GestureArena.js +23 -3
  64. package/dist/gestures/GestureHost.d.ts +123 -3
  65. package/dist/gestures/GestureHost.d.ts.map +1 -1
  66. package/dist/gestures/GestureHost.js +791 -12
  67. package/dist/gestures/GestureRecognizer.d.ts +15 -1
  68. package/dist/gestures/GestureRecognizer.d.ts.map +1 -1
  69. package/dist/gestures/GestureRecognizer.js +12 -0
  70. package/dist/gestures/PanGestureRecognizer.d.ts +8 -0
  71. package/dist/gestures/PanGestureRecognizer.d.ts.map +1 -1
  72. package/dist/gestures/PinchGestureRecognizer.d.ts +5 -1
  73. package/dist/gestures/PinchGestureRecognizer.d.ts.map +1 -1
  74. package/dist/gestures/PinchGestureRecognizer.js +10 -0
  75. package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts +3 -3
  76. package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts.map +1 -1
  77. package/dist/gestures/ScreenEdgePanGestureRecognizer.js +7 -5
  78. package/dist/gestures/nativeScroll.d.ts +16 -2
  79. package/dist/gestures/nativeScroll.d.ts.map +1 -1
  80. package/dist/gestures/nativeScroll.js +39 -8
  81. package/dist/gestures/recognizerInternals.d.ts +4 -2
  82. package/dist/gestures/recognizerInternals.d.ts.map +1 -1
  83. package/dist/gestures/types.d.ts +11 -0
  84. package/dist/gestures/types.d.ts.map +1 -1
  85. package/dist/gestures/types.js +7 -0
  86. package/dist/gestures/wheelSegmenter.d.ts +45 -0
  87. package/dist/gestures/wheelSegmenter.d.ts.map +1 -0
  88. package/dist/gestures/wheelSegmenter.js +98 -0
  89. package/dist/motion/animator.d.ts.map +1 -1
  90. package/dist/motion/animator.js +140 -19
  91. package/dist/motion/paging.d.ts +58 -0
  92. package/dist/motion/paging.d.ts.map +1 -0
  93. package/dist/motion/paging.js +128 -0
  94. package/dist/motion/uikit/index.d.ts +5 -3
  95. package/dist/motion/uikit/index.d.ts.map +1 -1
  96. package/dist/motion/uikit/index.js +5 -3
  97. package/dist/motion/uikit/paging.d.ts +160 -0
  98. package/dist/motion/uikit/paging.d.ts.map +1 -0
  99. package/dist/motion/uikit/paging.js +162 -0
  100. package/dist/motion/uikit/progressBar.d.ts +59 -0
  101. package/dist/motion/uikit/progressBar.d.ts.map +1 -0
  102. package/dist/motion/uikit/progressBar.js +82 -0
  103. package/dist/motion/uikit/scroll.d.ts +0 -19
  104. package/dist/motion/uikit/scroll.d.ts.map +1 -1
  105. package/dist/motion/uikit/scroll.js +2 -21
  106. package/dist/motion/uikit/slider.d.ts +97 -0
  107. package/dist/motion/uikit/slider.d.ts.map +1 -0
  108. package/dist/motion/uikit/slider.js +111 -0
  109. package/dist/motion/uikit/springs.d.ts +46 -1
  110. package/dist/motion/uikit/springs.d.ts.map +1 -1
  111. package/dist/motion/uikit/springs.js +48 -4
  112. package/dist/utils/selectionLock.d.ts +43 -0
  113. package/dist/utils/selectionLock.d.ts.map +1 -1
  114. package/dist/utils/selectionLock.js +394 -6
  115. package/package.json +5 -5
  116. package/src/AppShell/AppShell.doc.mjs +10 -0
  117. package/src/AppShell/AppShell.tsx +48 -2
  118. package/src/BottomSheet/BottomSheetPanel.tsx +16 -9
  119. package/src/Chat/Chat.doc.mjs +1 -0
  120. package/src/Chat/ChatComposerInput.doc.mjs +11 -0
  121. package/src/Chat/ChatComposerInput.tsx +11 -0
  122. package/src/Chat/ChatMessage.tsx +2 -2
  123. package/src/Chat/chatMessage.vars.stylex.ts +8 -8
  124. package/src/CommandPalette/CommandPaletteInput.tsx +6 -0
  125. package/src/DateInput/MonthScroller.tsx +63 -5
  126. package/src/Drawer/Drawer.tsx +6 -0
  127. package/src/DropdownMenu/useMenuOverflow.ts +44 -8
  128. package/src/Field/PanelSearchInput.tsx +6 -0
  129. package/src/Field/textEntryTraits.ts +6 -6
  130. package/src/Layer/layerClickBoundary.ts +2 -2
  131. package/src/Layer/usePressPin.ts +481 -0
  132. package/src/Layer/useTouchTrigger.ts +37 -1
  133. package/src/Link/Link.tsx +3 -0
  134. package/src/ProgressBar/ProgressBar.doc.mjs +3 -3
  135. package/src/ProgressBar/ProgressBar.tsx +82 -11
  136. package/src/ProgressBar/ProgressBarTouch.ts +109 -0
  137. package/src/Selector/Selector.tsx +29 -2
  138. package/src/Slider/Slider.doc.mjs +11 -7
  139. package/src/Slider/Slider.tsx +539 -132
  140. package/src/Slider/sliderTouchController.ts +1209 -0
  141. package/src/Slider/useSliderTouch.ts +78 -0
  142. package/src/Tooltip/Tooltip.doc.mjs +21 -6
  143. package/src/Tooltip/Tooltip.tsx +42 -5
  144. package/src/Tooltip/index.ts +1 -0
  145. package/src/Tooltip/useTooltip.doc.mjs +4 -2
  146. package/src/Tooltip/useTooltip.tsx +84 -21
  147. package/src/Typeahead/BaseTypeahead.tsx +16 -8
  148. package/src/gestures/GestureArena.ts +45 -4
  149. package/src/gestures/GestureHost.ts +949 -12
  150. package/src/gestures/GestureRecognizer.ts +28 -0
  151. package/src/gestures/PanGestureRecognizer.ts +8 -0
  152. package/src/gestures/PinchGestureRecognizer.ts +16 -1
  153. package/src/gestures/README.md +82 -3
  154. package/src/gestures/ScreenEdgePanGestureRecognizer.ts +7 -5
  155. package/src/gestures/nativeScroll.ts +49 -9
  156. package/src/gestures/recognizerInternals.ts +9 -2
  157. package/src/gestures/types.ts +12 -0
  158. package/src/gestures/wheelSegmenter.ts +114 -0
  159. package/src/motion/MEASUREMENTS.md +8 -1
  160. package/src/motion/README.md +6 -3
  161. package/src/motion/animator.ts +193 -25
  162. package/src/motion/paging.ts +138 -0
  163. package/src/motion/uikit/index.ts +5 -3
  164. package/src/motion/uikit/paging.ts +162 -0
  165. package/src/motion/uikit/progressBar.ts +82 -0
  166. package/src/motion/uikit/scroll.ts +2 -18
  167. package/src/motion/uikit/slider.ts +105 -0
  168. package/src/motion/uikit/springs.ts +37 -2
  169. package/src/utils/selectionLock.ts +501 -6
  170. package/dist/motion/selectPage.d.ts +0 -26
  171. package/dist/motion/selectPage.d.ts.map +0 -1
  172. package/dist/motion/selectPage.js +0 -55
  173. package/src/motion/selectPage.ts +0 -55
@@ -0,0 +1,481 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file usePressPin.ts
7
+ * @input A hover layer's trigger, open state, show and hide, and its
8
+ * `triggerBehavior` and `touchTrigger` options
9
+ * @output usePressPin, the press toggle that pins a hover layer open;
10
+ * isPressPinTrigger; stopPressPinClick; the LayerTriggerBehavior type
11
+ * @position Core-internal: one call per hover layer, useTooltip's. It owns
12
+ * the layer's touch trigger, so the host makes no second touch-trigger call.
13
+ *
14
+ * SYNC: When modified, update:
15
+ * - /packages/core/src/Tooltip/useTooltip.tsx
16
+ * - /packages/core/src/Layer/usePressPin.test.tsx
17
+ */
18
+
19
+ import {
20
+ useCallback,
21
+ useEffect,
22
+ useRef,
23
+ useState,
24
+ type MouseEvent as ReactMouseEvent,
25
+ type RefObject,
26
+ } from 'react';
27
+ import {useAnnounce} from '../hooks/useAnnounce';
28
+ import {useDevWarning} from '../hooks/useDevWarning';
29
+ import {stopInReactTree} from './layerClickBoundary';
30
+ import {
31
+ useTouchTriggerInternal,
32
+ type LayerTouchTrigger,
33
+ type UseTouchTriggerReturn,
34
+ } from './useTouchTrigger';
35
+
36
+ /**
37
+ * How the trigger opens the layer. `adaptive`, the default, previews it on
38
+ * hover and focus, and a tap opens it unless the trigger acts. While
39
+ * `touchTrigger` remains, it decides those taps. `hover` previews it on hover
40
+ * and focus, and no touch or pen tap opens it. `toggle` pins it on every
41
+ * press of a button trigger.
42
+ */
43
+ export type LayerTriggerBehavior = 'adaptive' | 'hover' | 'toggle';
44
+
45
+ /** The clicks a press pin answered, so the wrapper can stop them (`stopPressPinClick`). */
46
+ const pressPinClicks = new WeakSet<Event>();
47
+
48
+ /**
49
+ * The layer component's wrapper handler. It stops a press that pinned or
50
+ * closed the layer in the React tree only, after the trigger's own handlers.
51
+ * No clickable row, card, or container around the trigger then receives it,
52
+ * and document and window listeners still do. An anchored trigger has no
53
+ * wrapper, so a plain `onClick` element around it still receives the press.
54
+ */
55
+ export function stopPressPinClick(event: ReactMouseEvent): void {
56
+ if (pressPinClicks.has(event.nativeEvent)) {
57
+ stopInReactTree(event);
58
+ }
59
+ }
60
+
61
+ /** Whether presses on this element can pin a layer: a button, by role. */
62
+ export function isPressPinTrigger(element: Element): boolean {
63
+ const role = element.getAttribute('role');
64
+ if (role != null && role !== '') {
65
+ return role === 'button';
66
+ }
67
+ return element.tagName === 'BUTTON';
68
+ }
69
+
70
+ export interface UsePressPinOptions {
71
+ /** The layer's `triggerBehavior`. */
72
+ triggerBehavior: LayerTriggerBehavior;
73
+ /** Whether the layer's triggers are live at all. */
74
+ isEnabled: boolean;
75
+ /** The controlled open value, or `undefined` when uncontrolled. */
76
+ isOpen: boolean | undefined;
77
+ /** Whether the layer shows now. */
78
+ isLayerOpen: boolean;
79
+ /** The layer surface's element id. */
80
+ layerId: string;
81
+ /** The component its development warning names. */
82
+ component: string;
83
+ /** Shows the layer now, with no delay. */
84
+ show: () => void;
85
+ /** Hides the layer now. */
86
+ hide: () => void;
87
+ /** Reports a controlled layer's toggled value without changing it. */
88
+ report: (isOpen: boolean) => void;
89
+ /** The layer's `label`: a press that pins the layer says it once, in place of its text. */
90
+ label?: string;
91
+ /** The layer's `touchTrigger`, which decides taps only under `adaptive`. */
92
+ touchTrigger: LayerTouchTrigger;
93
+ /** The host's trigger element, which the touch trigger reads. */
94
+ triggerRef: RefObject<HTMLElement | null>;
95
+ }
96
+
97
+ export interface UsePressPinReturn {
98
+ /** Whether presses on this trigger pin the layer: toggle mode and a button. */
99
+ isToggleTrigger: (element: HTMLElement | null) => boolean;
100
+ /**
101
+ * Whether a press holds the layer open now: a pin on a wired trigger while
102
+ * the layer shows, or a controlled layer's open value. The host's hover and
103
+ * focus hides ask it.
104
+ */
105
+ isHeldOpen: () => boolean;
106
+ /** Wires the trigger element, or unwires it with `null`. */
107
+ attach: (element: HTMLElement | null) => void;
108
+ /** Drops the pin: Escape, or a hide that came from elsewhere. */
109
+ unpin: () => void;
110
+ /**
111
+ * While pinned, focus moving onto an element outside the trigger and the
112
+ * layer closes it. Returns whether the pin decided. The pin calls it for its
113
+ * trigger, and a host calls it for the layer's own content.
114
+ */
115
+ handleFocusLeave: (event: FocusEvent) => boolean;
116
+ /**
117
+ * Whether the layer shows pinned on a wired trigger. The host then announces
118
+ * its rendered text, unless the layer has a `label`.
119
+ */
120
+ isPinShown: boolean;
121
+ /** The layer's touch trigger (useTouchTriggerInternal), wired to the pin. */
122
+ touch: Pick<
123
+ UseTouchTriggerReturn,
124
+ | 'isTouchPointerRef'
125
+ | 'isTouchInteraction'
126
+ | 'handlePointerEnter'
127
+ | 'handlePointerDown'
128
+ | 'clearTapOpen'
129
+ >;
130
+ }
131
+
132
+ function restoreAttribute(
133
+ element: HTMLElement,
134
+ name: string,
135
+ value: string | null,
136
+ ): void {
137
+ if (value == null) {
138
+ element.removeAttribute(name);
139
+ } else {
140
+ element.setAttribute(name, value);
141
+ }
142
+ }
143
+
144
+ function writeAttribute(
145
+ element: HTMLElement,
146
+ name: string,
147
+ value: string | null,
148
+ ): void {
149
+ if (element.getAttribute(name) !== value) {
150
+ restoreAttribute(element, name, value);
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Pins a hover layer open on a press of a button trigger, under
156
+ * `triggerBehavior="toggle"`. The same press closes it, and so do Escape, a
157
+ * press or scroll outside, and focus that leaves the trigger and the layer.
158
+ * The press stops at the trigger. A trigger that is not a button gets a
159
+ * development warning and keeps tap-only opening.
160
+ */
161
+ export function usePressPin(options: UsePressPinOptions): UsePressPinReturn {
162
+ const {triggerBehavior, isOpen, isLayerOpen, layerId} = options;
163
+ const isToggleMode = triggerBehavior === 'toggle';
164
+
165
+ // DOM listeners read the latest options and announcer through refs. The
166
+ // announcer is the layer's own: inside a modal, the modal's region.
167
+ const optionsRef = useRef(options);
168
+ optionsRef.current = options;
169
+ const announce = useAnnounce();
170
+ const announceRef = useRef(announce);
171
+ announceRef.current = announce;
172
+
173
+ const isPinnedRef = useRef(false);
174
+ const [isPinned, setIsPinnedState] = useState(false);
175
+ const setPinned = useCallback((next: boolean) => {
176
+ isPinnedRef.current = next;
177
+ setIsPinnedState(next);
178
+ }, []);
179
+
180
+ // Refs hold the wired trigger and last offered element for a later rewire.
181
+ const triggerRef = useRef<HTMLElement | null>(null);
182
+ const offeredRef = useRef<HTMLElement | null>(null);
183
+ // A primitive: re-attaching the same trigger renders nothing.
184
+ const [offeredKind, setOfferedKind] = useState<'none' | 'other' | 'button'>(
185
+ 'none',
186
+ );
187
+ // The trigger's own aria-expanded and aria-controls, restored on unwiring.
188
+ const ownStateRef = useRef<{
189
+ expanded: string | null;
190
+ controls: string | null;
191
+ } | null>(null);
192
+
193
+ // The touch trigger's outside dismissal (a press or scroll outside) closes
194
+ // a pinned layer through the pin, so the pin drops with it.
195
+ const hideFromTouch = useCallback(() => {
196
+ const current = optionsRef.current;
197
+ if (current.triggerBehavior === 'toggle' && triggerRef.current != null) {
198
+ if (current.isOpen !== undefined) {
199
+ current.report(false);
200
+ return;
201
+ }
202
+ if (isPinnedRef.current) {
203
+ setPinned(false);
204
+ }
205
+ }
206
+ current.hide();
207
+ }, [setPinned]);
208
+
209
+ const {
210
+ isTouchPointerRef,
211
+ isTouchInteraction,
212
+ handlePointerEnter,
213
+ handlePointerDown,
214
+ clearTapOpen,
215
+ armOutsideDismiss,
216
+ } = useTouchTriggerInternal({
217
+ touchTrigger: isToggleMode
218
+ ? 'tap'
219
+ : triggerBehavior === 'hover'
220
+ ? 'none'
221
+ : options.touchTrigger,
222
+ isEnabled: options.isEnabled,
223
+ isControlled: isOpen !== undefined,
224
+ isOpen: isLayerOpen,
225
+ layerId,
226
+ triggerRef: options.triggerRef,
227
+ show: options.show,
228
+ hide: hideFromTouch,
229
+ });
230
+
231
+ const isToggleTrigger = useCallback(
232
+ (element: HTMLElement | null) =>
233
+ optionsRef.current.triggerBehavior === 'toggle' &&
234
+ element != null &&
235
+ isPressPinTrigger(element),
236
+ [],
237
+ );
238
+
239
+ const close = useCallback(() => {
240
+ const current = optionsRef.current;
241
+ clearTapOpen();
242
+ if (current.isOpen !== undefined) {
243
+ current.report(false);
244
+ return;
245
+ }
246
+ setPinned(false);
247
+ current.hide();
248
+ }, [setPinned, clearTapOpen]);
249
+
250
+ const announceName = useCallback(() => {
251
+ const current = optionsRef.current;
252
+ const name = current.label?.trim() ?? '';
253
+ if (name !== '') {
254
+ announceRef.current(name, 'polite');
255
+ }
256
+ }, []);
257
+
258
+ const toggle = useCallback(() => {
259
+ const current = optionsRef.current;
260
+ if (current.isOpen !== undefined) {
261
+ if (!current.isOpen) {
262
+ armOutsideDismiss();
263
+ announceName();
264
+ } else {
265
+ clearTapOpen();
266
+ }
267
+ current.report(!current.isOpen);
268
+ return;
269
+ }
270
+ // A pin counts only while its layer shows; a closed layer pins again.
271
+ if (isPinnedRef.current && current.isLayerOpen) {
272
+ setPinned(false);
273
+ clearTapOpen();
274
+ current.hide();
275
+ } else {
276
+ setPinned(true);
277
+ current.show();
278
+ armOutsideDismiss();
279
+ announceName();
280
+ }
281
+ }, [setPinned, announceName, armOutsideDismiss, clearTapOpen]);
282
+
283
+ // Enter or Space presses a non-native button, which gets no click from the
284
+ // browser. A same-task click from its own key handler is that press.
285
+ const keyPressedRef = useRef(false);
286
+ const handlePress = useCallback(
287
+ (event: MouseEvent) => {
288
+ const current = optionsRef.current;
289
+ if (!current.isEnabled || current.triggerBehavior !== 'toggle') {
290
+ return;
291
+ }
292
+ // The press stops at its trigger: no label control, link, or submit.
293
+ event.preventDefault();
294
+ pressPinClicks.add(event);
295
+ if (keyPressedRef.current && event.detail === 0) {
296
+ return;
297
+ }
298
+ toggle();
299
+ },
300
+ [toggle],
301
+ );
302
+ const handleKeyDown = useCallback(
303
+ (event: KeyboardEvent) => {
304
+ const current = optionsRef.current;
305
+ if (
306
+ !current.isEnabled ||
307
+ current.triggerBehavior !== 'toggle' ||
308
+ event.repeat ||
309
+ event.isComposing ||
310
+ (event.key !== 'Enter' && event.key !== ' ')
311
+ ) {
312
+ return;
313
+ }
314
+ event.preventDefault();
315
+ keyPressedRef.current = true;
316
+ setTimeout(() => {
317
+ keyPressedRef.current = false;
318
+ }, 0);
319
+ toggle();
320
+ },
321
+ [toggle],
322
+ );
323
+
324
+ const isHeldOpen = useCallback(() => {
325
+ const current = optionsRef.current;
326
+ if (current.triggerBehavior !== 'toggle' || triggerRef.current == null) {
327
+ return false;
328
+ }
329
+ return current.isOpen !== undefined
330
+ ? current.isOpen && current.isLayerOpen
331
+ : isPinnedRef.current && current.isLayerOpen;
332
+ }, []);
333
+
334
+ const handleFocusLeave = useCallback(
335
+ (event: FocusEvent) => {
336
+ if (!isHeldOpen()) {
337
+ return false;
338
+ }
339
+ const next = event.relatedTarget;
340
+ const trigger = triggerRef.current;
341
+ // Focus that leaves for no element is a press's to decide. The trigger
342
+ // can sit in another document, so the check uses its own window.
343
+ const view = trigger?.ownerDocument.defaultView;
344
+ if (view == null || !(next instanceof view.Node)) {
345
+ return true;
346
+ }
347
+ const surface =
348
+ trigger?.ownerDocument.getElementById(optionsRef.current.layerId) ??
349
+ null;
350
+ if (
351
+ trigger?.contains(next) === true ||
352
+ surface?.contains(next) === true
353
+ ) {
354
+ return true;
355
+ }
356
+ close();
357
+ return true;
358
+ },
359
+ [isHeldOpen, close],
360
+ );
361
+
362
+ const unwire = useCallback(() => {
363
+ const previous = triggerRef.current;
364
+ if (previous == null) {
365
+ return;
366
+ }
367
+ previous.removeEventListener('click', handlePress);
368
+ previous.removeEventListener('keydown', handleKeyDown);
369
+ previous.removeEventListener('focusout', handleFocusLeave);
370
+ const own = ownStateRef.current;
371
+ if (own != null) {
372
+ restoreAttribute(previous, 'aria-expanded', own.expanded);
373
+ restoreAttribute(previous, 'aria-controls', own.controls);
374
+ }
375
+ ownStateRef.current = null;
376
+ triggerRef.current = null;
377
+ }, [handlePress, handleKeyDown, handleFocusLeave]);
378
+
379
+ // The element and mode last settled; the same pair again does nothing.
380
+ const settledRef = useRef<{
381
+ element: HTMLElement | null;
382
+ mode: LayerTriggerBehavior;
383
+ } | null>(null);
384
+
385
+ const wire = useCallback(
386
+ (element: HTMLElement | null) => {
387
+ const current = optionsRef.current;
388
+ const settled = settledRef.current;
389
+ if (
390
+ settled != null &&
391
+ settled.element === element &&
392
+ settled.mode === current.triggerBehavior
393
+ ) {
394
+ return;
395
+ }
396
+ settledRef.current = {element, mode: current.triggerBehavior};
397
+ unwire();
398
+ if (element == null || current.triggerBehavior !== 'toggle') {
399
+ return;
400
+ }
401
+ if (!isPressPinTrigger(element)) {
402
+ return;
403
+ }
404
+ element.addEventListener('click', handlePress);
405
+ if (element.tagName !== 'BUTTON') {
406
+ element.addEventListener('keydown', handleKeyDown);
407
+ }
408
+ // Its own focus-leave listener: `focusTrigger="never"` attaches none.
409
+ element.addEventListener('focusout', handleFocusLeave);
410
+ ownStateRef.current = {
411
+ expanded: element.getAttribute('aria-expanded'),
412
+ controls: element.getAttribute('aria-controls'),
413
+ };
414
+ triggerRef.current = element;
415
+ },
416
+ [unwire, handlePress, handleKeyDown, handleFocusLeave],
417
+ );
418
+
419
+ const attach = useCallback(
420
+ (element: HTMLElement | null) => {
421
+ offeredRef.current = element;
422
+ // Hosts detach and re-attach within one commit, so a detach is not kept.
423
+ if (element != null) {
424
+ setOfferedKind(isPressPinTrigger(element) ? 'button' : 'other');
425
+ }
426
+ wire(element);
427
+ },
428
+ [wire],
429
+ );
430
+
431
+ const isWired = isToggleMode && offeredKind === 'button';
432
+ useDevWarning(
433
+ options.component,
434
+ '`triggerBehavior="toggle"` pins only a button trigger (a <button> or role="button"); this trigger keeps tap-only opening.',
435
+ isToggleMode && offeredKind === 'other',
436
+ );
437
+
438
+ // A later `triggerBehavior` change rewires here; the ref does not rerun.
439
+ useEffect(() => {
440
+ wire(offeredRef.current);
441
+ }, [triggerBehavior, wire]);
442
+
443
+ const unpin = useCallback(() => {
444
+ if (isPinnedRef.current) {
445
+ setPinned(false);
446
+ }
447
+ }, [setPinned]);
448
+
449
+ // Each path that hides a pinned layer drops the pin itself, so a layer that
450
+ // does not show is never reported expanded.
451
+ const isExpanded = (isOpen !== undefined ? isOpen : isPinned) && isLayerOpen;
452
+
453
+ // The trigger's aria-expanded and aria-controls follow the pin. No deps: a
454
+ // host can swap the trigger on any commit; equal values are not rewritten.
455
+ useEffect(() => {
456
+ const trigger = triggerRef.current;
457
+ if (!isToggleMode || !isWired || trigger == null) {
458
+ return;
459
+ }
460
+ writeAttribute(trigger, 'aria-expanded', isExpanded ? 'true' : 'false');
461
+ writeAttribute(trigger, 'aria-controls', isLayerOpen ? layerId : null);
462
+ });
463
+
464
+ const isPinShown = isToggleMode && isWired && isExpanded;
465
+
466
+ return {
467
+ isToggleTrigger,
468
+ isHeldOpen,
469
+ attach,
470
+ unpin,
471
+ handleFocusLeave,
472
+ isPinShown,
473
+ touch: {
474
+ isTouchPointerRef,
475
+ isTouchInteraction,
476
+ handlePointerEnter,
477
+ handlePointerDown,
478
+ clearTapOpen,
479
+ },
480
+ };
481
+ }
@@ -7,7 +7,8 @@
7
7
  * @input Touch trigger mode, layer open state, trigger ref, show/hide callbacks
8
8
  * @output Exports useTouchTrigger hook, isActionTrigger, LayerTouchTrigger type,
9
9
  * tapLandsOnDisabledControl, TOUCH_POINTER_TYPES and useTapStop (internal:
10
- * not re-exported from index)
10
+ * not re-exported from index). Also useTouchTriggerInternal, with the
11
+ * outside dismissal's arm for the press pin (Core-internal).
11
12
  * @position Layer hook; shared touch behavior for useTooltip and useHoverCard
12
13
  *
13
14
  * SYNC: When modified, update:
@@ -281,6 +282,21 @@ export interface UseTouchTriggerReturn {
281
282
  clearTapOpen: () => void;
282
283
  }
283
284
 
285
+ /**
286
+ * useTouchTrigger's return plus the outside dismissal's arm, for Core's own
287
+ * press pin (usePressPin). Not exported from the Layer entry point.
288
+ *
289
+ * @internal
290
+ */
291
+ export interface UseTouchTriggerInternalReturn extends UseTouchTriggerReturn {
292
+ /**
293
+ * Arms the outside dismissal a tap-open gets: a press that lifts outside
294
+ * the trigger and the layer, or a scroll that starts there, hides it. A
295
+ * press pin arms it too.
296
+ */
297
+ armOutsideDismiss: () => void;
298
+ }
299
+
284
300
  /**
285
301
  * Touch behavior shared by the hover layers.
286
302
  *
@@ -301,6 +317,25 @@ export interface UseTouchTriggerReturn {
301
317
  export function useTouchTrigger(
302
318
  options: UseTouchTriggerOptions,
303
319
  ): UseTouchTriggerReturn {
320
+ const touch = useTouchTriggerInternal(options);
321
+ return {
322
+ isTouchPointerRef: touch.isTouchPointerRef,
323
+ isTouchInteraction: touch.isTouchInteraction,
324
+ handlePointerEnter: touch.handlePointerEnter,
325
+ handlePointerDown: touch.handlePointerDown,
326
+ clearTapOpen: touch.clearTapOpen,
327
+ };
328
+ }
329
+
330
+ /**
331
+ * useTouchTrigger, plus the outside dismissal's arm that a press pin uses to
332
+ * close on a press or scroll outside.
333
+ *
334
+ * @internal
335
+ */
336
+ export function useTouchTriggerInternal(
337
+ options: UseTouchTriggerOptions,
338
+ ): UseTouchTriggerInternalReturn {
304
339
  const {
305
340
  touchTrigger,
306
341
  isEnabled,
@@ -503,5 +538,6 @@ export function useTouchTrigger(
503
538
  handlePointerEnter,
504
539
  handlePointerDown,
505
540
  clearTapOpen: disarmOutsideDismiss,
541
+ armOutsideDismiss,
506
542
  };
507
543
  }
package/src/Link/Link.tsx CHANGED
@@ -98,6 +98,9 @@ const styles = stylex.create({
98
98
  backgroundColor: 'transparent',
99
99
  borderStyle: 'none',
100
100
  padding: 0,
101
+ // Browsers centre a button's text; a wrapped label lines up as an
102
+ // anchor's does.
103
+ textAlign: 'inherit',
101
104
  pointerEvents: 'auto',
102
105
  position: 'relative',
103
106
  },
@@ -160,7 +160,7 @@ export const docs = {
160
160
  usage: {
161
161
  anatomy,
162
162
  description:
163
- "A horizontal bar showing the completion progress of a task. Use it for operations where the duration is known, or as an animated indicator when progress can't be calculated. Supports semantic color variants, value labels, and custom formatting.",
163
+ "A horizontal bar showing the completion progress of a task. Use it for operations where the duration is known, or as an animated indicator when progress can't be calculated. Supports semantic color variants, value labels, and custom formatting. Under a finger a new value animates as on iOS: linearly, in proportion to how far it moves (a full track takes one second at the theme's default tempo), and the fill keeps a minimum visible length above zero.",
164
164
  bestPractices: [
165
165
  {
166
166
  guidance: true,
@@ -304,7 +304,7 @@ export const docsZh = {
304
304
  usage: {
305
305
  anatomy,
306
306
  description:
307
- "A horizontal bar showing the completion progress of a task. Use it for operations where the duration is known, or as an animated indicator when progress can't be calculated. Supports semantic color variants, value labels, and custom formatting.",
307
+ "A horizontal bar showing the completion progress of a task. Use it for operations where the duration is known, or as an animated indicator when progress can't be calculated. Supports semantic color variants, value labels, and custom formatting. Under a finger a new value animates as on iOS: linearly, in proportion to how far it moves (a full track takes one second at the theme's default tempo), and the fill keeps a minimum visible length above zero.",
308
308
  bestPractices: [
309
309
  {
310
310
  guidance: true,
@@ -347,7 +347,7 @@ export const docsDense = {
347
347
  usage: {
348
348
  anatomy,
349
349
  description:
350
- "A horizontal bar showing the completion progress of a task. Use it for operations where the duration is known, or as an animated indicator when progress can't be calculated. Supports semantic color variants, value labels, and custom formatting.",
350
+ "A horizontal bar showing the completion progress of a task. Use it for operations where the duration is known, or as an animated indicator when progress can't be calculated. Supports semantic color variants, value labels, and custom formatting. Under a finger a new value animates as on iOS: linearly, in proportion to how far it moves (a full track takes one second at the theme's default tempo), and the fill keeps a minimum visible length above zero.",
351
351
  bestPractices: [
352
352
  {
353
353
  guidance: true,