@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,8 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
import React, {
|
|
1
|
+
import {
|
|
3
2
|
ComponentPropsWithoutRef,
|
|
3
|
+
CSSProperties,
|
|
4
4
|
forwardRef,
|
|
5
|
-
|
|
5
|
+
ReactNode,
|
|
6
6
|
Ref,
|
|
7
7
|
useCallback,
|
|
8
8
|
useImperativeHandle,
|
|
@@ -10,77 +10,164 @@ import React, {
|
|
|
10
10
|
useRef,
|
|
11
11
|
} from 'react'
|
|
12
12
|
|
|
13
|
+
import type { AxisOptions } from '@tremolo-ui/dom'
|
|
13
14
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
rawValue,
|
|
17
|
-
stepValue,
|
|
15
|
+
applyDelta,
|
|
16
|
+
linearScale,
|
|
18
17
|
toFixed,
|
|
19
18
|
InputEventOption,
|
|
20
|
-
|
|
19
|
+
ModifierState,
|
|
20
|
+
type ModifierValue,
|
|
21
|
+
selectModifier,
|
|
22
|
+
type Scale,
|
|
21
23
|
} from '@tremolo-ui/functions'
|
|
22
24
|
|
|
23
|
-
import {
|
|
25
|
+
import { useComposedRefs } from '../../compose-refs'
|
|
26
|
+
import { useCheckSteps } from '../../hooks/_internal/useCheckSteps'
|
|
27
|
+
import { useDragValue } from '../../hooks/useDragValue'
|
|
24
28
|
import { useWheel } from '../../hooks/useWheel'
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
reverse: false,
|
|
35
|
-
wheel: ['raw', 1] as InputEventOption,
|
|
36
|
-
keyboard: ['raw', 1] as InputEventOption,
|
|
37
|
-
}
|
|
29
|
+
import {
|
|
30
|
+
DEFAULT_DRAG_SENSITIVITY,
|
|
31
|
+
DEFAULT_KEYBOARD_OPTIONS,
|
|
32
|
+
DEFAULT_WHEEL_OPTIONS,
|
|
33
|
+
} from '../../input-event'
|
|
34
|
+
|
|
35
|
+
import { Area } from './Area'
|
|
36
|
+
import { toXY, XY, XYInput, XYPadProvider } from './context'
|
|
37
|
+
import { Thumb, XYPadThumbMethods } from './Thumb'
|
|
38
38
|
|
|
39
39
|
const defaultExternalStyles: XYPadProps['externalStyles'] = {
|
|
40
|
-
userSelectNone: true,
|
|
41
40
|
cursor: 'pointer',
|
|
42
41
|
}
|
|
43
42
|
|
|
44
43
|
/**
|
|
45
44
|
* Two-dimensional slider component.
|
|
45
|
+
*
|
|
46
|
+
* The per-axis settings mirror `Slider`, given as `[x, y]` tuples. A plain
|
|
47
|
+
* value applies to both axes.
|
|
46
48
|
*/
|
|
47
|
-
export interface
|
|
48
|
-
|
|
49
|
-
value: number
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
skew?: number
|
|
56
|
-
reverse?: boolean
|
|
49
|
+
export interface XYPadProps {
|
|
50
|
+
/** The current value as `[x, y]`. The pad shows only this, so update it from `onChange`. */
|
|
51
|
+
value: XY<number>
|
|
52
|
+
/** The value at the start of each axis, as `[x, y]` or one number for both. */
|
|
53
|
+
min: XYInput<number>
|
|
54
|
+
/** The value at the end of each axis, as `[x, y]` or one number for both. */
|
|
55
|
+
max: XYInput<number>
|
|
56
|
+
|
|
57
57
|
/**
|
|
58
|
-
*
|
|
58
|
+
* Granularity of each axis, as `[x, y]` or one number for both. A drag, the
|
|
59
|
+
* wheel and the arrow keys snap the value to multiples of it.
|
|
60
|
+
*
|
|
61
|
+
* @default 1
|
|
59
62
|
*/
|
|
60
|
-
|
|
63
|
+
step?: XYInput<number>
|
|
61
64
|
/**
|
|
62
|
-
*
|
|
65
|
+
* How the value of each axis is distributed across the travel, as `[x, y]`
|
|
66
|
+
* or one scale for both.
|
|
67
|
+
*
|
|
68
|
+
* Pick one of the scales from `@tremolo-ui/functions`: `linearScale`,
|
|
69
|
+
* `exponentialScale`, `curveScale(n)`, `symmetricSkewScale(n)`, or
|
|
70
|
+
* `skewScale(n)` for a value that has to match a JUCE parameter.
|
|
71
|
+
*
|
|
72
|
+
* @default linearScale
|
|
63
73
|
*/
|
|
64
|
-
|
|
65
|
-
|
|
74
|
+
scale?: XYInput<Scale>
|
|
75
|
+
/**
|
|
76
|
+
* Grow an axis the other way, as `[x, y]` or one for both. By default x grows
|
|
77
|
+
* rightwards and y downwards. The arrow keys follow the direction on screen.
|
|
78
|
+
*
|
|
79
|
+
* @default false
|
|
80
|
+
*/
|
|
81
|
+
reverse?: XYInput<boolean>
|
|
66
82
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
83
|
+
/**
|
|
84
|
+
* How much one notch of the wheel moves the value. It only acts while the
|
|
85
|
+
* focus is inside, so that scrolling the page past the pad leaves it alone.
|
|
86
|
+
* Scrolling sideways, or with shift held, moves x; otherwise it moves y.
|
|
87
|
+
*
|
|
88
|
+
* `['raw', n]` moves the value by `n`, and `['normalized', n]` by `n` of the
|
|
89
|
+
* range of that axis. The result is snapped to `step`, except for an amount
|
|
90
|
+
* set on a modifier key (`{ default: …, alt: … }`). `null` turns the wheel
|
|
91
|
+
* off.
|
|
92
|
+
*
|
|
93
|
+
* @default ['raw', 1]
|
|
94
|
+
*/
|
|
95
|
+
wheel?: ModifierValue<InputEventOption> | null
|
|
96
|
+
/**
|
|
97
|
+
* How much a drag moves the value, per modifier key.
|
|
98
|
+
*
|
|
99
|
+
* `1` is the pointer position itself, which is what a drag normally is here.
|
|
100
|
+
* **Anything else turns the drag relative**: `0.1` makes the same movement
|
|
101
|
+
* cover a tenth of the travel, so the value stops following the pointer and
|
|
102
|
+
* starts moving a tenth as fast. Shift is bound to `0.1` by default, to
|
|
103
|
+
* match what it does on the arrow keys.
|
|
104
|
+
*
|
|
105
|
+
* Pressing or releasing the key mid-drag does not disturb the value: the
|
|
106
|
+
* travel so far is kept and the new sensitivity applies from there. **The
|
|
107
|
+
* pointer and the value stay apart for the rest of the drag** — snapping
|
|
108
|
+
* them back together on release would move the value nobody asked to move.
|
|
109
|
+
*
|
|
110
|
+
* @default { default: 1, shift: 0.1 }
|
|
111
|
+
*/
|
|
112
|
+
dragSensitivity?: ModifierValue<number>
|
|
70
113
|
|
|
114
|
+
/**
|
|
115
|
+
* How much one arrow key press moves the value. Left and right move x, up
|
|
116
|
+
* and down move y.
|
|
117
|
+
*
|
|
118
|
+
* `['raw', n]` moves the value by `n`, and `['normalized', n]` by `n` of the
|
|
119
|
+
* range of that axis. The result is snapped to `step`, except for an amount
|
|
120
|
+
* set on a modifier key, which is what lets shift move off the grid. `null`
|
|
121
|
+
* turns the arrow keys off.
|
|
122
|
+
*
|
|
123
|
+
* The default moves by 1, and by 0.1 with shift. With a `step` above 1, raise
|
|
124
|
+
* the amount to match: 1 would round straight back to where it started, and
|
|
125
|
+
* a development build warns about it.
|
|
126
|
+
*
|
|
127
|
+
* @default { default: ['raw', 1], shift: ['raw', 0.1] }
|
|
128
|
+
*/
|
|
129
|
+
keyboard?: ModifierValue<InputEventOption> | null
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The cursor to show while dragging. It is set on the dragged element, so it
|
|
133
|
+
* stays while the pointer is outside the pad.
|
|
134
|
+
*
|
|
135
|
+
* @default { cursor: 'pointer' }
|
|
136
|
+
*/
|
|
71
137
|
externalStyles?: {
|
|
72
|
-
|
|
73
|
-
cursor?: Cursor
|
|
138
|
+
cursor?: CSSProperties['cursor']
|
|
74
139
|
}
|
|
75
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Make the pad unchangeable and remove its thumb from the tab order.
|
|
143
|
+
* The parts carry `data-disabled` while it is set.
|
|
144
|
+
*/
|
|
76
145
|
disabled?: boolean
|
|
146
|
+
/**
|
|
147
|
+
* Make the pad unchangeable while leaving its thumb focusable.
|
|
148
|
+
* The parts carry `data-readonly` while it is set.
|
|
149
|
+
*/
|
|
77
150
|
readonly?: boolean
|
|
78
151
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
152
|
+
/** Called with the new value when a drag, the wheel or an arrow key moves it. */
|
|
153
|
+
onChange?: (value: XY<number>) => void
|
|
154
|
+
/** Called when a drag starts, with the value where the area was pressed. */
|
|
155
|
+
onDragStart?: (value: XY<number>) => void
|
|
156
|
+
/** Called when the drag ends, with the value it ended on. */
|
|
157
|
+
onDragEnd?: (value: XY<number>) => void
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The pad renders exactly what you compose here; there is no default
|
|
161
|
+
* markup to fall back to.
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* <XYPad.Root value={[x, y]} min={0} max={100} onChange={setValue}>
|
|
165
|
+
* <XYPad.Area>
|
|
166
|
+
* <XYPad.Thumb />
|
|
167
|
+
* </XYPad.Area>
|
|
168
|
+
* </XYPad.Root>
|
|
169
|
+
*/
|
|
170
|
+
children: ReactNode
|
|
84
171
|
}
|
|
85
172
|
|
|
86
173
|
export interface XYPadMethods {
|
|
@@ -92,11 +179,24 @@ export interface XYPadMethods {
|
|
|
92
179
|
type Props = XYPadProps &
|
|
93
180
|
Omit<ComponentPropsWithoutRef<'div'>, keyof XYPadProps>
|
|
94
181
|
|
|
95
|
-
|
|
182
|
+
/**
|
|
183
|
+
* The wheel only acts while the focus is inside, so that scrolling a page past
|
|
184
|
+
* the control does not change its value.
|
|
185
|
+
*/
|
|
186
|
+
const WHEEL_OPTIONS = { requireFocus: true }
|
|
187
|
+
|
|
188
|
+
export const Root = /* @__PURE__ */ forwardRef<XYPadMethods, Props>(
|
|
96
189
|
(
|
|
97
190
|
{
|
|
98
|
-
|
|
99
|
-
|
|
191
|
+
value,
|
|
192
|
+
min: _min,
|
|
193
|
+
max: _max,
|
|
194
|
+
step: _step = 1,
|
|
195
|
+
scale: _scale = linearScale,
|
|
196
|
+
reverse: _reverse = false,
|
|
197
|
+
wheel = DEFAULT_WHEEL_OPTIONS,
|
|
198
|
+
keyboard = DEFAULT_KEYBOARD_OPTIONS,
|
|
199
|
+
dragSensitivity = DEFAULT_DRAG_SENSITIVITY,
|
|
100
200
|
className,
|
|
101
201
|
style,
|
|
102
202
|
externalStyles: _externalStyles,
|
|
@@ -114,238 +214,224 @@ export const Root = forwardRef<XYPadMethods, Props>(
|
|
|
114
214
|
}: Props,
|
|
115
215
|
forwardedRef,
|
|
116
216
|
) => {
|
|
117
|
-
const x = useMemo(() => {
|
|
118
|
-
return { ...defaultValueOptions, ..._x }
|
|
119
|
-
}, [_x])
|
|
120
|
-
const y = useMemo(() => {
|
|
121
|
-
return { ...defaultValueOptions, ..._y }
|
|
122
|
-
}, [_y])
|
|
123
|
-
|
|
124
217
|
// -- state and ref ---
|
|
125
218
|
const rootRef = useRef<HTMLDivElement>(null)
|
|
126
|
-
const
|
|
219
|
+
const areaRef = useRef<HTMLDivElement>(null)
|
|
127
220
|
const thumbRef = useRef<XYPadThumbMethods>(null)
|
|
128
221
|
|
|
129
222
|
// --- interpret props ---
|
|
130
223
|
const externalStyles = { ...defaultExternalStyles, ..._externalStyles }
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
throw new Error('only <XYPadThumb> or <XYPadArea>')
|
|
148
|
-
}
|
|
149
|
-
} else {
|
|
150
|
-
throw new Error('children is an invalid element.')
|
|
151
|
-
}
|
|
152
|
-
})
|
|
153
|
-
}
|
|
224
|
+
const inactive = disabled || readonly
|
|
225
|
+
|
|
226
|
+
const min = useMemo(() => toXY(_min), [_min])
|
|
227
|
+
const max = useMemo(() => toXY(_max), [_max])
|
|
228
|
+
const step = useMemo(() => toXY(_step), [_step])
|
|
229
|
+
const scale = useMemo(() => toXY(_scale), [_scale])
|
|
230
|
+
const reverse = useMemo(() => toXY(_reverse), [_reverse])
|
|
231
|
+
|
|
232
|
+
const percent = useMemo((): XY<number> => {
|
|
233
|
+
const normalized = [0, 1].map((i) =>
|
|
234
|
+
scale[i].normalize(value[i], min[i], max[i]),
|
|
235
|
+
)
|
|
236
|
+
return [0, 1].map((i) =>
|
|
237
|
+
toFixed((reverse[i] ? 1 - normalized[i] : normalized[i]) * 100),
|
|
238
|
+
) as XY<number>
|
|
239
|
+
}, [value, min, max, scale, reverse])
|
|
154
240
|
|
|
155
241
|
// --- internal functions ---
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
x.reverse,
|
|
171
|
-
x.skew,
|
|
172
|
-
x.step,
|
|
173
|
-
y.max,
|
|
174
|
-
y.min,
|
|
175
|
-
y.reverse,
|
|
176
|
-
y.skew,
|
|
177
|
-
y.step,
|
|
178
|
-
],
|
|
242
|
+
// `AxisOptions` extends `ValueRange`, so the same pair also describes the
|
|
243
|
+
// scaling for `applyDelta`. Its `reverse` only concerns the drag, where
|
|
244
|
+
// positions follow the screen; the key and wheel handlers below flip the
|
|
245
|
+
// direction themselves.
|
|
246
|
+
const axis = useMemo(
|
|
247
|
+
(): XY<AxisOptions> =>
|
|
248
|
+
[0, 1].map((i) => ({
|
|
249
|
+
min: min[i],
|
|
250
|
+
max: max[i],
|
|
251
|
+
step: step[i],
|
|
252
|
+
scale: scale[i],
|
|
253
|
+
reverse: reverse[i],
|
|
254
|
+
})) as XY<AxisOptions>,
|
|
255
|
+
[min, max, step, scale, reverse],
|
|
179
256
|
)
|
|
180
257
|
|
|
181
|
-
|
|
258
|
+
useCheckSteps({
|
|
259
|
+
component: 'XYPad',
|
|
260
|
+
axis: 'x',
|
|
261
|
+
range: axis[0],
|
|
262
|
+
keyboard,
|
|
263
|
+
wheel,
|
|
264
|
+
})
|
|
265
|
+
useCheckSteps({
|
|
266
|
+
component: 'XYPad',
|
|
267
|
+
axis: 'y',
|
|
268
|
+
range: axis[1],
|
|
269
|
+
keyboard,
|
|
270
|
+
wheel,
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
const withAxis = useCallback(
|
|
274
|
+
(axis: 0 | 1, next: number): XY<number> =>
|
|
275
|
+
axis === 0 ? [next, value[1]] : [value[0], next],
|
|
276
|
+
[value],
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
/** @param i 0 = x, 1 = y */
|
|
280
|
+
const nudge = useCallback(
|
|
182
281
|
(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
target.skew,
|
|
194
|
-
)
|
|
195
|
-
v = rawValue(n + x, target.min, target.max, target.skew)
|
|
196
|
-
} else {
|
|
197
|
-
v = target.value + x
|
|
198
|
-
}
|
|
199
|
-
return clamp(stepValue(v, target.step), target.min, target.max)
|
|
200
|
-
},
|
|
201
|
-
[],
|
|
282
|
+
i: 0 | 1,
|
|
283
|
+
direction: number,
|
|
284
|
+
option: ModifierValue<InputEventOption>,
|
|
285
|
+
modifiers: ModifierState,
|
|
286
|
+
): XY<number> =>
|
|
287
|
+
withAxis(
|
|
288
|
+
i,
|
|
289
|
+
applyDelta(value[i], direction, option, axis[i], modifiers),
|
|
290
|
+
),
|
|
291
|
+
[value, axis, withAxis],
|
|
202
292
|
)
|
|
203
293
|
|
|
204
294
|
const handleKeyDown = useCallback(
|
|
205
295
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
206
|
-
if (!onChange || readonly) return
|
|
207
296
|
const key = event.key
|
|
208
|
-
if (['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(key))
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
297
|
+
if (!['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(key))
|
|
298
|
+
return
|
|
299
|
+
|
|
300
|
+
// The key picks the axis, whichever of the two inputs holds the
|
|
301
|
+
// focus: the pad is one control to the person moving it, and the focus
|
|
302
|
+
// lands on the x input, so reading the axis off the input would leave
|
|
303
|
+
// the y axis with no keys at all.
|
|
304
|
+
const i: 0 | 1 = key === 'ArrowRight' || key === 'ArrowLeft' ? 0 : 1
|
|
305
|
+
event.preventDefault()
|
|
306
|
+
if (!onChange || inactive || !keyboard) return
|
|
307
|
+
let direction = 1
|
|
308
|
+
if (key === 'ArrowLeft' || key === 'ArrowUp') direction *= -1
|
|
309
|
+
if (reverse[i]) direction *= -1
|
|
310
|
+
onChange(nudge(i, direction, keyboard, event))
|
|
223
311
|
},
|
|
224
|
-
[onChange,
|
|
312
|
+
[onChange, inactive, keyboard, reverse, nudge],
|
|
225
313
|
)
|
|
226
314
|
|
|
227
315
|
// --- hooks ---
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const valueX = clamp(
|
|
242
|
-
stepValue(rawValue(nx, x.min, x.max, x.skew), x.step),
|
|
243
|
-
x.min,
|
|
244
|
-
x.max,
|
|
245
|
-
)
|
|
246
|
-
const valueY = clamp(
|
|
247
|
-
stepValue(rawValue(ny, y.min, y.max, y.skew), y.step),
|
|
248
|
-
y.min,
|
|
249
|
-
y.max,
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
onDragStart?.(valueX, valueY)
|
|
253
|
-
},
|
|
254
|
-
onDragEnd: (nx, ny) => {
|
|
255
|
-
if (readonly) return
|
|
256
|
-
|
|
257
|
-
if (externalStyles.userSelectNone) removeUserSelectNone()
|
|
258
|
-
|
|
259
|
-
const valueX = clamp(
|
|
260
|
-
stepValue(rawValue(nx, x.min, x.max, x.skew), x.step),
|
|
261
|
-
x.min,
|
|
262
|
-
x.max,
|
|
263
|
-
)
|
|
264
|
-
const valueY = clamp(
|
|
265
|
-
stepValue(rawValue(ny, y.min, y.max, y.skew), y.step),
|
|
266
|
-
y.min,
|
|
267
|
-
y.max,
|
|
268
|
-
)
|
|
269
|
-
onDragEnd?.(valueX, valueY)
|
|
270
|
-
},
|
|
316
|
+
|
|
317
|
+
const { refCallback: dragRefCallback } = useDragValue<HTMLDivElement>({
|
|
318
|
+
axis,
|
|
319
|
+
baseElementRef: areaRef,
|
|
320
|
+
sensitivity: (state) =>
|
|
321
|
+
selectModifier(dragSensitivity, state.event).value,
|
|
322
|
+
updateOnPointerDown: true,
|
|
323
|
+
cursor: inactive ? undefined : externalStyles.cursor,
|
|
324
|
+
shouldStart: () => !inactive,
|
|
325
|
+
onChange: (v) => {
|
|
326
|
+
if (inactive) return
|
|
327
|
+
onChange?.(v)
|
|
271
328
|
},
|
|
272
|
-
|
|
329
|
+
onDragStart: (v) => {
|
|
330
|
+
if (inactive) return
|
|
331
|
+
thumbRef.current?.focus()
|
|
332
|
+
onDragStart?.(v)
|
|
333
|
+
},
|
|
334
|
+
onDragEnd: (v) => {
|
|
335
|
+
if (inactive) return
|
|
336
|
+
onDragEnd?.(v)
|
|
337
|
+
},
|
|
338
|
+
})
|
|
273
339
|
|
|
274
340
|
const wheelRefCallback = useWheel<HTMLDivElement>((event) => {
|
|
275
|
-
if (!onChange ||
|
|
276
|
-
|
|
277
|
-
|
|
341
|
+
if (!onChange || inactive || !wheel) return
|
|
342
|
+
// Browsers turn shift+wheel into horizontal scrolling: `deltaY` comes
|
|
343
|
+
// out empty and `deltaX` carries the movement. Reading whichever axis
|
|
344
|
+
// moved keeps shift working as the x-axis modifier — and picks up a
|
|
345
|
+
// trackpad's own horizontal gesture, which never had a modifier.
|
|
346
|
+
const horizontal = event.deltaX !== 0
|
|
347
|
+
const delta = horizontal ? event.deltaX : event.deltaY
|
|
348
|
+
if (delta === 0) return
|
|
349
|
+
const i: 0 | 1 = horizontal || event.shiftKey ? 0 : 1
|
|
278
350
|
event.preventDefault()
|
|
279
|
-
let
|
|
280
|
-
if (
|
|
281
|
-
if (
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
351
|
+
let direction = 1
|
|
352
|
+
if (delta < 0) direction *= -1
|
|
353
|
+
if (reverse[i]) direction *= -1
|
|
354
|
+
onChange(nudge(i, direction, wheel, event))
|
|
355
|
+
}, WHEEL_OPTIONS)
|
|
356
|
+
|
|
357
|
+
// Composed once, so React attaches the refs a single time instead of
|
|
358
|
+
// detaching and re-attaching on every render.
|
|
359
|
+
const rootRefCallback = useComposedRefs<HTMLDivElement>(
|
|
360
|
+
rootRef,
|
|
361
|
+
dragRefCallback,
|
|
362
|
+
wheelRefCallback,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
const context = useMemo(
|
|
366
|
+
() => ({
|
|
367
|
+
value,
|
|
368
|
+
min,
|
|
369
|
+
max,
|
|
370
|
+
step,
|
|
371
|
+
scale,
|
|
372
|
+
reverse,
|
|
373
|
+
disabled,
|
|
374
|
+
readonly,
|
|
375
|
+
onChange,
|
|
376
|
+
percent,
|
|
377
|
+
areaRef,
|
|
378
|
+
thumbRef,
|
|
379
|
+
}),
|
|
380
|
+
[
|
|
381
|
+
value,
|
|
382
|
+
min,
|
|
383
|
+
max,
|
|
384
|
+
step,
|
|
385
|
+
scale,
|
|
386
|
+
reverse,
|
|
387
|
+
disabled,
|
|
388
|
+
readonly,
|
|
389
|
+
onChange,
|
|
390
|
+
percent,
|
|
391
|
+
],
|
|
392
|
+
)
|
|
289
393
|
|
|
290
394
|
useImperativeHandle(forwardedRef, () => {
|
|
291
395
|
return {
|
|
292
396
|
focus() {
|
|
293
|
-
thumbRef.current?.focus()
|
|
397
|
+
if (!disabled) thumbRef.current?.focus()
|
|
294
398
|
},
|
|
295
399
|
blur() {
|
|
296
400
|
thumbRef.current?.blur()
|
|
297
401
|
},
|
|
298
402
|
original: rootRef,
|
|
299
403
|
}
|
|
300
|
-
}, [])
|
|
404
|
+
}, [disabled])
|
|
301
405
|
|
|
302
406
|
return (
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
{...props}
|
|
330
|
-
>
|
|
331
|
-
<div className="tremolo-xy-pad-area-wrapper" ref={areaElementRef}>
|
|
332
|
-
<Area
|
|
333
|
-
__thumb={
|
|
334
|
-
<Thumb
|
|
335
|
-
ref={thumbRef}
|
|
336
|
-
__disabled={disabled}
|
|
337
|
-
__readonly={readonly}
|
|
338
|
-
__css={{
|
|
339
|
-
top: `${percentY}%`,
|
|
340
|
-
left: `${percentX}%`,
|
|
341
|
-
}}
|
|
342
|
-
{...thumbProps}
|
|
343
|
-
/>
|
|
344
|
-
}
|
|
345
|
-
{...areaProps}
|
|
346
|
-
/>
|
|
407
|
+
<XYPadProvider value={context}>
|
|
408
|
+
{/* oxlint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- the group handles pointer and keyboard input shared by its two range controls */}
|
|
409
|
+
<div
|
|
410
|
+
className={className}
|
|
411
|
+
ref={rootRefCallback}
|
|
412
|
+
role="group"
|
|
413
|
+
tabIndex={-1}
|
|
414
|
+
data-disabled={disabled ? '' : undefined}
|
|
415
|
+
data-readonly={readonly ? '' : undefined}
|
|
416
|
+
style={style}
|
|
417
|
+
onPointerDown={onPointerDown}
|
|
418
|
+
onKeyDown={(event) => {
|
|
419
|
+
handleKeyDown(event)
|
|
420
|
+
onKeyDown?.(event)
|
|
421
|
+
}}
|
|
422
|
+
onFocus={(event) => {
|
|
423
|
+
if (!disabled) thumbRef.current?.focus()
|
|
424
|
+
onFocus?.(event)
|
|
425
|
+
}}
|
|
426
|
+
onBlur={(event) => {
|
|
427
|
+
thumbRef.current?.blur()
|
|
428
|
+
onBlur?.(event)
|
|
429
|
+
}}
|
|
430
|
+
{...props}
|
|
431
|
+
>
|
|
432
|
+
{children}
|
|
347
433
|
</div>
|
|
348
|
-
</
|
|
434
|
+
</XYPadProvider>
|
|
349
435
|
)
|
|
350
436
|
},
|
|
351
437
|
)
|
|
@@ -361,3 +447,9 @@ export const XYPad = {
|
|
|
361
447
|
|
|
362
448
|
export { type XYPadThumbProps, type XYPadThumbMethods } from './Thumb'
|
|
363
449
|
export { type XYPadAreaProps } from './Area'
|
|
450
|
+
export {
|
|
451
|
+
useXYPadContext,
|
|
452
|
+
type XYPadContextValue,
|
|
453
|
+
type XY,
|
|
454
|
+
type XYInput,
|
|
455
|
+
} from './context'
|