@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,90 +1,247 @@
1
- import clsx from 'clsx'
2
- import { ComponentPropsWithoutRef, forwardRef, ReactNode } from 'react'
1
+ import {
2
+ ComponentPropsWithoutRef,
3
+ CSSProperties,
4
+ forwardRef,
5
+ ReactNode,
6
+ useCallback,
7
+ useImperativeHandle,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ } from 'react'
3
12
 
4
- import { InputEventOption } from '@tremolo-ui/functions'
13
+ import {
14
+ applyDelta,
15
+ InputEventOption,
16
+ ModifierState,
17
+ type ModifierValue,
18
+ linearScale,
19
+ selectInputEvent,
20
+ type Scale,
21
+ type ValueRange,
22
+ } from '@tremolo-ui/functions'
23
+
24
+ import { useCheckSteps } from '../../hooks/_internal/useCheckSteps'
25
+ import { useWheel } from '../../hooks/useWheel'
26
+ import {
27
+ DEFAULT_DRAG_SENSITIVITY,
28
+ DEFAULT_KEYBOARD_OPTIONS,
29
+ DEFAULT_WHEEL_OPTIONS,
30
+ } from '../../input-event'
5
31
 
6
32
  import { NumberInputProvider } from './context'
7
33
  import { DecrementStepper } from './DecrementStepper'
8
34
  import { IncrementStepper } from './IncrementStepper'
9
- import { InternalInput } from './InternalInput'
35
+ import { InputField } from './InputField'
10
36
  import { Stepper } from './Stepper'
11
- import { Units } from './type'
12
-
13
- /*
14
- TODO
15
- - numberMode: focusが当たった時だけ、parse + type="number"にする
16
- - skew
17
- */
18
37
 
