@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.
- package/dist/compose-refs.cjs +72 -0
- package/dist/compose-refs.cjs.map +1 -0
- package/dist/compose-refs.d.cts +43 -0
- package/dist/compose-refs.d.cts.map +1 -0
- package/dist/compose-refs.d.ts +43 -0
- package/dist/compose-refs.d.ts.map +1 -0
- package/dist/compose-refs.js +70 -0
- package/dist/compose-refs.js.map +1 -0
- package/dist/index.cjs +2272 -1650
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1461 -475
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +1461 -475
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2258 -1648
- package/dist/index.js.map +1 -1
- package/package.json +32 -31
- package/src/components/AnimationCanvas/index.tsx +162 -194
- package/src/components/Knob/ActiveLine.tsx +9 -8
- package/src/components/Knob/InactiveLine.tsx +11 -14
- package/src/components/Knob/SVGRoot.tsx +10 -17
- package/src/components/Knob/Thumb.tsx +33 -11
- package/src/components/Knob/context.tsx +73 -89
- package/src/components/Knob/index.tsx +227 -105
- package/src/components/NumberInput/DecrementStepper.tsx +21 -50
- package/src/components/NumberInput/IncrementStepper.tsx +22 -50
- package/src/components/NumberInput/InputField.tsx +206 -0
- package/src/components/NumberInput/Stepper.tsx +150 -21
- package/src/components/NumberInput/StepperButton.tsx +95 -0
- package/src/components/NumberInput/context.tsx +104 -149
- package/src/components/NumberInput/index.tsx +461 -110
- package/src/components/Piano/index.tsx +413 -263
- package/src/components/Piano/keyboard-shortcuts.ts +61 -0
- package/src/components/PointsEditor/Background.tsx +19 -4
- package/src/components/PointsEditor/Container.tsx +92 -22
- package/src/components/PointsEditor/Point.tsx +293 -53
- package/src/components/PointsEditor/SelectionBox.tsx +54 -0
- package/src/components/PointsEditor/context.tsx +97 -75
- package/src/components/PointsEditor/index.tsx +497 -62
- package/src/components/Slider/Marks.tsx +84 -0
- package/src/components/Slider/MarksOption.tsx +115 -0
- package/src/components/Slider/Thumb.tsx +99 -54
- package/src/components/Slider/Track.tsx +73 -62
- package/src/components/Slider/context.tsx +40 -53
- package/src/components/Slider/index.tsx +237 -190
- package/src/components/Slider/type.ts +27 -9
- package/src/components/XYPad/Area.tsx +52 -33
- package/src/components/XYPad/Thumb.tsx +121 -60
- package/src/components/XYPad/context.tsx +49 -0
- package/src/components/XYPad/index.tsx +338 -246
- package/src/components/_util/Placement.tsx +68 -0
- package/src/components/_util/VisuallyHiddenRangeInput.tsx +24 -0
- package/src/components/_util/css-length.ts +13 -0
- package/src/{components/_util/composeRefs.tsx → compose-refs/index.tsx} +33 -5
- package/src/hooks/{useCallbackRef.ts → _internal/useCallbackRef.ts} +3 -1
- package/src/hooks/_internal/useCheckSteps.ts +170 -0
- package/src/hooks/useAnimationFrame.ts +17 -7
- package/src/hooks/useDrag.ts +39 -9
- package/src/hooks/useDragValue.ts +194 -0
- package/src/hooks/useEventListener.ts +11 -7
- package/src/hooks/useLongPress.ts +42 -8
- package/src/hooks/useMIDIAccess.ts +82 -11
- package/src/hooks/useMIDIInput.ts +45 -13
- package/src/hooks/useMIDIMessage.ts +23 -5
- package/src/hooks/useWheel.ts +29 -9
- package/src/index.ts +50 -27
- package/src/input-event.ts +32 -0
- package/dist/index.css +0 -536
- package/src/Globals.d.ts +0 -3
- package/src/components/AnimationCanvas/canvas.ts +0 -46
- package/src/components/Knob/index.css +0 -77
- package/src/components/NumberInput/InternalInput.tsx +0 -216
- package/src/components/NumberInput/index.css +0 -185
- package/src/components/NumberInput/type.ts +0 -58
- package/src/components/Piano/KeyLabel.tsx +0 -51
- package/src/components/Piano/context.tsx +0 -73
- package/src/components/Piano/index.css +0 -90
- package/src/components/Piano/key.tsx +0 -205
- package/src/components/Piano/keyboardShortcuts.ts +0 -31
- package/src/components/PointsEditor/index.css +0 -30
- package/src/components/Slider/Scale.tsx +0 -52
- package/src/components/Slider/ScaleOption.tsx +0 -103
- package/src/components/Slider/index.css +0 -112
- package/src/components/XYPad/index.css +0 -48
- package/src/components/_util/index.ts +0 -28
- package/src/components/_util/type.ts +0 -1
- package/src/hooks/useDragWithElement.ts +0 -100
- package/src/hooks/usePianoDrag.ts +0 -75
- package/src/hooks/useRefCallbackEvent.ts +0 -34
- package/src/styles/global.css +0 -19
|
@@ -1,90 +1,247 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
90
|
+
parse?: (text: string) => number
|
|
39
91
|
|
|
40
92
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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
|
-
|
|
97
|
+
clampValue?: boolean
|
|
50
98
|
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
*
|
|
62
|
-
*
|
|
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
|
-
|
|
170
|
+
selectOnFocus?: 'all' | 'number' | 'none'
|
|
65
171
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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<'
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
|
185
|
-
export { type
|
|
186
|
-
export { type
|
|
187
|
-
export { 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'
|