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

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 (131) 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/Link/Link.d.ts.map +1 -1
  25. package/dist/Link/Link.js +1 -0
  26. package/dist/Selector/Selector.d.ts.map +1 -1
  27. package/dist/Selector/Selector.js +25 -13
  28. package/dist/Slider/Slider.d.ts.map +1 -1
  29. package/dist/Slider/Slider.js +404 -131
  30. package/dist/Slider/sliderTouchController.d.ts +189 -0
  31. package/dist/Slider/sliderTouchController.d.ts.map +1 -0
  32. package/dist/Slider/sliderTouchController.js +1017 -0
  33. package/dist/Slider/useSliderTouch.d.ts +22 -0
  34. package/dist/Slider/useSliderTouch.d.ts.map +1 -0
  35. package/dist/Slider/useSliderTouch.js +56 -0
  36. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  37. package/dist/Typeahead/BaseTypeahead.js +19 -9
  38. package/dist/astryx.css +33 -6
  39. package/dist/gestures/GestureArena.d.ts.map +1 -1
  40. package/dist/gestures/GestureArena.js +4 -1
  41. package/dist/gestures/GestureHost.d.ts +72 -2
  42. package/dist/gestures/GestureHost.d.ts.map +1 -1
  43. package/dist/gestures/GestureHost.js +643 -5
  44. package/dist/gestures/GestureRecognizer.d.ts +15 -1
  45. package/dist/gestures/GestureRecognizer.d.ts.map +1 -1
  46. package/dist/gestures/GestureRecognizer.js +12 -0
  47. package/dist/gestures/PanGestureRecognizer.d.ts +8 -0
  48. package/dist/gestures/PanGestureRecognizer.d.ts.map +1 -1
  49. package/dist/gestures/PinchGestureRecognizer.d.ts +5 -1
  50. package/dist/gestures/PinchGestureRecognizer.d.ts.map +1 -1
  51. package/dist/gestures/PinchGestureRecognizer.js +10 -0
  52. package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts +3 -3
  53. package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts.map +1 -1
  54. package/dist/gestures/ScreenEdgePanGestureRecognizer.js +7 -5
  55. package/dist/gestures/nativeScroll.d.ts +16 -2
  56. package/dist/gestures/nativeScroll.d.ts.map +1 -1
  57. package/dist/gestures/nativeScroll.js +39 -8
  58. package/dist/gestures/recognizerInternals.d.ts +4 -2
  59. package/dist/gestures/recognizerInternals.d.ts.map +1 -1
  60. package/dist/gestures/types.d.ts +11 -0
  61. package/dist/gestures/types.d.ts.map +1 -1
  62. package/dist/gestures/types.js +7 -0
  63. package/dist/gestures/wheelSegmenter.d.ts +45 -0
  64. package/dist/gestures/wheelSegmenter.d.ts.map +1 -0
  65. package/dist/gestures/wheelSegmenter.js +98 -0
  66. package/dist/motion/animator.d.ts.map +1 -1
  67. package/dist/motion/animator.js +140 -19
  68. package/dist/motion/paging.d.ts +58 -0
  69. package/dist/motion/paging.d.ts.map +1 -0
  70. package/dist/motion/paging.js +128 -0
  71. package/dist/motion/uikit/index.d.ts +2 -1
  72. package/dist/motion/uikit/index.d.ts.map +1 -1
  73. package/dist/motion/uikit/index.js +2 -1
  74. package/dist/motion/uikit/paging.d.ts +160 -0
  75. package/dist/motion/uikit/paging.d.ts.map +1 -0
  76. package/dist/motion/uikit/paging.js +162 -0
  77. package/dist/motion/uikit/scroll.d.ts +0 -19
  78. package/dist/motion/uikit/scroll.d.ts.map +1 -1
  79. package/dist/motion/uikit/scroll.js +2 -21
  80. package/dist/motion/uikit/slider.d.ts +97 -0
  81. package/dist/motion/uikit/slider.d.ts.map +1 -0
  82. package/dist/motion/uikit/slider.js +111 -0
  83. package/dist/motion/uikit/springs.d.ts +46 -1
  84. package/dist/motion/uikit/springs.d.ts.map +1 -1
  85. package/dist/motion/uikit/springs.js +48 -4
  86. package/package.json +5 -5
  87. package/src/AppShell/AppShell.doc.mjs +10 -0
  88. package/src/AppShell/AppShell.tsx +48 -2
  89. package/src/BottomSheet/BottomSheetPanel.tsx +16 -9
  90. package/src/Chat/Chat.doc.mjs +1 -0
  91. package/src/Chat/ChatComposerInput.doc.mjs +11 -0
  92. package/src/Chat/ChatComposerInput.tsx +11 -0
  93. package/src/Chat/ChatMessage.tsx +2 -2
  94. package/src/Chat/chatMessage.vars.stylex.ts +8 -8
  95. package/src/CommandPalette/CommandPaletteInput.tsx +6 -0
  96. package/src/DateInput/MonthScroller.tsx +63 -5
  97. package/src/Drawer/Drawer.tsx +6 -0
  98. package/src/DropdownMenu/useMenuOverflow.ts +44 -8
  99. package/src/Field/PanelSearchInput.tsx +6 -0
  100. package/src/Field/textEntryTraits.ts +6 -6
  101. package/src/Link/Link.tsx +3 -0
  102. package/src/Selector/Selector.tsx +29 -2
  103. package/src/Slider/Slider.doc.mjs +11 -7
  104. package/src/Slider/Slider.tsx +539 -132
  105. package/src/Slider/sliderTouchController.ts +1209 -0
  106. package/src/Slider/useSliderTouch.ts +78 -0
  107. package/src/Typeahead/BaseTypeahead.tsx +16 -8
  108. package/src/gestures/GestureArena.ts +1 -0
  109. package/src/gestures/GestureHost.ts +765 -5
  110. package/src/gestures/GestureRecognizer.ts +28 -0
  111. package/src/gestures/PanGestureRecognizer.ts +8 -0
  112. package/src/gestures/PinchGestureRecognizer.ts +16 -1
  113. package/src/gestures/README.md +63 -0
  114. package/src/gestures/ScreenEdgePanGestureRecognizer.ts +7 -5
  115. package/src/gestures/nativeScroll.ts +49 -9
  116. package/src/gestures/recognizerInternals.ts +9 -2
  117. package/src/gestures/types.ts +12 -0
  118. package/src/gestures/wheelSegmenter.ts +114 -0
  119. package/src/motion/MEASUREMENTS.md +8 -1
  120. package/src/motion/README.md +3 -2
  121. package/src/motion/animator.ts +193 -25
  122. package/src/motion/paging.ts +138 -0
  123. package/src/motion/uikit/index.ts +2 -1
  124. package/src/motion/uikit/paging.ts +162 -0
  125. package/src/motion/uikit/scroll.ts +2 -18
  126. package/src/motion/uikit/slider.ts +105 -0
  127. package/src/motion/uikit/springs.ts +37 -2
  128. package/dist/motion/selectPage.d.ts +0 -26
  129. package/dist/motion/selectPage.d.ts.map +0 -1
  130. package/dist/motion/selectPage.js +0 -55
  131. package/src/motion/selectPage.ts +0 -55
