@tremolo-ui/react 0.4.0 → 0.6.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/compose-refs.cjs +72 -0
- package/dist/compose-refs.cjs.map +1 -0
- package/dist/compose-refs.d.cts +43 -0
- package/dist/compose-refs.d.cts.map +1 -0
- package/dist/compose-refs.d.ts +43 -0
- package/dist/compose-refs.d.ts.map +1 -0
- package/dist/compose-refs.js +70 -0
- package/dist/compose-refs.js.map +1 -0
- package/dist/index.cjs +2272 -1650
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1461 -475
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +1461 -475
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2258 -1648
- package/dist/index.js.map +1 -1
- package/package.json +32 -31
- package/src/components/AnimationCanvas/index.tsx +162 -194
- package/src/components/Knob/ActiveLine.tsx +9 -8
- package/src/components/Knob/InactiveLine.tsx +11 -14
- package/src/components/Knob/SVGRoot.tsx +10 -17
- package/src/components/Knob/Thumb.tsx +33 -11
- package/src/components/Knob/context.tsx +73 -89
- package/src/components/Knob/index.tsx +227 -105
- package/src/components/NumberInput/DecrementStepper.tsx +21 -50
- package/src/components/NumberInput/IncrementStepper.tsx +22 -50
- package/src/components/NumberInput/InputField.tsx +206 -0
- package/src/components/NumberInput/Stepper.tsx +150 -21
- package/src/components/NumberInput/StepperButton.tsx +95 -0
- package/src/components/NumberInput/context.tsx +104 -149
- package/src/components/NumberInput/index.tsx +461 -110
- package/src/components/Piano/index.tsx +413 -263
- package/src/components/Piano/keyboard-shortcuts.ts +61 -0
- package/src/components/PointsEditor/Background.tsx +19 -4
- package/src/components/PointsEditor/Container.tsx +92 -22
- package/src/components/PointsEditor/Point.tsx +293 -53
- package/src/components/PointsEditor/SelectionBox.tsx +54 -0
- package/src/components/PointsEditor/context.tsx +97 -75
- package/src/components/PointsEditor/index.tsx +497 -62
- package/src/components/Slider/Marks.tsx +84 -0
- package/src/components/Slider/MarksOption.tsx +115 -0
- package/src/components/Slider/Thumb.tsx +99 -54
- package/src/components/Slider/Track.tsx +73 -62
- package/src/components/Slider/context.tsx +40 -53
- package/src/components/Slider/index.tsx +237 -190
- package/src/components/Slider/type.ts +27 -9
- package/src/components/XYPad/Area.tsx +52 -33
- package/src/components/XYPad/Thumb.tsx +121 -60
- package/src/components/XYPad/context.tsx +49 -0
- package/src/components/XYPad/index.tsx +338 -246
- package/src/components/_util/Placement.tsx +68 -0
- package/src/components/_util/VisuallyHiddenRangeInput.tsx +24 -0
- package/src/components/_util/css-length.ts +13 -0
- package/src/{components/_util/composeRefs.tsx → compose-refs/index.tsx} +33 -5
- package/src/hooks/{useCallbackRef.ts → _internal/useCallbackRef.ts} +3 -1
- package/src/hooks/_internal/useCheckSteps.ts +170 -0
- package/src/hooks/useAnimationFrame.ts +17 -7
- package/src/hooks/useDrag.ts +39 -9
- package/src/hooks/useDragValue.ts +194 -0
- package/src/hooks/useEventListener.ts +11 -7
- package/src/hooks/useLongPress.ts +42 -8
- package/src/hooks/useMIDIAccess.ts +82 -11
- package/src/hooks/useMIDIInput.ts +45 -13
- package/src/hooks/useMIDIMessage.ts +23 -5
- package/src/hooks/useWheel.ts +29 -9
- package/src/index.ts +50 -27
- package/src/input-event.ts +32 -0
- package/dist/index.css +0 -536
- package/src/Globals.d.ts +0 -3
- package/src/components/AnimationCanvas/canvas.ts +0 -46
- package/src/components/Knob/index.css +0 -77
- package/src/components/NumberInput/InternalInput.tsx +0 -216
- package/src/components/NumberInput/index.css +0 -185
- package/src/components/NumberInput/type.ts +0 -58
- package/src/components/Piano/KeyLabel.tsx +0 -51
- package/src/components/Piano/context.tsx +0 -73
- package/src/components/Piano/index.css +0 -90
- package/src/components/Piano/key.tsx +0 -205
- package/src/components/Piano/keyboardShortcuts.ts +0 -31
- package/src/components/PointsEditor/index.css +0 -30
- package/src/components/Slider/Scale.tsx +0 -52
- package/src/components/Slider/ScaleOption.tsx +0 -103
- package/src/components/Slider/index.css +0 -112
- package/src/components/XYPad/index.css +0 -48
- package/src/components/_util/index.ts +0 -28
- package/src/components/_util/type.ts +0 -1
- package/src/hooks/useDragWithElement.ts +0 -100
- package/src/hooks/usePianoDrag.ts +0 -75
- package/src/hooks/useRefCallbackEvent.ts +0 -34
- package/src/styles/global.css +0 -19
|
@@ -1,92 +1,172 @@
|
|
|
1
|
-
import clsx from 'clsx'
|
|
2
1
|
import React, {
|
|
3
2
|
ComponentPropsWithoutRef,
|
|
4
3
|
CSSProperties,
|
|
5
4
|
forwardRef,
|
|
6
|
-
|
|
5
|
+
ReactNode,
|
|
7
6
|
useCallback,
|
|
8
7
|
useImperativeHandle,
|
|
9
8
|
useMemo,
|
|
10
9
|
useRef,
|
|
11
10
|
} from 'react'
|
|
12
11
|
|
|
12
|
+
import type { AxisOptions, XY } from '@tremolo-ui/dom'
|
|
13
13
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
rawValue,
|
|
17
|
-
stepValue,
|
|
14
|
+
applyDelta,
|
|
15
|
+
linearScale,
|
|
18
16
|
toFixed,
|
|
19
|
-
InputEventOption,
|
|
20
|
-
|
|
17
|
+
type InputEventOption,
|
|
18
|
+
type ModifierValue,
|
|
19
|
+
selectModifier,
|
|
21
20
|
xor,
|
|
21
|
+
type Scale,
|
|
22
22
|
} from '@tremolo-ui/functions'
|
|
23
23
|
|
|
24
|
-
import {
|
|
24
|
+
import { useComposedRefs } from '../../compose-refs'
|
|
25
|
+
import { useCheckSteps } from '../../hooks/_internal/useCheckSteps'
|
|
26
|
+
import { useDragValue } from '../../hooks/useDragValue'
|
|
25
27
|
import { useWheel } from '../../hooks/useWheel'
|
|
26
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
DEFAULT_DRAG_SENSITIVITY,
|
|
30
|
+
DEFAULT_KEYBOARD_OPTIONS,
|
|
31
|
+
DEFAULT_WHEEL_OPTIONS,
|
|
32
|
+
} from '../../input-event'
|
|
27
33
|
|
|
28
34
|
import { SliderProvider } from './context'
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import { Thumb, SliderThumbMethods
|
|
32
|
-
import { Track
|
|
35
|
+
import { Marks } from './Marks'
|
|
36
|
+
import { MarksOption } from './MarksOption'
|
|
37
|
+
import { Thumb, SliderThumbMethods } from './Thumb'
|
|
38
|
+
import { Track } from './Track'
|
|
33
39
|
|
|
34
40
|
const defaultExternalStyles: SliderProps['externalStyles'] = {
|
|
35
|
-
userSelectNone: true,
|
|
36
41
|
cursor: 'pointer',
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
export interface SliderProps {
|
|
40
|
-
|
|
45
|
+
/** The current value. The slider shows only this, so update it from `onChange`. */
|
|
41
46
|
value: number
|
|
47
|
+
/** The value at the start of the travel. */
|
|
42
48
|
min: number
|
|
49
|
+
/** The value at the end of the travel. */
|
|
43
50
|
max: number
|
|
44
51
|
|
|
45
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Granularity of the value. A drag, the wheel and the arrow keys snap it to
|
|
54
|
+
* multiples of `step`.
|
|
55
|
+
*
|
|
56
|
+
* @default 1
|
|
57
|
+
*/
|
|
46
58
|
step?: number
|
|
47
|
-
skew?: number // TODO | SkewFunction
|
|
48
59
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
60
|
+
* How the value is distributed across the travel.
|
|
61
|
+
*
|
|
62
|
+
* Pick one of the scales from `@tremolo-ui/functions`: `linearScale`,
|
|
63
|
+
* `exponentialScale`, `curveScale(n)`, `symmetricSkewScale(n)`, or
|
|
64
|
+
* `skewScale(n)` for a value that has to match a JUCE parameter.
|
|
65
|
+
*
|
|
66
|
+
* @default linearScale
|
|
67
|
+
*/
|
|
68
|
+
scale?: Scale
|
|
69
|
+
/**
|
|
70
|
+
* Run the slider vertically, with the value growing upwards. The range input
|
|
71
|
+
* inside the thumb takes its `aria-orientation` from this.
|
|
72
|
+
*
|
|
73
|
+
* @default false
|
|
51
74
|
*/
|
|
52
75
|
vertical?: boolean
|
|
76
|
+
/**
|
|
77
|
+
* Grow the value the other way: leftwards, or downwards when `vertical`.
|
|
78
|
+
* The arrow keys follow the direction on screen.
|
|
79
|
+
*
|
|
80
|
+
* @default false
|
|
81
|
+
*/
|
|
53
82
|
reverse?: boolean
|
|
54
83
|
|
|
55
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* The cursor to show while dragging. It is set on the dragged element, so it
|
|
86
|
+
* stays while the pointer is outside the slider.
|
|
87
|
+
*
|
|
88
|
+
* @default { cursor: 'pointer' }
|
|
89
|
+
*/
|
|
56
90
|
externalStyles?: {
|
|
57
|
-
|
|
58
|
-
cursor?: Cursor
|
|
91
|
+
cursor?: CSSProperties['cursor']
|
|
59
92
|
}
|
|
60
93
|
/**
|
|
61
|
-
* wheel
|
|
62
|
-
*
|
|
94
|
+
* How much one notch of the wheel moves the value. It only acts while the
|
|
95
|
+
* focus is inside, so that scrolling the page past the slider leaves it
|
|
96
|
+
* alone.
|
|
97
|
+
*
|
|
98
|
+
* `['raw', n]` moves the value by `n`, and `['normalized', n]` by `n` of the
|
|
99
|
+
* range between `min` and `max`. The result is snapped to `step`, except for
|
|
100
|
+
* an amount set on a modifier key (`{ default: …, shift: … }`). `null` turns
|
|
101
|
+
* the wheel off.
|
|
102
|
+
*
|
|
103
|
+
* @default ['raw', 1]
|
|
63
104
|
*/
|
|
64
|
-
wheel?: InputEventOption | null
|
|
105
|
+
wheel?: ModifierValue<InputEventOption> | null
|
|
65
106
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
107
|
+
* How much a drag moves the value, per modifier key.
|
|
108
|
+
*
|
|
109
|
+
* `1` is the pointer position itself, which is what a drag normally is here.
|
|
110
|
+
* **Anything else turns the drag relative**: `0.1` makes the same movement
|
|
111
|
+
* cover a tenth of the travel, so the value stops following the pointer and
|
|
112
|
+
* starts moving a tenth as fast. Shift is bound to `0.1` by default, to
|
|
113
|
+
* match what it does on the arrow keys.
|
|
114
|
+
*
|
|
115
|
+
* Pressing or releasing the key mid-drag does not disturb the value: the
|
|
116
|
+
* travel so far is kept and the new sensitivity applies from there. **The
|
|
117
|
+
* pointer and the value stay apart for the rest of the drag** — snapping
|
|
118
|
+
* them back together on release would move the value nobody asked to move.
|
|
119
|
+
*
|
|
120
|
+
* @default { default: 1, shift: 0.1 }
|
|
68
121
|
*/
|
|
69
|
-
|
|
122
|
+
dragSensitivity?: ModifierValue<number>
|
|
70
123
|
|
|
71
124
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
125
|
+
* How much one arrow key press moves the value.
|
|
126
|
+
*
|
|
127
|
+
* `['raw', n]` moves the value by `n`, and `['normalized', n]` by `n` of the
|
|
128
|
+
* range between `min` and `max`. The result is snapped to `step`, except for
|
|
129
|
+
* an amount set on a modifier key, which is what lets shift move off the
|
|
130
|
+
* grid. `null` turns the arrow keys off.
|
|
131
|
+
*
|
|
132
|
+
* The default moves by 1, and by 0.1 with shift. With a `step` above 1, raise
|
|
133
|
+
* the amount to match: 1 would round straight back to where it started, and
|
|
134
|
+
* a development build warns about it.
|
|
135
|
+
*
|
|
136
|
+
* @default { default: ['raw', 1], shift: ['raw', 0.1] }
|
|
137
|
+
*/
|
|
138
|
+
keyboard?: ModifierValue<InputEventOption> | null
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Make the slider unchangeable and remove it from the tab order.
|
|
142
|
+
* The parts carry `data-disabled` while it is set.
|
|
75
143
|
*/
|
|
76
144
|
disabled?: boolean
|
|
77
145
|
/**
|
|
78
146
|
* Make the value unchangeable.
|
|
79
|
-
*
|
|
147
|
+
* The parts carry `data-readonly` while it is set.
|
|
80
148
|
*/
|
|
81
149
|
readonly?: boolean
|
|
82
150
|
className?: string
|
|
83
151
|
style?: CSSProperties
|
|
152
|
+
/** Called with the new value when a drag, the wheel or an arrow key moves it. */
|
|
84
153
|
onChange?: (value: number) => void
|
|
154
|
+
/** Called when a drag starts, with the value where the track was pressed. */
|
|
85
155
|
onDragStart?: (value: number) => void
|
|
156
|
+
/** Called when the drag ends, with the value it ended on. */
|
|
86
157
|
onDragEnd?: (value: number) => void
|
|
87
|
-
/**
|
|
88
|
-
|
|
89
|
-
|
|
158
|
+
/**
|
|
159
|
+
* The slider renders exactly what you compose here; there is no default
|
|
160
|
+
* markup to fall back to.
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* <Slider.Root value={value} min={0} max={100} onChange={setValue}>
|
|
164
|
+
* <Slider.Track>
|
|
165
|
+
* <Slider.Thumb aria-label="Level" />
|
|
166
|
+
* </Slider.Track>
|
|
167
|
+
* </Slider.Root>
|
|
168
|
+
*/
|
|
169
|
+
children: ReactNode
|
|
90
170
|
}
|
|
91
171
|
|
|
92
172
|
export interface SliderMethods {
|
|
@@ -97,19 +177,26 @@ export interface SliderMethods {
|
|
|
97
177
|
type Props = SliderProps &
|
|
98
178
|
Omit<ComponentPropsWithoutRef<'div'>, keyof SliderProps>
|
|
99
179
|
|
|
100
|
-
|
|
180
|
+
/**
|
|
181
|
+
* The wheel only acts while the focus is inside, so that scrolling a page past
|
|
182
|
+
* the control does not change its value.
|
|
183
|
+
*/
|
|
184
|
+
const WHEEL_OPTIONS = { requireFocus: true }
|
|
185
|
+
|
|
186
|
+
export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
|
|
101
187
|
(
|
|
102
188
|
{
|
|
103
189
|
value,
|
|
104
190
|
min,
|
|
105
191
|
max,
|
|
106
192
|
step = 1,
|
|
107
|
-
|
|
193
|
+
scale = linearScale,
|
|
108
194
|
vertical = false,
|
|
109
195
|
reverse = false,
|
|
110
196
|
externalStyles: _externalStyles,
|
|
111
|
-
wheel =
|
|
112
|
-
keyboard =
|
|
197
|
+
wheel = DEFAULT_WHEEL_OPTIONS,
|
|
198
|
+
keyboard = DEFAULT_KEYBOARD_OPTIONS,
|
|
199
|
+
dragSensitivity = DEFAULT_DRAG_SENSITIVITY,
|
|
113
200
|
disabled = false,
|
|
114
201
|
readonly = false,
|
|
115
202
|
onChange,
|
|
@@ -127,13 +214,13 @@ const Root = forwardRef<SliderMethods, Props>(
|
|
|
127
214
|
forwardedRef,
|
|
128
215
|
) => {
|
|
129
216
|
// -- state and ref ---
|
|
130
|
-
const
|
|
217
|
+
const trackRef = useRef<HTMLDivElement>(null)
|
|
131
218
|
const thumbRef = useRef<SliderThumbMethods>(null)
|
|
132
|
-
|
|
133
219
|
// --- interpret props ---
|
|
134
220
|
const externalStyles = { ...defaultExternalStyles, ..._externalStyles }
|
|
221
|
+
const inactive = disabled || readonly
|
|
135
222
|
|
|
136
|
-
const p = toFixed(
|
|
223
|
+
const p = toFixed(scale.normalize(value, min, max) * 100)
|
|
137
224
|
const rev = toFixed(100 - p)
|
|
138
225
|
// NOTE
|
|
139
226
|
// normal -> normal (right)
|
|
@@ -146,165 +233,143 @@ const Root = forwardRef<SliderMethods, Props>(
|
|
|
146
233
|
)
|
|
147
234
|
const percent = displayReversed ? rev : p
|
|
148
235
|
|
|
149
|
-
let trackProps: SliderTrackProps = {}
|
|
150
|
-
let thumbProps: SliderThumbProps = {}
|
|
151
|
-
let scaleComponent: ReactElement | undefined = undefined
|
|
152
|
-
if (children != undefined) {
|
|
153
|
-
React.Children.forEach(children, (child) => {
|
|
154
|
-
if (React.isValidElement(child)) {
|
|
155
|
-
if (child.type == Thumb) {
|
|
156
|
-
thumbProps = child.props as SliderThumbProps
|
|
157
|
-
} else if (child.type == Track) {
|
|
158
|
-
trackProps = child.props as SliderTrackProps
|
|
159
|
-
} else if (child.type == Scale) {
|
|
160
|
-
scaleComponent = child
|
|
161
|
-
} else {
|
|
162
|
-
throw new Error('only <SliderThumb> or <SliderTrack>')
|
|
163
|
-
}
|
|
164
|
-
} else {
|
|
165
|
-
throw new Error('children is an invalid element.')
|
|
166
|
-
}
|
|
167
|
-
})
|
|
168
|
-
}
|
|
169
|
-
|
|
170
236
|
// --- internal functions ---
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
onChange(v2)
|
|
178
|
-
},
|
|
179
|
-
[displayReversed, max, min, onChange, readonly, skew, step, vertical],
|
|
237
|
+
// The pointer is normalized against the track on both axes; only the one
|
|
238
|
+
// the slider runs along is read back. `AxisOptions` extends `ValueRange`,
|
|
239
|
+
// so the same object also describes the scaling for `applyDelta`.
|
|
240
|
+
const axis: AxisOptions = useMemo(
|
|
241
|
+
() => ({ min, max, step, scale, reverse: displayReversed }),
|
|
242
|
+
[min, max, step, scale, displayReversed],
|
|
180
243
|
)
|
|
181
244
|
|
|
182
|
-
|
|
183
|
-
(eventType: InputEventOption[0], x: number) => {
|
|
184
|
-
let newValue
|
|
185
|
-
if (eventType == 'normalized') {
|
|
186
|
-
const n = normalizeValue(value, min, max, skew)
|
|
187
|
-
newValue = rawValue(n + x, min, max, skew)
|
|
188
|
-
} else {
|
|
189
|
-
newValue = value + x
|
|
190
|
-
}
|
|
191
|
-
return clamp(stepValue(newValue, step), min, max)
|
|
192
|
-
},
|
|
193
|
-
[max, min, skew, step, value],
|
|
194
|
-
)
|
|
245
|
+
useCheckSteps({ component: 'Slider', range: axis, keyboard, wheel })
|
|
195
246
|
|
|
196
247
|
const handleKeyDown = useCallback(
|
|
197
248
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
198
|
-
if (!keyboard || !onChange || readonly) return
|
|
199
249
|
const key = event.key
|
|
200
|
-
if (['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(key))
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
250
|
+
if (!['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(key))
|
|
251
|
+
return
|
|
252
|
+
event.preventDefault()
|
|
253
|
+
if (!keyboard || !onChange || inactive) return
|
|
254
|
+
let direction = key === 'ArrowRight' || key === 'ArrowUp' ? 1 : -1
|
|
255
|
+
if (reverse) direction *= -1
|
|
256
|
+
onChange(applyDelta(value, direction, keyboard, axis, event))
|
|
207
257
|
},
|
|
208
|
-
[keyboard, onChange,
|
|
258
|
+
[keyboard, onChange, inactive, reverse, value, axis],
|
|
209
259
|
)
|
|
210
260
|
|
|
211
261
|
// --- hooks ---
|
|
212
|
-
const
|
|
213
|
-
{
|
|
214
|
-
baseElementRef: trackElementRef,
|
|
215
|
-
cursor: readonly ? undefined : externalStyles.cursor,
|
|
216
|
-
updateOnPointerDown: true,
|
|
217
|
-
onDrag: onDrag,
|
|
218
|
-
onDragStart: (nx, ny) => {
|
|
219
|
-
if (readonly) return
|
|
220
|
-
if (externalStyles.userSelectNone) addUserSelectNone()
|
|
221
|
-
|
|
222
|
-
thumbRef.current?.focus()
|
|
223
|
-
onDragStart?.(
|
|
224
|
-
clamp(
|
|
225
|
-
stepValue(rawValue(vertical ? ny : nx, min, max, skew), step),
|
|
226
|
-
min,
|
|
227
|
-
max,
|
|
228
|
-
),
|
|
229
|
-
)
|
|
230
|
-
},
|
|
231
|
-
onDragEnd: (nx, ny) => {
|
|
232
|
-
if (readonly) return
|
|
262
|
+
const valueOf = (v: XY<number>) => v[vertical ? 1 : 0]
|
|
233
263
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
264
|
+
const { refCallback: dragRefCallback } = useDragValue<HTMLDivElement>({
|
|
265
|
+
axis,
|
|
266
|
+
baseElementRef: trackRef,
|
|
267
|
+
cursor: inactive ? undefined : externalStyles.cursor,
|
|
268
|
+
shouldStart: () => !inactive,
|
|
269
|
+
sensitivity: (state) =>
|
|
270
|
+
selectModifier(dragSensitivity, state.event).value,
|
|
271
|
+
updateOnPointerDown: true,
|
|
272
|
+
onChange: (v) => {
|
|
273
|
+
if (inactive) return
|
|
274
|
+
onChange?.(valueOf(v))
|
|
244
275
|
},
|
|
245
|
-
|
|
276
|
+
onDragStart: (v) => {
|
|
277
|
+
if (inactive) return
|
|
278
|
+
thumbRef.current?.focus()
|
|
279
|
+
onDragStart?.(valueOf(v))
|
|
280
|
+
},
|
|
281
|
+
onDragEnd: (v) => {
|
|
282
|
+
if (inactive) return
|
|
283
|
+
onDragEnd?.(valueOf(v))
|
|
284
|
+
},
|
|
285
|
+
})
|
|
246
286
|
|
|
247
287
|
const wheelRefCallback = useWheel<HTMLDivElement>((event) => {
|
|
248
|
-
if (!wheel || !onChange ||
|
|
288
|
+
if (!wheel || !onChange || inactive) return
|
|
249
289
|
event.preventDefault()
|
|
250
|
-
let
|
|
251
|
-
if (!vertical && event.deltaX
|
|
252
|
-
|
|
290
|
+
let direction
|
|
291
|
+
if (!vertical && event.deltaX !== 0) {
|
|
292
|
+
direction = event.deltaX > 0 ? 1 : -1
|
|
253
293
|
} else {
|
|
254
|
-
if (event.deltaY
|
|
255
|
-
|
|
294
|
+
if (event.deltaY === 0) return
|
|
295
|
+
direction = event.deltaY > 0 ? -1 : 1
|
|
256
296
|
}
|
|
257
|
-
if (
|
|
258
|
-
onChange(
|
|
259
|
-
})
|
|
297
|
+
if (reverse) direction *= -1
|
|
298
|
+
onChange(applyDelta(value, direction, wheel, axis, event))
|
|
299
|
+
}, WHEEL_OPTIONS)
|
|
300
|
+
|
|
301
|
+
// Composed once, so React attaches the refs a single time instead of
|
|
302
|
+
// detaching and re-attaching on every render.
|
|
303
|
+
const rootRefCallback = useComposedRefs<HTMLDivElement>(
|
|
304
|
+
dragRefCallback,
|
|
305
|
+
wheelRefCallback,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
const context = useMemo(
|
|
309
|
+
() => ({
|
|
310
|
+
value,
|
|
311
|
+
min,
|
|
312
|
+
max,
|
|
313
|
+
step,
|
|
314
|
+
scale,
|
|
315
|
+
vertical,
|
|
316
|
+
reverse,
|
|
317
|
+
disabled,
|
|
318
|
+
readonly,
|
|
319
|
+
onChange,
|
|
320
|
+
percent,
|
|
321
|
+
trackRef,
|
|
322
|
+
thumbRef,
|
|
323
|
+
}),
|
|
324
|
+
[
|
|
325
|
+
value,
|
|
326
|
+
min,
|
|
327
|
+
max,
|
|
328
|
+
step,
|
|
329
|
+
scale,
|
|
330
|
+
vertical,
|
|
331
|
+
reverse,
|
|
332
|
+
disabled,
|
|
333
|
+
readonly,
|
|
334
|
+
onChange,
|
|
335
|
+
percent,
|
|
336
|
+
],
|
|
337
|
+
)
|
|
260
338
|
|
|
261
339
|
useImperativeHandle(forwardedRef, () => {
|
|
262
340
|
return {
|
|
263
341
|
focus() {
|
|
264
|
-
thumbRef.current?.focus()
|
|
342
|
+
if (!disabled) thumbRef.current?.focus()
|
|
265
343
|
},
|
|
266
344
|
blur() {
|
|
267
345
|
thumbRef.current?.blur()
|
|
268
346
|
},
|
|
269
347
|
}
|
|
270
|
-
}, [])
|
|
348
|
+
}, [disabled])
|
|
271
349
|
|
|
272
350
|
return (
|
|
273
|
-
<SliderProvider
|
|
274
|
-
|
|
275
|
-
max={max}
|
|
276
|
-
step={step}
|
|
277
|
-
skew={skew}
|
|
278
|
-
vertical={vertical}
|
|
279
|
-
reverse={reverse}
|
|
280
|
-
disabled={disabled}
|
|
281
|
-
readonly={readonly}
|
|
282
|
-
>
|
|
351
|
+
<SliderProvider value={context}>
|
|
352
|
+
{/* oxlint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- the group is the pointer and keyboard event area, while the nested range input carries its control semantics */}
|
|
283
353
|
<div
|
|
284
|
-
className={
|
|
285
|
-
ref={
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
354
|
+
className={className}
|
|
355
|
+
ref={rootRefCallback}
|
|
356
|
+
role="group"
|
|
357
|
+
// A press lands on the track or the thumb, neither of which can hold
|
|
358
|
+
// focus, and the browser answers that by clearing the focus to the
|
|
359
|
+
// body — undoing the focus the drag just gave the input. Taking the
|
|
360
|
+
// focus here keeps it inside, and `onFocus` passes it to the input.
|
|
289
361
|
tabIndex={-1}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
aria-orientation={vertical ? 'vertical' : 'horizontal'}
|
|
295
|
-
aria-disabled={disabled}
|
|
296
|
-
aria-readonly={readonly}
|
|
297
|
-
style={{
|
|
298
|
-
margin: `calc(${styleHelper(thumbProps?.size ?? 22)} / 2)`, // half thumb size
|
|
299
|
-
...style,
|
|
300
|
-
}}
|
|
362
|
+
data-orientation={vertical ? 'vertical' : 'horizontal'}
|
|
363
|
+
data-disabled={disabled ? '' : undefined}
|
|
364
|
+
data-readonly={readonly ? '' : undefined}
|
|
365
|
+
style={style}
|
|
301
366
|
onPointerDown={onPointerDown}
|
|
302
367
|
onKeyDown={(event) => {
|
|
303
368
|
handleKeyDown(event)
|
|
304
369
|
onKeyDown?.(event)
|
|
305
370
|
}}
|
|
306
371
|
onFocus={(event) => {
|
|
307
|
-
thumbRef.current?.focus()
|
|
372
|
+
if (!disabled) thumbRef.current?.focus()
|
|
308
373
|
onFocus?.(event)
|
|
309
374
|
}}
|
|
310
375
|
onBlur={(event) => {
|
|
@@ -313,25 +378,7 @@ const Root = forwardRef<SliderMethods, Props>(
|
|
|
313
378
|
}}
|
|
314
379
|
{...props}
|
|
315
380
|
>
|
|
316
|
-
|
|
317
|
-
style={{
|
|
318
|
-
display: 'flex',
|
|
319
|
-
flexDirection: vertical ? 'row' : 'column',
|
|
320
|
-
width: !vertical ? '100%' : undefined,
|
|
321
|
-
height: vertical ? '100%' : undefined,
|
|
322
|
-
}}
|
|
323
|
-
>
|
|
324
|
-
<div className="tremolo-slider-track-wrapper" ref={trackElementRef}>
|
|
325
|
-
<Track
|
|
326
|
-
__percent={percent}
|
|
327
|
-
__thumb={
|
|
328
|
-
<Thumb ref={thumbRef} __percent={percent} {...thumbProps} />
|
|
329
|
-
}
|
|
330
|
-
{...trackProps}
|
|
331
|
-
/>
|
|
332
|
-
</div>
|
|
333
|
-
{scaleComponent}
|
|
334
|
-
</div>
|
|
381
|
+
{children}
|
|
335
382
|
</div>
|
|
336
383
|
</SliderProvider>
|
|
337
384
|
)
|
|
@@ -345,13 +392,13 @@ export const Slider = {
|
|
|
345
392
|
Root,
|
|
346
393
|
Thumb,
|
|
347
394
|
Track,
|
|
348
|
-
|
|
349
|
-
|
|
395
|
+
Marks,
|
|
396
|
+
MarksOption,
|
|
350
397
|
}
|
|
351
398
|
|
|
352
|
-
export { useSliderContext } from './context'
|
|
399
|
+
export { useSliderContext, type SliderContextValue } from './context'
|
|
353
400
|
export { type SliderThumbMethods, type SliderThumbProps } from './Thumb'
|
|
354
401
|
export { type SliderTrackProps } from './Track'
|
|
355
|
-
export { type
|
|
356
|
-
export { type
|
|
357
|
-
export { type
|
|
402
|
+
export { type SliderMarksProps } from './Marks'
|
|
403
|
+
export { type SliderMarksOptionProps } from './MarksOption'
|
|
404
|
+
export { type MarksOptions } from './type'
|
|
@@ -1,26 +1,44 @@
|
|
|
1
1
|
import { decimalPart, toFixed } from '@tremolo-ui/functions'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* How `Slider.Marks` fills itself in when it is given no children: one option
|
|
5
|
+
* every `per`, or every `step` of the slider. The object form turns off the
|
|
6
|
+
* mark or the label for the whole set; a single option is customized by
|
|
7
|
+
* writing `Slider.MarksOption` out instead.
|
|
8
|
+
*/
|
|
9
|
+
export type MarksOptions =
|
|
10
|
+
| 'step'
|
|
11
|
+
| number
|
|
12
|
+
| {
|
|
13
|
+
per: 'step' | number
|
|
14
|
+
mark?: boolean
|
|
15
|
+
label?: boolean
|
|
16
|
+
}
|
|
5
17
|
|
|
6
18
|
export function generateOptionsList(
|
|
7
|
-
options:
|
|
19
|
+
options: MarksOptions,
|
|
8
20
|
min: number,
|
|
9
21
|
max: number,
|
|
10
22
|
step: number,
|
|
11
23
|
) {
|
|
24
|
+
const {
|
|
25
|
+
per,
|
|
26
|
+
mark = true,
|
|
27
|
+
label = true,
|
|
28
|
+
} = typeof options === 'object' ? options : { per: options }
|
|
12
29
|
const optionsList: {
|
|
13
30
|
value: number
|
|
14
|
-
|
|
31
|
+
mark: boolean
|
|
32
|
+
label: boolean
|
|
15
33
|
}[] = []
|
|
16
|
-
const
|
|
17
|
-
const count = Math.floor(max /
|
|
34
|
+
const interval = per === 'step' ? step : per
|
|
35
|
+
const count = Math.floor(max / interval) - Math.ceil(min / interval) + 1
|
|
18
36
|
for (let i = 0; i < count; i++) {
|
|
19
37
|
const value = toFixed(
|
|
20
|
-
|
|
21
|
-
decimalPart(
|
|
38
|
+
interval * (Math.ceil(min / interval) + i),
|
|
39
|
+
decimalPart(interval)?.length,
|
|
22
40
|
)
|
|
23
|
-
optionsList.push({ value: value,
|
|
41
|
+
optionsList.push({ value: value, mark: mark, label: label })
|
|
24
42
|
}
|
|
25
43
|
return optionsList
|
|
26
44
|
}
|