@tremolo-ui/react 0.8.0 → 0.9.0
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/index.cjs +416 -252
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +256 -110
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +256 -110
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +368 -203
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
- package/src/components/AnimationCanvas/index.tsx +4 -18
- package/src/components/FileInput/index.tsx +4 -1
- package/src/components/Knob/Thumb.tsx +19 -19
- package/src/components/Knob/index.tsx +85 -41
- package/src/components/NumberInput/InputField.tsx +9 -7
- package/src/components/NumberInput/Stepper.tsx +30 -6
- package/src/components/NumberInput/StepperButton.tsx +12 -4
- package/src/components/NumberInput/context.tsx +24 -2
- package/src/components/NumberInput/index.tsx +142 -27
- package/src/components/Piano/index.tsx +24 -7
- package/src/components/PointsEditor/Container.tsx +1 -1
- package/src/components/PointsEditor/Point.tsx +61 -30
- package/src/components/PointsEditor/context.tsx +3 -4
- package/src/components/PointsEditor/index.tsx +14 -15
- package/src/components/Slider/Marks.tsx +3 -2
- package/src/components/Slider/MarksOption.tsx +3 -2
- package/src/components/Slider/Thumb.tsx +20 -11
- package/src/components/Slider/Track.tsx +15 -13
- package/src/components/Slider/context.tsx +3 -3
- package/src/components/Slider/index.tsx +85 -48
- package/src/components/XYPad/Thumb.tsx +18 -8
- package/src/components/XYPad/context.tsx +1 -1
- package/src/components/XYPad/index.tsx +74 -42
- package/src/components/_util/VisuallyHiddenRangeInput.tsx +1 -1
- package/src/hooks/_internal/useChangeGesture.ts +93 -0
- package/src/hooks/_internal/useCheckSteps.ts +1 -1
- package/src/hooks/useAnimationFrame.ts +37 -15
- package/src/hooks/useEventListener.ts +93 -22
- package/src/hooks/useWheel.ts +4 -4
- package/src/index.ts +9 -5
- package/src/hooks/useInterval.ts +0 -27
|
@@ -9,16 +9,15 @@ import {
|
|
|
9
9
|
|
|
10
10
|
export type PointsEditorContextValue = {
|
|
11
11
|
disabled: boolean
|
|
12
|
-
|
|
12
|
+
readOnly: boolean
|
|
13
13
|
/** Inherited by every `Point`; `null` turns the wheel off. */
|
|
14
14
|
wheel: ModifierValue<InputEventOption> | null
|
|
15
15
|
/** Inherited by every `Point`; `null` turns the keyboard off. */
|
|
16
16
|
keyboard: ModifierValue<InputEventOption> | null
|
|
17
17
|
/** Inherited by every `Point`. See `PointsEditorProps.dragSensitivity`. */
|
|
18
18
|
dragSensitivity: ModifierValue<number>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
19
|
+
/** Inherited by every `Point`. See `PointsEditorProps.dragCursor`. */
|
|
20
|
+
dragCursor: CSSProperties['cursor']
|
|
22
21
|
/**
|
|
23
22
|
* `Container` registers its element here; `Point` normalizes the pointer
|
|
24
23
|
* against it, so a point is placed by its position within the container.
|
|
@@ -14,10 +14,12 @@ import {
|
|
|
14
14
|
DEFAULT_DRAG_SENSITIVITY,
|
|
15
15
|
type InputEventOption,
|
|
16
16
|
type ModifierValue,
|
|
17
|
-
POINTS_EDITOR_DEFAULT_KEYBOARD,
|
|
18
|
-
POINTS_EDITOR_DEFAULT_WHEEL,
|
|
19
17
|
type SelectionBoxRect,
|
|
20
18
|
} from '@tremolo-ui/dom'
|
|
19
|
+
import {
|
|
20
|
+
POINTS_EDITOR_DEFAULT_KEYBOARD,
|
|
21
|
+
POINTS_EDITOR_DEFAULT_WHEEL,
|
|
22
|
+
} from '@tremolo-ui/dom/internal'
|
|
21
23
|
|
|
22
24
|
import { Background } from './Background'
|
|
23
25
|
import { Container } from './Container'
|
|
@@ -38,17 +40,15 @@ export interface PointsEditorProps {
|
|
|
38
40
|
* Make the points unmovable.
|
|
39
41
|
* The parts carry `data-readonly` while it is set.
|
|
40
42
|
*/
|
|
41
|
-
|
|
43
|
+
readOnly?: boolean
|
|
42
44
|
|
|
43
45
|
/**
|
|
44
46
|
* The cursor to show while dragging a point. It is set on the dragged
|
|
45
47
|
* point, so it stays while the pointer is outside it.
|
|
46
48
|
*
|
|
47
|
-
* @default
|
|
49
|
+
* @default 'grabbing'
|
|
48
50
|
*/
|
|
49
|
-
|
|
50
|
-
cursor?: CSSProperties['cursor']
|
|
51
|
-
}
|
|
51
|
+
dragCursor?: CSSProperties['cursor']
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
54
|
* How much one notch of the wheel moves the focused `Point`. Scrolling
|
|
@@ -155,7 +155,7 @@ export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
|
155
155
|
(
|
|
156
156
|
{
|
|
157
157
|
disabled = false,
|
|
158
|
-
|
|
158
|
+
readOnly = false,
|
|
159
159
|
wheel = POINTS_EDITOR_DEFAULT_WHEEL,
|
|
160
160
|
keyboard = POINTS_EDITOR_DEFAULT_KEYBOARD,
|
|
161
161
|
dragSensitivity = DEFAULT_DRAG_SENSITIVITY,
|
|
@@ -163,7 +163,7 @@ export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
|
163
163
|
selection: selectionProp,
|
|
164
164
|
defaultSelection,
|
|
165
165
|
onSelectionChange,
|
|
166
|
-
|
|
166
|
+
dragCursor = 'grabbing',
|
|
167
167
|
style,
|
|
168
168
|
className,
|
|
169
169
|
children,
|
|
@@ -174,7 +174,6 @@ export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
|
174
174
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
175
175
|
// Picked apart so that the memo below depends on values rather than on the
|
|
176
176
|
// object literal a caller writes inline, which is new on every render.
|
|
177
|
-
const { cursor = 'grabbing' } = externalStyles ?? {}
|
|
178
177
|
|
|
179
178
|
// --- selection ---
|
|
180
179
|
// The selection lives here, where React state can hold it; which points a
|
|
@@ -214,11 +213,11 @@ export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
|
214
213
|
const context = useMemo(
|
|
215
214
|
() => ({
|
|
216
215
|
disabled,
|
|
217
|
-
|
|
216
|
+
readOnly,
|
|
218
217
|
wheel,
|
|
219
218
|
keyboard,
|
|
220
219
|
dragSensitivity,
|
|
221
|
-
|
|
220
|
+
dragCursor,
|
|
222
221
|
containerRef,
|
|
223
222
|
selectable,
|
|
224
223
|
selection,
|
|
@@ -227,11 +226,11 @@ export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
|
227
226
|
}),
|
|
228
227
|
[
|
|
229
228
|
disabled,
|
|
230
|
-
|
|
229
|
+
readOnly,
|
|
231
230
|
wheel,
|
|
232
231
|
keyboard,
|
|
233
232
|
dragSensitivity,
|
|
234
|
-
|
|
233
|
+
dragCursor,
|
|
235
234
|
selectable,
|
|
236
235
|
selection,
|
|
237
236
|
editor,
|
|
@@ -245,7 +244,7 @@ export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
|
245
244
|
ref={forwardedRef}
|
|
246
245
|
className={className}
|
|
247
246
|
data-disabled={disabled ? '' : undefined}
|
|
248
|
-
data-readonly={
|
|
247
|
+
data-readonly={readOnly ? '' : undefined}
|
|
249
248
|
style={
|
|
250
249
|
{
|
|
251
250
|
// The layers inside are placed against this box.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ComponentPropsWithoutRef, CSSProperties } from 'react'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { type MarksOptions } from '@tremolo-ui/dom'
|
|
4
|
+
import { cssLength, sliderMarks } from '@tremolo-ui/dom/internal'
|
|
4
5
|
|
|
5
6
|
import { Placement } from '../_util/Placement'
|
|
6
7
|
|
|
@@ -40,7 +41,7 @@ export function Marks({
|
|
|
40
41
|
const min = useSliderContext((s) => s.min)
|
|
41
42
|
const max = useSliderContext((s) => s.max)
|
|
42
43
|
const step = useSliderContext((s) => s.step)
|
|
43
|
-
const vertical = useSliderContext((s) => s.vertical)
|
|
44
|
+
const vertical = useSliderContext((s) => s.orientation === 'vertical')
|
|
44
45
|
const reverse = useSliderContext((s) => s.reverse)
|
|
45
46
|
|
|
46
47
|
const optionsList = options ? sliderMarks(options, min, max, step) : []
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ComponentPropsWithoutRef, CSSProperties } from 'react'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { valuePercent } from '@tremolo-ui/dom'
|
|
4
|
+
import { cssLength } from '@tremolo-ui/dom/internal'
|
|
4
5
|
|
|
5
6
|
import { useCheckPlacement } from '../_util/Placement'
|
|
6
7
|
|
|
@@ -62,7 +63,7 @@ export function MarksOption({
|
|
|
62
63
|
const min = useSliderContext((s) => s.min)
|
|
63
64
|
const max = useSliderContext((s) => s.max)
|
|
64
65
|
const scale = useSliderContext((s) => s.scale)
|
|
65
|
-
const vertical = useSliderContext((s) => s.vertical)
|
|
66
|
+
const vertical = useSliderContext((s) => s.orientation === 'vertical')
|
|
66
67
|
const reverse = useSliderContext((s) => s.reverse)
|
|
67
68
|
|
|
68
69
|
// The marks have to sit on the same curve the thumb runs along.
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ComponentPropsWithoutRef,
|
|
3
|
+
CSSProperties,
|
|
3
4
|
forwardRef,
|
|
5
|
+
Ref,
|
|
4
6
|
useImperativeHandle,
|
|
5
7
|
useRef,
|
|
6
|
-
CSSProperties,
|
|
7
8
|
} from 'react'
|
|
8
9
|
|
|
9
10
|
import { useCheckPlacement } from '../_util/Placement'
|
|
@@ -22,6 +23,12 @@ export interface SliderThumbProps {
|
|
|
22
23
|
color?: string
|
|
23
24
|
|
|
24
25
|
style?: CSSProperties & CSSVariables<'color' | 'translate'>
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Receives `focus` and `blur` for the range input inside, which do nothing
|
|
29
|
+
* while the slider is disabled. `ref` reaches the thumb element itself.
|
|
30
|
+
*/
|
|
31
|
+
actionsRef?: Ref<SliderThumbMethods>
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
export interface SliderThumbMethods {
|
|
@@ -32,7 +39,7 @@ export interface SliderThumbMethods {
|
|
|
32
39
|
type Props = SliderThumbProps &
|
|
33
40
|
Omit<ComponentPropsWithoutRef<'div'>, keyof SliderThumbProps>
|
|
34
41
|
|
|
35
|
-
export const Thumb = /* @__PURE__ */ forwardRef<
|
|
42
|
+
export const Thumb = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
36
43
|
function Thumb(
|
|
37
44
|
{
|
|
38
45
|
color,
|
|
@@ -48,6 +55,7 @@ export const Thumb = /* @__PURE__ */ forwardRef<SliderThumbMethods, Props>(
|
|
|
48
55
|
'aria-labelledby': ariaLabelledby,
|
|
49
56
|
'aria-describedby': ariaDescribedby,
|
|
50
57
|
'aria-valuetext': ariaValuetext,
|
|
58
|
+
actionsRef,
|
|
51
59
|
...props
|
|
52
60
|
},
|
|
53
61
|
forwardedRef,
|
|
@@ -58,9 +66,9 @@ export const Thumb = /* @__PURE__ */ forwardRef<SliderThumbMethods, Props>(
|
|
|
58
66
|
min,
|
|
59
67
|
max,
|
|
60
68
|
step,
|
|
61
|
-
|
|
69
|
+
orientation,
|
|
62
70
|
disabled,
|
|
63
|
-
|
|
71
|
+
readOnly,
|
|
64
72
|
onChange,
|
|
65
73
|
percent,
|
|
66
74
|
thumbRef,
|
|
@@ -78,15 +86,16 @@ export const Thumb = /* @__PURE__ */ forwardRef<SliderThumbMethods, Props>(
|
|
|
78
86
|
},
|
|
79
87
|
})
|
|
80
88
|
|
|
81
|
-
useImperativeHandle(
|
|
89
|
+
useImperativeHandle(actionsRef, methods, [disabled])
|
|
82
90
|
// Root focuses the thumb when a drag starts, wherever the user placed it.
|
|
83
91
|
useImperativeHandle(thumbRef, methods, [disabled])
|
|
84
92
|
|
|
85
93
|
return (
|
|
86
94
|
<div
|
|
95
|
+
ref={forwardedRef}
|
|
87
96
|
className={className}
|
|
88
97
|
data-disabled={disabled ? '' : undefined}
|
|
89
|
-
data-readonly={
|
|
98
|
+
data-readonly={readOnly ? '' : undefined}
|
|
90
99
|
{...props}
|
|
91
100
|
style={{
|
|
92
101
|
...{ '--color': color },
|
|
@@ -101,8 +110,8 @@ export const Thumb = /* @__PURE__ */ forwardRef<SliderThumbMethods, Props>(
|
|
|
101
110
|
// Where the thumb sits is the component's decision, not a style: a
|
|
102
111
|
// `left` from the caller would take it off the track, so it is
|
|
103
112
|
// written after theirs.
|
|
104
|
-
top: vertical ? `${percent}%` : '50%',
|
|
105
|
-
left:
|
|
113
|
+
top: orientation === 'vertical' ? `${percent}%` : '50%',
|
|
114
|
+
left: orientation === 'vertical' ? '50%' : `${percent}%`,
|
|
106
115
|
}}
|
|
107
116
|
>
|
|
108
117
|
<VisuallyHiddenRangeInput
|
|
@@ -112,14 +121,14 @@ export const Thumb = /* @__PURE__ */ forwardRef<SliderThumbMethods, Props>(
|
|
|
112
121
|
max={max}
|
|
113
122
|
step={step}
|
|
114
123
|
disabled={disabled}
|
|
115
|
-
aria-readonly={
|
|
116
|
-
aria-orientation={
|
|
124
|
+
aria-readonly={readOnly}
|
|
125
|
+
aria-orientation={orientation}
|
|
117
126
|
aria-label={ariaLabel}
|
|
118
127
|
aria-labelledby={ariaLabelledby}
|
|
119
128
|
aria-describedby={ariaDescribedby}
|
|
120
129
|
aria-valuetext={ariaValuetext}
|
|
121
130
|
onChange={(event) => {
|
|
122
|
-
if (
|
|
131
|
+
if (readOnly) {
|
|
123
132
|
event.currentTarget.value = String(value)
|
|
124
133
|
return
|
|
125
134
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ComponentPropsWithoutRef, CSSProperties, forwardRef } from 'react'
|
|
2
2
|
|
|
3
|
-
import { cssLength } from '@tremolo-ui/dom'
|
|
3
|
+
import { cssLength } from '@tremolo-ui/dom/internal'
|
|
4
4
|
|
|
5
5
|
import { useComposedRefs } from '../../compose-refs'
|
|
6
6
|
import { Placement } from '../_util/Placement'
|
|
@@ -18,13 +18,15 @@ export interface SliderTrackProps {
|
|
|
18
18
|
/** How thick the track is across that axis. Sets `--thickness`. */
|
|
19
19
|
thickness?: number | string
|
|
20
20
|
|
|
21
|
-
/** Colour of the part from `min` to the value. Sets `--active`. */
|
|
22
|
-
|
|
23
|
-
/** Colour of the rest of the track. Sets `--inactive`. */
|
|
24
|
-
|
|
21
|
+
/** Colour of the part from `min` to the value. Sets `--active-color`. */
|
|
22
|
+
activeColor?: string
|
|
23
|
+
/** Colour of the rest of the track. Sets `--inactive-color`. */
|
|
24
|
+
inactiveColor?: string
|
|
25
25
|
|
|
26
26
|
style?: CSSProperties &
|
|
27
|
-
CSSVariables<
|
|
27
|
+
CSSVariables<
|
|
28
|
+
'length' | 'thickness' | 'active-color' | 'inactive-color' | 'percent'
|
|
29
|
+
>
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
type Props = SliderTrackProps &
|
|
@@ -35,8 +37,8 @@ export const Track = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
|
35
37
|
{
|
|
36
38
|
length,
|
|
37
39
|
thickness,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
activeColor,
|
|
41
|
+
inactiveColor,
|
|
40
42
|
children,
|
|
41
43
|
className,
|
|
42
44
|
style,
|
|
@@ -44,7 +46,7 @@ export const Track = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
|
44
46
|
},
|
|
45
47
|
forwardedRef,
|
|
46
48
|
) {
|
|
47
|
-
const {
|
|
49
|
+
const { orientation, reverse, disabled, percent, trackRef } =
|
|
48
50
|
useSliderContext()
|
|
49
51
|
|
|
50
52
|
// The track is what the pointer position is normalized against, so the
|
|
@@ -56,14 +58,14 @@ export const Track = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
|
56
58
|
ref={composedRef}
|
|
57
59
|
className={className}
|
|
58
60
|
data-disabled={disabled ? '' : undefined}
|
|
59
|
-
data-orientation={
|
|
61
|
+
data-orientation={orientation}
|
|
60
62
|
// Which end the value grows from. `percent` is already the position on
|
|
61
63
|
// screen, so this only says which side of it is the filled one.
|
|
62
|
-
data-flipped={vertical !== reverse ? '' : undefined}
|
|
64
|
+
data-flipped={(orientation === 'vertical') !== reverse ? '' : undefined}
|
|
63
65
|
style={
|
|
64
66
|
{
|
|
65
|
-
'--active':
|
|
66
|
-
'--inactive':
|
|
67
|
+
'--active-color': activeColor,
|
|
68
|
+
'--inactive-color': inactiveColor,
|
|
67
69
|
'--length': cssLength(length),
|
|
68
70
|
'--thickness': cssLength(thickness),
|
|
69
71
|
// Where the value sits, for the theme to paint the fill with. The
|
|
@@ -11,14 +11,14 @@ export type SliderContextValue = {
|
|
|
11
11
|
step: number
|
|
12
12
|
/** How the value is distributed across the travel. */
|
|
13
13
|
scale: Scale
|
|
14
|
-
|
|
14
|
+
orientation: 'horizontal' | 'vertical'
|
|
15
15
|
reverse: boolean
|
|
16
16
|
disabled: boolean
|
|
17
|
-
|
|
17
|
+
readOnly: boolean
|
|
18
18
|
onChange?: (value: number) => void
|
|
19
19
|
/**
|
|
20
20
|
* Position of the thumb, 0-100, already accounting for the display direction
|
|
21
|
-
* implied by `
|
|
21
|
+
* implied by `orientation` and `reverse`.
|
|
22
22
|
*/
|
|
23
23
|
percent: number
|
|
24
24
|
|
|
@@ -3,6 +3,7 @@ import React, {
|
|
|
3
3
|
CSSProperties,
|
|
4
4
|
forwardRef,
|
|
5
5
|
ReactNode,
|
|
6
|
+
Ref,
|
|
6
7
|
useCallback,
|
|
7
8
|
useImperativeHandle,
|
|
8
9
|
useMemo,
|
|
@@ -10,22 +11,26 @@ import React, {
|
|
|
10
11
|
} from 'react'
|
|
11
12
|
|
|
12
13
|
import {
|
|
13
|
-
applyDelta,
|
|
14
|
-
arrowKeyDirection,
|
|
15
14
|
type AxisOptions,
|
|
15
|
+
type ChangeSource,
|
|
16
16
|
DEFAULT_DRAG_SENSITIVITY,
|
|
17
17
|
DEFAULT_KEYBOARD_OPTIONS,
|
|
18
18
|
DEFAULT_WHEEL_OPTIONS,
|
|
19
19
|
type InputEventOption,
|
|
20
20
|
type ModifierValue,
|
|
21
|
-
selectModifier,
|
|
22
21
|
valuePercent,
|
|
23
|
-
wheelDirection,
|
|
24
22
|
type XY,
|
|
25
23
|
} from '@tremolo-ui/dom'
|
|
24
|
+
import {
|
|
25
|
+
applyDelta,
|
|
26
|
+
arrowKeyDirection,
|
|
27
|
+
selectModifier,
|
|
28
|
+
wheelDirection,
|
|
29
|
+
} from '@tremolo-ui/dom/internal'
|
|
26
30
|
import { linearScale, type Scale } from '@tremolo-ui/functions'
|
|
27
31
|
|
|
28
32
|
import { useComposedRefs } from '../../compose-refs'
|
|
33
|
+
import { useChangeGesture } from '../../hooks/_internal/useChangeGesture'
|
|
29
34
|
import { useCheckSteps } from '../../hooks/_internal/useCheckSteps'
|
|
30
35
|
import { useDragValue } from '../../hooks/useDragValue'
|
|
31
36
|
import { useWheel } from '../../hooks/useWheel'
|
|
@@ -36,10 +41,6 @@ import { MarksOption } from './MarksOption'
|
|
|
36
41
|
import { Thumb, SliderThumbMethods } from './Thumb'
|
|
37
42
|
import { Track } from './Track'
|
|
38
43
|
|
|
39
|
-
const defaultExternalStyles: SliderProps['externalStyles'] = {
|
|
40
|
-
cursor: 'pointer',
|
|
41
|
-
}
|
|
42
|
-
|
|
43
44
|
export interface SliderProps {
|
|
44
45
|
/** The current value. The slider shows only this, so update it from `onChange`. */
|
|
45
46
|
value: number
|
|
@@ -66,14 +67,15 @@ export interface SliderProps {
|
|
|
66
67
|
*/
|
|
67
68
|
scale?: Scale
|
|
68
69
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
70
|
+
* Which way the slider runs. A vertical slider grows upwards. Every part
|
|
71
|
+
* carries the same value as `data-orientation`, and the range input inside
|
|
72
|
+
* the thumb takes its `aria-orientation` from it.
|
|
71
73
|
*
|
|
72
|
-
* @default
|
|
74
|
+
* @default 'horizontal'
|
|
73
75
|
*/
|
|
74
|
-
|
|
76
|
+
orientation?: 'horizontal' | 'vertical'
|
|
75
77
|
/**
|
|
76
|
-
* Grow the value the other way: leftwards, or downwards when
|
|
78
|
+
* Grow the value the other way: leftwards, or downwards when vertical.
|
|
77
79
|
* The arrow keys follow the direction on screen.
|
|
78
80
|
*
|
|
79
81
|
* @default false
|
|
@@ -84,11 +86,9 @@ export interface SliderProps {
|
|
|
84
86
|
* The cursor to show while dragging. It is set on the dragged element, so it
|
|
85
87
|
* stays while the pointer is outside the slider.
|
|
86
88
|
*
|
|
87
|
-
* @default
|
|
89
|
+
* @default 'pointer'
|
|
88
90
|
*/
|
|
89
|
-
|
|
90
|
-
cursor?: CSSProperties['cursor']
|
|
91
|
-
}
|
|
91
|
+
dragCursor?: CSSProperties['cursor']
|
|
92
92
|
/**
|
|
93
93
|
* How much one notch of the wheel moves the value. It only acts while the
|
|
94
94
|
* focus is inside, so that scrolling the page past the slider leaves it
|
|
@@ -145,13 +145,28 @@ export interface SliderProps {
|
|
|
145
145
|
* Make the value unchangeable.
|
|
146
146
|
* The parts carry `data-readonly` while it is set.
|
|
147
147
|
*/
|
|
148
|
-
|
|
148
|
+
readOnly?: boolean
|
|
149
149
|
/** Called with the new value when a drag, the wheel or an arrow key moves it. */
|
|
150
150
|
onChange?: (value: number) => void
|
|
151
|
-
/**
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
151
|
+
/**
|
|
152
|
+
* Called when a change of the value starts — a press on the track, the first wheel notch
|
|
153
|
+
* or arrow key — with the value
|
|
154
|
+
* before it and what it is made with. A host recording automation can treat
|
|
155
|
+
* the control as touched from here until `onChangeEnd`.
|
|
156
|
+
*/
|
|
157
|
+
onChangeStart?: (value: number, source: ChangeSource) => void
|
|
158
|
+
/**
|
|
159
|
+
* Called when the change ends, with the value it ended on: on release, or
|
|
160
|
+
* `changeEndDelay` after the last wheel notch or arrow key.
|
|
161
|
+
*/
|
|
162
|
+
onChangeEnd?: (value: number, source: ChangeSource) => void
|
|
163
|
+
/**
|
|
164
|
+
* How long after the last wheel notch or arrow key the change counts as
|
|
165
|
+
* over, in milliseconds. Neither has an event that says it is done.
|
|
166
|
+
*
|
|
167
|
+
* @default 500
|
|
168
|
+
*/
|
|
169
|
+
changeEndDelay?: number
|
|
155
170
|
/**
|
|
156
171
|
* The slider renders exactly what you compose here; there is no default
|
|
157
172
|
* markup to fall back to.
|
|
@@ -164,6 +179,13 @@ export interface SliderProps {
|
|
|
164
179
|
* </Slider.Root>
|
|
165
180
|
*/
|
|
166
181
|
children: ReactNode
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Receives `focus` and `blur`, which act on the range input inside the thumb —
|
|
185
|
+
* the element that takes the focus — and do nothing while the slider is
|
|
186
|
+
* disabled. `ref` reaches the root element itself.
|
|
187
|
+
*/
|
|
188
|
+
actionsRef?: Ref<SliderMethods>
|
|
167
189
|
}
|
|
168
190
|
|
|
169
191
|
export interface SliderMethods {
|
|
@@ -180,7 +202,7 @@ type Props = SliderProps &
|
|
|
180
202
|
*/
|
|
181
203
|
const WHEEL_OPTIONS = { requireFocus: true }
|
|
182
204
|
|
|
183
|
-
export const Root = /* @__PURE__ */ forwardRef<
|
|
205
|
+
export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
184
206
|
(
|
|
185
207
|
{
|
|
186
208
|
value,
|
|
@@ -188,17 +210,18 @@ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
|
|
|
188
210
|
max,
|
|
189
211
|
step = 1,
|
|
190
212
|
scale = linearScale,
|
|
191
|
-
|
|
213
|
+
orientation = 'horizontal',
|
|
192
214
|
reverse = false,
|
|
193
|
-
|
|
215
|
+
dragCursor = 'pointer',
|
|
194
216
|
wheel = DEFAULT_WHEEL_OPTIONS,
|
|
195
217
|
keyboard = DEFAULT_KEYBOARD_OPTIONS,
|
|
196
218
|
dragSensitivity = DEFAULT_DRAG_SENSITIVITY,
|
|
197
219
|
disabled = false,
|
|
198
|
-
|
|
220
|
+
readOnly = false,
|
|
199
221
|
onChange,
|
|
200
|
-
|
|
201
|
-
|
|
222
|
+
onChangeStart,
|
|
223
|
+
onChangeEnd,
|
|
224
|
+
changeEndDelay,
|
|
202
225
|
className,
|
|
203
226
|
style,
|
|
204
227
|
children,
|
|
@@ -206,6 +229,7 @@ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
|
|
|
206
229
|
onBlur,
|
|
207
230
|
onPointerDown,
|
|
208
231
|
onKeyDown,
|
|
232
|
+
actionsRef,
|
|
209
233
|
...props
|
|
210
234
|
}: Props,
|
|
211
235
|
forwardedRef,
|
|
@@ -214,14 +238,14 @@ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
|
|
|
214
238
|
const trackRef = useRef<HTMLDivElement>(null)
|
|
215
239
|
const thumbRef = useRef<SliderThumbMethods>(null)
|
|
216
240
|
// --- interpret props ---
|
|
217
|
-
const
|
|
218
|
-
const inactive = disabled || readonly
|
|
241
|
+
const inactive = disabled || readOnly
|
|
219
242
|
|
|
220
243
|
// Measured from the left or the top, as CSS places things:
|
|
221
244
|
// normal -> normal (right)
|
|
222
245
|
// vertical -> reversed (up)
|
|
223
246
|
// reverse -> reversed (left)
|
|
224
247
|
// vertical & reverse -> normal (down)
|
|
248
|
+
const vertical = orientation === 'vertical'
|
|
225
249
|
const displayReversed = vertical !== reverse
|
|
226
250
|
const percent = valuePercent(value, { min, max, scale }, displayReversed)
|
|
227
251
|
|
|
@@ -236,13 +260,27 @@ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
|
|
|
236
260
|
|
|
237
261
|
useCheckSteps({ component: 'Slider', range: axis, keyboard, wheel })
|
|
238
262
|
|
|
263
|
+
const gesture = useChangeGesture(
|
|
264
|
+
value,
|
|
265
|
+
{ onChangeStart, onChangeEnd, changeEndDelay },
|
|
266
|
+
inactive,
|
|
267
|
+
)
|
|
268
|
+
const change = useCallback(
|
|
269
|
+
(next: number) => {
|
|
270
|
+
gesture.changed(next)
|
|
271
|
+
onChange?.(next)
|
|
272
|
+
},
|
|
273
|
+
[gesture, onChange],
|
|
274
|
+
)
|
|
275
|
+
|
|
239
276
|
const handleKeyDown = useCallback(
|
|
240
277
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
241
278
|
const direction = arrowKeyDirection(event.key)
|
|
242
279
|
if (direction === null) return
|
|
243
280
|
event.preventDefault()
|
|
244
281
|
if (!keyboard || !onChange || inactive) return
|
|
245
|
-
|
|
282
|
+
gesture.pulse('keyboard')
|
|
283
|
+
change(
|
|
246
284
|
applyDelta(
|
|
247
285
|
value,
|
|
248
286
|
reverse ? -direction : direction,
|
|
@@ -252,7 +290,7 @@ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
|
|
|
252
290
|
),
|
|
253
291
|
)
|
|
254
292
|
},
|
|
255
|
-
[keyboard, onChange, inactive, reverse, value, axis],
|
|
293
|
+
[keyboard, onChange, inactive, reverse, value, axis, gesture, change],
|
|
256
294
|
)
|
|
257
295
|
|
|
258
296
|
// --- hooks ---
|
|
@@ -261,24 +299,21 @@ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
|
|
|
261
299
|
const { refCallback: dragRefCallback } = useDragValue<HTMLDivElement>({
|
|
262
300
|
axis,
|
|
263
301
|
baseElementRef: trackRef,
|
|
264
|
-
cursor: inactive ? undefined :
|
|
302
|
+
cursor: inactive ? undefined : dragCursor,
|
|
265
303
|
shouldStart: () => !inactive,
|
|
266
304
|
sensitivity: (state) =>
|
|
267
305
|
selectModifier(dragSensitivity, state.event).value,
|
|
268
306
|
updateOnPointerDown: true,
|
|
269
307
|
onChange: (v) => {
|
|
270
308
|
if (inactive) return
|
|
271
|
-
|
|
309
|
+
change(valueOf(v))
|
|
272
310
|
},
|
|
273
|
-
onDragStart: (
|
|
311
|
+
onDragStart: () => {
|
|
274
312
|
if (inactive) return
|
|
313
|
+
gesture.hold('pointer')
|
|
275
314
|
thumbRef.current?.focus()
|
|
276
|
-
onDragStart?.(valueOf(v))
|
|
277
|
-
},
|
|
278
|
-
onDragEnd: (v) => {
|
|
279
|
-
if (inactive) return
|
|
280
|
-
onDragEnd?.(valueOf(v))
|
|
281
315
|
},
|
|
316
|
+
onDragEnd: () => gesture.end(),
|
|
282
317
|
})
|
|
283
318
|
|
|
284
319
|
const wheelRefCallback = useWheel<HTMLDivElement>((event) => {
|
|
@@ -288,7 +323,8 @@ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
|
|
|
288
323
|
const direction = wheelDirection(event, { horizontal: !vertical })
|
|
289
324
|
if (direction === null) return
|
|
290
325
|
event.preventDefault()
|
|
291
|
-
|
|
326
|
+
gesture.pulse('wheel')
|
|
327
|
+
change(
|
|
292
328
|
applyDelta(value, reverse ? -direction : direction, wheel, axis, event),
|
|
293
329
|
)
|
|
294
330
|
}, WHEEL_OPTIONS)
|
|
@@ -296,6 +332,7 @@ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
|
|
|
296
332
|
// Composed once, so React attaches the refs a single time instead of
|
|
297
333
|
// detaching and re-attaching on every render.
|
|
298
334
|
const rootRefCallback = useComposedRefs<HTMLDivElement>(
|
|
335
|
+
forwardedRef,
|
|
299
336
|
dragRefCallback,
|
|
300
337
|
wheelRefCallback,
|
|
301
338
|
)
|
|
@@ -307,10 +344,10 @@ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
|
|
|
307
344
|
max,
|
|
308
345
|
step,
|
|
309
346
|
scale,
|
|
310
|
-
|
|
347
|
+
orientation,
|
|
311
348
|
reverse,
|
|
312
349
|
disabled,
|
|
313
|
-
|
|
350
|
+
readOnly,
|
|
314
351
|
onChange,
|
|
315
352
|
percent,
|
|
316
353
|
trackRef,
|
|
@@ -322,16 +359,16 @@ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
|
|
|
322
359
|
max,
|
|
323
360
|
step,
|
|
324
361
|
scale,
|
|
325
|
-
|
|
362
|
+
orientation,
|
|
326
363
|
reverse,
|
|
327
364
|
disabled,
|
|
328
|
-
|
|
365
|
+
readOnly,
|
|
329
366
|
onChange,
|
|
330
367
|
percent,
|
|
331
368
|
],
|
|
332
369
|
)
|
|
333
370
|
|
|
334
|
-
useImperativeHandle(
|
|
371
|
+
useImperativeHandle(actionsRef, () => {
|
|
335
372
|
return {
|
|
336
373
|
focus() {
|
|
337
374
|
if (!disabled) thumbRef.current?.focus()
|
|
@@ -354,9 +391,9 @@ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
|
|
|
354
391
|
// body — undoing the focus the drag just gave the input. Taking the
|
|
355
392
|
// focus here keeps it inside, and `onFocus` passes it to the input.
|
|
356
393
|
tabIndex={-1}
|
|
357
|
-
data-orientation={
|
|
394
|
+
data-orientation={orientation}
|
|
358
395
|
data-disabled={disabled ? '' : undefined}
|
|
359
|
-
data-readonly={
|
|
396
|
+
data-readonly={readOnly ? '' : undefined}
|
|
360
397
|
style={style}
|
|
361
398
|
onPointerDown={onPointerDown}
|
|
362
399
|
onKeyDown={(event) => {
|