@@ -4,8 +4,8 @@
4
4
 
5
5
  /**
6
6
  * @file Slider.tsx
7
- * @input Uses React, useId, useRef, useCallback, useEffect, Field, Tooltip, useTooltip, VisuallyHidden
8
- * @output Exports Slider and its props; unfilled marks share the track token; modifier-only key presses do not restore the thumb focus ring; a drag belongs to the pointer that started it; two coincident range thumbs separate toward the press or the first movement
7
+ * @input Uses React, useId, useRef, useCallback, useEffect, Field, useTooltip, VisuallyHidden, the touch presentation resolver and useSliderTouch (UISlider's touch tracking)
8
+ * @output Exports Slider and its props. Unfilled marks share the track token. Modifier-only key presses do not restore the thumb focus ring. A drag belongs to the pointer that started it. The thumbs and the filled range travel by `translate` in container units, so moving them never lays out. Padding on the control moves no part. The value tooltip anchors to a box that layout places at the value. A drag on a thumb is a pan for every pointer, tracked as UISlider does. Under a coarse primary pointer, each thumb's `::before` is its touch target. A finger's tap on the track sets the value on its lift. A mouse press on the track keeps the pointer handlers. Two coincident range thumbs separate toward the press or the first movement
9
9
  * @position Core implementation; consumed by index.ts, tested by Slider.test.tsx
10
10
  *
11
11
  * SYNC: When modified, update these files to stay in sync:
@@ -16,11 +16,11 @@
16
16
  * - /packages/cli/assets/templates/blocks/components/Slider/ (showcase blocks)
17
17
  */
18
18
  import { useEffect, useId, useMemo, useRef, useState, useCallback } from 'react';
19
+ import { flushSync } from 'react-dom';
19
20
  import * as stylex from '@stylexjs/stylex';
20
21
  import "../theme/tokens.stylex.js";
21
22
  import { colorVars, spacingVars, radiusVars, durationVars, easeVars, typographyVars, typeScaleVars } from "../theme/tokens.stylex.js";
22
23
  import { Field } from "../Field/Field.js";
23
- import { Tooltip } from "../Tooltip/Tooltip.js";
24
24
  import { useTooltip } from "../Tooltip/index.js";
25
25
  import { VisuallyHidden } from "../VisuallyHidden/index.js";
26
26
  import { mergeProps, rtlStyles } from "../utils/index.js";
@@ -30,11 +30,15 @@ import { useInteractionModalityTracking } from "../utils/interactionModality.js"
30
30
  import { isRtlElement } from "../hooks/isRtlElement.js";
31
31
  import { useFieldInputStyling } from "../Field/fieldParts.js";
32
32
  import { themeProps } from "../utils/themeProps.js";
33
+ import { isPrimaryClick } from "../utils/primaryClick.js";
34
+ import { useTouchPresentation } from "../utils/touchPresentation.js";
35
+ import { slider as UIKIT_SLIDER } from "../motion/uikit/slider.js";
36
+ import { useSliderTouch } from "./useSliderTouch.js";
33
37
  import { useMergedRefs } from "../hooks/useMergedRefs.js";
34
38
  import { useResolvedRequired } from "../hooks/useResolvedRequired.js";
35
39
  import { registerEnglishFallback, useComponentTranslator } from "../i18n/useComponentTranslator.js";
36
40
  import english from "../i18n/generated-english/Slider/Slider.generated.js";
37
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
41
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
38
42
  registerEnglishFallback(english);
39
43
  // =============================================================================
40
44
  // Types
@@ -47,6 +51,30 @@ registerEnglishFallback(english);
47
51
  const TRACK_SIZE = 4;
48
52
  const THUMB_SIZE = 20;
49
53
 
