@tremolo-ui/react 0.4.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 +1125 -1437
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +58 -93
- package/dist/index.d.cts +561 -243
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +561 -243
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1125 -1438
- package/dist/index.js.map +1 -1
- package/package.json +9 -10
- 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 +90 -61
- 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 +104 -24
- 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 +127 -146
- 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 +190 -230
- package/src/hooks/useDrag.ts +1 -1
- package/src/hooks/useDragValue.ts +149 -0
- package/src/hooks/useEventListener.ts +1 -1
- package/src/hooks/useWheel.ts +29 -9
- package/src/index.ts +15 -9
- package/src/components/AnimationCanvas/canvas.ts +0 -46
- package/src/components/NumberInput/InternalInput.tsx +0 -216
- 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/_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/hooks/{useCallbackRef.ts → _internal/useCallbackRef.ts} +0 -0
|
@@ -3,33 +3,33 @@ import React, {
|
|
|
3
3
|
ComponentPropsWithoutRef,
|
|
4
4
|
CSSProperties,
|
|
5
5
|
forwardRef,
|
|
6
|
-
|
|
6
|
+
ReactNode,
|
|
7
7
|
useCallback,
|
|
8
8
|
useImperativeHandle,
|
|
9
9
|
useMemo,
|
|
10
10
|
useRef,
|
|
11
11
|
} from 'react'
|
|
12
12
|
|
|
13
|
+
import type { AxisOptions, XY } from '@tremolo-ui/dom'
|
|
13
14
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
rawValue,
|
|
17
|
-
stepValue,
|
|
15
|
+
applyDelta,
|
|
16
|
+
linearScale,
|
|
18
17
|
toFixed,
|
|
19
18
|
InputEventOption,
|
|
20
|
-
styleHelper,
|
|
21
19
|
xor,
|
|
20
|
+
type Scale,
|
|
22
21
|
} from '@tremolo-ui/functions'
|
|
23
22
|
|
|
24
|
-
import {
|
|
23
|
+
import { useDragValue } from '../../hooks/useDragValue'
|
|
25
24
|
import { useWheel } from '../../hooks/useWheel'
|
|
26
25
|
import { addUserSelectNone, Cursor, removeUserSelectNone } from '../_util'
|
|
26
|
+
import { useComposedRefs } from '../_util/composeRefs'
|
|
27
27
|
|
|
28
28
|
import { SliderProvider } from './context'
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import { Thumb, SliderThumbMethods
|
|
32
|
-
import { Track
|
|
29
|
+
import { Marks } from './Marks'
|
|
30
|
+
import { MarksOption } from './MarksOption'
|
|
31
|
+
import { Thumb, SliderThumbMethods } from './Thumb'
|
|
32
|
+
import { Track } from './Track'
|
|
33
33
|
|
|
34
34
|
const defaultExternalStyles: SliderProps['externalStyles'] = {
|
|
35
35
|
userSelectNone: true,
|
|
@@ -44,7 +44,16 @@ export interface SliderProps {
|
|
|
44
44
|
|
|
45
45
|
// optional
|
|
46
46
|
step?: number
|
|
47
|
-
|
|
47
|
+
/**
|
|
48
|
+
* How the value is distributed across the travel.
|
|
49
|
+
*
|
|
50
|
+
* Pick one of the scales from `@tremolo-ui/functions`: `linearScale`,
|
|
51
|
+
* `exponentialScale`, `curveScale(n)`, `symmetricSkewScale(n)`, or
|
|
52
|
+
* `skewScale(n)` for a value that has to match a JUCE parameter.
|
|
53
|
+
*
|
|
54
|
+
* @default linearScale
|
|
55
|
+
*/
|
|
56
|
+
scale?: Scale
|
|
48
57
|
/**
|
|
49
58
|
* slider orientation
|
|
50
59
|
* aria-orientation property is also applied.
|
|
@@ -84,9 +93,18 @@ export interface SliderProps {
|
|
|
84
93
|
onChange?: (value: number) => void
|
|
85
94
|
onDragStart?: (value: number) => void
|
|
86
95
|
onDragEnd?: (value: number) => void
|
|
87
|
-
/**
|
|
88
|
-
|
|
89
|
-
|
|
96
|
+
/**
|
|
97
|
+
* The slider renders exactly what you compose here; there is no default
|
|
98
|
+
* markup to fall back to.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* <Slider.Root value={value} min={0} max={100} onChange={setValue}>
|
|
102
|
+
* <Slider.Track>
|
|
103
|
+
* <Slider.Thumb />
|
|
104
|
+
* </Slider.Track>
|
|
105
|
+
* </Slider.Root>
|
|
106
|
+
*/
|
|
107
|
+
children: ReactNode
|
|
90
108
|
}
|
|
91
109
|
|
|
92
110
|
export interface SliderMethods {
|
|
@@ -97,14 +115,20 @@ export interface SliderMethods {
|
|
|
97
115
|
type Props = SliderProps &
|
|
98
116
|
Omit<ComponentPropsWithoutRef<'div'>, keyof SliderProps>
|
|
99
117
|
|
|
100
|
-
|
|
118
|
+
/**
|
|
119
|
+
* The wheel only acts while the focus is inside, so that scrolling a page past
|
|
120
|
+
* the control does not change its value.
|
|
121
|
+
*/
|
|
122
|
+
const WHEEL_OPTIONS = { requireFocus: true }
|
|
123
|
+
|
|
124
|
+
export const Root = forwardRef<SliderMethods, Props>(
|
|
101
125
|
(
|
|
102
126
|
{
|
|
103
127
|
value,
|
|
104
128
|
min,
|
|
105
129
|
max,
|
|
106
130
|
step = 1,
|
|
107
|
-
|
|
131
|
+
scale = linearScale,
|
|
108
132
|
vertical = false,
|
|
109
133
|
reverse = false,
|
|
110
134
|
externalStyles: _externalStyles,
|
|
@@ -127,13 +151,13 @@ const Root = forwardRef<SliderMethods, Props>(
|
|
|
127
151
|
forwardedRef,
|
|
128
152
|
) => {
|
|
129
153
|
// -- state and ref ---
|
|
130
|
-
const
|
|
154
|
+
const trackRef = useRef<HTMLDivElement>(null)
|
|
131
155
|
const thumbRef = useRef<SliderThumbMethods>(null)
|
|
132
156
|
|
|
133
157
|
// --- interpret props ---
|
|
134
158
|
const externalStyles = { ...defaultExternalStyles, ..._externalStyles }
|
|
135
159
|
|
|
136
|
-
const p = toFixed(
|
|
160
|
+
const p = toFixed(scale.normalize(value, min, max) * 100)
|
|
137
161
|
const rev = toFixed(100 - p)
|
|
138
162
|
// NOTE
|
|
139
163
|
// normal -> normal (right)
|
|
@@ -146,51 +170,13 @@ const Root = forwardRef<SliderMethods, Props>(
|
|
|
146
170
|
)
|
|
147
171
|
const percent = displayReversed ? rev : p
|
|
148
172
|
|
|
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
173
|
// --- internal functions ---
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
onChange(v2)
|
|
178
|
-
},
|
|
179
|
-
[displayReversed, max, min, onChange, readonly, skew, step, vertical],
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
const updateValueByEvent = useCallback(
|
|
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],
|
|
174
|
+
// The pointer is normalized against the track on both axes; only the one
|
|
175
|
+
// the slider runs along is read back. `AxisOptions` extends `ValueRange`,
|
|
176
|
+
// so the same object also describes the scaling for `applyDelta`.
|
|
177
|
+
const axis: AxisOptions = useMemo(
|
|
178
|
+
() => ({ min, max, step, scale, reverse: displayReversed }),
|
|
179
|
+
[min, max, step, scale, displayReversed],
|
|
194
180
|
)
|
|
195
181
|
|
|
196
182
|
const handleKeyDown = useCallback(
|
|
@@ -199,64 +185,91 @@ const Root = forwardRef<SliderMethods, Props>(
|
|
|
199
185
|
const key = event.key
|
|
200
186
|
if (['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(key)) {
|
|
201
187
|
event.preventDefault()
|
|
202
|
-
let
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
onChange(updateValueByEvent(keyboard[0], x))
|
|
188
|
+
let direction = key == 'ArrowRight' || key == 'ArrowUp' ? 1 : -1
|
|
189
|
+
if (reverse) direction *= -1
|
|
190
|
+
onChange(applyDelta(value, direction, keyboard, axis))
|
|
206
191
|
}
|
|
207
192
|
},
|
|
208
|
-
[keyboard, onChange, readonly, reverse,
|
|
193
|
+
[keyboard, onChange, readonly, reverse, value, axis],
|
|
209
194
|
)
|
|
210
195
|
|
|
211
196
|
// --- 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()
|
|
197
|
+
const valueOf = (v: XY<number>) => v[vertical ? 1 : 0]
|
|
221
198
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
199
|
+
const { refCallback: dragRefCallback } = useDragValue<HTMLDivElement>({
|
|
200
|
+
axis,
|
|
201
|
+
baseElementRef: trackRef,
|
|
202
|
+
cursor: readonly ? undefined : externalStyles.cursor,
|
|
203
|
+
updateOnPointerDown: true,
|
|
204
|
+
onChange: (v) => {
|
|
205
|
+
if (readonly) return
|
|
206
|
+
onChange?.(valueOf(v))
|
|
207
|
+
},
|
|
208
|
+
onDragStart: (v) => {
|
|
209
|
+
if (readonly) return
|
|
210
|
+
if (externalStyles.userSelectNone) addUserSelectNone()
|
|
233
211
|
|
|
234
|
-
|
|
212
|
+
thumbRef.current?.focus()
|
|
213
|
+
onDragStart?.(valueOf(v))
|
|
214
|
+
},
|
|
215
|
+
onDragEnd: (v) => {
|
|
216
|
+
if (readonly) return
|
|
235
217
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
min,
|
|
240
|
-
max,
|
|
241
|
-
),
|
|
242
|
-
)
|
|
243
|
-
},
|
|
218
|
+
if (externalStyles.userSelectNone) removeUserSelectNone()
|
|
219
|
+
|
|
220
|
+
onDragEnd?.(valueOf(v))
|
|
244
221
|
},
|
|
245
|
-
)
|
|
222
|
+
})
|
|
246
223
|
|
|
247
224
|
const wheelRefCallback = useWheel<HTMLDivElement>((event) => {
|
|
248
225
|
if (!wheel || !onChange || readonly) return
|
|
249
226
|
event.preventDefault()
|
|
250
|
-
let
|
|
227
|
+
let direction
|
|
251
228
|
if (!vertical && event.deltaX != 0) {
|
|
252
|
-
|
|
229
|
+
direction = event.deltaX > 0 ? 1 : -1
|
|
253
230
|
} else {
|
|
254
231
|
if (event.deltaY == 0) return
|
|
255
|
-
|
|
232
|
+
direction = event.deltaY > 0 ? -1 : 1
|
|
256
233
|
}
|
|
257
|
-
if (vertical && reverse)
|
|
258
|
-
onChange(
|
|
259
|
-
})
|
|
234
|
+
if (vertical && reverse) direction *= -1
|
|
235
|
+
onChange(applyDelta(value, direction, wheel, axis))
|
|
236
|
+
}, WHEEL_OPTIONS)
|
|
237
|
+
|
|
238
|
+
// Composed once, so React attaches the refs a single time instead of
|
|
239
|
+
// detaching and re-attaching on every render.
|
|
240
|
+
const rootRefCallback = useComposedRefs<HTMLDivElement>(
|
|
241
|
+
dragRefCallback,
|
|
242
|
+
wheelRefCallback,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
const context = useMemo(
|
|
246
|
+
() => ({
|
|
247
|
+
value,
|
|
248
|
+
min,
|
|
249
|
+
max,
|
|
250
|
+
step,
|
|
251
|
+
scale,
|
|
252
|
+
vertical,
|
|
253
|
+
reverse,
|
|
254
|
+
disabled,
|
|
255
|
+
readonly,
|
|
256
|
+
percent,
|
|
257
|
+
trackRef,
|
|
258
|
+
thumbRef,
|
|
259
|
+
}),
|
|
260
|
+
[
|
|
261
|
+
value,
|
|
262
|
+
min,
|
|
263
|
+
max,
|
|
264
|
+
step,
|
|
265
|
+
scale,
|
|
266
|
+
vertical,
|
|
267
|
+
reverse,
|
|
268
|
+
disabled,
|
|
269
|
+
readonly,
|
|
270
|
+
percent,
|
|
271
|
+
],
|
|
272
|
+
)
|
|
260
273
|
|
|
261
274
|
useImperativeHandle(forwardedRef, () => {
|
|
262
275
|
return {
|
|
@@ -270,22 +283,10 @@ const Root = forwardRef<SliderMethods, Props>(
|
|
|
270
283
|
}, [])
|
|
271
284
|
|
|
272
285
|
return (
|
|
273
|
-
<SliderProvider
|
|
274
|
-
min={min}
|
|
275
|
-
max={max}
|
|
276
|
-
step={step}
|
|
277
|
-
skew={skew}
|
|
278
|
-
vertical={vertical}
|
|
279
|
-
reverse={reverse}
|
|
280
|
-
disabled={disabled}
|
|
281
|
-
readonly={readonly}
|
|
282
|
-
>
|
|
286
|
+
<SliderProvider value={context}>
|
|
283
287
|
<div
|
|
284
288
|
className={clsx('tremolo-slider', className)}
|
|
285
|
-
ref={
|
|
286
|
-
wheelRefCallback(div)
|
|
287
|
-
dragRefCallback(div)
|
|
288
|
-
}}
|
|
289
|
+
ref={rootRefCallback}
|
|
289
290
|
tabIndex={-1}
|
|
290
291
|
role="slider"
|
|
291
292
|
aria-valuenow={value}
|
|
@@ -294,10 +295,8 @@ const Root = forwardRef<SliderMethods, Props>(
|
|
|
294
295
|
aria-orientation={vertical ? 'vertical' : 'horizontal'}
|
|
295
296
|
aria-disabled={disabled}
|
|
296
297
|
aria-readonly={readonly}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
...style,
|
|
300
|
-
}}
|
|
298
|
+
data-vertical={vertical}
|
|
299
|
+
style={style}
|
|
301
300
|
onPointerDown={onPointerDown}
|
|
302
301
|
onKeyDown={(event) => {
|
|
303
302
|
handleKeyDown(event)
|
|
@@ -313,25 +312,7 @@ const Root = forwardRef<SliderMethods, Props>(
|
|
|
313
312
|
}}
|
|
314
313
|
{...props}
|
|
315
314
|
>
|
|
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>
|
|
315
|
+
{children}
|
|
335
316
|
</div>
|
|
336
317
|
</SliderProvider>
|
|
337
318
|
)
|
|
@@ -345,13 +326,13 @@ export const Slider = {
|
|
|
345
326
|
Root,
|
|
346
327
|
Thumb,
|
|
347
328
|
Track,
|
|
348
|
-
|
|
349
|
-
|
|
329
|
+
Marks,
|
|
330
|
+
MarksOption,
|
|
350
331
|
}
|
|
351
332
|
|
|
352
333
|
export { useSliderContext } from './context'
|
|
353
334
|
export { type SliderThumbMethods, type SliderThumbProps } from './Thumb'
|
|
354
335
|
export { type SliderTrackProps } from './Track'
|
|
355
|
-
export { type
|
|
356
|
-
export { type
|
|
357
|
-
export { type
|
|
336
|
+
export { type MarksProps } from './Marks'
|
|
337
|
+
export { type MarksOptionProps } from './MarksOption'
|
|
338
|
+
export { type MarksOptions, type MarksType } from './type'
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { decimalPart, toFixed } from '@tremolo-ui/functions'
|
|
2
2
|
|
|
3
|
-
export type
|
|
4
|
-
export type
|
|
3
|
+
export type MarksType = 'mark' | 'mark-number' | 'number'
|
|
4
|
+
export type MarksOptions = ['step', MarksType] | [number, MarksType]
|
|
5
5
|
|
|
6
6
|
export function generateOptionsList(
|
|
7
|
-
options:
|
|
7
|
+
options: MarksOptions,
|
|
8
8
|
min: number,
|
|
9
9
|
max: number,
|
|
10
10
|
step: number,
|
|
11
11
|
) {
|
|
12
12
|
const optionsList: {
|
|
13
13
|
value: number
|
|
14
|
-
type:
|
|
14
|
+
type: MarksType
|
|
15
15
|
}[] = []
|
|
16
16
|
const per = options[0] == 'step' ? step : options[0]
|
|
17
17
|
const count = Math.floor(max / per) - Math.ceil(min / per) + 1
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import clsx from 'clsx'
|
|
2
|
-
import { ComponentPropsWithoutRef, CSSProperties,
|
|
2
|
+
import { ComponentPropsWithoutRef, CSSProperties, ReactNode, Ref } from 'react'
|
|
3
|
+
|
|
4
|
+
import { useComposedRefs } from '../_util/composeRefs'
|
|
5
|
+
|
|
6
|
+
import { useXYPadContext } from './context'
|
|
3
7
|
|
|
4
8
|
export interface XYPadAreaProps {
|
|
5
9
|
width?: number | string
|
|
@@ -7,10 +11,9 @@ export interface XYPadAreaProps {
|
|
|
7
11
|
color?: string
|
|
8
12
|
className?: string
|
|
9
13
|
style?: CSSProperties
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
__thumb?: ReactElement
|
|
14
|
+
/** `<XYPad.Thumb />` goes here. */
|
|
15
|
+
children?: ReactNode
|
|
16
|
+
ref?: Ref<HTMLDivElement>
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
export function Area({
|
|
@@ -20,12 +23,19 @@ export function Area({
|
|
|
20
23
|
children,
|
|
21
24
|
className,
|
|
22
25
|
style,
|
|
23
|
-
|
|
26
|
+
ref,
|
|
24
27
|
...props
|
|
25
28
|
}: XYPadAreaProps &
|
|
26
29
|
Omit<ComponentPropsWithoutRef<'div'>, keyof XYPadAreaProps>) {
|
|
30
|
+
const { areaRef } = useXYPadContext()
|
|
31
|
+
|
|
32
|
+
// The area is what the pointer position is normalized against, so the
|
|
33
|
+
// context ref is composed with any ref the caller passed.
|
|
34
|
+
const composedRef = useComposedRefs<HTMLDivElement>(ref, areaRef)
|
|
35
|
+
|
|
27
36
|
return (
|
|
28
37
|
<div
|
|
38
|
+
ref={composedRef}
|
|
29
39
|
className={clsx('tremolo-xy-pad-area', className)}
|
|
30
40
|
style={{
|
|
31
41
|
...{ '--color': color },
|
|
@@ -36,7 +46,6 @@ export function Area({
|
|
|
36
46
|
{...props}
|
|
37
47
|
>
|
|
38
48
|
{children}
|
|
39
|
-
{__thumb}
|
|
40
49
|
</div>
|
|
41
50
|
)
|
|
42
51
|
}
|
|
@@ -2,30 +2,26 @@ import clsx from 'clsx'
|
|
|
2
2
|
import {
|
|
3
3
|
ComponentPropsWithoutRef,
|
|
4
4
|
CSSProperties,
|
|
5
|
-
forwardRef,
|
|
6
5
|
ReactNode,
|
|
6
|
+
Ref,
|
|
7
7
|
useImperativeHandle,
|
|
8
8
|
useRef,
|
|
9
9
|
} from 'react'
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
size?: number | string
|
|
13
|
-
width?: number | string
|
|
14
|
-
height?: number | string
|
|
11
|
+
import { useXYPadContext } from './context'
|
|
15
12
|
|
|
13
|
+
export interface XYPadThumbProps {
|
|
14
|
+
/**
|
|
15
|
+
* Size comes from the `--thumb-size` CSS variable on `XYPad.Root`, so that
|
|
16
|
+
* the root can reserve the matching amount of space around the area.
|
|
17
|
+
*/
|
|
16
18
|
color?: string
|
|
17
19
|
|
|
18
20
|
wrapperClassName?: string
|
|
19
21
|
wrapperStyle?: CSSProperties
|
|
20
22
|
|
|
21
23
|
children?: ReactNode
|
|
22
|
-
|
|
23
|
-
/** @internal */
|
|
24
|
-
__disabled?: boolean
|
|
25
|
-
/** @internal */
|
|
26
|
-
__readonly?: boolean
|
|
27
|
-
/** @internal */
|
|
28
|
-
__css?: CSSProperties
|
|
24
|
+
ref?: Ref<XYPadThumbMethods>
|
|
29
25
|
}
|
|
30
26
|
|
|
31
27
|
export interface XYPadThumbMethods {
|
|
@@ -36,67 +32,59 @@ export interface XYPadThumbMethods {
|
|
|
36
32
|
type Props = XYPadThumbProps &
|
|
37
33
|
Omit<ComponentPropsWithoutRef<'div'>, keyof XYPadThumbProps>
|
|
38
34
|
|
|
39
|
-
export
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
__disabled,
|
|
52
|
-
__readonly,
|
|
53
|
-
__css,
|
|
54
|
-
...props
|
|
55
|
-
}: Props,
|
|
56
|
-
ref,
|
|
57
|
-
) => {
|
|
58
|
-
const wrapperRef = useRef<HTMLDivElement>(null)
|
|
35
|
+
export function Thumb({
|
|
36
|
+
color,
|
|
37
|
+
children,
|
|
38
|
+
wrapperClassName,
|
|
39
|
+
wrapperStyle,
|
|
40
|
+
className,
|
|
41
|
+
style,
|
|
42
|
+
ref,
|
|
43
|
+
...props
|
|
44
|
+
}: Props) {
|
|
45
|
+
const elementRef = useRef<HTMLDivElement>(null)
|
|
46
|
+
const { disabled, readonly, percent, thumbRef } = useXYPadContext()
|
|
59
47
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
}, [])
|
|
48
|
+
const methods = () => ({
|
|
49
|
+
focus() {
|
|
50
|
+
elementRef.current?.focus()
|
|
51
|
+
},
|
|
52
|
+
blur() {
|
|
53
|
+
elementRef.current?.blur()
|
|
54
|
+
},
|
|
55
|
+
})
|
|
70
56
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
{
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
57
|
+
useImperativeHandle(ref, methods, [])
|
|
58
|
+
// Root focuses the thumb when a drag starts, wherever the user placed it.
|
|
59
|
+
useImperativeHandle(thumbRef, methods, [])
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div
|
|
63
|
+
className={clsx('tremolo-xy-pad-thumb-wrapper', wrapperClassName)}
|
|
64
|
+
style={{
|
|
65
|
+
left: `${percent[0]}%`,
|
|
66
|
+
top: `${percent[1]}%`,
|
|
67
|
+
...wrapperStyle,
|
|
68
|
+
}}
|
|
69
|
+
{...props}
|
|
70
|
+
>
|
|
71
|
+
{children ? (
|
|
72
|
+
children
|
|
73
|
+
) : (
|
|
74
|
+
// default thumb
|
|
75
|
+
<div
|
|
76
|
+
ref={elementRef}
|
|
77
|
+
className={clsx('tremolo-xy-pad-thumb', className)}
|
|
78
|
+
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
|
79
|
+
tabIndex={0}
|
|
80
|
+
aria-disabled={disabled}
|
|
81
|
+
aria-readonly={readonly}
|
|
82
|
+
style={{
|
|
83
|
+
...{ '--color': color },
|
|
84
|
+
...style,
|
|
85
|
+
}}
|
|
86
|
+
></div>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
@@ -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
|
+
}
|