@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
@@ -3,39 +3,33 @@ import React, {
3
3
  ComponentPropsWithoutRef,
4
4
  CSSProperties,
5
5
  forwardRef,
6
- ReactElement,
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
- clamp,
15
- normalizeValue,
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 { useDragWithElement } from '../../hooks/useDragWithElement'
25
- import { useRefCallbackEvent } from '../../hooks/useRefCallbackEvent'
26
- import {
27
- addUserSelectNone,
28
- Cursor,
29
- removeUserSelectNone,
30
- resetCursorStyle,
31
- setCursorStyle,
32
- } from '../_util'
23
+ import { useDragValue } from '../../hooks/useDragValue'
24
+ import { useWheel } from '../../hooks/useWheel'
25
+ import { addUserSelectNone, Cursor, removeUserSelectNone } from '../_util'
26
+ import { useComposedRefs } from '../_util/composeRefs'
33
27
 
34
28
  import { SliderProvider } from './context'
35
- import { Scale } from './Scale'
36
- import { ScaleOption } from './ScaleOption'
37
- import { Thumb, SliderThumbMethods, SliderThumbProps } from './Thumb'
38
- import { Track, SliderTrackProps } from './Track'
29
+ import { Marks } from './Marks'
30
+ import { MarksOption } from './MarksOption'
31
+ import { Thumb, SliderThumbMethods } from './Thumb'
32
+ import { Track } from './Track'
39
33
 
40
34
  const defaultExternalStyles: SliderProps['externalStyles'] = {
41
35
  userSelectNone: true,
@@ -50,7 +44,16 @@ export interface SliderProps {
50
44
 
51
45
  // optional
52
46
  step?: number
53
- skew?: number // TODO | SkewFunction
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
54
57
  /**
55
58
  * slider orientation
56
59
  * aria-orientation property is also applied.
@@ -90,9 +93,18 @@ export interface SliderProps {
90
93
  onChange?: (value: number) => void
91
94
  onDragStart?: (value: number) => void
92
95
  onDragEnd?: (value: number) => void
93
- /** \<SliderThumb /> | \<SliderTrack /> */
94
- children?: ReactElement | ReactElement[]
95
- // ReactElement<typeof SliderThumb, typeof SliderTrack>[]
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
96
108
  }
97
109
 
98
110
  export interface SliderMethods {
@@ -103,14 +115,20 @@ export interface SliderMethods {
103
115
  type Props = SliderProps &
104
116
  Omit<ComponentPropsWithoutRef<'div'>, keyof SliderProps>
105
117
 
106
- const Root = forwardRef<SliderMethods, Props>(
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>(
107
125
  (
108
126
  {
109
127
  value,
110
128
  min,
111
129
  max,
112
130
  step = 1,
113
- skew = 1,
131
+ scale = linearScale,
114
132
  vertical = false,
115
133
  reverse = false,
116
134
  externalStyles: _externalStyles,
@@ -133,13 +151,13 @@ const Root = forwardRef<SliderMethods, Props>(
133
151
  forwardedRef,
134
152
  ) => {
135
153
  // -- state and ref ---
136
- const trackElementRef = useRef<HTMLDivElement>(null)
154
+ const trackRef = useRef<HTMLDivElement>(null)
137
155
  const thumbRef = useRef<SliderThumbMethods>(null)
138
156
 
139
157
  // --- interpret props ---
140
158
  const externalStyles = { ...defaultExternalStyles, ..._externalStyles }
141
159
 
142
- const p = toFixed(normalizeValue(value, min, max, skew) * 100)
160
+ const p = toFixed(scale.normalize(value, min, max) * 100)
143
161
  const rev = toFixed(100 - p)
144
162
  // NOTE
145
163
  // normal -> normal (right)
@@ -152,51 +170,13 @@ const Root = forwardRef<SliderMethods, Props>(
152
170
  )
153
171
  const percent = displayReversed ? rev : p
154
172
 
155
- let trackProps: SliderTrackProps = {}
156
- let thumbProps: SliderThumbProps = {}
157
- let scaleComponent: ReactElement | undefined = undefined
158
- if (children != undefined) {
159
- React.Children.forEach(children, (child) => {
160
- if (React.isValidElement(child)) {
161
- if (child.type == Thumb) {
162
- thumbProps = child.props as SliderThumbProps
163
- } else if (child.type == Track) {
164
- trackProps = child.props as SliderTrackProps
165
- } else if (child.type == Scale) {
166
- scaleComponent = child
167
- } else {
168
- throw new Error('only <SliderThumb> or <SliderTrack>')
169
- }
170
- } else {
171
- throw new Error('children is an invalid element.')
172
- }
173
- })
174
- }
175
-
176
173
  // --- internal functions ---
177
- const onDrag = useCallback(
178
- (nx: number, ny: number) => {
179
- if (!onChange || readonly) return
180
- const n = vertical ? ny : nx
181
- const v = rawValue(displayReversed ? 1 - n : n, min, max, skew)
182
- const v2 = clamp(stepValue(v, step), min, max)
183
- onChange(v2)
184
- },
185
- [displayReversed, max, min, onChange, readonly, skew, step, vertical],
186
- )
187
-
188
- const updateValueByEvent = useCallback(
189
- (eventType: InputEventOption[0], x: number) => {
190
- let newValue
191
- if (eventType == 'normalized') {
192
- const n = normalizeValue(value, min, max, skew)
193
- newValue = rawValue(n + x, min, max, skew)
194
- } else {
195
- newValue = value + x
196
- }
197
- return clamp(stepValue(newValue, step), min, max)
198
- },
199
- [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],
200
180
  )
201
181
 
202
182
  const handleKeyDown = useCallback(
@@ -205,69 +185,90 @@ const Root = forwardRef<SliderMethods, Props>(
205
185
  const key = event.key
206
186
  if (['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(key)) {
207
187
  event.preventDefault()
208
- let x =
209
- key == 'ArrowRight' || key == 'ArrowUp' ? keyboard[1] : -keyboard[1]
210
- if (reverse) x *= -1
211
- 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))
212
191
  }
213
192
  },
214
- [keyboard, onChange, readonly, reverse, updateValueByEvent],
193
+ [keyboard, onChange, readonly, reverse, value, axis],
215
194
  )
216
195
 
217
196
  // --- hooks ---
218
- const { refHandler: touchMoveRefCallback, pointerDownHandler } =
219
- useDragWithElement<HTMLDivElement>({
220
- baseElementRef: trackElementRef,
221
- onDrag: onDrag,
222
- onDragStart: (nx, ny) => {
223
- if (readonly) return
224
- if (externalStyles.userSelectNone) addUserSelectNone()
225
- if (externalStyles.cursor) setCursorStyle(externalStyles.cursor)
197
+ const valueOf = (v: XY<number>) => v[vertical ? 1 : 0]
226
198
 
227
- thumbRef.current?.focus()
228
- onDragStart?.(
229
- clamp(
230
- stepValue(rawValue(vertical ? ny : nx, min, max, skew), step),
231
- min,
232
- max,
233
- ),
234
- )
235
- },
236
- onDragEnd: (nx, ny) => {
237
- if (readonly) return
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()
238
211
 
239
- if (externalStyles.userSelectNone) removeUserSelectNone()
240
- if (externalStyles.cursor) resetCursorStyle()
212
+ thumbRef.current?.focus()
213
+ onDragStart?.(valueOf(v))
214
+ },
215
+ onDragEnd: (v) => {
216
+ if (readonly) return
241
217
 
242
- onDragEnd?.(
243
- clamp(
244
- stepValue(rawValue(vertical ? ny : nx, min, max, skew), step),
245
- min,
246
- max,
247
- ),
248
- )
249
- },
250
- })
218
+ if (externalStyles.userSelectNone) removeUserSelectNone()
251
219
 
252
- const wheelRefCallback = useRefCallbackEvent(
253
- 'wheel',
254
- (event) => {
255
- if (!wheel || !onChange || readonly) return
256
- event.preventDefault()
257
- let x
258
- if (!vertical && event.deltaX != 0) {
259
- x = event.deltaX > 0 ? wheel[1] : -wheel[1]
260
- } else {
261
- if (event.deltaY == 0) return
262
- x = event.deltaY > 0 ? -wheel[1] : wheel[1]
263
- }
264
- if (vertical && reverse) x *= -1
265
- onChange(updateValueByEvent(wheel[0], x))
220
+ onDragEnd?.(valueOf(v))
266
221
  },
267
- {
268
- passive: false,
269
- },
270
- [wheel, vertical, readonly, onChange, updateValueByEvent],
222
+ })
223
+
224
+ const wheelRefCallback = useWheel<HTMLDivElement>((event) => {
225
+ if (!wheel || !onChange || readonly) return
226
+ event.preventDefault()
227
+ let direction
228
+ if (!vertical && event.deltaX != 0) {
229
+ direction = event.deltaX > 0 ? 1 : -1
230
+ } else {
231
+ if (event.deltaY == 0) return
232
+ direction = event.deltaY > 0 ? -1 : 1
233
+ }
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
+ ],
271
272
  )
272
273
 
273
274
  useImperativeHandle(forwardedRef, () => {
@@ -282,22 +283,10 @@ const Root = forwardRef<SliderMethods, Props>(
282
283
  }, [])
283
284
 
284
285
  return (
285
- <SliderProvider
286
- min={min}
287
- max={max}
288
- step={step}
289
- skew={skew}
290
- vertical={vertical}
291
- reverse={reverse}
292
- disabled={disabled}
293
- readonly={readonly}
294
- >
286
+ <SliderProvider value={context}>
295
287
  <div
296
288
  className={clsx('tremolo-slider', className)}
297
- ref={(div) => {
298
- wheelRefCallback(div)
299
- touchMoveRefCallback(div)
300
- }}
289
+ ref={rootRefCallback}
301
290
  tabIndex={-1}
302
291
  role="slider"
303
292
  aria-valuenow={value}
@@ -306,14 +295,9 @@ const Root = forwardRef<SliderMethods, Props>(
306
295
  aria-orientation={vertical ? 'vertical' : 'horizontal'}
307
296
  aria-disabled={disabled}
308
297
  aria-readonly={readonly}
309
- style={{
310
- margin: `calc(${styleHelper(thumbProps?.size ?? 22)} / 2)`, // half thumb size
311
- ...style,
312
- }}
313
- onPointerDown={(event) => {
314
- pointerDownHandler(event)
315
- onPointerDown?.(event)
316
- }}
298
+ data-vertical={vertical}
299
+ style={style}
300
+ onPointerDown={onPointerDown}
317
301
  onKeyDown={(event) => {
318
302
  handleKeyDown(event)
319
303
  onKeyDown?.(event)
@@ -328,25 +312,7 @@ const Root = forwardRef<SliderMethods, Props>(
328
312
  }}
329
313
  {...props}
330
314
  >
331
- <div
332
- style={{
333
- display: 'flex',
334
- flexDirection: vertical ? 'row' : 'column',
335
- width: !vertical ? '100%' : undefined,
336
- height: vertical ? '100%' : undefined,
337
- }}
338
- >
339
- <div className="tremolo-slider-track-wrapper" ref={trackElementRef}>
340
- <Track
341
- __percent={percent}
342
- __thumb={
343
- <Thumb ref={thumbRef} __percent={percent} {...thumbProps} />
344
- }
345
- {...trackProps}
346
- />
347
- </div>
348
- {scaleComponent}
349
- </div>
315
+ {children}
350
316
  </div>
351
317
  </SliderProvider>
352
318
  )
@@ -360,13 +326,13 @@ export const Slider = {
360
326
  Root,
361
327
  Thumb,
362
328
  Track,
363
- Scale,
364
- ScaleOption,
329
+ Marks,
330
+ MarksOption,
365
331
  }
366
332
 
367
333
  export { useSliderContext } from './context'
368
334
  export { type SliderThumbMethods, type SliderThumbProps } from './Thumb'
369
335
  export { type SliderTrackProps } from './Track'
370
- export { type ScaleProps } from './Scale'
371
- export { type ScaleOptionProps } from './ScaleOption'
372
- export { type ScaleOptions, type ScaleType } from './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 ScaleType = 'mark' | 'mark-number' | 'number'
4
- export type ScaleOptions = ['step', ScaleType] | [number, ScaleType]
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: ScaleOptions,
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: ScaleType
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, ReactElement } from 'react'
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
- children?: ReactElement
11
-
12
- /** inherit */
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
- __thumb,
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
- export interface XYPadThumbProps {
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 const Thumb = forwardRef<XYPadThumbMethods, Props>(
40
- (
41
- {
42
- size,
43
- width = 22,
44
- height = 22,
45
- color,
46
- children,
47
- wrapperClassName,
48
- wrapperStyle,
49
- className,
50
- style,
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
- useImperativeHandle(ref, () => {
61
- return {
62
- focus() {
63
- wrapperRef.current?.focus()
64
- },
65
- blur() {
66
- wrapperRef.current?.blur()
67
- },
68
- }
69
- }, [])
48
+ const methods = () => ({
49
+ focus() {
50
+ elementRef.current?.focus()
51
+ },
52
+ blur() {
53
+ elementRef.current?.blur()
54
+ },
55
+ })
70
56
 
71
- return (
72
- <div
73
- className={clsx('tremolo-xy-pad-thumb-wrapper', wrapperClassName)}
74
- style={{
75
- ...__css,
76
- ...wrapperStyle,
77
- }}
78
- {...props}
79
- >
80
- {children ? (
81
- children
82
- ) : (
83
- // default thumb
84
- <div
85
- ref={wrapperRef}
86
- className={clsx('tremolo-xy-pad-thumb', className)}
87
- // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
88
- tabIndex={0}
89
- aria-disabled={__disabled}
90
- aria-readonly={__readonly}
91
- style={{
92
- ...{ '--color': color },
93
- width: size ?? width,
94
- height: size ?? height,
95
- ...style,
96
- }}
97
- ></div>
98
- )}
99
- </div>
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
+ }