54
+ /**
55
+ * Where a thumb sits, as its progress along the travel (0 at `min`, 1 at
56
+ * `max`), written inline on the thumb. CSS computes its translate from it
57
+ * against the control's size, in container units. So moving the thumb
58
+ * restyles and composites but never lays out, and the server renders the
59
+ * right place. A value tooltip or text display still lays out as its text
60
+ * changes.
61
+ */
62
+ const THUMB_PROGRESS = '--_slider-progress';
63
+ /** A thumb's touch-target insets under a finger, which its `::before` draws. */
64
+ const HIT_INSET = {
65
+ top: '--_slider-hit-top',
66
+ bottom: '--_slider-hit-bottom',
67
+ inlineStart: '--_slider-hit-start',
68
+ inlineEnd: '--_slider-hit-end'
69
+ };
70
+ /** Half a thumb: the inset of the thumb's travel at each end of the track. */
71
+ const THUMB_INSET_PX = THUMB_SIZE / 2;
72
+ /** The filled range's ends as progress, written inline on the fill parts. */
73
+ const FILL_START = '--_slider-fill-start';
74
+ const FILL_END = '--_slider-fill-end';
75
+
76
+ // The private progress properties a thumb and the fill parts publish inline.
77
+
50
78
  // =============================================================================
51
79
  // Styles
52
80
  // =============================================================================
