@astryxdesign/core 0.7.6 → 0.7.7-canary.36f8ea1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AppShell/AppShell.d.ts.map +1 -1
- package/dist/AppShell/AppShell.js +23 -3
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +16 -9
- package/dist/Chat/ChatComposerInput.d.ts +9 -0
- package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
- package/dist/Chat/ChatComposerInput.js +2 -0
- package/dist/Chat/ChatMessage.js +2 -2
- package/dist/Chat/chatMessage.vars.stylex.d.ts +8 -8
- package/dist/Chat/chatMessage.vars.stylex.js +8 -8
- package/dist/Drawer/Drawer.d.ts.map +1 -1
- package/dist/Drawer/Drawer.js +6 -0
- package/dist/DropdownMenu/useMenuOverflow.d.ts +13 -3
- package/dist/DropdownMenu/useMenuOverflow.d.ts.map +1 -1
- package/dist/DropdownMenu/useMenuOverflow.js +31 -8
- package/dist/Link/Link.d.ts.map +1 -1
- package/dist/Link/Link.js +1 -0
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +25 -13
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +404 -131
- package/dist/Slider/sliderTouchController.d.ts +189 -0
- package/dist/Slider/sliderTouchController.d.ts.map +1 -0
- package/dist/Slider/sliderTouchController.js +1017 -0
- package/dist/Slider/useSliderTouch.d.ts +22 -0
- package/dist/Slider/useSliderTouch.d.ts.map +1 -0
- package/dist/Slider/useSliderTouch.js +56 -0
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +10 -8
- package/dist/astryx.css +33 -6
- package/dist/gestures/GestureArena.d.ts.map +1 -1
- package/dist/gestures/GestureArena.js +4 -1
- package/dist/gestures/GestureHost.d.ts +72 -2
- package/dist/gestures/GestureHost.d.ts.map +1 -1
- package/dist/gestures/GestureHost.js +643 -5
- package/dist/gestures/GestureRecognizer.d.ts +15 -1
- package/dist/gestures/GestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/GestureRecognizer.js +12 -0
- package/dist/gestures/PanGestureRecognizer.d.ts +8 -0
- package/dist/gestures/PanGestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/PinchGestureRecognizer.d.ts +5 -1
- package/dist/gestures/PinchGestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/PinchGestureRecognizer.js +10 -0
- package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts +3 -3
- package/dist/gestures/ScreenEdgePanGestureRecognizer.d.ts.map +1 -1
- package/dist/gestures/ScreenEdgePanGestureRecognizer.js +7 -5
- package/dist/gestures/nativeScroll.d.ts +16 -2
- package/dist/gestures/nativeScroll.d.ts.map +1 -1
- package/dist/gestures/nativeScroll.js +39 -8
- package/dist/gestures/recognizerInternals.d.ts +4 -2
- package/dist/gestures/recognizerInternals.d.ts.map +1 -1
- package/dist/gestures/types.d.ts +11 -0
- package/dist/gestures/types.d.ts.map +1 -1
- package/dist/gestures/types.js +7 -0
- package/dist/gestures/wheelSegmenter.d.ts +45 -0
- package/dist/gestures/wheelSegmenter.d.ts.map +1 -0
- package/dist/gestures/wheelSegmenter.js +98 -0
- package/dist/motion/animator.d.ts.map +1 -1
- package/dist/motion/animator.js +77 -14
- package/dist/motion/uikit/index.d.ts +2 -1
- package/dist/motion/uikit/index.d.ts.map +1 -1
- package/dist/motion/uikit/index.js +2 -1
- package/dist/motion/uikit/slider.d.ts +97 -0
- package/dist/motion/uikit/slider.d.ts.map +1 -0
- package/dist/motion/uikit/slider.js +111 -0
- package/dist/motion/uikit/springs.d.ts +46 -1
- package/dist/motion/uikit/springs.d.ts.map +1 -1
- package/dist/motion/uikit/springs.js +48 -4
- package/package.json +5 -5
- package/src/AppShell/AppShell.doc.mjs +10 -0
- package/src/AppShell/AppShell.tsx +48 -2
- package/src/BottomSheet/BottomSheetPanel.tsx +16 -9
- package/src/Chat/Chat.doc.mjs +1 -0
- package/src/Chat/ChatComposerInput.doc.mjs +11 -0
- package/src/Chat/ChatComposerInput.tsx +11 -0
- package/src/Chat/ChatMessage.tsx +2 -2
- package/src/Chat/chatMessage.vars.stylex.ts +8 -8
- package/src/Drawer/Drawer.tsx +6 -0
- package/src/DropdownMenu/useMenuOverflow.ts +44 -8
- package/src/Link/Link.tsx +3 -0
- package/src/Selector/Selector.tsx +29 -2
- package/src/Slider/Slider.doc.mjs +11 -7
- package/src/Slider/Slider.tsx +539 -132
- package/src/Slider/sliderTouchController.ts +1209 -0
- package/src/Slider/useSliderTouch.ts +78 -0
- package/src/Typeahead/BaseTypeahead.tsx +9 -8
- package/src/gestures/GestureArena.ts +1 -0
- package/src/gestures/GestureHost.ts +765 -5
- package/src/gestures/GestureRecognizer.ts +28 -0
- package/src/gestures/PanGestureRecognizer.ts +8 -0
- package/src/gestures/PinchGestureRecognizer.ts +16 -1
- package/src/gestures/README.md +63 -0
- package/src/gestures/ScreenEdgePanGestureRecognizer.ts +7 -5
- package/src/gestures/nativeScroll.ts +49 -9
- package/src/gestures/recognizerInternals.ts +9 -2
- package/src/gestures/types.ts +12 -0
- package/src/gestures/wheelSegmenter.ts +114 -0
- package/src/motion/animator.ts +115 -19
- package/src/motion/uikit/index.ts +2 -1
- package/src/motion/uikit/slider.ts +105 -0
- package/src/motion/uikit/springs.ts +37 -2
package/src/Slider/Slider.tsx
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file Slider.tsx
|
|
7
|
-
* @input Uses React, useId, useRef, useCallback, useEffect, Field,
|
|
8
|
-
* @output Exports Slider and its props
|
|
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:
|
|
@@ -23,10 +23,15 @@ import {
|
|
|
23
23
|
useRef,
|
|
24
24
|
useState,
|
|
25
25
|
useCallback,
|
|
26
|
+
type CSSProperties,
|
|
26
27
|
type FocusEvent,
|
|
27
28
|
type KeyboardEvent,
|
|
28
29
|
type PointerEvent,
|
|
30
|
+
type ReactElement,
|
|
31
|
+
type ReactNode,
|
|
32
|
+
type Ref,
|
|
29
33
|
} from 'react';
|
|
34
|
+
import {flushSync} from 'react-dom';
|
|
30
35
|
import * as stylex from '@stylexjs/stylex';
|
|
31
36
|
import {
|
|
32
37
|
colorVars,
|
|
@@ -38,7 +43,6 @@ import {
|
|
|
38
43
|
typeScaleVars,
|
|
39
44
|
} from '../theme/tokens.stylex';
|
|
40
45
|
import {Field} from '../Field/Field';
|
|
41
|
-
import {Tooltip} from '../Tooltip/Tooltip';
|
|
42
46
|
import {useTooltip} from '../Tooltip';
|
|
43
47
|
import {VisuallyHidden} from '../VisuallyHidden';
|
|
44
48
|
import type {InputStatus} from '../Field/types';
|
|
@@ -53,6 +57,14 @@ import {
|
|
|
53
57
|
} from '../Field/fieldParts';
|
|
54
58
|
import type {SizeValue} from '../utils/types';
|
|
55
59
|
import {themeProps} from '../utils/themeProps';
|
|
60
|
+
import {isPrimaryClick} from '../utils/primaryClick';
|
|
61
|
+
import {useTouchPresentation} from '../utils/touchPresentation';
|
|
62
|
+
import {slider as UIKIT_SLIDER} from '../motion/uikit/slider';
|
|
63
|
+
import {
|
|
64
|
+
useSliderTouch,
|
|
65
|
+
type SliderTouchModel,
|
|
66
|
+
type SliderTouchParts,
|
|
67
|
+
} from './useSliderTouch';
|
|
56
68
|
|
|
57
69
|
import {useMergedRefs} from '../hooks/useMergedRefs';
|
|
58
70
|
import {useResolvedRequired} from '../hooks/useResolvedRequired';
|
|
@@ -169,6 +181,35 @@ export type SliderProps = SliderSingleProps | SliderRangeProps;
|
|
|
169
181
|
const TRACK_SIZE = 4;
|
|
170
182
|
const THUMB_SIZE = 20;
|
|
171
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Where a thumb sits, as its progress along the travel (0 at `min`, 1 at
|
|
186
|
+
* `max`), written inline on the thumb. CSS computes its translate from it
|
|
187
|
+
* against the control's size, in container units. So moving the thumb
|
|
188
|
+
* restyles and composites but never lays out, and the server renders the
|
|
189
|
+
* right place. A value tooltip or text display still lays out as its text
|
|
190
|
+
* changes.
|
|
191
|
+
*/
|
|
192
|
+
const THUMB_PROGRESS = '--_slider-progress';
|
|
193
|
+
/** A thumb's touch-target insets under a finger, which its `::before` draws. */
|
|
194
|
+
const HIT_INSET = {
|
|
195
|
+
top: '--_slider-hit-top',
|
|
196
|
+
bottom: '--_slider-hit-bottom',
|
|
197
|
+
inlineStart: '--_slider-hit-start',
|
|
198
|
+
inlineEnd: '--_slider-hit-end',
|
|
199
|
+
} as const;
|
|
200
|
+
/** Half a thumb: the inset of the thumb's travel at each end of the track. */
|
|
201
|
+
const THUMB_INSET_PX = THUMB_SIZE / 2;
|
|
202
|
+
/** The filled range's ends as progress, written inline on the fill parts. */
|
|
203
|
+
const FILL_START = '--_slider-fill-start';
|
|
204
|
+
const FILL_END = '--_slider-fill-end';
|
|
205
|
+
|
|
206
|
+
// The private progress properties a thumb and the fill parts publish inline.
|
|
207
|
+
type ThumbStyle = React.CSSProperties &
|
|
208
|
+
Record<typeof THUMB_PROGRESS, number> &
|
|
209
|
+
Partial<Record<(typeof HIT_INSET)[keyof typeof HIT_INSET], string>>;
|
|
210
|
+
type FillStyle = React.CSSProperties &
|
|
211
|
+
Record<typeof FILL_START | typeof FILL_END, number>;
|
|
212
|
+
|
|
172
213
|
// =============================================================================
|
|
173
214
|
// Styles
|
|
174
215
|
// =============================================================================
|
|
@@ -184,10 +225,43 @@ const styles = stylex.create({
|
|
|
184
225
|
display: 'flex',
|
|
185
226
|
alignItems: 'center',
|
|
186
227
|
flexGrow: 1,
|
|
187
|
-
|
|
228
|
+
// Under a finger (a coarse primary pointer) the control leaves the browser
|
|
229
|
+
// its pans. So a swipe from the track or across a thumb scrolls the page.
|
|
230
|
+
// The gesture host narrows that per element from the recognizers attached
|
|
231
|
+
// to it (a thumb's pan keeps its own axis) and never widens it. A held
|
|
232
|
+
// thumb's pan locks text selection and the callout until the lift. A
|
|
233
|
+
// mouse or a trackpad keeps the control's `none`.
|
|
234
|
+
touchAction: {
|
|
235
|
+
default: 'none',
|
|
236
|
+
'@media (pointer: coarse)': 'manipulation',
|
|
237
|
+
},
|
|
238
|
+
// The control's text is never selectable. WebKit applies only the
|
|
239
|
+
// prefixed property. So both are written. A held finger on the track
|
|
240
|
+
// then starts no selection either. The gesture host's lock leaves this
|
|
241
|
+
// always-on suppression as it is.
|
|
242
|
+
WebkitUserSelect: 'none',
|
|
188
243
|
userSelect: 'none',
|
|
189
244
|
isolation: 'isolate',
|
|
190
245
|
},
|
|
246
|
+
// Every part of the control sits in this box, which fills the control's
|
|
247
|
+
// padding box. So padding on the control (a theme rule, `controlStyle`)
|
|
248
|
+
// moves no part. The thumbs and the fill travel in container units of it
|
|
249
|
+
// (`cqi` / `cqb`). Its size never depends on its content.
|
|
250
|
+
travelBox: {
|
|
251
|
+
position: 'absolute',
|
|
252
|
+
inset: 0,
|
|
253
|
+
containerType: 'size',
|
|
254
|
+
},
|
|
255
|
+
// Where layout places a thumb at its value: the value tooltip's position
|
|
256
|
+
// anchor. CSS anchor positioning reads the anchor's layout box, which the
|
|
257
|
+
// thumb's `translate` does not move in every engine. So the tooltip anchors
|
|
258
|
+
// here while hover and focus stay on the thumb.
|
|
259
|
+
valueAnchor: {
|
|
260
|
+
position: 'absolute',
|
|
261
|
+
width: THUMB_SIZE,
|
|
262
|
+
height: THUMB_SIZE,
|
|
263
|
+
pointerEvents: 'none',
|
|
264
|
+
},
|
|
191
265
|
trackContainerHorizontal: {
|
|
192
266
|
height: THUMB_SIZE,
|
|
193
267
|
// The whole track is the tap target (a click anywhere on it moves the
|
|
@@ -246,26 +320,95 @@ const styles = stylex.create({
|
|
|
246
320
|
bottom: 0,
|
|
247
321
|
width: TRACK_SIZE,
|
|
248
322
|
},
|
|
249
|
-
|
|
323
|
+
// The filled range is a full-length bar moved into place inside two
|
|
324
|
+
// clips, so it travels by translate alone. The clip has the track's shape
|
|
325
|
+
// and rounds the start of a single slider's fill. The mover's start edge
|
|
326
|
+
// cuts the fill at the range's start, and the fill's own end sits at the
|
|
327
|
+
// range's end. Both ends of a range hide under its thumbs.
|
|
328
|
+
fillClip: {
|
|
250
329
|
position: 'absolute',
|
|
251
|
-
|
|
330
|
+
overflow: 'hidden',
|
|
252
331
|
borderRadius: radiusVars['--radius-full'],
|
|
332
|
+
pointerEvents: 'none',
|
|
253
333
|
},
|
|
254
|
-
|
|
334
|
+
fillClipHorizontal: {
|
|
335
|
+
insetInlineStart: 0,
|
|
336
|
+
insetInlineEnd: 0,
|
|
255
337
|
height: TRACK_SIZE,
|
|
256
338
|
top: '50%',
|
|
257
339
|
transform: 'translateY(-50%)',
|
|
258
340
|
},
|
|
259
|
-
|
|
341
|
+
fillClipVertical: {
|
|
342
|
+
top: 0,
|
|
343
|
+
bottom: 0,
|
|
260
344
|
width: TRACK_SIZE,
|
|
261
345
|
},
|
|
346
|
+
fillMover: {
|
|
347
|
+
position: 'absolute',
|
|
348
|
+
inset: 0,
|
|
349
|
+
overflow: 'hidden',
|
|
350
|
+
},
|
|
351
|
+
// A range's start is round, like the track's ends (the clip already
|
|
352
|
+
// rounds a single slider's start). It shows through a disabled thumb's
|
|
353
|
+
// translucent fill.
|
|
354
|
+
fillMoverRangeStartHorizontal: {
|
|
355
|
+
borderStartStartRadius: radiusVars['--radius-full'],
|
|
356
|
+
borderEndStartRadius: radiusVars['--radius-full'],
|
|
357
|
+
},
|
|
358
|
+
fillMoverRangeStartVertical: {
|
|
359
|
+
borderEndStartRadius: radiusVars['--radius-full'],
|
|
360
|
+
borderEndEndRadius: radiusVars['--radius-full'],
|
|
361
|
+
},
|
|
362
|
+
fillMoverRangeHorizontal: {
|
|
363
|
+
translate: {
|
|
364
|
+
default: `calc(${THUMB_INSET_PX}px + var(${FILL_START}) * (100cqi - ${THUMB_SIZE}px)) 0`,
|
|
365
|
+
':dir(rtl)': `calc(-${THUMB_INSET_PX}px - var(${FILL_START}) * (100cqi - ${THUMB_SIZE}px)) 0`,
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
fillMoverRangeVertical: {
|
|
369
|
+
translate: `0 calc(-${THUMB_INSET_PX}px - var(${FILL_START}) * (100cqb - ${THUMB_SIZE}px))`,
|
|
370
|
+
},
|
|
371
|
+
filledTrack: {
|
|
372
|
+
position: 'absolute',
|
|
373
|
+
inset: 0,
|
|
374
|
+
backgroundColor: colorVars['--color-accent'],
|
|
375
|
+
},
|
|
376
|
+
// The fill's end is round too, under the thumb.
|
|
377
|
+
filledTrackHorizontal: {
|
|
378
|
+
borderStartEndRadius: radiusVars['--radius-full'],
|
|
379
|
+
borderEndEndRadius: radiusVars['--radius-full'],
|
|
380
|
+
},
|
|
381
|
+
filledTrackVertical: {
|
|
382
|
+
borderStartStartRadius: radiusVars['--radius-full'],
|
|
383
|
+
borderStartEndRadius: radiusVars['--radius-full'],
|
|
384
|
+
},
|
|
385
|
+
// A single slider's fill runs from the track's start to the thumb's center.
|
|
386
|
+
filledTrackSingleHorizontal: {
|
|
387
|
+
translate: {
|
|
388
|
+
default: `calc(${THUMB_INSET_PX}px + var(${FILL_END}) * (100cqi - ${THUMB_SIZE}px) - 100cqi) 0`,
|
|
389
|
+
':dir(rtl)': `calc(100cqi - ${THUMB_INSET_PX}px - var(${FILL_END}) * (100cqi - ${THUMB_SIZE}px)) 0`,
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
filledTrackSingleVertical: {
|
|
393
|
+
translate: `0 calc(100cqb - ${THUMB_INSET_PX}px - var(${FILL_END}) * (100cqb - ${THUMB_SIZE}px))`,
|
|
394
|
+
},
|
|
395
|
+
// A range's fill runs from the lower thumb's center to the upper's. The
|
|
396
|
+
// mover already sits at the lower one.
|
|
397
|
+
filledTrackRangeHorizontal: {
|
|
398
|
+
translate: {
|
|
399
|
+
default: `calc((var(${FILL_END}) - var(${FILL_START})) * (100cqi - ${THUMB_SIZE}px) - 100cqi) 0`,
|
|
400
|
+
':dir(rtl)': `calc(100cqi - (var(${FILL_END}) - var(${FILL_START})) * (100cqi - ${THUMB_SIZE}px)) 0`,
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
filledTrackRangeVertical: {
|
|
404
|
+
translate: `0 calc(100cqb - (var(${FILL_END}) - var(${FILL_START})) * (100cqb - ${THUMB_SIZE}px))`,
|
|
405
|
+
},
|
|
262
406
|
thumb: {
|
|
263
407
|
position: 'absolute',
|
|
264
408
|
width: THUMB_SIZE,
|
|
265
409
|
height: THUMB_SIZE,
|
|
266
410
|
borderRadius: radiusVars['--radius-full'],
|
|
267
411
|
backgroundColor: colorVars['--color-accent'],
|
|
268
|
-
transform: 'translate(-50%, -50%)',
|
|
269
412
|
transitionProperty: 'background-color, box-shadow',
|
|
270
413
|
transitionDuration: {
|
|
271
414
|
default: durationVars['--duration-fast'],
|
|
@@ -280,16 +423,42 @@ const styles = stylex.create({
|
|
|
280
423
|
zIndex: 1,
|
|
281
424
|
},
|
|
282
425
|
thumbHorizontal: {
|
|
283
|
-
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
426
|
+
// Centered by its offset, not by a transform: the touch lift scales the
|
|
427
|
+
// thumb (`scale`), and CSS applies `transform` inside `scale`. So a
|
|
428
|
+
// centering translate would grow with it and lift the thumb off the
|
|
429
|
+
// track. UISlider's lens grows around its center.
|
|
430
|
+
top: `calc(50% - ${THUMB_SIZE / 2}px)`,
|
|
431
|
+
insetInlineStart: 0,
|
|
432
|
+
// The thumb starts at the track's inline start (the right edge under
|
|
433
|
+
// RTL). It travels by its progress along the track minus its own size,
|
|
434
|
+
// so its center lands where `insetPosition` places the fill and the
|
|
435
|
+
// marks. `translate` is physical, so it flips its sign under RTL.
|
|
436
|
+
translate: {
|
|
437
|
+
default: `calc(var(${THUMB_PROGRESS}) * (100cqi - ${THUMB_SIZE}px)) 0`,
|
|
438
|
+
':dir(rtl)': `calc(var(${THUMB_PROGRESS}) * (${THUMB_SIZE}px - 100cqi)) 0`,
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
// A thumb's touch target under a finger (UISlider's hit area): the thumb's
|
|
442
|
+
// own `::before`, invisible and centered on it, larger than it. So a touch
|
|
443
|
+
// presentation renders no more elements than a mouse one. Each side grows
|
|
444
|
+
// by the inset `thumbHitInsets` sets on the thumb.
|
|
445
|
+
thumbHit: {
|
|
446
|
+
'::before': {
|
|
447
|
+
content: '""',
|
|
448
|
+
position: 'absolute',
|
|
449
|
+
top: `var(${HIT_INSET.top})`,
|
|
450
|
+
bottom: `var(${HIT_INSET.bottom})`,
|
|
451
|
+
insetInlineStart: `var(${HIT_INSET.inlineStart})`,
|
|
452
|
+
insetInlineEnd: `var(${HIT_INSET.inlineEnd})`,
|
|
291
453
|
},
|
|
292
454
|
},
|
|
455
|
+
thumbVertical: {
|
|
456
|
+
bottom: 0,
|
|
457
|
+
// Centered by its offset (see thumbHorizontal): the same in both
|
|
458
|
+
// directions, since the thumb is symmetric.
|
|
459
|
+
insetInlineStart: `calc(50% - ${THUMB_SIZE / 2}px)`,
|
|
460
|
+
translate: `0 calc(var(${THUMB_PROGRESS}) * (${THUMB_SIZE}px - 100cqb))`,
|
|
461
|
+
},
|
|
293
462
|
thumbHover: {
|
|
294
463
|
backgroundColor: {
|
|
295
464
|
default: colorVars['--color-accent'],
|
|
@@ -439,12 +608,6 @@ function insetPosition(percent: number): string {
|
|
|
439
608
|
return cssLength(percent, THUMB_INSET - (percent / 100) * THUMB_SIZE);
|
|
440
609
|
}
|
|
441
610
|
|
|
442
|
-
/** Distance between two inset positions; the inset itself cancels out. */
|
|
443
|
-
function insetSpan(fromPercent: number, toPercent: number): string {
|
|
444
|
-
const delta = toPercent - fromPercent;
|
|
445
|
-
return cssLength(delta, -(delta / 100) * THUMB_SIZE);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
611
|
/** Inverse of `insetPosition`: an offset from the box start back to 0–1. */
|
|
449
612
|
function travelFraction(offset: number, size: number): number {
|
|
450
613
|
const travel = size - THUMB_SIZE;
|
|
@@ -463,6 +626,51 @@ function cssLength(percent: number, px: number): string {
|
|
|
463
626
|
return `calc(${round(percent)}% ${px < 0 ? '-' : '+'} ${Math.abs(round(px))}px)`;
|
|
464
627
|
}
|
|
465
628
|
|
|
629
|
+
/**
|
|
630
|
+
* A thumb's value tooltip: hover and focus on the thumb, position from a
|
|
631
|
+
* thumb-sized anchor that layout places at the value (styles.valueAnchor).
|
|
632
|
+
*/
|
|
633
|
+
function SliderValueTooltip({
|
|
634
|
+
content,
|
|
635
|
+
placement,
|
|
636
|
+
isOpen,
|
|
637
|
+
anchorStyle,
|
|
638
|
+
thumbRef,
|
|
639
|
+
children,
|
|
640
|
+
}: {
|
|
641
|
+
content: ReactNode;
|
|
642
|
+
placement: 'above' | 'start';
|
|
643
|
+
isOpen: boolean | undefined;
|
|
644
|
+
anchorStyle: CSSProperties;
|
|
645
|
+
thumbRef: Ref<HTMLDivElement> | undefined;
|
|
646
|
+
children: (trigger: {
|
|
647
|
+
ref: Ref<HTMLDivElement>;
|
|
648
|
+
describedBy: string;
|
|
649
|
+
}) => ReactElement;
|
|
650
|
+
}): ReactElement {
|
|
651
|
+
const tooltip = useTooltip({
|
|
652
|
+
placement,
|
|
653
|
+
delay: 0,
|
|
654
|
+
focusTrigger: 'always',
|
|
655
|
+
isOpen,
|
|
656
|
+
});
|
|
657
|
+
const triggerRef = useMergedRefs<HTMLDivElement>(
|
|
658
|
+
thumbRef,
|
|
659
|
+
tooltip.interactionRef,
|
|
660
|
+
);
|
|
661
|
+
return (
|
|
662
|
+
<>
|
|
663
|
+
<span
|
|
664
|
+
aria-hidden="true"
|
|
665
|
+
ref={tooltip.positionRef}
|
|
666
|
+
{...mergeProps(stylex.props(styles.valueAnchor), {style: anchorStyle})}
|
|
667
|
+
/>
|
|
668
|
+
{children({ref: triggerRef, describedBy: tooltip.describedBy})}
|
|
669
|
+
{tooltip.renderTooltip(content)}
|
|
670
|
+
</>
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
|
|
466
674
|
// =============================================================================
|
|
467
675
|
// Component
|
|
468
676
|
// =============================================================================
|
|
@@ -551,9 +759,19 @@ export function Slider({ref, ...props}: SliderProps) {
|
|
|
551
759
|
const requiredID = useId();
|
|
552
760
|
|
|
553
761
|
const trackRef = useRef<HTMLDivElement>(null);
|
|
762
|
+
const fillMoverRef = useRef<HTMLDivElement>(null);
|
|
763
|
+
const fillRef = useRef<HTMLDivElement>(null);
|
|
554
764
|
const draggingThumbRef = useRef<number | null>(null);
|
|
555
765
|
const [draggingThumb, setDraggingThumb] = useState<number | null>(null);
|
|
556
766
|
|
|
767
|
+
// The thumbs track as UISlider's for every pointer (useSliderTouch): a
|
|
768
|
+
// drag on a thumb is a pan. Their touch targets show only under a coarse
|
|
769
|
+
// primary pointer, after hydration: one touch model on every touch
|
|
770
|
+
// device. A mouse press on the track keeps the pointer
|
|
771
|
+
// handlers below.
|
|
772
|
+
const isTouchPresentation = useTouchPresentation().isTouch;
|
|
773
|
+
const hasTouchTargets = isTouchPresentation && !isDisabled;
|
|
774
|
+
|
|
557
775
|
// A thumb is a div[role="slider"], and `handlePointerDown` focuses it from
|
|
558
776
|
// script after preventDefault — which Chromium treats as focus-visible, so
|
|
559
777
|
// dragging with a mouse drew the keyboard ring (measured: `:focus-visible`
|
|
@@ -578,7 +796,7 @@ export function Slider({ref, ...props}: SliderProps) {
|
|
|
578
796
|
}, []);
|
|
579
797
|
|
|
580
798
|
// Disabled-reason tooltip. This is a *separate* useTooltip instance from the
|
|
581
|
-
// per-thumb value bubble (
|
|
799
|
+
// per-thumb value bubble (SliderValueTooltip below): it anchors to the
|
|
582
800
|
// track container and fires on hover/focus of the whole control. Disabled
|
|
583
801
|
// controls swallow pointer events, so the thumb stays perceivable via
|
|
584
802
|
// aria-disabled while pointer/keyboard handlers early-return on isDisabled.
|
|
@@ -799,6 +1017,27 @@ export function Slider({ref, ...props}: SliderProps) {
|
|
|
799
1017
|
if (isDisabled) {
|
|
800
1018
|
return;
|
|
801
1019
|
}
|
|
1020
|
+
// Only a primary button presses: a right click or a macOS ctrl-click
|
|
1021
|
+
// is the platform's context request, and a pen's
|
|
1022
|
+
// barrel button presses nothing either.
|
|
1023
|
+
if (!isPrimaryClick(e)) {
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
// A press on a thumb belongs to the slider's gestures (useSliderTouch),
|
|
1027
|
+
// which pan it for every pointer. The thumb still takes focus from any
|
|
1028
|
+
// press, as it did. A finger's or a pen's tap on the track sets the
|
|
1029
|
+
// value when it lifts. So only a mouse press on the track goes on.
|
|
1030
|
+
const thumb = (e.target as Element).closest<HTMLElement>(
|
|
1031
|
+
'[role="slider"]',
|
|
1032
|
+
);
|
|
1033
|
+
if (thumb != null) {
|
|
1034
|
+
thumb.focus();
|
|
1035
|
+
setKeyboardFocusThumb(null);
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
if (e.pointerType === 'touch' || e.pointerType === 'pen') {
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
802
1041
|
e.preventDefault();
|
|
803
1042
|
if (draggingThumbRef.current !== null) {
|
|
804
1043
|
return;
|
|
@@ -959,6 +1198,105 @@ export function Slider({ref, ...props}: SliderProps) {
|
|
|
959
1198
|
[endDrag],
|
|
960
1199
|
);
|
|
961
1200
|
|
|
1201
|
+
// The touch presentation's value model (useSliderTouch reads it fresh).
|
|
1202
|
+
const updateValueRef = useRef(updateValue);
|
|
1203
|
+
updateValueRef.current = updateValue;
|
|
1204
|
+
const touchModel: SliderTouchModel = {
|
|
1205
|
+
min,
|
|
1206
|
+
max,
|
|
1207
|
+
isRange,
|
|
1208
|
+
isHorizontal,
|
|
1209
|
+
isDisabled: isDisabled ?? false,
|
|
1210
|
+
thumbSize: THUMB_SIZE,
|
|
1211
|
+
values: () => valuesRef.current,
|
|
1212
|
+
// The value updateValue would report for this thumb.
|
|
1213
|
+
constrain: (index, raw) => {
|
|
1214
|
+
const next = clamp(snapToStep(raw, min, step), min, max);
|
|
1215
|
+
if (!isRange) {
|
|
1216
|
+
return next;
|
|
1217
|
+
}
|
|
1218
|
+
const minGap = minStepsBetweenThumbs * step;
|
|
1219
|
+
const [v0, v1] = valuesRef.current;
|
|
1220
|
+
return index === 0
|
|
1221
|
+
? clamp(Math.min(next, v1 - minGap), min, max)
|
|
1222
|
+
: clamp(Math.max(next, v0 + minGap), min, max);
|
|
1223
|
+
},
|
|
1224
|
+
bounds: index => {
|
|
1225
|
+
if (!isRange) {
|
|
1226
|
+
return [min, max];
|
|
1227
|
+
}
|
|
1228
|
+
const minGap = minStepsBetweenThumbs * step;
|
|
1229
|
+
const [v0, v1] = valuesRef.current;
|
|
1230
|
+
return index === 0
|
|
1231
|
+
? [min, clamp(v1 - minGap, min, max)]
|
|
1232
|
+
: [clamp(v0 + minGap, min, max), max];
|
|
1233
|
+
},
|
|
1234
|
+
change: (index, next, from) =>
|
|
1235
|
+
updateValueRef.current(index, next, true, from),
|
|
1236
|
+
// A finger paints the thumb pressed and opens its value tooltip.
|
|
1237
|
+
// `handlePointerDown` already focused it.
|
|
1238
|
+
press: index => {
|
|
1239
|
+
setDraggingThumb(index);
|
|
1240
|
+
// When a thumb of this slider holds focus, a drag handed to the other
|
|
1241
|
+
// thumb takes the focus along. Every press on a thumb focuses it.
|
|
1242
|
+
const thumbs =
|
|
1243
|
+
trackRef.current?.querySelectorAll<HTMLElement>('[role="slider"]');
|
|
1244
|
+
const active = trackRef.current?.ownerDocument.activeElement;
|
|
1245
|
+
if (index != null && thumbs != null && active != null) {
|
|
1246
|
+
const list = Array.from(thumbs);
|
|
1247
|
+
if (list.includes(active as HTMLElement) && list[index] !== active) {
|
|
1248
|
+
list[index]?.focus();
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
},
|
|
1252
|
+
// The touch interaction ended, in the handler that ended it: the lift, a
|
|
1253
|
+
// cancel or the coast's last frame. The owner renders the last value
|
|
1254
|
+
// now, from that same handler. Then the controller can hand the parts
|
|
1255
|
+
// back to the stylesheet, and onChangeEnd carries the rendered value.
|
|
1256
|
+
commit: (index, next) => {
|
|
1257
|
+
// 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
|
|
1258
|
+
flushSync(() => {
|
|
1259
|
+
if (next !== null) {
|
|
1260
|
+
updateValueRef.current(index, next);
|
|
1261
|
+
}
|
|
1262
|
+
setDraggingThumb(null);
|
|
1263
|
+
});
|
|
1264
|
+
},
|
|
1265
|
+
reportEnd: () => fireChangeEnd(),
|
|
1266
|
+
// A tap on the track, from useSliderTouch. It takes a mark's value on a
|
|
1267
|
+
// mark, else the point along the travel. It moves the thumb a track
|
|
1268
|
+
// press moves.
|
|
1269
|
+
valueAt: (x, y) => {
|
|
1270
|
+
const mark = trackRef.current?.ownerDocument
|
|
1271
|
+
.elementFromPoint?.(x, y)
|
|
1272
|
+
?.closest<HTMLElement>('[data-mark-value]');
|
|
1273
|
+
return mark != null
|
|
1274
|
+
? Number(mark.dataset.markValue)
|
|
1275
|
+
: getValueFromPosition(x, y);
|
|
1276
|
+
},
|
|
1277
|
+
thumbFor: value => getClosestThumb(value),
|
|
1278
|
+
};
|
|
1279
|
+
useSliderTouch({
|
|
1280
|
+
isEnabled: !isDisabled,
|
|
1281
|
+
model: touchModel,
|
|
1282
|
+
thumbCount: values.length,
|
|
1283
|
+
thumbsWrapped: valueDisplay === 'tooltip' && !showsDisabledMessage,
|
|
1284
|
+
getParts: (): SliderTouchParts | null => {
|
|
1285
|
+
const control = trackRef.current;
|
|
1286
|
+
if (control == null) {
|
|
1287
|
+
return null;
|
|
1288
|
+
}
|
|
1289
|
+
return {
|
|
1290
|
+
control,
|
|
1291
|
+
thumbs: Array.from(
|
|
1292
|
+
control.querySelectorAll<HTMLElement>('[role="slider"]'),
|
|
1293
|
+
),
|
|
1294
|
+
fillMover: fillMoverRef.current,
|
|
1295
|
+
fill: fillRef.current,
|
|
1296
|
+
};
|
|
1297
|
+
},
|
|
1298
|
+
});
|
|
1299
|
+
|
|
962
1300
|
// Keyboard handler
|
|
963
1301
|
const handleKeyDown = useCallback(
|
|
964
1302
|
(thumbIndex: number, e: KeyboardEvent<HTMLDivElement>) => {
|
|
@@ -1069,13 +1407,42 @@ export function Slider({ref, ...props}: SliderProps) {
|
|
|
1069
1407
|
};
|
|
1070
1408
|
|
|
1071
1409
|
// Render a thumb
|
|
1410
|
+
// Each thumb's touch target grows to UISlider's 48 pt around the thumb.
|
|
1411
|
+
// A range's two targets never reach into each other: on the side facing
|
|
1412
|
+
// the other thumb, the growth stops at half the gap. So a finger takes the
|
|
1413
|
+
// nearer thumb. Container units measure the travel, so the gap needs no
|
|
1414
|
+
// layout read.
|
|
1415
|
+
const hitGrowth = (UIKIT_SLIDER.thumbHitTarget - THUMB_SIZE) / 2;
|
|
1416
|
+
const thumbHitInsets = (thumbIndex: number) => {
|
|
1417
|
+
const full = `${-hitGrowth}px`;
|
|
1418
|
+
let facing = full;
|
|
1419
|
+
if (isRange && max > min) {
|
|
1420
|
+
const spread = (values[1] - values[0]) / (max - min);
|
|
1421
|
+
const travel = isHorizontal ? '100cqi' : '100cqb';
|
|
1422
|
+
facing = `calc(-1 * clamp(0px, (${spread} * (${travel} - ${THUMB_SIZE}px) - ${THUMB_SIZE}px) / 2, ${hitGrowth}px))`;
|
|
1423
|
+
}
|
|
1424
|
+
// Values grow toward the inline end, and upward on a vertical slider.
|
|
1425
|
+
const towardLower = isRange && thumbIndex === 1 ? facing : full;
|
|
1426
|
+
const towardUpper = isRange && thumbIndex === 0 ? facing : full;
|
|
1427
|
+
const [top, bottom, inlineStart, inlineEnd] = isHorizontal
|
|
1428
|
+
? [full, full, towardLower, towardUpper]
|
|
1429
|
+
: [towardUpper, towardLower, full, full];
|
|
1430
|
+
return {
|
|
1431
|
+
[HIT_INSET.top]: top,
|
|
1432
|
+
[HIT_INSET.bottom]: bottom,
|
|
1433
|
+
[HIT_INSET.inlineStart]: inlineStart,
|
|
1434
|
+
[HIT_INSET.inlineEnd]: inlineEnd,
|
|
1435
|
+
};
|
|
1436
|
+
};
|
|
1437
|
+
|
|
1072
1438
|
const renderThumb = (thumbIndex: number) => {
|
|
1073
1439
|
const val = values[thumbIndex];
|
|
1074
1440
|
const percent = getPercent(val, min, max);
|
|
1075
1441
|
|
|
1076
|
-
const positionStyle =
|
|
1077
|
-
|
|
1078
|
-
|
|
1442
|
+
const positionStyle: ThumbStyle = {
|
|
1443
|
+
[THUMB_PROGRESS]: percent / 100,
|
|
1444
|
+
...(hasTouchTargets ? thumbHitInsets(thumbIndex) : null),
|
|
1445
|
+
};
|
|
1079
1446
|
|
|
1080
1447
|
// In range mode each thumb keeps a short individual name that composes
|
|
1081
1448
|
// with the group label (announced via the group's aria-labelledby), per
|
|
@@ -1104,10 +1471,12 @@ export function Slider({ref, ...props}: SliderProps) {
|
|
|
1104
1471
|
const tooltipPlacement = isHorizontal ? 'above' : 'start';
|
|
1105
1472
|
|
|
1106
1473
|
const thumbElement = (
|
|
1474
|
+
trigger: {ref: Ref<HTMLDivElement>; describedBy: string} | null,
|
|
1475
|
+
) => (
|
|
1107
1476
|
<div
|
|
1108
1477
|
key={thumbIndex}
|
|
1109
1478
|
{...(!isRange ? rest : undefined)}
|
|
1110
|
-
ref={!isRange ? ref : undefined}
|
|
1479
|
+
ref={trigger != null ? trigger.ref : !isRange ? ref : undefined}
|
|
1111
1480
|
id={!isRange ? id : undefined}
|
|
1112
1481
|
role="slider"
|
|
1113
1482
|
// A disabled thumb keeps its Tab stop (spec:AST-085 FR1).
|
|
@@ -1125,7 +1494,11 @@ export function Slider({ref, ...props}: SliderProps) {
|
|
|
1125
1494
|
!isRange ? joinAriaIDs(labelID, ariaLabelledByProp) : undefined
|
|
1126
1495
|
}
|
|
1127
1496
|
aria-describedby={
|
|
1128
|
-
[
|
|
1497
|
+
[
|
|
1498
|
+
!isRange ? ariaDescribedByProp : undefined,
|
|
1499
|
+
ariaDescribedBy,
|
|
1500
|
+
trigger?.describedBy,
|
|
1501
|
+
]
|
|
1129
1502
|
.filter(Boolean)
|
|
1130
1503
|
.join(' ') || undefined
|
|
1131
1504
|
}
|
|
@@ -1145,14 +1518,13 @@ export function Slider({ref, ...props}: SliderProps) {
|
|
|
1145
1518
|
}),
|
|
1146
1519
|
stylex.props(
|
|
1147
1520
|
styles.thumb,
|
|
1148
|
-
isHorizontal
|
|
1149
|
-
? styles.thumbHorizontal
|
|
1150
|
-
: rtlStyles.centerInline('50%'),
|
|
1521
|
+
isHorizontal ? styles.thumbHorizontal : styles.thumbVertical,
|
|
1151
1522
|
!isDisabled && styles.thumbHover,
|
|
1152
1523
|
!isDisabled && draggingThumb === thumbIndex && styles.thumbPressed,
|
|
1153
1524
|
keyboardFocusThumb === thumbIndex &&
|
|
1154
1525
|
focusOutlineStyles.focusVisible,
|
|
1155
1526
|
isDisabled && styles.thumbDisabled,
|
|
1527
|
+
hasTouchTargets && styles.thumbHit,
|
|
1156
1528
|
),
|
|
1157
1529
|
undefined,
|
|
1158
1530
|
positionStyle,
|
|
@@ -1161,44 +1533,42 @@ export function Slider({ref, ...props}: SliderProps) {
|
|
|
1161
1533
|
);
|
|
1162
1534
|
|
|
1163
1535
|
if (useValueTooltip) {
|
|
1536
|
+
const anchorAt = `calc(${insetPosition(percent)} - ${THUMB_SIZE / 2}px)`;
|
|
1164
1537
|
return (
|
|
1165
|
-
<
|
|
1538
|
+
<SliderValueTooltip
|
|
1166
1539
|
key={thumbIndex}
|
|
1167
1540
|
content={displayValue(val)}
|
|
1168
1541
|
placement={tooltipPlacement}
|
|
1169
|
-
delay={0}
|
|
1170
|
-
focusTrigger="always"
|
|
1171
1542
|
// A drag keeps its own thumb's value showing and closes the other
|
|
1172
1543
|
// thumb's, which a drag handed over from it may have forced open.
|
|
1173
1544
|
isOpen={
|
|
1174
1545
|
draggingThumb === null ? undefined : draggingThumb === thumbIndex
|
|
1175
|
-
}
|
|
1546
|
+
}
|
|
1547
|
+
anchorStyle={
|
|
1548
|
+
isHorizontal
|
|
1549
|
+
? {
|
|
1550
|
+
insetInlineStart: anchorAt,
|
|
1551
|
+
top: `calc(50% - ${THUMB_SIZE / 2}px)`,
|
|
1552
|
+
}
|
|
1553
|
+
: {
|
|
1554
|
+
bottom: anchorAt,
|
|
1555
|
+
insetInlineStart: `calc(50% - ${THUMB_SIZE / 2}px)`,
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
thumbRef={!isRange ? ref : undefined}>
|
|
1176
1559
|
{thumbElement}
|
|
1177
|
-
</
|
|
1560
|
+
</SliderValueTooltip>
|
|
1178
1561
|
);
|
|
1179
1562
|
}
|
|
1180
1563
|
|
|
1181
|
-
return thumbElement;
|
|
1564
|
+
return thumbElement(null);
|
|
1182
1565
|
};
|
|
1183
1566
|
|
|
1184
|
-
//
|
|
1185
|
-
// inset
|
|
1186
|
-
const
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
const p0 = getPercent(v0, min, max);
|
|
1190
|
-
const p1 = getPercent(v1, min, max);
|
|
1191
|
-
if (isHorizontal) {
|
|
1192
|
-
return {insetInlineStart: insetPosition(p0), width: insetSpan(p0, p1)};
|
|
1193
|
-
}
|
|
1194
|
-
return {bottom: insetPosition(p0), height: insetSpan(p0, p1)};
|
|
1195
|
-
}
|
|
1196
|
-
const p = getPercent(values[0], min, max);
|
|
1197
|
-
if (isHorizontal) {
|
|
1198
|
-
return {insetInlineStart: '0%', width: insetPosition(p)};
|
|
1199
|
-
}
|
|
1200
|
-
return {bottom: '0%', height: insetPosition(p)};
|
|
1201
|
-
})();
|
|
1567
|
+
// The filled range's ends as progress: it ends at the thumb centers. So it
|
|
1568
|
+
// uses the same inset travel as the thumbs (see styles.fillClip).
|
|
1569
|
+
const fillStart = isRange ? getPercent(values[0], min, max) / 100 : 0;
|
|
1570
|
+
const fillEnd = getPercent(values[isRange ? 1 : 0], min, max) / 100;
|
|
1571
|
+
const fillStyle: FillStyle = {[FILL_START]: fillStart, [FILL_END]: fillEnd};
|
|
1202
1572
|
|
|
1203
1573
|
// Text value display
|
|
1204
1574
|
const textDisplay =
|
|
@@ -1305,95 +1675,132 @@ export function Slider({ref, ...props}: SliderProps) {
|
|
|
1305
1675
|
controlClassName,
|
|
1306
1676
|
controlStyle,
|
|
1307
1677
|
)}>
|
|
1308
|
-
{
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
{/* Filled track */}
|
|
1323
|
-
<div
|
|
1324
|
-
aria-hidden="true"
|
|
1325
|
-
{...mergeProps(
|
|
1326
|
-
stylex.props(
|
|
1327
|
-
styles.filledTrack,
|
|
1328
|
-
isHorizontal
|
|
1329
|
-
? styles.filledTrackHorizontal
|
|
1330
|
-
: [styles.filledTrackVertical, rtlStyles.centerInline('0px')],
|
|
1331
|
-
),
|
|
1332
|
-
{style: filledStyle},
|
|
1333
|
-
)}
|
|
1334
|
-
/>
|
|
1678
|
+
<div {...stylex.props(styles.travelBox)}>
|
|
1679
|
+
{/* Background track */}
|
|
1680
|
+
<div
|
|
1681
|
+
aria-hidden="true"
|
|
1682
|
+
{...mergeProps(
|
|
1683
|
+
themeProps('slider-track', {orientation}),
|
|
1684
|
+
stylex.props(
|
|
1685
|
+
styles.track,
|
|
1686
|
+
isHorizontal
|
|
1687
|
+
? styles.trackHorizontal
|
|
1688
|
+
: [styles.trackVertical, rtlStyles.centerInline('0px')],
|
|
1689
|
+
),
|
|
1690
|
+
)}
|
|
1691
|
+
/>
|
|
1335
1692
|
|
|
1336
|
-
|
|
1337
|
-
{marks && (
|
|
1693
|
+
{/* Filled track */}
|
|
1338
1694
|
<div
|
|
1339
1695
|
aria-hidden="true"
|
|
1340
1696
|
{...stylex.props(
|
|
1341
|
-
styles.
|
|
1697
|
+
styles.fillClip,
|
|
1342
1698
|
isHorizontal
|
|
1343
|
-
? styles.
|
|
1344
|
-
: styles.
|
|
1699
|
+
? styles.fillClipHorizontal
|
|
1700
|
+
: [styles.fillClipVertical, rtlStyles.centerInline('0px')],
|
|
1345
1701
|
)}>
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1702
|
+
<div
|
|
1703
|
+
ref={fillMoverRef}
|
|
1704
|
+
{...mergeProps(
|
|
1705
|
+
stylex.props(
|
|
1706
|
+
styles.fillMover,
|
|
1707
|
+
isRange &&
|
|
1708
|
+
(isHorizontal
|
|
1709
|
+
? [
|
|
1710
|
+
styles.fillMoverRangeHorizontal,
|
|
1711
|
+
styles.fillMoverRangeStartHorizontal,
|
|
1712
|
+
]
|
|
1713
|
+
: [
|
|
1714
|
+
styles.fillMoverRangeVertical,
|
|
1715
|
+
styles.fillMoverRangeStartVertical,
|
|
1716
|
+
]),
|
|
1717
|
+
),
|
|
1718
|
+
{style: fillStyle},
|
|
1719
|
+
)}>
|
|
1720
|
+
<div
|
|
1721
|
+
ref={fillRef}
|
|
1722
|
+
{...mergeProps(
|
|
1723
|
+
stylex.props(
|
|
1724
|
+
styles.filledTrack,
|
|
1725
|
+
isHorizontal
|
|
1726
|
+
? styles.filledTrackHorizontal
|
|
1727
|
+
: styles.filledTrackVertical,
|
|
1728
|
+
isRange
|
|
1729
|
+
? isHorizontal
|
|
1730
|
+
? styles.filledTrackRangeHorizontal
|
|
1731
|
+
: styles.filledTrackRangeVertical
|
|
1732
|
+
: isHorizontal
|
|
1733
|
+
? styles.filledTrackSingleHorizontal
|
|
1734
|
+
: styles.filledTrackSingleVertical,
|
|
1735
|
+
),
|
|
1736
|
+
{style: fillStyle},
|
|
1737
|
+
)}
|
|
1738
|
+
/>
|
|
1739
|
+
</div>
|
|
1740
|
+
</div>
|
|
1741
|
+
|
|
1742
|
+
{/* Marks */}
|
|
1743
|
+
{marks && (
|
|
1744
|
+
<div
|
|
1745
|
+
aria-hidden="true"
|
|
1746
|
+
{...stylex.props(
|
|
1747
|
+
styles.marksContainer,
|
|
1748
|
+
isHorizontal
|
|
1749
|
+
? styles.marksContainerHorizontal
|
|
1750
|
+
: styles.marksContainerVertical,
|
|
1751
|
+
)}>
|
|
1752
|
+
{marks.map(mark => {
|
|
1753
|
+
const percent = getPercent(mark.value, min, max);
|
|
1754
|
+
const markPos = isHorizontal
|
|
1755
|
+
? {insetInlineStart: insetPosition(percent)}
|
|
1756
|
+
: {bottom: insetPosition(percent)};
|
|
1757
|
+
// Marks at or inside the filled region take the fill color:
|
|
1758
|
+
// at or behind the thumb in single mode, between the thumbs
|
|
1759
|
+
// in range mode.
|
|
1760
|
+
const isFilled = isRange
|
|
1761
|
+
? mark.value >= values[0] && mark.value <= values[1]
|
|
1762
|
+
: mark.value <= values[0];
|
|
1763
|
+
return (
|
|
1764
|
+
<div key={mark.value}>
|
|
1765
|
+
<div
|
|
1766
|
+
data-testid="slider-mark"
|
|
1376
1767
|
data-mark-value={mark.value}
|
|
1377
1768
|
{...mergeProps(
|
|
1378
1769
|
stylex.props(
|
|
1379
|
-
styles.
|
|
1770
|
+
styles.mark,
|
|
1380
1771
|
isHorizontal
|
|
1381
|
-
? styles.
|
|
1382
|
-
: styles.
|
|
1772
|
+
? styles.markHorizontal
|
|
1773
|
+
: styles.markVertical,
|
|
1774
|
+
isFilled && styles.markFilled,
|
|
1383
1775
|
),
|
|
1384
1776
|
{style: markPos},
|
|
1385
|
-
)}
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1777
|
+
)}
|
|
1778
|
+
/>
|
|
1779
|
+
{mark.label && (
|
|
1780
|
+
<span
|
|
1781
|
+
data-testid="slider-mark-label"
|
|
1782
|
+
data-mark-value={mark.value}
|
|
1783
|
+
{...mergeProps(
|
|
1784
|
+
stylex.props(
|
|
1785
|
+
styles.markLabel,
|
|
1786
|
+
isHorizontal
|
|
1787
|
+
? styles.markLabelHorizontal
|
|
1788
|
+
: styles.markLabelVertical,
|
|
1789
|
+
),
|
|
1790
|
+
{style: markPos},
|
|
1791
|
+
)}>
|
|
1792
|
+
{mark.label}
|
|
1793
|
+
</span>
|
|
1794
|
+
)}
|
|
1795
|
+
</div>
|
|
1796
|
+
);
|
|
1797
|
+
})}
|
|
1798
|
+
</div>
|
|
1799
|
+
)}
|
|
1394
1800
|
|
|
1395
|
-
|
|
1396
|
-
|
|
1801
|
+
{/* Thumbs */}
|
|
1802
|
+
{values.map((_, i) => renderThumb(i))}
|
|
1803
|
+
</div>
|
|
1397
1804
|
</div>
|
|
1398
1805
|
|
|
1399
1806
|
{textDisplay}
|