@tremolo-ui/react 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/compose-refs.cjs +72 -0
  2. package/dist/compose-refs.cjs.map +1 -0
  3. package/dist/compose-refs.d.cts +43 -0
  4. package/dist/compose-refs.d.cts.map +1 -0
  5. package/dist/compose-refs.d.ts +43 -0
  6. package/dist/compose-refs.d.ts.map +1 -0
  7. package/dist/compose-refs.js +70 -0
  8. package/dist/compose-refs.js.map +1 -0
  9. package/dist/index.cjs +2272 -1650
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +1461 -475
  12. package/dist/index.d.cts.map +1 -1
  13. package/dist/index.d.ts +1461 -475
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +2258 -1648
  16. package/dist/index.js.map +1 -1
  17. package/package.json +32 -31
  18. package/src/components/AnimationCanvas/index.tsx +162 -194
  19. package/src/components/Knob/ActiveLine.tsx +9 -8
  20. package/src/components/Knob/InactiveLine.tsx +11 -14
  21. package/src/components/Knob/SVGRoot.tsx +10 -17
  22. package/src/components/Knob/Thumb.tsx +33 -11
  23. package/src/components/Knob/context.tsx +73 -89
  24. package/src/components/Knob/index.tsx +227 -105
  25. package/src/components/NumberInput/DecrementStepper.tsx +21 -50
  26. package/src/components/NumberInput/IncrementStepper.tsx +22 -50
  27. package/src/components/NumberInput/InputField.tsx +206 -0
  28. package/src/components/NumberInput/Stepper.tsx +150 -21
  29. package/src/components/NumberInput/StepperButton.tsx +95 -0
  30. package/src/components/NumberInput/context.tsx +104 -149
  31. package/src/components/NumberInput/index.tsx +461 -110
  32. package/src/components/Piano/index.tsx +413 -263
  33. package/src/components/Piano/keyboard-shortcuts.ts +61 -0
  34. package/src/components/PointsEditor/Background.tsx +19 -4
  35. package/src/components/PointsEditor/Container.tsx +92 -22
  36. package/src/components/PointsEditor/Point.tsx +293 -53
  37. package/src/components/PointsEditor/SelectionBox.tsx +54 -0
  38. package/src/components/PointsEditor/context.tsx +97 -75
  39. package/src/components/PointsEditor/index.tsx +497 -62
  40. package/src/components/Slider/Marks.tsx +84 -0
  41. package/src/components/Slider/MarksOption.tsx +115 -0
  42. package/src/components/Slider/Thumb.tsx +99 -54
  43. package/src/components/Slider/Track.tsx +73 -62
  44. package/src/components/Slider/context.tsx +40 -53
  45. package/src/components/Slider/index.tsx +237 -190
  46. package/src/components/Slider/type.ts +27 -9
  47. package/src/components/XYPad/Area.tsx +52 -33
  48. package/src/components/XYPad/Thumb.tsx +121 -60
  49. package/src/components/XYPad/context.tsx +49 -0
  50. package/src/components/XYPad/index.tsx +338 -246
  51. package/src/components/_util/Placement.tsx +68 -0
  52. package/src/components/_util/VisuallyHiddenRangeInput.tsx +24 -0
  53. package/src/components/_util/css-length.ts +13 -0
  54. package/src/{components/_util/composeRefs.tsx → compose-refs/index.tsx} +33 -5
  55. package/src/hooks/{useCallbackRef.ts → _internal/useCallbackRef.ts} +3 -1
  56. package/src/hooks/_internal/useCheckSteps.ts +170 -0
  57. package/src/hooks/useAnimationFrame.ts +17 -7
  58. package/src/hooks/useDrag.ts +39 -9
  59. package/src/hooks/useDragValue.ts +194 -0
  60. package/src/hooks/useEventListener.ts +11 -7
  61. package/src/hooks/useLongPress.ts +42 -8
  62. package/src/hooks/useMIDIAccess.ts +82 -11
  63. package/src/hooks/useMIDIInput.ts +45 -13
  64. package/src/hooks/useMIDIMessage.ts +23 -5
  65. package/src/hooks/useWheel.ts +29 -9
  66. package/src/index.ts +50 -27
  67. package/src/input-event.ts +32 -0
  68. package/dist/index.css +0 -536
  69. package/src/Globals.d.ts +0 -3
  70. package/src/components/AnimationCanvas/canvas.ts +0 -46
  71. package/src/components/Knob/index.css +0 -77
  72. package/src/components/NumberInput/InternalInput.tsx +0 -216
  73. package/src/components/NumberInput/index.css +0 -185
  74. package/src/components/NumberInput/type.ts +0 -58
  75. package/src/components/Piano/KeyLabel.tsx +0 -51
  76. package/src/components/Piano/context.tsx +0 -73
  77. package/src/components/Piano/index.css +0 -90
  78. package/src/components/Piano/key.tsx +0 -205
  79. package/src/components/Piano/keyboardShortcuts.ts +0 -31
  80. package/src/components/PointsEditor/index.css +0 -30
  81. package/src/components/Slider/Scale.tsx +0 -52
  82. package/src/components/Slider/ScaleOption.tsx +0 -103
  83. package/src/components/Slider/index.css +0 -112
  84. package/src/components/XYPad/index.css +0 -48
  85. package/src/components/_util/index.ts +0 -28
  86. package/src/components/_util/type.ts +0 -1
  87. package/src/hooks/useDragWithElement.ts +0 -100
  88. package/src/hooks/usePianoDrag.ts +0 -75
  89. package/src/hooks/useRefCallbackEvent.ts +0 -34
  90. package/src/styles/global.css +0 -19