@@ -57,7 +85,8 @@ const styles = {
57
85
  k1xSpc: "x78zum5",
58
86
  kGNEyG: "x6s0dn4",
59
87
  kzQI83: "x1iyjqo2",
60
- kFalU9: "x5ve5x3",
88
+ kFalU9: "x5ve5x3 x1xekgvp",
89
+ kO7zv1: "x1winvzj",
61
90
  kfSwDN: "x87ps6o",
62
91
  kHBbk8: "xc8icb0",
63
92
  $$css: true
@@ -103,29 +132,89 @@ const styles = {
103
132
  kzqmXN: "x51ohtg",
104
133
  $$css: true
105
134
  },
106
- filledTrack: {
135
+ fillClip: {
107
136
  kVAEAm: "x10l6tqk",
108
- kWkggS: "x1ewilqj",
137
+ kVQacm: "xb3r6kr",
109
138
  kaIpWk: "xjspbzw",
139
+ kfzvcC: "x47corl",
110
140
  $$css: true
111
141
  },
112
- filledTrackHorizontal: {
142
+ fillClipHorizontal: {
143
+ kLqNvP: "x1o0tod",
144
+ kt4wiu: "xtijo5x",
113
145
  kZKoxP: "xqu0tyb",
114
146
  k87sOh: "xwa60dl",
115
147
  k3aq6I: "x1cb1t30",
116
148
  $$css: true
117
149
  },
118
- filledTrackVertical: {
150
+ fillClipVertical: {
151
+ k87sOh: "x13vifvy",
152
+ krVfgx: "x1ey2m1c",
119
153
  kzqmXN: "x51ohtg",
120
154
  $$css: true
121
155
  },
156
+ fillMover: {
157
+ kVAEAm: "x10l6tqk",
158
+ kpwlN0: "x10a8y8t",
159
+ kVQacm: "xb3r6kr",
160
+ $$css: true
161
+ },
162
+ fillMoverRangeStartHorizontal: {
163
+ krdFHd: "x19415el",
164
+ kVL7Gh: "x1wc3881",
165
+ $$css: true
166
+ },
167
+ fillMoverRangeStartVertical: {
168
+ kVL7Gh: "x1wc3881",
169
+ kT0f0o: "x1dgc8on",
170
+ $$css: true
171
+ },
172
+ fillMoverRangeHorizontal: {
173
+ kIY38u: "xwj4vw1 x8re4u",
174
+ $$css: true
175
+ },
176
+ fillMoverRangeVertical: {
177
+ kIY38u: "x1p0n03b",
178
+ $$css: true
179
+ },
180
+ filledTrack: {
181
+ kVAEAm: "x10l6tqk",
182
+ kpwlN0: "x10a8y8t",
183
+ kWkggS: "x1ewilqj",
184
+ $$css: true
185
+ },
186
+ filledTrackHorizontal: {
187
+ kfmiAY: "x8agd5o",
188
+ kT0f0o: "x1dgc8on",
189
+ $$css: true
190
+ },
191
+ filledTrackVertical: {
192
+ krdFHd: "x19415el",
193
+ kfmiAY: "x8agd5o",
194
+ $$css: true
195
+ },
196
+ filledTrackSingleHorizontal: {
197
+ kIY38u: "x80n2h1 x107x6cy",
198
+ $$css: true
199
+ },
200
+ filledTrackSingleVertical: {
201
+ kIY38u: "x1hj1i93",
202
+ $$css: true
203
+ },
204
+ filledTrackRangeHorizontal: {
205
+ kIY38u: "xhvvmt5 x1pq48qv",
206
+ $$css: true
207
+ },
208
+ filledTrackRangeVertical: {
209
+ kIY38u: "x18n2uh1",
210
+ $$css: true
211
+ },
122
212
  thumb: {
123
213
  kVAEAm: "x10l6tqk",
124
214
  kzqmXN: "xw4jnvo",
125
215
  kZKoxP: "x1qx5ct2",
126
216
  kaIpWk: "xjspbzw",
127
217
  kWkggS: "x1ewilqj",
128
- k3aq6I: "x11lhmoz",
129
218
  k1ekBW: "x106061f",
130
219
  kIyJzY: "xuedmi6 x12w9bfk",
131
220
  kAMwcw: "xlr8y92",
@@ -135,8 +224,24 @@ const styles = {
135
224
  $$css: true
136
225
  },
137
226
  thumbHorizontal: {
138
- k87sOh: "xwa60dl",
139
- k3aq6I: "x11lhmoz xoffwj3",
227
+ k87sOh: "xsf42hv",
228
+ kLqNvP: "x1o0tod",
229
+ kIY38u: "x1kfzoes xis8qvo",
230
+ $$css: true
231
+ },
232
+ thumbHit: {
233
+ kgeoSG: "x1cpjm7i",
234
+ kEoFBp: "x1hmns74",
235
+ k96MEf: "xw5uirw",
236
+ k5Ofw7: "x17wid7s",
237
+ kSJ0CW: "xu2wnl0",
238
+ koaPKo: "x1s13ptm",
239
+ $$css: true
240
+ },
241
+ thumbVertical: {
242
+ krVfgx: "x1ey2m1c",
243
+ kLqNvP: "x94kg92",
244
+ kIY38u: "xqt2767",
140
245
  $$css: true
141
246
  },
142
247
  thumbHover: {
@@ -213,12 +318,6 @@ function insetPosition(percent) {
213
318
  return cssLength(percent, THUMB_INSET - percent / 100 * THUMB_SIZE);
214
319
  }
215
320
 
216
- /** Distance between two inset positions; the inset itself cancels out. */
217
- function insetSpan(fromPercent, toPercent) {
218
- const delta = toPercent - fromPercent;
219
- return cssLength(delta, -(delta / 100) * THUMB_SIZE);
220
- }
221
-
222
321
  /** Inverse of `insetPosition`: an offset from the box start back to 0–1. */
223
322
  function travelFraction(offset, size) {
224
323
  const travel = size - THUMB_SIZE;
@@ -236,6 +335,41 @@ function cssLength(percent, px) {
236
335
  return `calc(${round(percent)}% ${px < 0 ? '-' : '+'} ${Math.abs(round(px))}px)`;
237
336
  }
238
337
 
338
+ /**
339
+ * A thumb's value tooltip: hover and focus on the thumb, position from a
340
+ * thumb-sized anchor that layout places at the value (styles.valueAnchor).
341
+ */
342
+ function SliderValueTooltip({
343
+ content,
344
+ placement,
345
+ isOpen,
346
+ anchorStyle,
347
+ thumbRef,
348
+ children
349
+ }) {
350
+ const tooltip = useTooltip({
351
+ placement,
352
+ delay: 0,
353
+ focusTrigger: 'always',
354
+ isOpen
355
+ });
356
+ const triggerRef = useMergedRefs(thumbRef, tooltip.interactionRef);
357
+ return /*#__PURE__*/_jsxs(_Fragment, {
358
+ children: [/*#__PURE__*/_jsx("span", {
359
+ "aria-hidden": "true",
360
+ ref: tooltip.positionRef,
361
+ ...mergeProps({
362
+ className: "x10l6tqk xw4jnvo x1qx5ct2 x47corl"
363
+ }, {
364
+ style: anchorStyle
365
+ })
366
+ }), children({
367
+ ref: triggerRef,
368
+ describedBy: tooltip.describedBy
369
+ }), tooltip.renderTooltip(content)]
370
+ });
371
+ }
372
+
239
373
  // =============================================================================
240
374
  // Component
241
375
  // =============================================================================
@@ -324,9 +458,19 @@ export function Slider({
324
458
  const statusMessageID = useId();
325
459
  const requiredID = useId();
326
460
  const trackRef = useRef(null);
461
+ const fillMoverRef = useRef(null);
462
+ const fillRef = useRef(null);
327
463
  const draggingThumbRef = useRef(null);
328
464
  const [draggingThumb, setDraggingThumb] = useState(null);
329
465
 
466
+ // The thumbs track as UISlider's for every pointer (useSliderTouch): a
467
+ // drag on a thumb is a pan. Their touch targets show only under a coarse
468
+ // primary pointer, after hydration: one touch model on every touch
469
+ // device. A mouse press on the track keeps the pointer
470
+ // handlers below.
471
+ const isTouchPresentation = useTouchPresentation().isTouch;
472
+ const hasTouchTargets = isTouchPresentation && !isDisabled;
473
+
330
474
  // A thumb is a div[role="slider"], and `handlePointerDown` focuses it from
331
475
  // script after preventDefault — which Chromium treats as focus-visible, so
332
476
  // dragging with a mouse drew the keyboard ring (measured: `:focus-visible`
@@ -346,7 +490,7 @@ export function Slider({
346
490
  }, []);
347
491
 
348
492
  // Disabled-reason tooltip. This is a *separate* useTooltip instance from the
349
- // per-thumb value bubble (the `<Tooltip>` component below): it anchors to the
493
+ // per-thumb value bubble (SliderValueTooltip below): it anchors to the
350
494
  // track container and fires on hover/focus of the whole control. Disabled
351
495
  // controls swallow pointer events, so the thumb stays perceivable via
352
496
  // aria-disabled while pointer/keyboard handlers early-return on isDisabled.
@@ -510,6 +654,25 @@ export function Slider({
510
654
  if (isDisabled) {
511
655
  return;
512
656
  }
657
+ // Only a primary button presses: a right click or a macOS ctrl-click
658
+ // is the platform's context request, and a pen's
659
+ // barrel button presses nothing either.
660
+ if (!isPrimaryClick(e)) {
661
+ return;
662
+ }
663
+ // A press on a thumb belongs to the slider's gestures (useSliderTouch),
664
+ // which pan it for every pointer. The thumb still takes focus from any
665
+ // press, as it did. A finger's or a pen's tap on the track sets the
666
+ // value when it lifts. So only a mouse press on the track goes on.
667
+ const thumb = e.target.closest('[role="slider"]');
668
+ if (thumb != null) {
669
+ thumb.focus();
670
+ setKeyboardFocusThumb(null);
671
+ return;
672
+ }
673
+ if (e.pointerType === 'touch' || e.pointerType === 'pen') {
674
+ return;
675
+ }
513
676
  e.preventDefault();
514
677
  if (draggingThumbRef.current !== null) {
515
678
  return;
@@ -626,6 +789,93 @@ export function Slider({
626
789
  }, [isDisabled, getValueFromPosition, updateValue]);
627
790
  const handlePointerUp = useCallback(e => endDrag(e.pointerId), [endDrag]);
628
791
 
792
+ // The touch presentation's value model (useSliderTouch reads it fresh).
793
+ const updateValueRef = useRef(updateValue);
794
+ updateValueRef.current = updateValue;
795
+ const touchModel = {
796
+ min,
797
+ max,
798
+ isRange,
799
+ isHorizontal,
800
+ isDisabled: isDisabled ?? false,
801
+ thumbSize: THUMB_SIZE,
802
+ values: () => valuesRef.current,
803
+ // The value updateValue would report for this thumb.
804
+ constrain: (index, raw) => {
805
+ const next = clamp(snapToStep(raw, min, step), min, max);
806
+ if (!isRange) {
807
+ return next;
808
+ }
809
+ const minGap = minStepsBetweenThumbs * step;
810
+ const [v0, v1] = valuesRef.current;
811
+ return index === 0 ? clamp(Math.min(next, v1 - minGap), min, max) : clamp(Math.max(next, v0 + minGap), min, max);
812
+ },
813
+ bounds: index => {
814
+ if (!isRange) {
815
+ return [min, max];
816
+ }
817
+ const minGap = minStepsBetweenThumbs * step;
818
+ const [v0, v1] = valuesRef.current;
819
+ return index === 0 ? [min, clamp(v1 - minGap, min, max)] : [clamp(v0 + minGap, min, max), max];
820
+ },
821
+ change: (index, next, from) => updateValueRef.current(index, next, true, from),
822
+ // A finger paints the thumb pressed and opens its value tooltip.
823
+ // `handlePointerDown` already focused it.
824
+ press: index => {
825
+ setDraggingThumb(index);
826
+ // When a thumb of this slider holds focus, a drag handed to the other
827
+ // thumb takes the focus along. Every press on a thumb focuses it.
828
+ const thumbs = trackRef.current?.querySelectorAll('[role="slider"]');
829
+ const active = trackRef.current?.ownerDocument.activeElement;
830
+ if (index != null && thumbs != null && active != null) {
831
+ const list = Array.from(thumbs);
832
+ if (list.includes(active) && list[index] !== active) {
833
+ list[index]?.focus();
834
+ }
835
+ }
836
+ },
837
+ // The touch interaction ended, in the handler that ended it: the lift, a
838
+ // cancel or the coast's last frame. The owner renders the last value
839
+ // now, from that same handler. Then the controller can hand the parts
840
+ // back to the stylesheet, and onChangeEnd carries the rendered value.
841
+ commit: (index, next) => {
842
+ // eslint-disable-next-line @eslint-react/dom-no-flush-sync -- the owner must render the interaction's last value before the parts return to the stylesheet and onChangeEnd reads it
843
+ flushSync(() => {
844
+ if (next !== null) {
845
+ updateValueRef.current(index, next);
846
+ }
847
+ setDraggingThumb(null);
848
+ });
849
+ },
850
+ reportEnd: () => fireChangeEnd(),
851
+ // A tap on the track, from useSliderTouch. It takes a mark's value on a
852
+ // mark, else the point along the travel. It moves the thumb a track
853
+ // press moves.
854
+ valueAt: (x, y) => {
855
+ const mark = trackRef.current?.ownerDocument.elementFromPoint?.(x, y)?.closest('[data-mark-value]');
856
+ return mark != null ? Number(mark.dataset.markValue) : getValueFromPosition(x, y);
857
+ },
858
+ thumbFor: value => getClosestThumb(value)
859
+ };
860
+ useSliderTouch({
861
+ isEnabled: !isDisabled,
862
+ model: touchModel,
863
+ thumbCount: values.length,
864
+ thumbsWrapped: valueDisplay === 'tooltip' && !showsDisabledMessage,
865
+ getParts: () => {
866
+ const control = trackRef.current;
867
+ if (control == null) {
868
+ return null;
869
+ }
870
+ return {
871
+ control,
872
+ thumbs: Array.from(control.querySelectorAll('[role="slider"]')),
873
+ fillMover: fillMoverRef.current,
874
+ fill: fillRef.current
875
+ };
876
+ }
877
+ });
878
+
629
879
  // Keyboard handler
630
880
  const handleKeyDown = useCallback((thumbIndex, e) => {
631
881
  if (isDisabled) {
@@ -706,14 +956,37 @@ export function Slider({
706
956
  };
707
957
 
708
958
  // Render a thumb
959
+ // Each thumb's touch target grows to UISlider's 48 pt around the thumb.
960
+ // A range's two targets never reach into each other: on the side facing
961
+ // the other thumb, the growth stops at half the gap. So a finger takes the
962
+ // nearer thumb. Container units measure the travel, so the gap needs no
963
+ // layout read.
964
+ const hitGrowth = (UIKIT_SLIDER.thumbHitTarget - THUMB_SIZE) / 2;
965
+ const thumbHitInsets = thumbIndex => {
966
+ const full = `${-hitGrowth}px`;
967
+ let facing = full;
968
+ if (isRange && max > min) {
969
+ const spread = (values[1] - values[0]) / (max - min);
970
+ const travel = isHorizontal ? '100cqi' : '100cqb';
971
+ facing = `calc(-1 * clamp(0px, (${spread} * (${travel} - ${THUMB_SIZE}px) - ${THUMB_SIZE}px) / 2, ${hitGrowth}px))`;
972
+ }
973
+ // Values grow toward the inline end, and upward on a vertical slider.
974
+ const towardLower = isRange && thumbIndex === 1 ? facing : full;
975
+ const towardUpper = isRange && thumbIndex === 0 ? facing : full;
976
+ const [top, bottom, inlineStart, inlineEnd] = isHorizontal ? [full, full, towardLower, towardUpper] : [towardUpper, towardLower, full, full];
977
+ return {
978
+ [HIT_INSET.top]: top,
979
+ [HIT_INSET.bottom]: bottom,
980
+ [HIT_INSET.inlineStart]: inlineStart,
981
+ [HIT_INSET.inlineEnd]: inlineEnd
982
+ };
983
+ };
709
984
  const renderThumb = thumbIndex => {
710
985
  const val = values[thumbIndex];
711
986
  const percent = getPercent(val, min, max);
712
- const positionStyle = isHorizontal ? {
713
- insetInlineStart: insetPosition(percent)
714
- } : {
715
- bottom: insetPosition(percent),
716
- left: '50%'
987
+ const positionStyle = {
988
+ [THUMB_PROGRESS]: percent / 100,
989
+ ...(hasTouchTargets ? thumbHitInsets(thumbIndex) : null)
717
990
  };
718
991
 
719
992
  // In range mode each thumb keeps a short individual name that composes
@@ -735,9 +1008,9 @@ export function Slider({
735
1008
  // than stacking two tooltips over the same thumb.
736
1009
  const useValueTooltip = valueDisplay === 'tooltip' && !showsDisabledMessage;
737
1010
  const tooltipPlacement = isHorizontal ? 'above' : 'start';
738
- const thumbElement = /*#__PURE__*/_jsx("div", {
1011
+ const thumbElement = trigger => /*#__PURE__*/_jsx("div", {
739
1012
  ...(!isRange ? rest : undefined),
740
- ref: !isRange ? ref : undefined,
1013
+ ref: trigger != null ? trigger.ref : !isRange ? ref : undefined,
741
1014
  id: !isRange ? id : undefined,
742
1015
  role: "slider"
743
1016
  // A disabled thumb keeps its Tab stop (spec:AST-085 FR1).
@@ -754,61 +1027,46 @@ export function Slider({
754
1027
  // A caller aria-labelledby names the control together with its label.
755
1028
  ,
756
1029
  "aria-labelledby": !isRange ? joinAriaIDs(labelID, ariaLabelledByProp) : undefined,
757
- "aria-describedby": [!isRange ? ariaDescribedByProp : undefined, ariaDescribedBy].filter(Boolean).join(' ') || undefined,
1030
+ "aria-describedby": [!isRange ? ariaDescribedByProp : undefined, ariaDescribedBy, trigger?.describedBy].filter(Boolean).join(' ') || undefined,
758
1031
  onKeyDown: after(e => handleKeyDown(thumbIndex, e), !isRange ? onKeyDown : undefined),
759
1032
  onFocus: after(e => handleThumbFocus(thumbIndex, e), !isRange ? onFocus : undefined),
760
1033
  onBlur: after(handleThumbBlur, !isRange ? onBlur : undefined),
761
1034
  ...mergeProps(themeProps('slider-thumb', {
762
1035
  orientation,
763
1036
  disabled: isDisabled ? 'disabled' : null
764
- }), stylex.props(styles.thumb, isHorizontal ? styles.thumbHorizontal : rtlStyles.centerInline('50%'), !isDisabled && styles.thumbHover, !isDisabled && draggingThumb === thumbIndex && styles.thumbPressed, keyboardFocusThumb === thumbIndex && focusOutlineStyles.focusVisible, isDisabled && styles.thumbDisabled), undefined, positionStyle)
1037
+ }), stylex.props(styles.thumb, isHorizontal ? styles.thumbHorizontal : styles.thumbVertical, !isDisabled && styles.thumbHover, !isDisabled && draggingThumb === thumbIndex && styles.thumbPressed, keyboardFocusThumb === thumbIndex && focusOutlineStyles.focusVisible, isDisabled && styles.thumbDisabled, hasTouchTargets && styles.thumbHit), undefined, positionStyle)
765
1038
  }, thumbIndex);
766
1039
  if (useValueTooltip) {
767
- return /*#__PURE__*/_jsx(Tooltip, {
1040
+ const anchorAt = `calc(${insetPosition(percent)} - ${THUMB_SIZE / 2}px)`;
1041
+ return /*#__PURE__*/_jsx(SliderValueTooltip, {
768
1042
  content: displayValue(val),
769
- placement: tooltipPlacement,
770
- delay: 0,
771
- focusTrigger: "always"
1043
+ placement: tooltipPlacement
772
1044
  // A drag keeps its own thumb's value showing and closes the other
773
1045
  // thumb's, which a drag handed over from it may have forced open.
774
1046
  ,
775
1047
  isOpen: draggingThumb === null ? undefined : draggingThumb === thumbIndex,
1048
+ anchorStyle: isHorizontal ? {
1049
+ insetInlineStart: anchorAt,
1050
+ top: `calc(50% - ${THUMB_SIZE / 2}px)`
1051
+ } : {
1052
+ bottom: anchorAt,
1053
+ insetInlineStart: `calc(50% - ${THUMB_SIZE / 2}px)`
1054
+ },
1055
+ thumbRef: !isRange ? ref : undefined,
776
1056
  children: thumbElement
777
1057
  }, thumbIndex);
778
1058
  }
779
- return thumbElement;
1059
+ return thumbElement(null);
780
1060
  };
781
1061
 
782
- // Filled track position — ends at the thumb centre, so it uses the same
783
- // inset mapping as the thumb.
784
- const filledStyle = (() => {
785
- if (isRange) {
786
- const [v0, v1] = values;
787
- const p0 = getPercent(v0, min, max);
788
- const p1 = getPercent(v1, min, max);
789
- if (isHorizontal) {
790
- return {
791
- insetInlineStart: insetPosition(p0),
792
- width: insetSpan(p0, p1)
793
- };
794
- }
795
- return {
796
- bottom: insetPosition(p0),
797
- height: insetSpan(p0, p1)
798
- };
799
- }
800
- const p = getPercent(values[0], min, max);
801
- if (isHorizontal) {
802
- return {
803
- insetInlineStart: '0%',
804
- width: insetPosition(p)
805
- };
806
- }
807
- return {
808
- bottom: '0%',
809
- height: insetPosition(p)
810
- };
811
- })();
1062
+ // The filled range's ends as progress: it ends at the thumb centers. So it
1063
+ // uses the same inset travel as the thumbs (see styles.fillClip).
1064
+ const fillStart = isRange ? getPercent(values[0], min, max) / 100 : 0;
1065
+ const fillEnd = getPercent(values[isRange ? 1 : 0], min, max) / 100;
1066
+ const fillStyle = {
1067
+ [FILL_START]: fillStart,
1068
+ [FILL_END]: fillEnd
1069
+ };
812
1070
 
813
1071
  // Text value display
814
1072
  const textDisplay = valueDisplay === 'text' ? /*#__PURE__*/_jsx("span", {
@@ -855,7 +1113,7 @@ export function Slider({
855
1113
  type: "hidden",
856
1114
  name: htmlName,
857
1115
  value: String(v)
858
- }, i === 0 ? 'start' : 'end')), /*#__PURE__*/_jsxs("div", {
1116
+ }, i === 0 ? 'start' : 'end')), /*#__PURE__*/_jsx("div", {
859
1117
  ...(isRange ? rest : undefined),
860
1118
  ...(isRange ? {
861
1119
  onKeyDown,
@@ -876,75 +1134,90 @@ export function Slider({
876
1134
  orientation,
877
1135
  disabled: isDisabled ? 'disabled' : null
878
1136
  }), stylex.props(styles.trackContainer, isHorizontal ? styles.trackContainerHorizontal : styles.trackContainerVertical, isDisabled && styles.trackContainerDisabled, controlXstyle), controlClassName, controlStyle),
879
- children: [/*#__PURE__*/_jsx("div", {
880
- "aria-hidden": "true",
881
- ...mergeProps(themeProps('slider-track', {
882
- orientation
883
- }), stylex.props(styles.track, isHorizontal ? styles.trackHorizontal : [styles.trackVertical, rtlStyles.centerInline('0px')]))
884
- }), /*#__PURE__*/_jsx("div", {
885
- "aria-hidden": "true",
886
- ...mergeProps(stylex.props(styles.filledTrack, isHorizontal ? styles.filledTrackHorizontal : [styles.filledTrackVertical, rtlStyles.centerInline('0px')]), {
887
- style: filledStyle
888
- })
889
- }), marks && /*#__PURE__*/_jsx("div", {
890
- "aria-hidden": "true",
1137
+ children: /*#__PURE__*/_jsxs("div", {
891
1138
  ...{
892
- 0: {
893
- className: "x10l6tqk x13vifvy x1ey2m1c xbudbmw"
894
- },
895
- 1: {
896
- className: "x10l6tqk x1o0tod xtijo5x xwa60dl"
897
- }
898
- }[!!isHorizontal << 0],
899
- children: marks.map(mark => {
900
- const percent = getPercent(mark.value, min, max);
901
- const markPos = isHorizontal ? {
902
- insetInlineStart: insetPosition(percent)
903
- } : {
904
- bottom: insetPosition(percent)
905
- };
906
- // Marks at or inside the filled region take the fill color:
907
- // at or behind the thumb in single mode, between the thumbs
908
- // in range mode.
909
- const isFilled = isRange ? mark.value >= values[0] && mark.value <= values[1] : mark.value <= values[0];
910
- return /*#__PURE__*/_jsxs("div", {
911
- children: [/*#__PURE__*/_jsx("div", {
912
- "data-testid": "slider-mark",
913
- "data-mark-value": mark.value,
914
- ...mergeProps({
915
- 0: {
916
- className: "x10l6tqk xdsb6cv xjspbzw x36qwtl x1xc55vz x1m9mm8y"
917
- },
918
- 2: {
919
- className: "x10l6tqk xdsb6cv xjspbzw xfo62xy xdk7pt x11lhmoz xoffwj3"
920
- },
921
- 1: {
922
- className: "x10l6tqk xjspbzw x36qwtl x1xc55vz x1m9mm8y x1ewilqj"
923
- },
924
- 3: {
925
- className: "x10l6tqk xjspbzw xfo62xy xdk7pt x11lhmoz xoffwj3 x1ewilqj"
926
- }
927
- }[!!isHorizontal << 1 | !!isFilled << 0], {
928
- style: markPos
1139
+ className: "x10l6tqk x10a8y8t xpqogu8"
1140
+ },
1141
+ children: [/*#__PURE__*/_jsx("div", {
1142
+ "aria-hidden": "true",
1143
+ ...mergeProps(themeProps('slider-track', {
1144
+ orientation
1145
+ }), stylex.props(styles.track, isHorizontal ? styles.trackHorizontal : [styles.trackVertical, rtlStyles.centerInline('0px')]))
1146
+ }), /*#__PURE__*/_jsx("div", {
1147
+ "aria-hidden": "true",
1148
+ ...stylex.props(styles.fillClip, isHorizontal ? styles.fillClipHorizontal : [styles.fillClipVertical, rtlStyles.centerInline('0px')]),
1149
+ children: /*#__PURE__*/_jsx("div", {
1150
+ ref: fillMoverRef,
1151
+ ...mergeProps(stylex.props(styles.fillMover, isRange && (isHorizontal ? [styles.fillMoverRangeHorizontal, styles.fillMoverRangeStartHorizontal] : [styles.fillMoverRangeVertical, styles.fillMoverRangeStartVertical])), {
1152
+ style: fillStyle
1153
+ }),
1154
+ children: /*#__PURE__*/_jsx("div", {
1155
+ ref: fillRef,
1156
+ ...mergeProps(stylex.props(styles.filledTrack, isHorizontal ? styles.filledTrackHorizontal : styles.filledTrackVertical, isRange ? isHorizontal ? styles.filledTrackRangeHorizontal : styles.filledTrackRangeVertical : isHorizontal ? styles.filledTrackSingleHorizontal : styles.filledTrackSingleVertical), {
1157
+ style: fillStyle
929
1158
  })
930
- }), mark.label && /*#__PURE__*/_jsx("span", {
931
- "data-testid": "slider-mark-label",
932
- "data-mark-value": mark.value,
933
- ...mergeProps({
934
- 0: {
935
- className: "x10l6tqk x9ynric x141an7d xv1l7n4 xuxw1ft x131p8rn x9p6ekw"
936
- },
937
- 1: {
938
- className: "x10l6tqk x9ynric x141an7d xv1l7n4 xuxw1ft xuuh30 x1nyx83j xuivejd"
939
- }
940
- }[!!isHorizontal << 0], {
941
- style: markPos
942
- }),
943
- children: mark.label
944
- })]
945
- }, mark.value);
946
- })
947
- }), values.map((_, i) => renderThumb(i))]
1159
+ })
1160
+ })
1161
+ }), marks && /*#__PURE__*/_jsx("div", {
1162
+ "aria-hidden": "true",
1163
+ ...{
1164
+ 0: {
1165
+ className: "x10l6tqk x13vifvy x1ey2m1c xbudbmw"
1166
+ },
1167
+ 1: {
1168
+ className: "x10l6tqk x1o0tod xtijo5x xwa60dl"
1169
+ }
1170
+ }[!!isHorizontal << 0],
1171
+ children: marks.map(mark => {
1172
+ const percent = getPercent(mark.value, min, max);
1173
+ const markPos = isHorizontal ? {
1174
+ insetInlineStart: insetPosition(percent)
1175
+ } : {
1176
+ bottom: insetPosition(percent)
1177
+ };
1178
+ // Marks at or inside the filled region take the fill color:
1179
+ // at or behind the thumb in single mode, between the thumbs
1180
+ // in range mode.
1181
+ const isFilled = isRange ? mark.value >= values[0] && mark.value <= values[1] : mark.value <= values[0];
1182
+ return /*#__PURE__*/_jsxs("div", {
1183
+ children: [/*#__PURE__*/_jsx("div", {
1184
+ "data-testid": "slider-mark",
1185
+ "data-mark-value": mark.value,
1186
+ ...mergeProps({
1187
+ 0: {
1188
+ className: "x10l6tqk xdsb6cv xjspbzw x36qwtl x1xc55vz x1m9mm8y"
1189
+ },
1190
+ 2: {
1191
+ className: "x10l6tqk xdsb6cv xjspbzw xfo62xy xdk7pt x11lhmoz xoffwj3"
1192
+ },
1193
+ 1: {
1194
+ className: "x10l6tqk xjspbzw x36qwtl x1xc55vz x1m9mm8y x1ewilqj"
1195
+ },
1196
+ 3: {
1197
+ className: "x10l6tqk xjspbzw xfo62xy xdk7pt x11lhmoz xoffwj3 x1ewilqj"
1198
+ }
1199
+ }[!!isHorizontal << 1 | !!isFilled << 0], {
1200
+ style: markPos
1201
+ })
1202
+ }), mark.label && /*#__PURE__*/_jsx("span", {
1203
+ "data-testid": "slider-mark-label",
1204
+ "data-mark-value": mark.value,
1205
+ ...mergeProps({
1206
+ 0: {
1207
+ className: "x10l6tqk x9ynric x141an7d xv1l7n4 xuxw1ft x131p8rn x9p6ekw"
1208
+ },
1209
+ 1: {
1210
+ className: "x10l6tqk x9ynric x141an7d xv1l7n4 xuxw1ft xuuh30 x1nyx83j xuivejd"
1211
+ }
1212
+ }[!!isHorizontal << 0], {
1213
+ style: markPos
1214
+ }),
1215
+ children: mark.label
1216
+ })]
1217
+ }, mark.value);
1218
+ })
1219
+ }), values.map((_, i) => renderThumb(i))]
1220
+ })
948
1221
  }), textDisplay]
949
1222
  }), conveysRequired && /*#__PURE__*/_jsx(VisuallyHidden, {
950
1223
  id: requiredID,