@tremolo-ui/react 0.3.0 → 0.5.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 +1162 -1554
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +58 -93
- package/dist/index.d.cts +592 -278
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +592 -278
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1163 -1555
- package/dist/index.js.map +1 -1
- package/package.json +12 -13
- package/src/components/AnimationCanvas/index.tsx +84 -155
- package/src/components/Knob/ActiveLine.tsx +6 -6
- package/src/components/Knob/InactiveLine.tsx +9 -11
- package/src/components/Knob/SVGRoot.tsx +9 -18
- package/src/components/Knob/context.tsx +57 -89
- package/src/components/Knob/index.css +6 -2
- package/src/components/Knob/index.tsx +95 -83
- package/src/components/NumberInput/DecrementStepper.tsx +17 -50
- package/src/components/NumberInput/IncrementStepper.tsx +17 -50
- package/src/components/NumberInput/InputField.tsx +123 -0
- package/src/components/NumberInput/Stepper.tsx +90 -12
- package/src/components/NumberInput/context.tsx +74 -146
- package/src/components/NumberInput/index.css +38 -102
- package/src/components/NumberInput/index.tsx +252 -98
- package/src/components/NumberInput/stepperButton.tsx +90 -0
- package/src/components/Piano/index.tsx +272 -288
- package/src/components/Piano/keyboardShortcuts.ts +34 -4
- package/src/components/PointsEditor/Background.tsx +8 -2
- package/src/components/PointsEditor/Container.tsx +19 -9
- package/src/components/PointsEditor/Point.tsx +117 -52
- package/src/components/PointsEditor/context.tsx +28 -74
- package/src/components/PointsEditor/index.css +5 -0
- package/src/components/PointsEditor/index.tsx +77 -43
- package/src/components/Slider/{Scale.tsx → Marks.tsx} +8 -8
- package/src/components/Slider/{ScaleOption.tsx → MarksOption.tsx} +16 -18
- package/src/components/Slider/Thumb.tsx +55 -68
- package/src/components/Slider/Track.tsx +15 -15
- package/src/components/Slider/context.tsx +36 -51
- package/src/components/Slider/index.css +18 -5
- package/src/components/Slider/index.tsx +136 -170
- package/src/components/Slider/type.ts +4 -4
- package/src/components/XYPad/Area.tsx +16 -7
- package/src/components/XYPad/Thumb.tsx +62 -74
- package/src/components/XYPad/context.tsx +46 -0
- package/src/components/XYPad/index.css +12 -0
- package/src/components/XYPad/index.tsx +193 -250
- package/src/hooks/useDrag.ts +41 -76
- package/src/hooks/useDragValue.ts +149 -0
- package/src/hooks/useEventListener.ts +1 -1
- package/src/hooks/useWheel.ts +51 -0
- package/src/index.ts +15 -13
- package/src/components/AnimationCanvas/canvas.ts +0 -46
- package/src/components/DragObserver/index.tsx +0 -58
- package/src/components/NumberInput/InternalInput.tsx +0 -221
- 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/key.tsx +0 -205
- package/src/components/WheelObserver/index.tsx +0 -48
- package/src/components/_util/type.ts +0 -1
- package/src/hooks/useDragWithElement.ts +0 -72
- package/src/hooks/usePianoDrag.ts +0 -75
- package/src/hooks/useRefCallbackEvent.ts +0 -34
- /package/src/hooks/{useCallbackRef.ts → _internal/useCallbackRef.ts} +0 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createContext, RefObject, useContext } from 'react'
|
|
2
|
+
|
|
3
|
+
import { toXY, type XY, type XYInput } from '@tremolo-ui/dom'
|
|
4
|
+
import type { Scale } from '@tremolo-ui/functions'
|
|
5
|
+
|
|
6
|
+
import type { XYPadThumbMethods } from './Thumb'
|
|
7
|
+
|
|
8
|
+
export { toXY, type XY, type XYInput }
|
|
9
|
+
|
|
10
|
+
export type XYPadContextValue = {
|
|
11
|
+
value: XY<number>
|
|
12
|
+
min: XY<number>
|
|
13
|
+
max: XY<number>
|
|
14
|
+
step: XY<number>
|
|
15
|
+
/** How the value of each axis is distributed across the travel. */
|
|
16
|
+
scale: XY<Scale>
|
|
17
|
+
reverse: XY<boolean>
|
|
18
|
+
disabled: boolean
|
|
19
|
+
readonly: boolean
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Position of the thumb, 0-100 per axis, already accounting for `reverse`.
|
|
23
|
+
*/
|
|
24
|
+
percent: XY<number>
|
|
25
|
+
|
|
26
|
+
/** `Area` registers its element here; `Root` normalizes the pointer against it. */
|
|
27
|
+
areaRef: RefObject<HTMLDivElement | null>
|
|
28
|
+
/** `Thumb` registers itself here; `Root` focuses it when a drag starts. */
|
|
29
|
+
thumbRef: RefObject<XYPadThumbMethods | null>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const XYPadContext = createContext<XYPadContextValue | null>(null)
|
|
33
|
+
|
|
34
|
+
export const XYPadProvider = XYPadContext.Provider
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Everything here is derived during render, so there is no state to keep in
|
|
38
|
+
* sync: `value` comes from the props of `Root` and the rest follows from it.
|
|
39
|
+
*/
|
|
40
|
+
export function useXYPadContext(): XYPadContextValue
|
|
41
|
+
export function useXYPadContext<T>(selector: (state: XYPadContextValue) => T): T
|
|
42
|
+
export function useXYPadContext<T>(selector?: (state: XYPadContextValue) => T) {
|
|
43
|
+
const context = useContext(XYPadContext)
|
|
44
|
+
if (!context) throw new Error('Missing XYPadContext.Provider in the tree')
|
|
45
|
+
return selector ? selector(context) : context
|
|
46
|
+
}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
.tremolo-xy-pad {
|
|
2
|
+
/* Half the thumb size. The thumb overhangs the edges of the area, so the
|
|
3
|
+
pad reserves room for it. Override this when using a different size. */
|
|
4
|
+
--thumb-size: 22px;
|
|
5
|
+
|
|
2
6
|
display: inline-block;
|
|
7
|
+
margin: calc(var(--thumb-size) / 2);
|
|
3
8
|
cursor: pointer;
|
|
9
|
+
-webkit-tap-highlight-color: transparent;
|
|
10
|
+
|
|
11
|
+
&[aria-readonly='true'] {
|
|
12
|
+
cursor: not-allowed;
|
|
13
|
+
}
|
|
4
14
|
}
|
|
5
15
|
|
|
6
16
|
.tremolo-xy-pad-area {
|
|
@@ -19,6 +29,8 @@
|
|
|
19
29
|
.tremolo-xy-pad-thumb {
|
|
20
30
|
--color: oklch(59.5% 0.17548 266.373);
|
|
21
31
|
|
|
32
|
+
width: var(--thumb-size);
|
|
33
|
+
height: var(--thumb-size);
|
|
22
34
|
border-radius: 50%;
|
|
23
35
|
outline: none;
|
|
24
36
|
background-color: var(--color);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import clsx from 'clsx'
|
|
2
|
-
import
|
|
2
|
+
import {
|
|
3
3
|
ComponentPropsWithoutRef,
|
|
4
4
|
forwardRef,
|
|
5
|
-
|
|
5
|
+
ReactNode,
|
|
6
6
|
Ref,
|
|
7
7
|
useCallback,
|
|
8
8
|
useImperativeHandle,
|
|
@@ -10,37 +10,23 @@ 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
|
+
type Scale,
|
|
21
20
|
} from '@tremolo-ui/functions'
|
|
22
21
|
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
Cursor,
|
|
28
|
-
removeUserSelectNone,
|
|
29
|
-
resetCursorStyle,
|
|
30
|
-
setCursorStyle,
|
|
31
|
-
} from '../_util'
|
|
32
|
-
import { composeRefs } from '../_util/composeRefs'
|
|
33
|
-
|
|
34
|
-
import { Area, XYPadAreaProps } from './Area'
|
|
35
|
-
import { Thumb, XYPadThumbMethods, XYPadThumbProps } from './Thumb'
|
|
22
|
+
import { useDragValue } from '../../hooks/useDragValue'
|
|
23
|
+
import { useWheel } from '../../hooks/useWheel'
|
|
24
|
+
import { addUserSelectNone, Cursor, removeUserSelectNone } from '../_util'
|
|
25
|
+
import { useComposedRefs } from '../_util/composeRefs'
|
|
36
26
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
reverse: false,
|
|
41
|
-
wheel: ['raw', 1] as InputEventOption,
|
|
42
|
-
keyboard: ['raw', 1] as InputEventOption,
|
|
43
|
-
}
|
|
27
|
+
import { Area } from './Area'
|
|
28
|
+
import { toXY, XY, XYInput, XYPadProvider } from './context'
|
|
29
|
+
import { Thumb, XYPadThumbMethods } from './Thumb'
|
|
44
30
|
|
|
45
31
|
const defaultExternalStyles: XYPadProps['externalStyles'] = {
|
|
46
32
|
userSelectNone: true,
|
|
@@ -49,30 +35,38 @@ const defaultExternalStyles: XYPadProps['externalStyles'] = {
|
|
|
49
35
|
|
|
50
36
|
/**
|
|
51
37
|
* Two-dimensional slider component.
|
|
38
|
+
*
|
|
39
|
+
* The per-axis settings mirror `Slider`, given as `[x, y]` tuples. A plain
|
|
40
|
+
* value applies to both axes.
|
|
52
41
|
*/
|
|
53
|
-
export interface
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
max: number
|
|
42
|
+
export interface XYPadProps {
|
|
43
|
+
value: XY<number>
|
|
44
|
+
min: XYInput<number>
|
|
45
|
+
max: XYInput<number>
|
|
58
46
|
|
|
59
|
-
|
|
60
|
-
step?: number
|
|
61
|
-
skew?: number
|
|
62
|
-
reverse?: boolean
|
|
47
|
+
step?: XYInput<number>
|
|
63
48
|
/**
|
|
64
|
-
*
|
|
49
|
+
* How the value of each axis is distributed across the travel.
|
|
50
|
+
*
|
|
51
|
+
* Pick one of the scales from `@tremolo-ui/functions`: `linearScale`,
|
|
52
|
+
* `exponentialScale`, `curveScale(n)`, `symmetricSkewScale(n)`, or
|
|
53
|
+
* `skewScale(n)` for a value that has to match a JUCE parameter.
|
|
54
|
+
*
|
|
55
|
+
* @default linearScale
|
|
56
|
+
*/
|
|
57
|
+
scale?: XYInput<Scale>
|
|
58
|
+
reverse?: XYInput<boolean>
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* wheel control option. Shift selects the x axis.
|
|
62
|
+
* If null, no event will be triggered
|
|
65
63
|
*/
|
|
66
64
|
wheel?: InputEventOption | null
|
|
67
65
|
/**
|
|
68
66
|
* keyboard control option
|
|
67
|
+
* If null, no event will be triggered
|
|
69
68
|
*/
|
|
70
69
|
keyboard?: InputEventOption | null
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export interface XYPadProps {
|
|
74
|
-
x: ValueOptions
|
|
75
|
-
y: ValueOptions
|
|
76
70
|
|
|
77
71
|
externalStyles?: {
|
|
78
72
|
userSelectNone?: boolean
|
|
@@ -82,11 +76,22 @@ export interface XYPadProps {
|
|
|
82
76
|
disabled?: boolean
|
|
83
77
|
readonly?: boolean
|
|
84
78
|
|
|
85
|
-
onChange?: (
|
|
86
|
-
onDragStart?: (
|
|
87
|
-
onDragEnd?: (
|
|
88
|
-
|
|
89
|
-
|
|
79
|
+
onChange?: (value: XY<number>) => void
|
|
80
|
+
onDragStart?: (value: XY<number>) => void
|
|
81
|
+
onDragEnd?: (value: XY<number>) => void
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The pad renders exactly what you compose here; there is no default
|
|
85
|
+
* markup to fall back to.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* <XYPad.Root value={[x, y]} min={0} max={100} onChange={setValue}>
|
|
89
|
+
* <XYPad.Area>
|
|
90
|
+
* <XYPad.Thumb />
|
|
91
|
+
* </XYPad.Area>
|
|
92
|
+
* </XYPad.Root>
|
|
93
|
+
*/
|
|
94
|
+
children: ReactNode
|
|
90
95
|
}
|
|
91
96
|
|
|
92
97
|
export interface XYPadMethods {
|
|
@@ -98,11 +103,23 @@ export interface XYPadMethods {
|
|
|
98
103
|
type Props = XYPadProps &
|
|
99
104
|
Omit<ComponentPropsWithoutRef<'div'>, keyof XYPadProps>
|
|
100
105
|
|
|
106
|
+
/**
|
|
107
|
+
* The wheel only acts while the focus is inside, so that scrolling a page past
|
|
108
|
+
* the control does not change its value.
|
|
109
|
+
*/
|
|
110
|
+
const WHEEL_OPTIONS = { requireFocus: true }
|
|
111
|
+
|
|
101
112
|
export const Root = forwardRef<XYPadMethods, Props>(
|
|
102
113
|
(
|
|
103
114
|
{
|
|
104
|
-
|
|
105
|
-
|
|
115
|
+
value,
|
|
116
|
+
min: _min,
|
|
117
|
+
max: _max,
|
|
118
|
+
step: _step = 1,
|
|
119
|
+
scale: _scale = linearScale,
|
|
120
|
+
reverse: _reverse = false,
|
|
121
|
+
wheel = ['raw', 1],
|
|
122
|
+
keyboard = ['raw', 1],
|
|
106
123
|
className,
|
|
107
124
|
style,
|
|
108
125
|
externalStyles: _externalStyles,
|
|
@@ -120,185 +137,132 @@ export const Root = forwardRef<XYPadMethods, Props>(
|
|
|
120
137
|
}: Props,
|
|
121
138
|
forwardedRef,
|
|
122
139
|
) => {
|
|
123
|
-
const x = useMemo(() => {
|
|
124
|
-
return { ...defaultValueOptions, ..._x }
|
|
125
|
-
}, [_x])
|
|
126
|
-
const y = useMemo(() => {
|
|
127
|
-
return { ...defaultValueOptions, ..._y }
|
|
128
|
-
}, [_y])
|
|
129
|
-
|
|
130
140
|
// -- state and ref ---
|
|
131
141
|
const rootRef = useRef<HTMLDivElement>(null)
|
|
132
|
-
const
|
|
142
|
+
const areaRef = useRef<HTMLDivElement>(null)
|
|
133
143
|
const thumbRef = useRef<XYPadThumbMethods>(null)
|
|
134
144
|
|
|
135
145
|
// --- interpret props ---
|
|
136
146
|
const externalStyles = { ...defaultExternalStyles, ..._externalStyles }
|
|
137
147
|
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
} else {
|
|
153
|
-
throw new Error('only <XYPadThumb> or <XYPadArea>')
|
|
154
|
-
}
|
|
155
|
-
} else {
|
|
156
|
-
throw new Error('children is an invalid element.')
|
|
157
|
-
}
|
|
158
|
-
})
|
|
159
|
-
}
|
|
148
|
+
const min = useMemo(() => toXY(_min), [_min])
|
|
149
|
+
const max = useMemo(() => toXY(_max), [_max])
|
|
150
|
+
const step = useMemo(() => toXY(_step), [_step])
|
|
151
|
+
const scale = useMemo(() => toXY(_scale), [_scale])
|
|
152
|
+
const reverse = useMemo(() => toXY(_reverse), [_reverse])
|
|
153
|
+
|
|
154
|
+
const percent = useMemo((): XY<number> => {
|
|
155
|
+
const normalized = [0, 1].map((i) =>
|
|
156
|
+
scale[i].normalize(value[i], min[i], max[i]),
|
|
157
|
+
)
|
|
158
|
+
return [0, 1].map((i) =>
|
|
159
|
+
toFixed((reverse[i] ? 1 - normalized[i] : normalized[i]) * 100),
|
|
160
|
+
) as XY<number>
|
|
161
|
+
}, [value, min, max, scale, reverse])
|
|
160
162
|
|
|
161
163
|
// --- internal functions ---
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
x.reverse,
|
|
177
|
-
x.skew,
|
|
178
|
-
x.step,
|
|
179
|
-
y.max,
|
|
180
|
-
y.min,
|
|
181
|
-
y.reverse,
|
|
182
|
-
y.skew,
|
|
183
|
-
y.step,
|
|
184
|
-
],
|
|
164
|
+
// `AxisOptions` extends `ValueRange`, so the same pair also describes the
|
|
165
|
+
// scaling for `applyDelta`. Its `reverse` only concerns the drag, where
|
|
166
|
+
// positions follow the screen; the key and wheel handlers below flip the
|
|
167
|
+
// direction themselves.
|
|
168
|
+
const axis = useMemo(
|
|
169
|
+
(): XY<AxisOptions> =>
|
|
170
|
+
[0, 1].map((i) => ({
|
|
171
|
+
min: min[i],
|
|
172
|
+
max: max[i],
|
|
173
|
+
step: step[i],
|
|
174
|
+
scale: scale[i],
|
|
175
|
+
reverse: reverse[i],
|
|
176
|
+
})) as XY<AxisOptions>,
|
|
177
|
+
[min, max, step, scale, reverse],
|
|
185
178
|
)
|
|
186
179
|
|
|
187
|
-
const
|
|
188
|
-
(
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
target.max,
|
|
199
|
-
target.skew,
|
|
200
|
-
)
|
|
201
|
-
v = rawValue(n + x, target.min, target.max, target.skew)
|
|
202
|
-
} else {
|
|
203
|
-
v = target.value + x
|
|
204
|
-
}
|
|
205
|
-
return clamp(stepValue(v, target.step), target.min, target.max)
|
|
206
|
-
},
|
|
207
|
-
[],
|
|
180
|
+
const withAxis = useCallback(
|
|
181
|
+
(axis: 0 | 1, next: number): XY<number> =>
|
|
182
|
+
axis == 0 ? [next, value[1]] : [value[0], next],
|
|
183
|
+
[value],
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
/** @param i 0 = x, 1 = y */
|
|
187
|
+
const nudge = useCallback(
|
|
188
|
+
(i: 0 | 1, direction: number, option: InputEventOption): XY<number> =>
|
|
189
|
+
withAxis(i, applyDelta(value[i], direction, option, axis[i])),
|
|
190
|
+
[value, axis, withAxis],
|
|
208
191
|
)
|
|
209
192
|
|
|
210
193
|
const handleKeyDown = useCallback(
|
|
211
194
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
212
|
-
if (!onChange || readonly) return
|
|
195
|
+
if (!onChange || readonly || !keyboard) return
|
|
213
196
|
const key = event.key
|
|
214
197
|
if (['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(key)) {
|
|
215
|
-
const
|
|
216
|
-
const target = isHorizontal ? x : y
|
|
217
|
-
if (!target.keyboard) return
|
|
198
|
+
const i: 0 | 1 = key == 'ArrowRight' || key == 'ArrowLeft' ? 0 : 1
|
|
218
199
|
event.preventDefault()
|
|
219
|
-
let
|
|
220
|
-
if (key == 'ArrowLeft' || key == 'ArrowUp')
|
|
221
|
-
if (
|
|
222
|
-
|
|
223
|
-
if (isHorizontal) {
|
|
224
|
-
onChange(newValue, y.value)
|
|
225
|
-
} else {
|
|
226
|
-
onChange(x.value, newValue)
|
|
227
|
-
}
|
|
200
|
+
let direction = 1
|
|
201
|
+
if (key == 'ArrowLeft' || key == 'ArrowUp') direction *= -1
|
|
202
|
+
if (reverse[i]) direction *= -1
|
|
203
|
+
onChange(nudge(i, direction, keyboard))
|
|
228
204
|
}
|
|
229
205
|
},
|
|
230
|
-
[onChange, readonly,
|
|
206
|
+
[onChange, readonly, keyboard, reverse, nudge],
|
|
231
207
|
)
|
|
232
208
|
|
|
233
209
|
// --- hooks ---
|
|
234
|
-
const { refHandler: touchMoveRefCallback, pointerDownHandler } =
|
|
235
|
-
useDragWithElement<HTMLDivElement>({
|
|
236
|
-
baseElementRef: areaElementRef,
|
|
237
|
-
onDrag: onDrag,
|
|
238
|
-
onDragStart: (nx, ny) => {
|
|
239
|
-
if (readonly) return
|
|
240
|
-
|
|
241
|
-
if (externalStyles.userSelectNone) addUserSelectNone()
|
|
242
|
-
if (externalStyles.cursor) setCursorStyle(externalStyles.cursor)
|
|
243
|
-
|
|
244
|
-
thumbRef.current?.focus()
|
|
245
|
-
|
|
246
|
-
const valueX = clamp(
|
|
247
|
-
stepValue(rawValue(nx, x.min, x.max, x.skew), x.step),
|
|
248
|
-
x.min,
|
|
249
|
-
x.max,
|
|
250
|
-
)
|
|
251
|
-
const valueY = clamp(
|
|
252
|
-
stepValue(rawValue(ny, y.min, y.max, y.skew), y.step),
|
|
253
|
-
y.min,
|
|
254
|
-
y.max,
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
// TODO: ドラッグせず、pointer downだけの場合でも、onChange を発火させるべき
|
|
258
|
-
// onChange()
|
|
259
|
-
onDragStart?.(valueX, valueY)
|
|
260
|
-
},
|
|
261
|
-
onDragEnd: (nx, ny) => {
|
|
262
|
-
if (readonly) return
|
|
263
210
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
},
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
const wheelRefCallback = useRefCallbackEvent(
|
|
282
|
-
'wheel',
|
|
283
|
-
(event) => {
|
|
284
|
-
if (!onChange || readonly) return
|
|
285
|
-
const target = event.shiftKey ? x : y
|
|
286
|
-
if (!target.wheel) return
|
|
287
|
-
event.preventDefault()
|
|
288
|
-
let delta = target.wheel[1]
|
|
289
|
-
if (event.deltaY < 0) delta *= -1
|
|
290
|
-
if (target.reverse) delta *= -1
|
|
291
|
-
const newValue = updateValueByEvent(target.wheel[0], target, delta)
|
|
292
|
-
if (event.shiftKey) {
|
|
293
|
-
onChange(newValue, y.value)
|
|
294
|
-
} else {
|
|
295
|
-
onChange(x.value, newValue)
|
|
296
|
-
}
|
|
211
|
+
const { refCallback: dragRefCallback } = useDragValue<HTMLDivElement>({
|
|
212
|
+
axis,
|
|
213
|
+
baseElementRef: areaRef,
|
|
214
|
+
updateOnPointerDown: true,
|
|
215
|
+
cursor: readonly ? undefined : externalStyles.cursor,
|
|
216
|
+
onChange: (v) => {
|
|
217
|
+
if (readonly) return
|
|
218
|
+
onChange?.(v)
|
|
219
|
+
},
|
|
220
|
+
onDragStart: (v) => {
|
|
221
|
+
if (readonly) return
|
|
222
|
+
if (externalStyles.userSelectNone) addUserSelectNone()
|
|
223
|
+
thumbRef.current?.focus()
|
|
224
|
+
onDragStart?.(v)
|
|
297
225
|
},
|
|
298
|
-
{
|
|
299
|
-
|
|
226
|
+
onDragEnd: (v) => {
|
|
227
|
+
if (readonly) return
|
|
228
|
+
if (externalStyles.userSelectNone) removeUserSelectNone()
|
|
229
|
+
onDragEnd?.(v)
|
|
300
230
|
},
|
|
301
|
-
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
const wheelRefCallback = useWheel<HTMLDivElement>((event) => {
|
|
234
|
+
if (!onChange || readonly || !wheel) return
|
|
235
|
+
const i: 0 | 1 = event.shiftKey ? 0 : 1
|
|
236
|
+
event.preventDefault()
|
|
237
|
+
let direction = 1
|
|
238
|
+
if (event.deltaY < 0) direction *= -1
|
|
239
|
+
if (reverse[i]) direction *= -1
|
|
240
|
+
onChange(nudge(i, direction, wheel))
|
|
241
|
+
}, WHEEL_OPTIONS)
|
|
242
|
+
|
|
243
|
+
// Composed once, so React attaches the refs a single time instead of
|
|
244
|
+
// detaching and re-attaching on every render.
|
|
245
|
+
const rootRefCallback = useComposedRefs<HTMLDivElement>(
|
|
246
|
+
rootRef,
|
|
247
|
+
dragRefCallback,
|
|
248
|
+
wheelRefCallback,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
const context = useMemo(
|
|
252
|
+
() => ({
|
|
253
|
+
value,
|
|
254
|
+
min,
|
|
255
|
+
max,
|
|
256
|
+
step,
|
|
257
|
+
scale,
|
|
258
|
+
reverse,
|
|
259
|
+
disabled,
|
|
260
|
+
readonly,
|
|
261
|
+
percent,
|
|
262
|
+
areaRef,
|
|
263
|
+
thumbRef,
|
|
264
|
+
}),
|
|
265
|
+
[value, min, max, step, scale, reverse, disabled, readonly, percent],
|
|
302
266
|
)
|
|
303
267
|
|
|
304
268
|
useImperativeHandle(forwardedRef, () => {
|
|
@@ -314,55 +278,33 @@ export const Root = forwardRef<XYPadMethods, Props>(
|
|
|
314
278
|
}, [])
|
|
315
279
|
|
|
316
280
|
return (
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
onBlur={(event) => {
|
|
343
|
-
thumbRef.current?.blur()
|
|
344
|
-
onBlur?.(event)
|
|
345
|
-
}}
|
|
346
|
-
{...props}
|
|
347
|
-
>
|
|
348
|
-
<div className="tremolo-xy-pad-area-wrapper" ref={areaElementRef}>
|
|
349
|
-
<Area
|
|
350
|
-
__thumb={
|
|
351
|
-
<Thumb
|
|
352
|
-
ref={thumbRef}
|
|
353
|
-
__disabled={disabled}
|
|
354
|
-
__readonly={readonly}
|
|
355
|
-
__css={{
|
|
356
|
-
top: `${percentY}%`,
|
|
357
|
-
left: `${percentX}%`,
|
|
358
|
-
}}
|
|
359
|
-
{...thumbProps}
|
|
360
|
-
/>
|
|
361
|
-
}
|
|
362
|
-
{...areaProps}
|
|
363
|
-
/>
|
|
281
|
+
<XYPadProvider value={context}>
|
|
282
|
+
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
|
283
|
+
<div
|
|
284
|
+
className={clsx('tremolo-xy-pad', className)}
|
|
285
|
+
ref={rootRefCallback}
|
|
286
|
+
tabIndex={-1}
|
|
287
|
+
aria-disabled={disabled}
|
|
288
|
+
aria-readonly={readonly}
|
|
289
|
+
style={style}
|
|
290
|
+
onPointerDown={onPointerDown}
|
|
291
|
+
onKeyDown={(event) => {
|
|
292
|
+
handleKeyDown(event)
|
|
293
|
+
onKeyDown?.(event)
|
|
294
|
+
}}
|
|
295
|
+
onFocus={(event) => {
|
|
296
|
+
thumbRef.current?.focus()
|
|
297
|
+
onFocus?.(event)
|
|
298
|
+
}}
|
|
299
|
+
onBlur={(event) => {
|
|
300
|
+
thumbRef.current?.blur()
|
|
301
|
+
onBlur?.(event)
|
|
302
|
+
}}
|
|
303
|
+
{...props}
|
|
304
|
+
>
|
|
305
|
+
{children}
|
|
364
306
|
</div>
|
|
365
|
-
</
|
|
307
|
+
</XYPadProvider>
|
|
366
308
|
)
|
|
367
309
|
},
|
|
368
310
|
)
|
|
@@ -378,3 +320,4 @@ export const XYPad = {
|
|
|
378
320
|
|
|
379
321
|
export { type XYPadThumbProps, type XYPadThumbMethods } from './Thumb'
|
|
380
322
|
export { type XYPadAreaProps } from './Area'
|
|
323
|
+
export { useXYPadContext, type XY, type XYInput } from './context'
|