19
38
  export interface NumberInputProps {
20
- value: number | string
39
+ /**
40
+ * The value. What the input shows is `format(value)`, except while the user
41
+ * is typing, when their own text stands until it is committed.
42
+ */
43
+ value: number
21
44
 
22
45
  /**
23
- * Number.MIN_SAFE_INTEGER
46
+ * Lowest value. Leave it out for no lower bound.
47
+ *
48
+ * It is enforced when a value is committed or stepped, while `clampValue` is
49
+ * on. A value below it carries `data-out-of-range` on `InputField`.
24
50
  */
25
51
  min?: number
26
52
  /**
27
- * Number.MAX_SAFE_INTEGER
53
+ * Highest value. Leave it out for no upper bound.
54
+ *
55
+ * It is enforced when a value is committed or stepped, while `clampValue` is
56
+ * on. A value above it carries `data-out-of-range` on `InputField`.
28
57
  */
29
58
  max?: number
59
+ /**
60
+ * Granularity of the value. The steppers, a drag on `Stepper`, the wheel and
61
+ * the arrow keys snap it to multiples of `step`; a typed value is left as it
62
+ * is.
63
+ *
64
+ * @default 1
65
+ */
30
66
  step?: number
67
+ /**
68
+ * How the value is distributed across the travel of a drag or a
69
+ * `'normalized'` wheel / keyboard nudge.
70
+ *
71
+ * @default linearScale
72
+ */
73
+ scale?: Scale
31
74
 
32
75
  /**
76
+ * Render the value as text. Plain digits by default.
77
+ *
78
+ * `unitFormat` from `@tremolo-ui/functions` builds this and `parse` together
79
+ * for a unit, as a pair to spread into the root.
80
+ *
33
81
  * @example
34
- * units='Hz'
35
- * units={[['Hz', 1], ['kHz', 1000]]}
36
- * units={[['ms', 1], ['s', 1000]]}
82
+ * <NumberInput.Root {...unitFormat('Hz', { digits: 2 })} value={v} />
83
+ */
84
+ format?: (value: number) => string
85
+ /**
86
+ * Read a value back out of the text. Has to undo `format`.
87
+ *
88
+ * Text with no number in it reads as `NaN`, which leaves the value alone.
37
89
  */
38
- units?: string | Units
90
+ parse?: (text: string) => number
39
91
 
40
92
  /**
41
- * Digits for rounding numbers.
42
- * @example
43
- * with:
44
- * units={[['Hz', 1], ['kHz', 1000]]}
45
- * digit={3}
46
- * results:
47
- * value=100 -> 100Hz, value=1600 -> 1.60Hz
93
+ * Keep the value within `min` and `max` when it is committed or stepped.
94
+ * Typing is never clamped, so that a value can be entered digit by digit.
95
+ * @default true
48
96
  */
49
- digit?: number
97
+ clampValue?: boolean
50
98
 
51
- readonly?: boolean
99
+ /**
100
+ * How much one notch of the wheel moves the value. It only acts while the
101
+ * focus is inside, so that scrolling the page past the input leaves it
102
+ * alone.
103
+ *
104
+ * `['raw', n]` moves the value by `n`, and `['normalized', n]` by `n` of the
105
+ * range between `min` and `max`. The result is snapped to `step`, except for
106
+ * an amount set on a modifier key (`{ default: …, shift: … }`). `null` turns
107
+ * the wheel off.
108
+ *
109
+ * @default ['raw', 1]
110
+ */
111
+ wheel?: ModifierValue<InputEventOption> | null
112
+ /**
113
+ * How much one arrow key press moves the value.
114
+ *
115
+ * `['raw', n]` moves the value by `n`, and `['normalized', n]` by `n` of the
116
+ * range between `min` and `max`. The result is snapped to `step`, except for
117
+ * an amount set on a modifier key, which is what lets shift move off the
118
+ * grid. `null` turns the arrow keys off.
119
+ *
120
+ * The default moves by 1, and by 0.1 with shift. With a `step` above 1, raise
121
+ * the amount to match: 1 would round straight back to where it started, and
122
+ * a development build warns about it.
123
+ *
124
+ * @default { default: ['raw', 1], shift: ['raw', 0.1] }
125
+ */
126
+ keyboard?: ModifierValue<InputEventOption> | null
127
+ /**
128
+ * Pixels of vertical drag on `Stepper` that move the value by one `step`.
129
+ * `null` turns the drag off.
130
+ * @default 1
131
+ */
132
+ drag?: number | null
52
133
 
53
- variant?: 'outline' | 'filled' | 'flushed' | 'unstyled'
134
+ /**
135
+ * How much a `Stepper` drag counts, per modifier key.
136
+ *
137
+ * `1` is `drag` pixels per `step`; `0.1` makes the same movement cover a
138
+ * tenth of that. Shift is bound to `0.1` by default, to match what it does
139
+ * on the arrow keys.
140
+ *
141
+ * A modifier entry is not snapped to `step`, which is what lets a finer
142
+ * amount move at all.
143
+ *
144
+ * @default { default: 1, shift: 0.1 }
145
+ */
146
+ dragSensitivity?: ModifierValue<number>
54
147
 
55
- selectWithFocus?: 'all' | 'number' | 'none'
56
- blurOnEnter?: boolean
57
- keepWithinRange?: boolean
58
- clampValueOnBlur?: boolean
148
+ /**
149
+ * Hide the cursor while dragging a `Stepper` and read the pointer movement
150
+ * directly, rather than letting it wander off across the screen.
151
+ *
152
+ * The drag is already relative, so the pointer position carries nothing —
153
+ * but it still runs into the edge of the screen, where the operating system
154
+ * pins it and the coordinates stop changing.
155
+ *
156
+ * Off by default: the browser shows its own notice, Esc takes the lock back,
157
+ * and the request can be refused. A refused request is not an error, and the
158
+ * drag carries on as an ordinary one.
159
+ *
160
+ * @default false
161
+ */
162
+ pointerLock?: boolean
59
163
 
60
164
  /**
61
- * wheel control option
62
- * If null, no event will be triggered
165
+ * Select the text when `InputField` takes focus: `'all'` selects all of it,
166
+ * `'number'` only the leading number, leaving whatever the format appended,
167
+ * and `'none'` leaves the caret where the click put it.
168
+ * @default 'none'
63
169
  */
64
- wheel?: InputEventOption | null
170
+ selectOnFocus?: 'all' | 'number' | 'none'
65
171
  /**
66
- * keyboard control option
67
- * If null, no event will be triggered
172
+ * Show the plain value while `InputField` has focus, dropping whatever
173
+ * `format` put around it: an input reading `1.23kHz` shows `1230` to be
174
+ * typed over.
175
+ *
176
+ * The number shown is the value itself, not the number inside the formatted
177
+ * text. Those differ whenever the format scales — `1.23` out of `1.23kHz`
178
+ * would read back as 1.23 and lose a factor of a thousand — and it is also
179
+ * why a rounded display no longer becomes the value: `1.6` shown as `2Hz`
180
+ * offers `1.6` for editing, not `2`.
181
+ *
182
+ * @default false
68
183
  */
69
- keyboard?: InputEventOption | null
70
-
71
- activeColor?: string
184
+ unformatOnFocus?: boolean
185
+ /**
186
+ * Put the caret in `InputField` back where it was after an arrow key steps
187
+ * the value.
188
+ *
189
+ * A controlled input whose `value` is replaced drops the caret at the end,
190
+ * so without this the second press of a repeated step always acts on the
191
+ * last digit. With it, the digit under the caret stays under the caret and
192
+ * a column can be held while stepping.
193
+ *
194
+ * The position is measured from the decimal point rather than from either
195
+ * end, so it survives the number growing or shrinking: the caret between
196
+ * `9` and `.9` is still between `10` and `.0`.
197
+ *
198
+ * It only restores the caret. Which digit it sits on does not change the
199
+ * size of the step — that is `keyboard`'s to say.
200
+ *
201
+ * @default false
202
+ */
203
+ keepCaretOnStep?: boolean
204
+ /**
205
+ * Commit and leave `InputField` when Enter is pressed. Enter commits either
206
+ * way.
207
+ * @default true
208
+ */
209
+ blurOnEnter?: boolean
72
210
 
73
- wrapperClassName?: string
211
+ /**
212
+ * Make the input unchangeable and remove it from the tab order.
213
+ * The parts carry `data-disabled` while it is set.
214
+ */
215
+ disabled?: boolean
216
+ /**
217
+ * Make the value unchangeable.
218
+ * The parts carry `data-readonly` while it is set.
219
+ */
220
+ readonly?: boolean
74
221
 
75
- onChange?: (value: number, text: string) => void
76
- onFocus?: (
77
- value: number,
78
- text: string,
79
- event: React.FocusEvent<HTMLInputElement, Element>,
80
- ) => void
81
- onBlur?: (
82
- value: number,
83
- text: string,
84
- event: React.FocusEvent<HTMLInputElement, Element>,
85
- ) => void
222
+ className?: string
223
+ style?: CSSProperties
224
+ /**
225
+ * Called with the new value. While the user types, it is called for every
226
+ * entry that reads as a number, unclamped; committing the entry calls it
227
+ * again if clamping changes the value.
228
+ */
229
+ onChange?: (value: number) => void
86
230
 
87
- children?: ReactNode
231
+ /**
232
+ * The input renders exactly what you compose here; there is no default
233
+ * markup to fall back to.
234
+ *
235
+ * @example
236
+ * <NumberInput.Root value={value} min={0} max={100} onChange={setValue}>
237
+ * <NumberInput.InputField />
238
+ * <NumberInput.Stepper>
239
+ * <NumberInput.IncrementStepper />
240
+ * <NumberInput.DecrementStepper />
241
+ * </NumberInput.Stepper>
242
+ * </NumberInput.Root>
243
+ */
244
+ children: ReactNode
88
245
  }
