@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
@@ -4,7 +4,7 @@
4
4
 
5
5
  /**
6
6
  * @file ProgressBar.tsx
7
- * @input Uses React, useId, stylex, color/spacing/radius/transition tokens
7
+ * @input Uses React, useId, stylex, tokens and ProgressBarTouch
8
8
  * @output Exports ProgressBar component, ProgressBarProps, ProgressBarVariant types
9
9
  * @position Core implementation; consumed by index.ts
10
10
  *
@@ -35,6 +35,7 @@ import type {BaseProps} from '../BaseProps';
35
35
  import {themeProps} from '../utils/themeProps';
36
36
  import {VisuallyHidden} from '../VisuallyHidden';
37
37
  import type {ProgressBarVariantMap} from './index';
38
+ import {useProgressBarTouch} from './ProgressBarTouch';
38
39
 
39
40
  // Every mark is wrapped in a Tooltip (marks always carry a label), so load it
40
41
  // lazily: a ProgressBar with no marks never bundles the Tooltip chunk. While it
@@ -249,10 +250,26 @@ const styles = stylex.create({
249
250
  // no transform), as before: the track's clip would cut the CanvasText
250
251
  // outline's start cap off a slid fill, and that outline is what keeps the
251
252
  // fill's shape visible there (architecture:theme-tokens INV11).
253
+ //
254
+ // Under a finger (TOUCH_QUERY, on every touch device), the fill moves as
255
+ // UIProgressView's setProgress(_:animated: true) does. It moves linearly
256
+ // from wherever it is drawn. Its duration is the `progress.fillTrackMs` role
257
+ // times the share of the track the value moved (`--_progress-bar-move`).
258
+ // Its curve is the `progress.curves.fill` role (`--_progress-bar-curve`).
259
+ // The touch layer (ProgressBarTouch) writes both, and this rule scales the
260
+ // duration by the theme's tempo. Above zero, the fill never draws shorter
261
+ // than the touch minimum (`--_progress-bar-min`, 16px on the 8px track). At
262
+ // zero it paints nothing (`fillEmpty`). Under reduced motion it lands at
263
+ // once, as everywhere (below). A mouse or trackpad keeps the theme's timing.
252
264
  fill: {
253
265
  width: {
254
266
  default: '100%',
255
- '@media (forced-colors: active)': 'var(--_progress-bar-fill, 0%)',
267
+ '@media (forced-colors: active)': {
268
+ default: 'var(--_progress-bar-fill, 0%)',
269
+ // Under a finger: the touch minimum and the flip offset (below).
270
+ '@media (pointer: coarse)':
271
+ 'calc(max(var(--_progress-bar-fill, 0%), var(--_progress-bar-min, 0px)) + var(--_progress-bar-flip, 0) * 0.01px)',
272
+ },
256
273
  },
257
274
  height: '100%',
258
275
  // Forced colors replace status fills with Canvas; this boundary keeps the
@@ -271,6 +288,19 @@ const styles = stylex.create({
271
288
  default: 'translateX(calc(100% - var(--_progress-bar-fill, 0%)))',
272
289
  '@media (forced-colors: active)': 'none',
273
290
  },
291
+ '@media (pointer: coarse)': {
292
+ // `--_progress-bar-flip` steps 0, 1, 2, 0… on every change. It moves
293
+ // the fill by that many hundredths of a pixel. So a change back to the
294
+ // value it just left is never the running transition's own start
295
+ // value. CSS shortens a transition that reverses to its start, which
296
+ // would halve a return mid-flight (10 → 90 → 10). It takes three
297
+ // values because two would repeat on that return.
298
+ default:
299
+ 'translateX(calc(max(var(--_progress-bar-fill, 0%), var(--_progress-bar-min, 0px)) - 100% + var(--_progress-bar-flip, 0) * 0.01px))',
300
+ ':dir(rtl)':
301
+ 'translateX(calc(min(calc(100% - var(--_progress-bar-fill, 0%)), calc(100% - var(--_progress-bar-min, 0px))) - var(--_progress-bar-flip, 0) * 0.01px))',
302
+ },
303
+ // After the touch key, so StyleX negates that key under forced colors.
274
304
  '@media (forced-colors: active)': 'none',
275
305
  },
276
306
  willChange: {
@@ -281,17 +311,36 @@ const styles = stylex.create({
281
311
  default: 'transform',
282
312
  '@media (forced-colors: active)': 'width',
283
313
  },
284
- // Under reduced motion a new value lands at once, on every pointer: the
285
- // fill never travels (design:reduced-motion DR1). The keys are the shared
286
- // reduced-motion queries, a fine pointer's and a coarse pointer's, spelled
287
- // as the record's other transitions spell them.
314
+ // Under reduced motion a new value lands at once, with a mouse as with a
315
+ // finger: the fill never travels. The keys are the shared reduced-motion
316
+ // queries, as the other transitions spell them. The coarse key comes after
317
+ // the touch key, so it wins there.
288
318
  transitionDuration: {
289
319
  default: durationVars['--duration-medium'],
320
+ // The touch layer's move (the `progress.fillTrackMs` role times the
321
+ // share moved), scaled by the theme's tempo.
322
+ '@media (pointer: coarse)': {
323
+ default:
324
+ 'calc(var(--_progress-bar-move, 0ms) * var(--_astryx-motion-tempo, 1))',
325
+ // Under forced colors the fill is sized by `width` instead. Touch
326
+ // motion never animates a layout property, so with a finger the new
327
+ // size lands at once there.
328
+ '@media (forced-colors: active)': '0s',
329
+ },
290
330
  '@media (prefers-reduced-motion: reduce) and (not (pointer: coarse))':
291
331
  '0s',
292
332
  '@media (prefers-reduced-motion: reduce) and (pointer: coarse)': '0s',
293
333
  },
294
- transitionTimingFunction: easeVars['--ease-standard'],
334
+ transitionTimingFunction: {
335
+ default: easeVars['--ease-standard'],
336
+ '@media (pointer: coarse)': 'var(--_progress-bar-curve, linear)',
337
+ },
338
+ },
339
+ fillEmpty: {
340
+ opacity: {
341
+ default: null,
342
+ '@media (pointer: coarse)': 0,
343
+ },
295
344
  },
296
345
  indeterminateFill: {
297
346
  height: '100%',
@@ -428,7 +477,15 @@ const markOnTrackStyles = stylex.create({
428
477
  // `CSSProperties` has no index signature for custom properties, so the fill's
429
478
  // inline style — which publishes the filled share as the private
430
479
  // `--_progress-bar-fill` — needs this augmentation to typecheck.
431
- type FillStyle = CSSProperties & Record<'--_progress-bar-fill', string>;
480
+ type FillStyle = CSSProperties &
481
+ Record<
482
+ | '--_progress-bar-fill'
483
+ | '--_progress-bar-move'
484
+ | '--_progress-bar-curve'
485
+ | '--_progress-bar-flip'
486
+ | '--_progress-bar-min',
487
+ string
488
+ >;
432
489
 
433
490
  function defaultFormatValueLabel(value: number, max: number): string {
434
491
  const pct = max > 0 ? Math.round((value / max) * 100) : 0;
@@ -509,7 +566,16 @@ export function ProgressBar({
509
566
  const showValueLabel = hasValueLabel && !isIndeterminate;
510
567
 
511
568
  const fillVariant = isDisabled ? 'disabled' : variant;
512
- const fillStyle: FillStyle = {'--_progress-bar-fill': `${percentage}%`};
569
+ // Under a finger the fill moves as UIProgressView's does and draws at least
570
+ // 16px; what that needs from script is the touch layer's (ProgressBarTouch).
571
+ const touch = useProgressBarTouch(
572
+ percentage,
573
+ !isIndeterminate && (marks?.length ?? 0) > 0,
574
+ );
575
+ const fillStyle: FillStyle = {
576
+ '--_progress-bar-fill': `${percentage}%`,
577
+ ...touch.fillVars,
578
+ };
513
579
 
514
580
  // Marks make no sense without a determinate value, so they are only drawn
515
581
  // in determinate mode. Non-finite mark values are dropped; the rest are
@@ -532,7 +598,7 @@ export function ProgressBar({
532
598
  value: mark.value,
533
599
  label: mark.label,
534
600
  pct,
535
- isOnFill: percentage > 0 && pct <= percentage,
601
+ isOnFill: percentage > 0 && pct <= touch.drawnPercentage,
536
602
  };
537
603
  })
538
604
  : [];
@@ -582,6 +648,7 @@ export function ProgressBar({
582
648
  aria-valuemax={isIndeterminate ? undefined : safeMax}
583
649
  aria-labelledby={labelId}
584
650
  aria-valuetext={isIndeterminate ? undefined : valueText}
651
+ ref={touch.trackRef}
585
652
  {...mergeProps(
586
653
  themeProps('progress-bar-track'),
587
654
  stylex.props(styles.track),
@@ -600,7 +667,11 @@ export function ProgressBar({
600
667
  <div
601
668
  {...mergeProps(
602
669
  themeProps('progress-bar-fill', {variant: fillVariant}),
603
- stylex.props(styles.fill, variantStyles[fillVariant]),
670
+ stylex.props(
671
+ styles.fill,
672
+ percentage === 0 && styles.fillEmpty,
673
+ variantStyles[fillVariant],
674
+ ),
604
675
  )}
605
676
  style={fillStyle}
606
677
  />
@@ -0,0 +1,109 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file ProgressBarTouch.ts
7
+ * @input Uses React, the touch presentation resolver, the shared resize
8
+ * observer, and the progress table's roles (through resolveMotion)
9
+ * @output useProgressBarTouch: the fill's touch variables (move length,
10
+ * curve, return offset, minimum length) and, under a finger with marks,
11
+ * the share of the track the fill draws
12
+ * @position ProgressBar's touch layer, imported only by ProgressBar.tsx
13
+ *
14
+ * SYNC: When modified, update /packages/core/src/ProgressBar/ProgressBar.tsx
15
+ * (its touch styles read these variables and the 16px minimum) and
16
+ * /packages/core/src/motion/uikit/progressBar.ts.
17
+ */
18
+
19
+ import {useEffect, useRef, useState, type RefObject} from 'react';
20
+ import {formatMotionRoleValue} from '../motion/roles';
21
+ import {resolveMotion} from '../motion/resolveMotion';
22
+ import {progressBar} from '../motion/uikit/progressBar';
23
+ import {observeResize} from '../utils/sharedResizeObserver';
24
+ import {useTouchPresentation} from '../utils/touchPresentation';
25
+
26
+ /** The track's height. */
27
+ const TRACK_HEIGHT_PX = 8;
28
+
29
+ /**
30
+ * Under a finger the fill never draws shorter than this above zero:
31
+ * UIProgressView's minimum, two track heights, on ProgressBar's 8px track.
32
+ */
33
+ export const TOUCH_MIN_FILL_PX =
34
+ progressBar.progressView.minimumFillHeights * TRACK_HEIGHT_PX;
35
+
36
+ export interface ProgressBarTouch {
37
+ /** The fill's touch timing variables, set inline on the fill. */
38
+ fillVars: Record<
39
+ | '--_progress-bar-move'
40
+ | '--_progress-bar-curve'
41
+ | '--_progress-bar-flip'
42
+ | '--_progress-bar-min',
43
+ string
44
+ >;
45
+ /** The track, measured under a finger when the bar has marks. */
46
+ trackRef: RefObject<HTMLDivElement | null>;
47
+ /** The share of the track the fill draws (0–100), for placing marks. */
48
+ drawnPercentage: number;
49
+ }
50
+
51
+ export function useProgressBarTouch(
52
+ percentage: number,
53
+ hasMarks: boolean,
54
+ ): ProgressBarTouch {
55
+ // The touch fill's duration comes from how far the value moved since the
56
+ // last value, the model change that UIKit measures. It holds until the next
57
+ // change, so a re-render with the same value keeps it.
58
+ const [motion, setMotion] = useState({percentage, change: 0, flip: 0});
59
+ const isNewValue = motion.percentage !== percentage;
60
+ // The share of the track the value moved (0–1). `flip` steps through three
61
+ // values on every change (see the fill's touch transform).
62
+ const change = isNewValue
63
+ ? Math.abs(percentage - motion.percentage) / 100
64
+ : motion.change;
65
+ const flip = isNewValue ? (motion.flip + 1) % 3 : motion.flip;
66
+ if (isNewValue) {
67
+ setMotion({percentage, change, flip});
68
+ }
69
+
70
+ // Under a finger the fill draws at least TOUCH_MIN_FILL_PX long, so a mark
71
+ // it covers there sits on the fill. That length is a share of the track's
72
+ // width, which only layout knows. So the track is measured, but only under
73
+ // a finger and only when the bar has marks.
74
+ const {isTouch} = useTouchPresentation();
75
+ const watchesTrack = isTouch && hasMarks;
76
+ const trackRef = useRef<HTMLDivElement | null>(null);
77
+ const [trackWidth, setTrackWidth] = useState(0);
78
+ useEffect(() => {
79
+ const element = trackRef.current;
80
+ if (!watchesTrack || element == null) {
81
+ return;
82
+ }
83
+ // The shared observer; its first call (on registration) carries no
84
+ // contentRect, so that one reads the box.
85
+ return observeResize(element, entry => {
86
+ setTrackWidth(
87
+ entry.contentRect?.width ?? element.getBoundingClientRect().width,
88
+ );
89
+ });
90
+ }, [watchesTrack]);
91
+ const drawnPercentage =
92
+ watchesTrack && percentage > 0 && trackWidth > 0
93
+ ? Math.max(percentage, (TOUCH_MIN_FILL_PX / trackWidth) * 100)
94
+ : percentage;
95
+
96
+ // CSS applies the theme's tempo to this duration.
97
+ const fillTrackMs = resolveMotion(null, progressBar, 'fillTrackMs');
98
+ const curve = resolveMotion(null, progressBar.curves.fill);
99
+ return {
100
+ fillVars: {
101
+ '--_progress-bar-move': `${Math.round(change * fillTrackMs * 1000) / 1000}ms`,
102
+ '--_progress-bar-curve': formatMotionRoleValue('curve', curve),
103
+ '--_progress-bar-flip': String(flip),
104
+ '--_progress-bar-min': `${TOUCH_MIN_FILL_PX}px`,
105
+ },
106
+ trackRef,
107
+ drawnPercentage,
108
+ };
109
+ }
@@ -46,6 +46,7 @@ import {
46
46
  } from '../Field';
