@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.
Files changed (64) hide show
  1. package/dist/index.cjs +1162 -1554
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +58 -93
  4. package/dist/index.d.cts +592 -278
  5. package/dist/index.d.cts.map +1 -1
  6. package/dist/index.d.ts +592 -278
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1163 -1555
  9. package/dist/index.js.map +1 -1
  10. package/package.json +12 -13
  11. package/src/components/AnimationCanvas/index.tsx +84 -155
  12. package/src/components/Knob/ActiveLine.tsx +6 -6
  13. package/src/components/Knob/InactiveLine.tsx +9 -11
  14. package/src/components/Knob/SVGRoot.tsx +9 -18
  15. package/src/components/Knob/context.tsx +57 -89
  16. package/src/components/Knob/index.css +6 -2
  17. package/src/components/Knob/index.tsx +95 -83
  18. package/src/components/NumberInput/DecrementStepper.tsx +17 -50
  19. package/src/components/NumberInput/IncrementStepper.tsx +17 -50
  20. package/src/components/NumberInput/InputField.tsx +123 -0
  21. package/src/components/NumberInput/Stepper.tsx +90 -12
  22. package/src/components/NumberInput/context.tsx +74 -146
  23. package/src/components/NumberInput/index.css +38 -102
  24. package/src/components/NumberInput/index.tsx +252 -98
  25. package/src/components/NumberInput/stepperButton.tsx +90 -0
  26. package/src/components/Piano/index.tsx +272 -288
  27. package/src/components/Piano/keyboardShortcuts.ts +34 -4
  28. package/src/components/PointsEditor/Background.tsx +8 -2
  29. package/src/components/PointsEditor/Container.tsx +19 -9
  30. package/src/components/PointsEditor/Point.tsx +117 -52
  31. package/src/components/PointsEditor/context.tsx +28 -74
  32. package/src/components/PointsEditor/index.css +5 -0
  33. package/src/components/PointsEditor/index.tsx +77 -43
  34. package/src/components/Slider/{Scale.tsx → Marks.tsx} +8 -8
  35. package/src/components/Slider/{ScaleOption.tsx → MarksOption.tsx} +16 -18
  36. package/src/components/Slider/Thumb.tsx +55 -68
  37. package/src/components/Slider/Track.tsx +15 -15
  38. package/src/components/Slider/context.tsx +36 -51
  39. package/src/components/Slider/index.css +18 -5
  40. package/src/components/Slider/index.tsx +136 -170
  41. package/src/components/Slider/type.ts +4 -4
  42. package/src/components/XYPad/Area.tsx +16 -7
  43. package/src/components/XYPad/Thumb.tsx +62 -74
  44. package/src/components/XYPad/context.tsx +46 -0
  45. package/src/components/XYPad/index.css +12 -0
  46. package/src/components/XYPad/index.tsx +193 -250
  47. package/src/hooks/useDrag.ts +41 -76
  48. package/src/hooks/useDragValue.ts +149 -0
  49. package/src/hooks/useEventListener.ts +1 -1
  50. package/src/hooks/useWheel.ts +51 -0
  51. package/src/index.ts +15 -13
  52. package/src/components/AnimationCanvas/canvas.ts +0 -46
  53. package/src/components/DragObserver/index.tsx +0 -58
  54. package/src/components/NumberInput/InternalInput.tsx +0 -221
  55. package/src/components/NumberInput/type.ts +0 -58
  56. package/src/components/Piano/KeyLabel.tsx +0 -51
  57. package/src/components/Piano/context.tsx +0 -73
  58. package/src/components/Piano/key.tsx +0 -205
  59. package/src/components/WheelObserver/index.tsx +0 -48
  60. package/src/components/_util/type.ts +0 -1
  61. package/src/hooks/useDragWithElement.ts +0 -72
  62. package/src/hooks/usePianoDrag.ts +0 -75
  63. package/src/hooks/useRefCallbackEvent.ts +0 -34
  64. /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 React, {
2
+ import {
3
3
  ComponentPropsWithoutRef,
4
4
  forwardRef,
5
- ReactElement,
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
- clamp,
15
- normalizeValue,
16
- rawValue,
17
- stepValue,
15
+ applyDelta,
16
+ linearScale,
18
17
  toFixed,
19
18
  InputEventOption,
20
- styleHelper,
19
+ type Scale,
21
20
  } from '@tremolo-ui/functions'
22
21
 
23
- import { useDragWithElement } from '../../hooks/useDragWithElement'
24
- import { useRefCallbackEvent } from '../../hooks/useRefCallbackEvent'
25
- import {
26
- addUserSelectNone,
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
- const defaultValueOptions = {
38
- skew: 1,
39
- step: 1,
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 ValueOptions {
54
- // required
55
- value: number
56
- min: number
57
- max: number
42
+ export interface XYPadProps {
43
+ value: XY<number>
44
+ min: XYInput<number>
45
+ max: XYInput<number>
58
46
 
59
- // optional
60
- step?: number
61
- skew?: number
62
- reverse?: boolean
47
+ step?: XYInput<number>
63
48
  /**
64
- * wheel control option
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?: (valueX: number, valueY: number) => void
86
- onDragStart?: (valueX: number, valueY: number) => void
87
- onDragEnd?: (valueX: number, valueY: number) => void
88
- /** \<XYPadThumb /> | \<XYPadArea /> */
89
- children?: ReactElement | ReactElement[]
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
- x: _x,
105
- y: _y,
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 areaElementRef = useRef<HTMLDivElement>(null)
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 nx = normalizeValue(x.value, x.min, x.max, x.skew)
139
- const ny = normalizeValue(y.value, y.min, y.max, y.skew)
140
- const percentX = toFixed((x.reverse ? 1 - nx : nx) * 100)
141
- const percentY = toFixed((y.reverse ? 1 - ny : ny) * 100)
142
-
143
- let areaProps: XYPadAreaProps = {}
144
- let thumbProps: XYPadThumbProps = {}
145
- if (children != undefined) {
146
- React.Children.forEach(children, (child) => {
147
- if (React.isValidElement(child)) {
148
- if (child.type == Thumb) {
149
- thumbProps = child.props as XYPadThumbProps
150
- } else if (child.type == Area) {
151
- areaProps = child.props as XYPadAreaProps
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
- const onDrag = useCallback(
163
- (nx: number, ny: number) => {
164
- if (!onChange || readonly) return
165
- const vx = rawValue(x.reverse ? 1 - nx : nx, x.min, x.max, x.skew)
166
- const vy = rawValue(y.reverse ? 1 - ny : ny, y.min, y.max, y.skew)
167
- const newX = clamp(stepValue(vx, x.step), x.min, x.max)
168
- const newY = clamp(stepValue(vy, y.step), y.min, y.max)
169
- onChange(newX, newY)
170
- },
171
- [
172
- onChange,
173
- readonly,
174
- x.max,
175
- x.min,
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 updateValueByEvent = useCallback(
188
- (
189
- eventType: InputEventOption[0],
190
- target: Required<ValueOptions>,
191
- x: number,
192
- ) => {
193
- let v
194
- if (eventType == 'normalized') {
195
- const n = normalizeValue(
196
- target.value,
197
- target.min,
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 isHorizontal = key == 'ArrowRight' || key == 'ArrowLeft'
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 delta = target.keyboard[1]
220
- if (key == 'ArrowLeft' || key == 'ArrowUp') delta *= -1
221
- if (target.reverse) delta *= -1
222
- const newValue = updateValueByEvent(target.keyboard[0], target, delta)
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, x, y, updateValueByEvent],
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
- if (externalStyles.userSelectNone) removeUserSelectNone()
265
- if (externalStyles.cursor) resetCursorStyle()
266
-
267
- const valueX = clamp(
268
- stepValue(rawValue(nx, x.min, x.max, x.skew), x.step),
269
- x.min,
270
- x.max,
271
- )
272
- const valueY = clamp(
273
- stepValue(rawValue(ny, y.min, y.max, y.skew), y.step),
274
- y.min,
275
- y.max,
276
- )
277
- onDragEnd?.(valueX, valueY)
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
- passive: false,
226
+ onDragEnd: (v) => {
227
+ if (readonly) return
228
+ if (externalStyles.userSelectNone) removeUserSelectNone()
229
+ onDragEnd?.(v)
300
230
  },
301
- [x, y, onChange, readonly, updateValueByEvent],
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
- // eslint-disable-next-line jsx-a11y/no-static-element-interactions
318
- <div
319
- className={clsx('tremolo-xy-pad', className)}
320
- ref={composeRefs(rootRef, wheelRefCallback, touchMoveRefCallback)}
321
- tabIndex={-1}
322
- aria-disabled={disabled}
323
- aria-readonly={readonly}
324
- style={{
325
- margin: `calc(${styleHelper(thumbProps?.size ?? 22)} / 2)`, // half thumb size
326
- cursor: readonly ? 'not-allowed' : 'pointer',
327
- WebkitTapHighlightColor: 'transparent',
328
- ...style,
329
- }}
330
- onPointerDown={(event) => {
331
- pointerDownHandler(event)
332
- onPointerDown?.(event)
333
- }}
334
- onKeyDown={(event) => {
335
- handleKeyDown(event)
336
- onKeyDown?.(event)
337
- }}
338
- onFocus={(event) => {
339
- thumbRef.current?.focus()
340
- onFocus?.(event)
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
- </div>
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'