@@ -1,89 +1,205 @@
1
- import clsx from 'clsx'
2
1
  import {
3
2
  ComponentPropsWithoutRef,
3
+ CSSProperties,
4
4
  forwardRef,
5
+ ReactNode,
5
6
  useCallback,
6
7
  useImperativeHandle,
8
+ useMemo,
7
9
  useRef,
8
- useState,
9
10
  } from 'react'
10
11
 
12
+ import type { AxisOptions, XY } from '@tremolo-ui/dom'
11
13
  import {
12
- clamp,
13
- InputEventOption,
14
- normalizeValue,
15
- rawValue,
16
- stepValue,
14
+ applyDelta,
15
+ type InputEventOption,
16
+ type ModifierValue,
17
+ selectModifier,
18
+ linearScale,
19
+ type Scale,
20
+ type ValueRange,
17
21
  } from '@tremolo-ui/functions'
18
22
 
19
- import { useDrag } from '../../hooks/useDrag'
23
+ import { useComposedRefs } from '../../compose-refs'
24
+ import { useCheckSteps } from '../../hooks/_internal/useCheckSteps'
25
+ import { useDragValue } from '../../hooks/useDragValue'
20
26
  import { useWheel } from '../../hooks/useWheel'
21
- import { addUserSelectNone, Cursor, removeUserSelectNone } from '../_util'
27
+ import {
28
+ DEFAULT_DRAG_SENSITIVITY,
29
+ DEFAULT_KEYBOARD_OPTIONS,
30
+ DEFAULT_WHEEL_OPTIONS,
31
+ } from '../../input-event'
32
+ import { cssLength } from '../_util/css-length'
22
33
 
23
34
  import { ActiveLine } from './ActiveLine'
24
- import { KnobProvider } from './context'
35
+ import { calcAngles, KnobProvider } from './context'
25
36
  import { InactiveLine } from './InactiveLine'
26
37
  import { SVGRoot } from './SVGRoot'
27
38
  import { Thumb } from './Thumb'
28
39
 
29
40
  const defaultExternalStyles: KnobProps['externalStyles'] = {
30
- userSelectNone: true,
31
41
  cursor: 'grabbing',
32
42
  }
33
43
 