89
246
 
90
247
  export interface NumberInputMethods {
@@ -92,78 +249,270 @@ export interface NumberInputMethods {
92
249
  blur: () => void
93
250
  }
94
251
 
252
+ /**
253
+ * A number at the start of the text, and nothing read after it. A half-typed
254
+ * entry still yields the number in front of it, but text with no number is
255
+ * `NaN` rather than 0, so that "unreadable" and "the user typed 0" stay apart.
256
+ */
257
+ const LEADING_NUMBER = /^\s*([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)/
258
+
259
+ const defaultFormat = (value: number) => String(value)
260
+
261
+ const defaultParse = (text: string) => {
262
+ const match = text.match(LEADING_NUMBER)
263
+ return match ? Number(match[1]) : NaN
264
+ }
265
+
95
266
  type Props = NumberInputProps &
96
- Omit<ComponentPropsWithoutRef<'input'>, keyof NumberInputProps | 'type'>
267
+ Omit<ComponentPropsWithoutRef<'div'>, keyof NumberInputProps>
97
268
 
98
- const Root = forwardRef<NumberInputMethods, Props>(
269
+ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
99
270
  (
100
271
  {
101
272
  value,
102
273
  min,
103
274
  max,
104
275
  step = 1,
105
- units,
106
- readonly = false,
107
- disabled = false,
108
- digit,
109
- variant = 'outline',
110
- selectWithFocus = 'none',
276
+ scale = linearScale,
277
+ format: formatProp,
278
+ parse: parseProp,
279
+ clampValue = true,
280
+ wheel = DEFAULT_WHEEL_OPTIONS,
281
+ keyboard = DEFAULT_KEYBOARD_OPTIONS,
282
+ drag = 1,
283
+ dragSensitivity = DEFAULT_DRAG_SENSITIVITY,
284
+ pointerLock = false,
285
+ selectOnFocus = 'none',
286
+ unformatOnFocus = false,
287
+ keepCaretOnStep = false,
111
288
  blurOnEnter = true,
112
- keepWithinRange = true,
113
- clampValueOnBlur = true,
114
- wheel = ['raw', 1],
115
- keyboard = ['raw', 1],
116
- activeColor,
117
- wrapperClassName,
289
+ disabled = false,
290
+ readonly = false,
118
291
  className,
292
+ style,
119
293
  onChange,
120
- onFocus,
121
- onBlur,
122
294
  children,
123
295
  ...props
124
296
  }: Props,
125
297
  forwardedRef,
126
298
  ) => {
127
- const colors: Record<string, string | undefined> = {
128
- '--active-color': activeColor,
129
- }
299
+ // --- state and ref ---
300
+ const inputRef = useRef<HTMLInputElement>(null)
301
+ /**
302
+ * The text being typed. The only state here: it is not a copy of `value`,
303
+ * but the half-finished entry that has no value to be derived from yet.
304
+ */
305
+ const [draft, setDraft] = useState<string | null>(null)
306
+ const inactive = disabled || readonly
307
+
308
+ // --- interpret props ---
309
+ const format = formatProp ?? defaultFormat
310
+ const parse = parseProp ?? defaultParse
311
+
312
+ // Normalized input needs a finite span even when an end is unbounded or
313
+ // the caller opted out of clamping. Safe integers provide one without
314
+ // overflowing the span calculation used by a scale.
315
+ const range: ValueRange = useMemo(
316
+ () => ({
317
+ min: (clampValue ? min : undefined) ?? Number.MIN_SAFE_INTEGER,
318
+ max: (clampValue ? max : undefined) ?? Number.MAX_SAFE_INTEGER,
319
+ step,
320
+ scale,
321
+ }),
322
+ [clampValue, min, max, step, scale],
323
+ )
324
+
325
+ // Raw input does not need a finite span for normalization, so its open
326
+ // ends can cover every finite JavaScript number instead of stopping at the
327
+ // safe-integer range.
328
+ const rawRange: ValueRange = useMemo(
329
+ () => ({
330
+ min: (clampValue ? min : undefined) ?? -Number.MAX_VALUE,
331
+ max: (clampValue ? max : undefined) ?? Number.MAX_VALUE,
332
+ step,
333
+ scale,
334
+ }),
335
+ [clampValue, min, max, step, scale],
336
+ )
337
+
338
+ // The pipeline range stands in the safe-integer range when an end is
339
+ // open, and there is no travel to sample across that. Probing is skipped
340
+ // rather than run against a range nobody set.
341
+ const probeRange = useMemo(
342
+ () =>
343
+ min !== undefined && max !== undefined && min < max
344
+ ? { min, max, step, scale }
345
+ : null,
346
+ [min, max, step, scale],
347
+ )
348
+
349
+ useCheckSteps({
350
+ component: 'NumberInput',
351
+ range: probeRange,
352
+ keyboard,
353
+ wheel,
354
+ format,
355
+ })
356
+
357
+ const text = draft ?? format(value)
358
+ const editing = draft !== null
359
+ const outOfRange =
360
+ !editing &&
361
+ ((min !== undefined && value < min) || (max !== undefined && value > max))
362
+
363
+ // --- internal functions ---
364
+ const handleDraft = useCallback(
365
+ (next: string) => {
366
+ if (inactive) return
367
+ setDraft(next)
368
+ // Deliberately unclamped: clamping here would make "1500" impossible
369
+ // to type into an input whose max is 100.
370
+ const parsed = parse(next)
371
+ if (Number.isFinite(parsed)) onChange?.(parsed)
372
+ },
373
+ [inactive, parse, onChange],
374
+ )
375
+
376
+ const changeValue = useCallback(
377
+ (next: number) => {
378
+ if (inactive) return
379
+ setDraft(null)
380
+ if (next !== value) onChange?.(next)
381
+ },
382
+ [inactive, value, onChange],
383
+ )
384
+
385
+ const commitDraft = useCallback(() => {
386
+ if (draft === null || inactive) return
387
+ const parsed = parse(draft)
388
+ // Text with no number in it is not a value. Dropping the draft puts the
389
+ // input back to what it was showing, rather than committing a zero the
390
+ // user never typed.
391
+ if (!Number.isFinite(parsed)) {
392
+ setDraft(null)
393
+ return
394
+ }
395
+ let committed = parsed
396
+ if (clampValue && min !== undefined) committed = Math.max(committed, min)
397
+ if (clampValue && max !== undefined) committed = Math.min(committed, max)
398
+ changeValue(committed)
399
+ }, [draft, inactive, parse, clampValue, min, max, changeValue])
400
+
401
+ const nudge = useCallback(
402
+ (
403
+ direction: number,
404
+ option: ModifierValue<InputEventOption>,
405
+ modifiers?: ModifierState,
406
+ ) => {
407
+ const [mode] = selectInputEvent(option, modifiers).option
408
+ changeValue(
409
+ applyDelta(
410
+ value,
411
+ direction,
412
+ option,
413
+ mode === 'raw' ? rawRange : range,
414
+ modifiers,
415
+ ),
416
+ )
417
+ },
418
+ [changeValue, value, rawRange, range],
419
+ )
420
+
421
+ // --- hooks ---
422
+ const wheelRefCallback = useWheel<HTMLDivElement>(
423
+ (event) => {
424
+ if (!wheel || inactive || event.deltaY === 0) return
425
+ event.preventDefault()
426
+ nudge(-Math.sign(event.deltaY), wheel, event)
427
+ },
428
+ { requireFocus: true },
429
+ )
430
+
431
+ const context = useMemo(
432
+ () => ({
433
+ value,
434
+ min,
435
+ max,
436
+ step,
437
+ scale,
438
+ disabled,
439
+ readonly,
440
+ clampValue,
441
+ range,
442
+ rawRange,
443
+ keyboard,
444
+ text,
445
+ editing,
446
+ outOfRange,
447
+ dragSensitivity,
448
+ pointerLock,
449
+ selectOnFocus,
450
+ unformatOnFocus,
451
+ keepCaretOnStep,
452
+ blurOnEnter,
453
+ atMin: clampValue && min !== undefined && value <= min,
454
+ atMax: clampValue && max !== undefined && value >= max,
455
+ drag,
456
+ format,
457
+ parse,
458
+ setDraft: handleDraft,
459
+ commitDraft,
460
+ changeValue,
461
+ nudge,
462
+ inputRef,
463
+ }),
464
+ [
465
+ value,
466
+ min,
467
+ max,
468
+ step,
469
+ scale,
470
+ disabled,
471
+ readonly,
472
+ clampValue,
473
+ range,
474
+ rawRange,
475
+ keyboard,
476
+ text,
477
+ editing,
478
+ outOfRange,
479
+ drag,
480
+ dragSensitivity,
481
+ pointerLock,
482
+ selectOnFocus,
483
+ unformatOnFocus,
484
+ keepCaretOnStep,
485
+ blurOnEnter,
486
+ format,
487
+ parse,
488
+ handleDraft,
489
+ commitDraft,
490
+ changeValue,
491
+ nudge,
492
+ ],
493
+ )
494
+
495
+ useImperativeHandle(forwardedRef, () => {
496
+ return {
497
+ focus() {
498
+ if (!disabled) inputRef.current?.focus()
499
+ },
500
+ blur() {
501
+ inputRef.current?.blur()
502
+ },
503
+ }
504
+ }, [disabled])
505
+
130
506
  return (
131
- <NumberInputProvider
132
- value={String(value)}
133
- min={min}
134
- max={max}
135
- step={step}
136
- units={units}
137
- digit={digit}
138
- readonly={readonly}
139
- keepWithinRange={keepWithinRange}
140
- onChange={onChange}
141
- >
507
+ <NumberInputProvider value={context}>
142
508
  <div
143
- className={clsx('tremolo-number-input-wrapper', wrapperClassName)}
144
- // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
145
- tabIndex={0}
146
- style={{
147
- ...colors,
148
- }}
149
- data-stepper={!!children}
150
- data-variant={variant}
509
+ ref={wheelRefCallback}
510
+ className={className}
511
+ data-disabled={disabled ? '' : undefined}
512
+ data-readonly={readonly ? '' : undefined}
513
+ style={style}
514
+ {...props}
151
515
  >
152
- <InternalInput
153
- ref={forwardedRef}
154
- readonly={readonly}
155
- disabled={disabled}
156
- selectWithFocus={selectWithFocus}
157
- blurOnEnter={blurOnEnter}
158
- clampValueOnBlur={clampValueOnBlur}
159
- wheel={wheel}
160
- keyboard={keyboard}
161
- className={className}
162
- onChange={onChange}
163
- onFocus={onFocus}
164
- onBlur={onBlur}
165
- {...props}
166
- />
167
516
  {children}
168
517
  </div>
169
518
  </NumberInputProvider>
@@ -176,12 +525,14 @@ const Root = forwardRef<NumberInputMethods, Props>(
176
525
  */
177
526
  export const NumberInput = {
178
527
  Root,
528
+ InputField,
179
529
  Stepper,
180
530
  IncrementStepper,
181
531
  DecrementStepper,
182
532
  }
183
533
 
184
- export { type StepperProps } from './Stepper'
185
- export { type IncrementStepperProps } from './IncrementStepper'
186
- export { type DecrementStepperProps } from './DecrementStepper'
187
- export { type Units } from './type'
534
+ export { useNumberInputContext, type NumberInputContextValue } from './context'
535
+ export { type NumberInputInputFieldProps } from './InputField'
536
+ export { type NumberInputStepperProps } from './Stepper'
537
+ export { type NumberInputIncrementStepperProps } from './IncrementStepper'
538
+ export { type NumberInputDecrementStepperProps } from './DecrementStepper'