47
47
  import {Divider} from '../Divider';
48
48
  import {layerAnimations} from '../Layer/layerAnimations.stylex';
49
+ import {reducedMotionFadeConsts} from '../motion/reducedMotion.stylex';
49
50
  import {useKeepLayerOpenProps, type LayerPlacement} from '../Layer/useLayer';
50
51
  import {InternalInputClearButton} from '../Field/InputClearButton';
51
52
  import {Spinner} from '../Spinner';
@@ -113,6 +114,14 @@ import english from '../i18n/generated-english/Selector/Selector.generated';
113
114
 
114
115
  registerEnglishFallback(english);
115
116
 
117
+ /**
118
+ * A guess, confirmed by the open measure: more options than this overflow
119
+ * the popover listbox's 300 px block size even at 24 px rows, under the
120
+ * smallest default row (28 px). A wrong guess (a theme with shorter rows, a
121
+ * taller bottom sheet) costs only the measure's correction.
122
+ */
123
+ const MOST_ROWS_THAT_FIT = 12;
124
+
116
125
  const styles = stylex.create({
117
126
  // Trigger container — the enhanced click target wrapping the combobox button and clear button as siblings
118
127
  triggerContainer: {
@@ -350,7 +359,21 @@ const styles = stylex.create({
350
359
  paddingBlock: spacingVars['--spacing-1'],
351
360
  paddingInline: spacingVars['--spacing-1'],
352
361
  opacity: 1,
353
- transition: `opacity ${durationVars['--duration-fast']}`,
362
+ // Under Reduce Motion the list appears at once on a fine pointer, and
363
+ // takes the shared cross-fade on a coarse one.
364
+ transitionProperty: 'opacity',
365
+ transitionDuration: {
366
+ default: durationVars['--duration-fast'],
367
+ '@media (prefers-reduced-motion: reduce) and (pointer: coarse)':
368
+ reducedMotionFadeConsts.duration,
369
+ '@media (prefers-reduced-motion: reduce) and (not (pointer: coarse))':
370
+ '0s',
371
+ },
372
+ transitionTimingFunction: {
373
+ default: 'ease',
374
+ '@media (prefers-reduced-motion: reduce) and (pointer: coarse)':
375
+ reducedMotionFadeConsts.easing,
376
+ },
354
377
  },
355
378
  dropdownInput: {
356
379
  // The input trigger's text inset includes its border. Mirror that extra
@@ -376,7 +399,8 @@ const styles = stylex.create({
376
399
  },
377
400
  dropdownHidden: {
378
401
  opacity: 0,
379
- transition: 'none',
402
+ // A longhand: the shorthand would lose to the list's longhands.
403
+ transitionProperty: 'none',
380
404
  },
381
405
 
382
406
  // Popover container (for anchor positioning)
@@ -1407,10 +1431,13 @@ export function Selector<T extends SelectorOptionType>(
1407
1431
  return match == null ? -1 : Number(match[1]);
1408
1432
  }, []);
1409
1433
  // Re-measured when the option set changes; the count is the dependency.
1434
+ // A list longer than the most rows that can fit cannot fit, so it starts
1435
+ // as a scroll container and the open measure only confirms it.
1410
1436
  const listboxHasOverflow = useMenuOverflow(
1411
1437
  listboxRef,
1412
1438
  filteredItems.length,
1413
1439
  surface.isOpen,
1440
+ filteredItems.length > MOST_ROWS_THAT_FIT,
1414
1441
  );
1415
1442
  const listboxPress = useMenuPress({
1416
1443
  menuRef: listboxRef,
@@ -237,12 +237,14 @@ export const docs = {
237
237
  {
238
238
  name: 'containerClassName',
239
239
  type: 'string',
240
- description: 'Class name for the field container. Same targeting as `containerXstyle`.',
240
+ description:
241
+ 'Class name for the field container. Same targeting as `containerXstyle`.',
241
242
  },
242
243
  {
243
244
  name: 'containerStyle',
244
245
  type: 'CSSProperties',
245
- description: 'Inline styles for the field container. Same targeting as `containerXstyle`.',
246
+ description:
247
+ 'Inline styles for the field container. Same targeting as `containerXstyle`.',
246
248
  },
247
249
  {
248
250
  name: 'controlXstyle',
@@ -295,7 +297,7 @@ export const docs = {
295
297
  ],
296
298
  anatomy,
297
299
  description:
298
- 'A draggable control for selecting a numeric value or range within defined bounds. Supports single value and range selection, tick marks, custom value formatting, and vertical orientation. Use it when users need to explore a continuous range, such as volume, price, or percentage.',
300
+ 'A draggable control for selecting a numeric value or range within defined bounds. Supports single value and range selection, tick marks, custom value formatting, and vertical orientation. Use it when users need to explore a continuous range, such as volume, price, or percentage. Under a finger, on any touch device, it tracks as UISlider does: only the thumb takes a drag, through a target larger than it, the value follows the finger’s travel rather than jumping to it, a release coasts on before `onChangeEnd`, a swipe from the track scrolls the page, and a tap on the track or a mark sets the value when the finger lifts (the thumb springs there). A mouse drag on a thumb tracks the same way, past the same 10 px in any direction, with no lift or coast; a mouse press on the track moves the thumb there, and only a primary click presses. A pen drags as a finger does, claiming only along the track, and its drag ends at the release with no coast. A wheel or a trackpad scroll over the slider goes to whatever scrolls under it.',
299
301
  bestPractices: [
300
302
  {
301
303
  guidance: true,
@@ -508,7 +510,7 @@ export const docsZh = {
508
510
  usage: {
509
511
  anatomy,
510
512
  description:
511
- 'A draggable control for selecting a numeric value or range within defined bounds. Supports single value and range selection, tick marks, custom value formatting, and vertical orientation. Use it when users need to explore a continuous range, such as volume, price, or percentage.',
513
+ 'A draggable control for selecting a numeric value or range within defined bounds. Supports single value and range selection, tick marks, custom value formatting, and vertical orientation. Use it when users need to explore a continuous range, such as volume, price, or percentage. Under a finger, on any touch device, it tracks as UISlider does: only the thumb takes a drag, through a target larger than it, the value follows the finger’s travel rather than jumping to it, a release coasts on before `onChangeEnd`, a swipe from the track scrolls the page, and a tap on the track or a mark sets the value when the finger lifts (the thumb springs there). A mouse drag on a thumb tracks the same way, past the same 10 px in any direction, with no lift or coast; a mouse press on the track moves the thumb there, and only a primary click presses. A pen drags as a finger does, claiming only along the track, and its drag ends at the release with no coast. A wheel or a trackpad scroll over the slider goes to whatever scrolls under it.',
512
514
  bestPractices: [
513
515
  {
514
516
  guidance: true,
@@ -544,7 +546,7 @@ export const docsDense = {
544
546
  usage: {
545
547
  anatomy,
546
548
  description:
547
- 'A draggable control for selecting a numeric value or range within defined bounds. Supports single value and range selection, tick marks, custom value formatting, and vertical orientation. Use it when users need to explore a continuous range, such as volume, price, or percentage.',
549
+ 'A draggable control for selecting a numeric value or range within defined bounds. Supports single value and range selection, tick marks, custom value formatting, and vertical orientation. Use it when users need to explore a continuous range, such as volume, price, or percentage. Under a finger, on any touch device, it tracks as UISlider does: only the thumb takes a drag, through a target larger than it, the value follows the finger’s travel rather than jumping to it, a release coasts on before `onChangeEnd`, a swipe from the track scrolls the page, and a tap on the track or a mark sets the value when the finger lifts (the thumb springs there). A mouse drag on a thumb tracks the same way, past the same 10 px in any direction, with no lift or coast; a mouse press on the track moves the thumb there, and only a primary click presses. A pen drags as a finger does, claiming only along the track, and its drag ends at the release with no coast. A wheel or a trackpad scroll over the slider goes to whatever scrolls under it.',
548
550
  bestPractices: [
549
551
  {
550
552
  guidance: true,
@@ -574,10 +576,12 @@ export const docsDense = {
574
576
  ],
575
577
  },
576
578
  propDescriptions: {
577
- containerXstyle: 'StyleX styles for the field container; no effect in InputGroup or horizontal-labels FormLayout. No root xstyle/className/style.',
579
+ containerXstyle:
580
+ 'StyleX styles for the field container; no effect in InputGroup or horizontal-labels FormLayout. No root xstyle/className/style.',
578
581
  containerClassName: 'Class name for the field container.',
579
582
  containerStyle: 'Inline styles for the field container.',
580
- controlXstyle: 'StyleX styles for the visual control box (border, background, icons); not the native input.',
583
+ controlXstyle:
584
+ 'StyleX styles for the visual control box (border, background, icons); not the native input.',
581
585
  controlClassName: 'Class name for the visual control box.',
582
586
  controlStyle: 'Inline styles for the visual control box.',
583
587
  label: 'Label text (always rendered for a11y).',