34
44
  export interface KnobProps {
35
- // required
45
+ /** The current value. The knob shows only this, so update it from `onChange`. */
36
46
  value: number
47
+ /** The value with the knob turned all the way down. */
37
48
  min: number
49
+ /** The value with the knob turned all the way up. */
38
50
  max: number
39
51
 
40
- // optional
52
+ /**
53
+ * Granularity of the value. A drag, the wheel and the arrow keys snap it to
54
+ * multiples of `step`.
55
+ *
56
+ * @default 1
57
+ */
41
58
  step?: number
42
- skew?: number // | SkewFunction // TODO
43
59
  /**
44
- * value set when double-clicking
45
- * restriction: enableDoubleClickDefault = true
60
+ * How the value is distributed across the travel.
61
+ *
62
+ * Pick one of the scales from `@tremolo-ui/functions`: `linearScale`,
63
+ * `exponentialScale`, `curveScale(n)`, `symmetricSkewScale(n)`, or
64
+ * `skewScale(n)` for a value that has to match a JUCE parameter.
65
+ *
66
+ * @default linearScale
67
+ */
68
+ scale?: Scale
69
+ /**
70
+ * The value a double click restores, while `enableDoubleClickDefault` is on.
46
71
  * @default min
47
- * @see enableDoubleClickDefault
48
72
  */
49
73
  defaultValue?: number
50
74
 
51
75
  /**
52
- * Value to be used as the starting point of the line when drawing.
76
+ * Where the active arc starts. Put it at the centre of a bipolar control,
77
+ * such as a pan knob, so that the arc grows from there either way.
53
78
  * @default min
54
79
  */
55
80
  startValue?: number
56
81
 
57
- /** width and height */
82
+ /**
83
+ * Width and height of the knob. Sets `--knob-size`; the size the theme
84
+ * gives it stands when this is omitted.
85
+ */
58
86
  size?: number | string
59
87
 
60
88
  /**
61
- * Global style to apply when dragged
62
- * @default defaultExternalStyles
89
+ * The cursor to show while dragging. It is set on the dragged element, so it
90
+ * stays while the pointer is outside the knob.
91
+ *
92
+ * @default { cursor: 'grabbing' }
63
93
  */
64
94
  externalStyles?: {
65
- userSelectNone?: boolean
66
- cursor?: Cursor
95
+ cursor?: CSSProperties['cursor']
67
96
  }
68
97
  /**
69
- * wheel control option
70
- * If null, no event will be triggered
98
+ * How much one notch of the wheel moves the value. It only acts while the
99
+ * focus is inside, so that scrolling the page past the knob leaves it alone.
100
+ *
101
+ * `['raw', n]` moves the value by `n`, and `['normalized', n]` by `n` of the
102
+ * range between `min` and `max`. The result is snapped to `step`, except for
103
+ * an amount set on a modifier key (`{ default: …, shift: … }`). `null` turns
104
+ * the wheel off.
105
+ *
106
+ * @default ['raw', 1]
71
107
  */
72
- wheel?: InputEventOption | null
108
+ wheel?: ModifierValue<InputEventOption> | null
73
109
  /**
74
- * keyboard control option
75
- * If null, no event will be triggered
110
+ * How much a drag moves the value, per modifier key.
111
+ *
112
+ * `1` is the normal travel of 100px for the whole range; `0.1` makes the
113
+ * same movement cover a tenth of it. Shift is bound to `0.1` by default, to
114
+ * match what it does on the arrow keys.
115
+ *
116
+ * Pressing or releasing the key mid-drag does not disturb the value: the
117
+ * travel so far is kept and the new sensitivity applies from there. It takes
118
+ * effect on the next movement, since a key on its own produces no pointer
119
+ * event.
120
+ *
121
+ * @default { default: 1, shift: 0.1 }
122
+ */
123
+ dragSensitivity?: ModifierValue<number>
124
+
125
+ /**
126
+ * Hide the cursor while dragging and read the pointer movement directly,
127
+ * rather than letting it wander off across the screen.
128
+ *
129
+ * A knob does not care where the pointer is, only how far it moved, and
130
+ * letting it wander costs twice: the cursor ends up far from the knob it is
131
+ * holding, and **the drag stops at the edge of the screen**, where the
132
+ * operating system pins the pointer and the coordinates stop changing. A
133
+ * `dragSensitivity` below 1 reaches that edge quickly.
134
+ *
135
+ * Off by default because it is not free: the browser shows its own notice,
136
+ * Esc takes the lock back, and the request needs a user gesture and can be
137
+ * refused. A refused request is not an error — the drag simply carries on as
138
+ * an ordinary one.
139
+ *
140
+ * @default false
141
+ */
142
+ pointerLock?: boolean
143
+
144
+ /**
145
+ * How much one arrow key press moves the value.
146
+ *
147
+ * `['raw', n]` moves the value by `n`, and `['normalized', n]` by `n` of the
148
+ * range between `min` and `max`. The result is snapped to `step`, except for
149
+ * an amount set on a modifier key, which is what lets shift move off the
150
+ * grid. `null` turns the arrow keys off.
151
+ *
152
+ * The default moves by 1, and by 0.1 with shift. With a `step` above 1, raise
153
+ * the amount to match: 1 would round straight back to where it started, and
154
+ * a development build warns about it.
155
+ *
156
+ * @default { default: ['raw', 1], shift: ['raw', 0.1] }
157
+ */
158
+ keyboard?: ModifierValue<InputEventOption> | null
159
+ /**
160
+ * Restore `defaultValue` on a double click.
161
+ * @default true
76
162
  */
77
- keyboard?: InputEventOption | null
78
163
  enableDoubleClickDefault?: boolean
79
164
 
165
+ /**
166
+ * Make the knob unchangeable and remove it from the tab order.
167
+ * The parts carry `data-disabled` while it is set.
168
+ */
80
169
  disabled?: boolean
170
+ /**
171
+ * Make the knob unchangeable while leaving it focusable.
172
+ * The parts carry `data-readonly` while it is set.
173
+ */
81
174
  readonly?: boolean
82
175
 
83
- /** angle range [degree] */
176
+ /**
177
+ * How far the knob turns from `min` to `max`, in degrees, centred on the
178
+ * top.
179
+ * @default 270
180
+ */
84
181
  angleRange?: number
85
182
 
183
+ /**
184
+ * Called with the new value when a drag, the wheel, an arrow key or a double
185
+ * click moves it.
186
+ */
86
187
  onChange?: (value: number) => void
188
+
189
+ /**
190
+ * The knob renders exactly what you compose here; there is no default
191
+ * markup to fall back to.
192
+ *
193
+ * @example
194
+ * <Knob.Root value={value} min={0} max={100} onChange={setValue}>
195
+ * <Knob.SVGRoot>
196
+ * <Knob.InactiveLine />
197
+ * <Knob.ActiveLine />
198
+ * <Knob.Thumb />
199
+ * </Knob.SVGRoot>
200
+ * </Knob.Root>
201
+ */
202
+ children: ReactNode
87
203
  }
88
204
 
89
205
  export interface KnobMethods {
@@ -93,20 +209,28 @@ export interface KnobMethods {
93
209
 
94
210
  type Props = KnobProps & Omit<ComponentPropsWithoutRef<'div'>, keyof KnobProps>
95
211
 
96
- const Root = forwardRef<KnobMethods, Props>(
212
+ /**
213
+ * The wheel only acts while the focus is inside, so that scrolling a page past
214
+ * the control does not change its value.
215
+ */
216
+ const WHEEL_OPTIONS = { requireFocus: true }
217
+
218
+ export const Root = /* @__PURE__ */ forwardRef<KnobMethods, Props>(
97
219
  (
98
220
  {
99
221
  value,
100
222
  min,
101
223
  max,
102
224
  step = 1,
103
- skew = 1,
225
+ scale = linearScale,
104
226
  defaultValue = min,
105
227
  startValue = min,
106
228
  size,
107
229
  externalStyles: _externalStyles,
108
- wheel = ['raw', 1],
109
- keyboard = ['raw', 1],
230
+ wheel = DEFAULT_WHEEL_OPTIONS,
231
+ keyboard = DEFAULT_KEYBOARD_OPTIONS,
232
+ dragSensitivity = DEFAULT_DRAG_SENSITIVITY,
233
+ pointerLock = false,
110
234
  enableDoubleClickDefault = true,
111
235
  disabled = false,
112
236
  readonly = false,
@@ -122,118 +246,114 @@ const Root = forwardRef<KnobMethods, Props>(
122
246
  }: Props,
123
247
  forwardedRef,
124
248
  ) => {
125
- const [dragging, setDragging] = useState(false)
126
- const valueRef = useRef(0)
127
249
  const elmRef = useRef<HTMLElement | SVGElement>(null)
128
250
 
129
251
  const externalStyles = { ...defaultExternalStyles, ..._externalStyles }
252
+ const inactive = disabled || readonly
130
253
 
131
254
  // --- internal functions ---
132
- const onDrag = useCallback(
133
- (_x: number, y: number) => {
134
- if (!onChange || readonly) return
135
- const v = rawValue(valueRef.current - y / 100, min, max, skew)
136
- const clamped = clamp(stepValue(v, step), min, max)
137
- onChange(clamped)
138
- },
139
- [max, min, onChange, readonly, skew, step],
255
+ const range: ValueRange = useMemo(
256
+ () => ({ min, max, step, scale }),
257
+ [min, max, step, scale],
140
258
  )
141
259
 
142
- const updateValueByEvent = useCallback(
143
- (eventType: InputEventOption[0], x: number) => {
144
- let newValue
145
- if (eventType == 'normalized') {
146
- const n = normalizeValue(value, min, max, skew)
147
- newValue = rawValue(n + x, min, max, skew)
148
- } else {
149
- newValue = value + x
150
- }
151
- return clamp(stepValue(newValue, step), min, max)
152
- },
153
- [max, min, skew, step, value],
154
- )
260
+ useCheckSteps({ component: 'Knob', range, keyboard, wheel })
155
261
 
156
262
  const handleKeyDown = useCallback(
157
263
  (event: React.KeyboardEvent<HTMLOrSVGElement>) => {
158
- if (!keyboard || !onChange || readonly) return
264
+ if (!keyboard || !onChange || inactive) return
159
265
  const key = event.key
160
266
  if (['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(key)) {
161
267
  event.preventDefault()
162
- const x =
163
- key == 'ArrowRight' || key == 'ArrowUp' ? keyboard[1] : -keyboard[1]
164
- onChange(updateValueByEvent(keyboard[0], x))
268
+ const direction = key === 'ArrowRight' || key === 'ArrowUp' ? 1 : -1
269
+ onChange(applyDelta(value, direction, keyboard, range, event))
165
270
  }
166
271
  },
167
- [keyboard, onChange, readonly, updateValueByEvent],
272
+ [keyboard, onChange, inactive, value, range],
168
273
  )
169
274
 
170
275
  // --- hooks ---
171
- const dragRefCallback = useDrag<HTMLElement | SVGElement>({
172
- cursor: readonly ? undefined : externalStyles.cursor,
173
- onDrag: onDrag,
174
- onDragStart: () => {
175
- setDragging(true)
176
- valueRef.current = normalizeValue(value, min, max, skew)
177
- if (externalStyles.userSelectNone) addUserSelectNone()
178
- },
179
- onDragEnd: () => {
180
- setDragging(false)
181
- if (externalStyles.userSelectNone) removeUserSelectNone()
276
+ // The knob has no travel of its own: the value moves away from where it
277
+ // stood when the drag started, 100px of movement spanning the whole range.
278
+ // Only the vertical axis carries a value, reversed so that dragging up
279
+ // raises it.
280
+ const axis = useMemo(
281
+ (): XY<AxisOptions> => [range, { ...range, reverse: true }],
282
+ [range],
283
+ )
284
+
285
+ const { refCallback: dragRefCallback, dragging } = useDragValue<
286
+ HTMLElement | SVGElement
287
+ >({
288
+ axis,
289
+ getValue: () => [value, value],
290
+ sensitivity: (state) =>
291
+ selectModifier(dragSensitivity, state.event).value,
292
+ threshold: 1,
293
+ cursor: inactive ? undefined : externalStyles.cursor,
294
+ pointerLock: inactive ? false : pointerLock,
295
+ shouldStart: () => !inactive,
296
+ onChange: (v) => {
297
+ if (inactive) return
298
+ onChange?.(v[1])
182
299
  },
183
300
  })
184
301
 
185
302
  const wheelRefCallback = useWheel<HTMLElement>((event) => {
186
- if (!wheel || readonly) return
303
+ if (!wheel || inactive) return
187
304
  event.preventDefault()
188
- if (!onChange || event.deltaY == 0) return
189
- const x = Math.sign(event.deltaY) * -wheel[1]
190
- onChange(updateValueByEvent(wheel[0], x))
191
- })
305
+ if (!onChange || event.deltaY === 0) return
306
+ onChange(applyDelta(value, -Math.sign(event.deltaY), wheel, range, event))
307
+ }, WHEEL_OPTIONS)
308
+
309
+ // Composed once, so React attaches the refs a single time instead of
310
+ // detaching and re-attaching on every render.
311
+ const rootRefCallback = useComposedRefs<HTMLElement | SVGElement>(
312
+ elmRef,
313
+ dragRefCallback,
314
+ wheelRefCallback,
315
+ )
316
+
317
+ const context = useMemo(() => {
318
+ const config = { value, min, max, step, scale, startValue, angleRange }
319
+ return { ...config, ...calcAngles(config) }
320
+ }, [value, min, max, step, scale, startValue, angleRange])
192
321
 
193
322
  useImperativeHandle(forwardedRef, () => {
194
323
  return {
195
324
  focus() {
196
- elmRef.current?.focus()
325
+ if (!disabled) elmRef.current?.focus()
197
326
  },
198
327
  blur() {
199
328
  elmRef.current?.blur()
200
329
  },
201
330
  }
202
- }, [])
331
+ }, [disabled])
203
332
 
204
333
  return (
205
- <KnobProvider
206
- value={value}
207
- min={min}
208
- max={max}
209
- step={step}
210
- skew={skew}
211
- startValue={startValue}
212
- angleRange={angleRange}
213
- >
334
+ <KnobProvider value={context}>
214
335
  <div
215
- ref={(elm) => {
216
- elmRef.current = elm
217
- wheelRefCallback(elm)
218
- dragRefCallback(elm)
219
- }}
220
- className={clsx('tremolo-knob', className)}
221
- tabIndex={0}
336
+ ref={rootRefCallback}
337
+ className={className}
338
+ tabIndex={disabled ? -1 : 0}
222
339
  role="slider"
223
340
  aria-valuenow={value}
224
341
  aria-valuemin={min}
225
342
  aria-valuemax={max}
226
343
  aria-disabled={disabled}
227
344
  aria-readonly={readonly}
228
- data-dragging={dragging}
229
- style={{
230
- width: size,
231
- height: size,
232
- ...style,
233
- }}
345
+ data-disabled={disabled ? '' : undefined}
346
+ data-readonly={readonly ? '' : undefined}
347
+ data-dragging={dragging ? '' : undefined}
348
+ style={
349
+ {
350
+ '--knob-size': cssLength(size),
351
+ ...style,
352
+ } as CSSProperties
353
+ }
234
354
  onPointerDown={onPointerDown}
235
355
  onDoubleClick={(event) => {
236
- if (enableDoubleClickDefault && onChange) {
356
+ if (!inactive && enableDoubleClickDefault && onChange) {
237
357
  onChange(defaultValue)
238
358
  }
239
359
  onDoubleClick?.(event)
@@ -244,7 +364,7 @@ const Root = forwardRef<KnobMethods, Props>(
244
364
  }}
245
365
  {...props}
246
366
  >
247
- {children || <SVGRoot />}
367
+ {children}
248
368
  </div>
249
369
  </KnobProvider>
250
370
  )
@@ -262,4 +382,6 @@ export const Knob = {
262
382
  Thumb,
263
383
  }
264
384
 
265
- export { useKnobContext } from './context'
385
+ export { useKnobContext, type KnobContextValue } from './context'
386
+ export { type KnobSVGRootProps } from './SVGRoot'
387
+ export { type KnobThumbProps } from './Thumb'
@@ -1,57 +1,28 @@
1
- import clsx from 'clsx'
2
- import { ComponentPropsWithoutRef, ReactNode } from 'react'
1
+ import { ComponentPropsWithoutRef } from 'react'
3
2
 
4
- import { useLongPress } from '../../hooks/useLongPress'
3
+ import {
4
+ StepperArrow,
5
+ StepperButton,
6
+ StepperButtonProps,
7
+ } from './StepperButton'
5
8
 
6
- import { useNumberInputContext } from './context'
7
-
8
- export interface DecrementStepperProps {
9
- size?: number
10
- children?: ReactNode
11
- }
12
-
13
- export function DecrementStepper({
14
- size = 12,
15
- children,
16
- className,
17
- ...props
18
- }: DecrementStepperProps &
19
- Omit<ComponentPropsWithoutRef<'div'>, keyof DecrementStepperProps>) {
20
- const min = useNumberInputContext((s) => s.min)
21
- const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber)
22
- const readonly = useNumberInputContext((s) => s.readonly)
23
- const keepWithinRange = useNumberInputContext((s) => s.keepWithinRange)
24
- const decrement = useNumberInputContext((s) => s.decrement)
25
-
26
- const press = useLongPress(decrement)
9
+ // An interface rather than an alias; see `NumberInputIncrementStepperProps`.
10
+ export interface NumberInputDecrementStepperProps extends StepperButtonProps {}
27
11
 
12
+ /** Lowers the value by one `step`, repeating while held. */
13
+ export function DecrementStepper(
14
+ props: NumberInputDecrementStepperProps &
15
+ Omit<
16
+ ComponentPropsWithoutRef<'div'>,
17
+ keyof NumberInputDecrementStepperProps
18
+ >,
19
+ ) {
28
20
  return (
29
- // eslint-disable-next-line jsx-a11y/role-supports-aria-props
30
- <div
31
- className={clsx('tremolo-number-input-decrement-stepper', className)}
32
- role="button"
33
- tabIndex={-1}
34
- aria-disabled={
35
- keepWithinRange && valueAsNumber <= (min ?? Number.MIN_SAFE_INTEGER)
36
- }
37
- aria-readonly={readonly}
38
- onPointerDown={press}
21
+ <StepperButton
22
+ direction={-1}
23
+ variant="decrement"
24
+ icon={<StepperArrow up={false} />}
39
25
  {...props}
40
- >
41
- {children || (
42
- <svg
43
- width={size}
44
- height={size}
45
- viewBox="0 0 24 24"
46
- fill="none"
47
- stroke="currentColor"
48
- strokeWidth="2"
49
- strokeLinecap="round"
50
- strokeLinejoin="round"
51
- >
52
- <polyline points="6 9 12 15 18 9" />
53
- </svg>
54
- )}
55
- </div>
26
+ />
56
27
  )
57
28
  }
@@ -1,57 +1,29 @@
1
- import clsx from 'clsx'
2
- import { ComponentPropsWithoutRef, ReactNode } from 'react'
1
+ import { ComponentPropsWithoutRef } from 'react'
3
2
 
4
- import { useLongPress } from '../../hooks/useLongPress'
3
+ import {
4
+ StepperArrow,
5
+ StepperButton,
6
+ StepperButtonProps,
7
+ } from './StepperButton'
5
8
 
6
- import { useNumberInputContext } from './context'
7
-
8
- export interface IncrementStepperProps {
9
- size?: number
10
- children?: ReactNode
11
- }
12
-
13
- export function IncrementStepper({
14
- size = 12,
15
- children,
16
- className,
17
- ...props
18
- }: IncrementStepperProps &
19
- Omit<ComponentPropsWithoutRef<'div'>, keyof IncrementStepperProps>) {
20
- const max = useNumberInputContext((s) => s.max)
21
- const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber)
22
- const readonly = useNumberInputContext((s) => s.readonly)
23
- const keepWithinRange = useNumberInputContext((s) => s.keepWithinRange)
24
- const increment = useNumberInputContext((s) => s.increment)
25
-
26
- const press = useLongPress(increment)
9
+ // An interface rather than an alias, so that typedoc lists the props: the
10
+ // shared `StepperButtonProps` is not exported for it to follow.
11
+ export interface NumberInputIncrementStepperProps extends StepperButtonProps {}
27
12
 
13
+ /** Raises the value by one `step`, repeating while held. */
14
+ export function IncrementStepper(
15
+ props: NumberInputIncrementStepperProps &
16
+ Omit<
17
+ ComponentPropsWithoutRef<'div'>,
18
+ keyof NumberInputIncrementStepperProps
19
+ >,
20
+ ) {
28
21
  return (
29
- // eslint-disable-next-line jsx-a11y/role-supports-aria-props
30
- <div
31
- className={clsx('tremolo-number-input-increment-stepper', className)}
32
- role="button"
33
- tabIndex={-1}
34
- aria-disabled={
35
- keepWithinRange && valueAsNumber >= (max ?? Number.MAX_SAFE_INTEGER)
36
- }
37
- aria-readonly={readonly}
38
- onPointerDown={press}
22
+ <StepperButton
23
+ direction={1}
24
+ variant="increment"
25
+ icon={<StepperArrow up />}
39
26
  {...props}
40
- >
41
- {children || (
42
- <svg
43
- width={size}
44
- height={size}
45
- viewBox="0 0 24 24"
46
- fill="none"
47
- stroke="currentColor"
48
- strokeWidth="2"
49
- strokeLinecap="round"
50
- strokeLinejoin="round"
51
- >
52
- <polyline points="18 15 12 9 6 15" />
53
- </svg>
54
- )}
55
- </div>
27
+ />
56
28
  )
57
29
  }