@tremolo-ui/react 0.6.0 → 0.8.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/index.cjs +522 -997
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +601 -329
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +601 -329
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +487 -953
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/components/DropZone/index.tsx +101 -0
- package/src/components/FileInput/Trigger.tsx +39 -0
- package/src/components/FileInput/context.tsx +24 -0
- package/src/components/FileInput/index.tsx +139 -0
- package/src/components/Knob/ActiveLine.tsx +5 -3
- package/src/components/Knob/InactiveLine.tsx +6 -4
- package/src/components/Knob/SVGRoot.tsx +3 -3
- package/src/components/Knob/Thumb.tsx +3 -2
- package/src/components/Knob/context.tsx +3 -74
- package/src/components/Knob/index.tsx +27 -21
- package/src/components/NumberInput/DecrementStepper.tsx +2 -13
- package/src/components/NumberInput/IncrementStepper.tsx +2 -14
- package/src/components/NumberInput/InputField.tsx +17 -38
- package/src/components/NumberInput/Stepper.tsx +44 -107
- package/src/components/NumberInput/StepperButton.tsx +16 -12
- package/src/components/NumberInput/context.tsx +8 -8
- package/src/components/NumberInput/index.tsx +41 -81
- package/src/components/Piano/index.tsx +39 -149
- package/src/components/PointsEditor/Background.tsx +2 -8
- package/src/components/PointsEditor/Container.tsx +17 -34
- package/src/components/PointsEditor/Point.tsx +41 -86
- package/src/components/PointsEditor/SelectionBox.tsx +2 -8
- package/src/components/PointsEditor/context.tsx +10 -61
- package/src/components/PointsEditor/index.tsx +31 -315
- package/src/components/Slider/Marks.tsx +8 -13
- package/src/components/Slider/MarksOption.tsx +11 -17
- package/src/components/Slider/Thumb.tsx +9 -15
- package/src/components/Slider/Track.tsx +7 -13
- package/src/components/Slider/index.tsx +33 -39
- package/src/components/XYPad/Area.tsx +26 -53
- package/src/components/XYPad/Thumb.tsx +8 -12
- package/src/components/XYPad/context.tsx +1 -3
- package/src/components/XYPad/index.tsx +35 -50
- package/src/components/_util/VisuallyHiddenRangeInput.tsx +3 -15
- package/src/css-variables.ts +23 -0
- package/src/hooks/_internal/useCheckSteps.ts +9 -147
- package/src/hooks/useDrag.ts +13 -4
- package/src/hooks/useDragValue.ts +17 -2
- package/src/hooks/useDropZone.ts +78 -0
- package/src/hooks/useLongPress.ts +35 -51
- package/src/hooks/useMIDIAccess.ts +0 -8
- package/src/hooks/useMIDIInput.ts +0 -3
- package/src/hooks/useWheel.ts +4 -1
- package/src/index.ts +14 -36
- package/src/components/Piano/keyboard-shortcuts.ts +0 -61
- package/src/components/Slider/type.ts +0 -44
- package/src/components/_util/css-length.ts +0 -13
- package/src/input-event.ts +0 -32
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ComponentPropsWithoutRef,
|
|
3
|
-
CSSProperties,
|
|
4
3
|
forwardRef,
|
|
5
4
|
useEffect,
|
|
6
5
|
useLayoutEffect,
|
|
@@ -8,39 +7,21 @@ import {
|
|
|
8
7
|
useState,
|
|
9
8
|
} from 'react'
|
|
10
9
|
|
|
10
|
+
import {
|
|
11
|
+
caretAtDecimalOffset,
|
|
12
|
+
caretDecimalOffset,
|
|
13
|
+
numberSpan,
|
|
14
|
+
} from '@tremolo-ui/dom'
|
|
15
|
+
|
|
11
16
|
import { useComposedRefs } from '../../compose-refs'
|
|
12
17
|
|
|
13
18
|
import { useNumberInputContext } from './context'
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
type Props = NumberInputInputFieldProps &
|
|
21
|
-
Omit<
|
|
22
|
-
ComponentPropsWithoutRef<'input'>,
|
|
23
|
-
keyof NumberInputInputFieldProps | 'type' | 'value' | 'defaultValue'
|
|
24
|
-
>
|
|
25
|
-
|
|
26
|
-
/** The leading number of the displayed text, whatever the format put around it. */
|
|
27
|
-
const NUMBER_PREFIX = /^\s*-?[\d.,]*/
|
|
28
|
-
|
|
29
|
-
const numberEnd = (text: string) =>
|
|
30
|
-
text.match(NUMBER_PREFIX)?.[0].length ?? text.length
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* The index the caret is measured against: the decimal point, or where one
|
|
34
|
-
* would go if the number has none.
|
|
35
|
-
*
|
|
36
|
-
* Measuring from an end instead would slide the caret across a digit whenever
|
|
37
|
-
* the number changed length — `9.9` to `10.0` gains a character in front, `10`
|
|
38
|
-
* to `9` loses one — which is exactly what stepping does.
|
|
39
|
-
*/
|
|
40
|
-
const decimalAnchor = (text: string) => {
|
|
41
|
-
const dot = text.indexOf('.')
|
|
42
|
-
return dot === -1 ? numberEnd(text) : dot
|
|
43
|
-
}
|
|
20
|
+
// The value belongs to `NumberInput.Root`, and the field is always text.
|
|
21
|
+
type Props = Omit<
|
|
22
|
+
ComponentPropsWithoutRef<'input'>,
|
|
23
|
+
'type' | 'value' | 'defaultValue'
|
|
24
|
+
>
|
|
44
25
|
|
|
45
26
|
/**
|
|
46
27
|
* The text field of a `NumberInput`, and the only place the value can be typed.
|
|
@@ -53,7 +34,7 @@ const decimalAnchor = (text: string) => {
|
|
|
53
34
|
*/
|
|
54
35
|
export const InputField = /* @__PURE__ */ forwardRef<HTMLInputElement, Props>(
|
|
55
36
|
function InputField(
|
|
56
|
-
{ className, style, onFocus, onBlur, onKeyDown, ...props },
|
|
37
|
+
{ className, style, onChange, onFocus, onBlur, onKeyDown, ...props },
|
|
57
38
|
forwardedRef,
|
|
58
39
|
) {
|
|
59
40
|
const {
|
|
@@ -93,10 +74,8 @@ export const InputField = /* @__PURE__ */ forwardRef<HTMLInputElement, Props>(
|
|
|
93
74
|
if (selectOnFocus === 'all') {
|
|
94
75
|
input.setSelectionRange(0, input.value.length)
|
|
95
76
|
} else {
|
|
96
|
-
input.
|
|
97
|
-
|
|
98
|
-
input.value.match(NUMBER_PREFIX)?.[0].length ?? 0,
|
|
99
|
-
)
|
|
77
|
+
const span = numberSpan(input.value)
|
|
78
|
+
if (span) input.setSelectionRange(span.start, span.end)
|
|
100
79
|
}
|
|
101
80
|
// Deliberately not re-run as the text changes: that would drag the
|
|
102
81
|
// selection back over what the user is typing.
|
|
@@ -120,8 +99,7 @@ export const InputField = /* @__PURE__ */ forwardRef<HTMLInputElement, Props>(
|
|
|
120
99
|
// caret has not moved either, since the key press was prevented.
|
|
121
100
|
if (!input || input.value === pending.from) return
|
|
122
101
|
|
|
123
|
-
const
|
|
124
|
-
const caret = Math.max(0, Math.min(place, numberEnd(input.value)))
|
|
102
|
+
const caret = caretAtDecimalOffset(input.value, pending.offset)
|
|
125
103
|
input.setSelectionRange(caret, caret)
|
|
126
104
|
})
|
|
127
105
|
|
|
@@ -160,6 +138,7 @@ export const InputField = /* @__PURE__ */ forwardRef<HTMLInputElement, Props>(
|
|
|
160
138
|
// stepped one no longer means anything.
|
|
161
139
|
caretAfterStep.current = null
|
|
162
140
|
setDraft(event.currentTarget.value)
|
|
141
|
+
onChange?.(event)
|
|
163
142
|
}}
|
|
164
143
|
onFocus={(event) => {
|
|
165
144
|
setFocused(true)
|
|
@@ -191,7 +170,7 @@ export const InputField = /* @__PURE__ */ forwardRef<HTMLInputElement, Props>(
|
|
|
191
170
|
const input = event.currentTarget
|
|
192
171
|
if (keepCaretOnStep && input.selectionStart !== null) {
|
|
193
172
|
caretAfterStep.current = {
|
|
194
|
-
offset: input.
|
|
173
|
+
offset: caretDecimalOffset(input.value, input.selectionStart),
|
|
195
174
|
from: input.value,
|
|
196
175
|
}
|
|
197
176
|
}
|
|
@@ -1,37 +1,24 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ComponentPropsWithoutRef,
|
|
3
|
-
CSSProperties,
|
|
4
3
|
forwardRef,
|
|
5
|
-
|
|
4
|
+
useEffect,
|
|
6
5
|
useMemo,
|
|
7
6
|
useRef,
|
|
7
|
+
useState,
|
|
8
8
|
} from 'react'
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
applyDelta,
|
|
12
|
-
mapModifier,
|
|
13
|
-
selectModifier,
|
|
14
|
-
type InputEventOption,
|
|
15
|
-
} from '@tremolo-ui/functions'
|
|
10
|
+
import { createStepperDrag, type StepperDragInstance } from '@tremolo-ui/dom'
|
|
16
11
|
|
|
17
12
|
import { useComposedRefs } from '../../compose-refs'
|
|
18
|
-
import { useDrag } from '../../hooks/useDrag'
|
|
19
13
|
|
|
20
14
|
import { StepperProvider, useNumberInputContext } from './context'
|
|
21
15
|
|
|
22
|
-
|
|
23
|
-
className?: string
|
|
24
|
-
style?: CSSProperties
|
|
25
|
-
/** `<NumberInput.IncrementStepper />` and `<NumberInput.DecrementStepper />` go here. */
|
|
26
|
-
children?: ReactNode
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
type Props = NumberInputStepperProps &
|
|
30
|
-
Omit<ComponentPropsWithoutRef<'div'>, keyof NumberInputStepperProps>
|
|
16
|
+
type Props = ComponentPropsWithoutRef<'div'>
|
|
31
17
|
|
|
32
18
|
/**
|
|
33
19
|
* The area the steppers sit in, and a drag handle in its own right: dragging it
|
|
34
|
-
* up and down moves the value one `step` every `drag` pixels.
|
|
20
|
+
* up and down moves the value one `step` every `drag` pixels. The counting is
|
|
21
|
+
* `createStepperDrag` in the core.
|
|
35
22
|
*
|
|
36
23
|
* The drag lives here rather than on `InputField` because `createDrag` turns
|
|
37
24
|
* off text selection on whatever element it is attached to.
|
|
@@ -40,7 +27,6 @@ export const Stepper = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
|
40
27
|
function Stepper({ className, style, children, ...props }, forwardedRef) {
|
|
41
28
|
const {
|
|
42
29
|
value,
|
|
43
|
-
step,
|
|
44
30
|
disabled,
|
|
45
31
|
readonly,
|
|
46
32
|
drag,
|
|
@@ -49,100 +35,51 @@ export const Stepper = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
|
49
35
|
rawRange,
|
|
50
36
|
changeValue,
|
|
51
37
|
} = useNumberInputContext()
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
* through the same `applyDelta` the wheel and the arrow keys do. Carrying
|
|
57
|
-
* the modifier map over rather than resolving it here is what keeps `step`
|
|
58
|
-
* out of the pipeline for a modifier entry — naming one is a request to move
|
|
59
|
-
* off the grid.
|
|
60
|
-
*/
|
|
61
|
-
const dragOptions = useMemo(
|
|
62
|
-
() =>
|
|
63
|
-
mapModifier(dragSensitivity, (factor): InputEventOption => [
|
|
64
|
-
'raw',
|
|
65
|
-
step * factor,
|
|
66
|
-
]),
|
|
67
|
-
[dragSensitivity, step],
|
|
68
|
-
)
|
|
38
|
+
// Only attached while it can do something: `createDrag` puts
|
|
39
|
+
// `touch-action: none` on the element, and a stepper that cannot be
|
|
40
|
+
// dragged has no reason to stop the page scrolling under a finger.
|
|
41
|
+
const enabled = drag !== null && !disabled && !readonly
|
|
69
42
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const originRef = useRef<{ y: number; value: number } | null>(null)
|
|
82
|
-
/**
|
|
83
|
-
* Which modifier the drag is currently counting at, and where the previous
|
|
84
|
-
* event was — a key produces no pointer event of its own, so a change is
|
|
85
|
-
* only seen on the next move and has to be dated back to the one before it.
|
|
86
|
-
*/
|
|
87
|
-
const factorRef = useRef<number>(1)
|
|
88
|
-
const previousYRef = useRef(0)
|
|
43
|
+
// See useDrag for why the node is held in state rather than a ref.
|
|
44
|
+
const [node, setNode] = useState<HTMLDivElement | null>(null)
|
|
45
|
+
const instanceRef = useRef<StepperDragInstance | null>(null)
|
|
46
|
+
const latest = useRef({ value, changeValue })
|
|
47
|
+
const options = {
|
|
48
|
+
range: rawRange,
|
|
49
|
+
pixels: drag ?? 1,
|
|
50
|
+
sensitivity: dragSensitivity,
|
|
51
|
+
pointerLock,
|
|
52
|
+
}
|
|
53
|
+
const optionsRef = useRef(options)
|
|
89
54
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
draggingRef.current = false
|
|
97
|
-
factorRef.current = selectModifier(dragSensitivity, state.event).value
|
|
98
|
-
},
|
|
99
|
-
onDrag: (_x, y, _dx, _dy, state) => {
|
|
100
|
-
if (inactive || drag === null) return
|
|
101
|
-
if (!originRef.current) {
|
|
102
|
-
originRef.current = { y, value }
|
|
103
|
-
previousYRef.current = y
|
|
104
|
-
return
|
|
105
|
-
}
|
|
55
|
+
// Runs after every render.
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
latest.current = { value, changeValue }
|
|
58
|
+
optionsRef.current = options
|
|
59
|
+
instanceRef.current?.update(options)
|
|
60
|
+
})
|
|
106
61
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!node || !enabled) return
|
|
64
|
+
const instance = createStepperDrag(node, {
|
|
65
|
+
...optionsRef.current,
|
|
66
|
+
getValue: () => latest.current.value,
|
|
67
|
+
onChange: (next) => latest.current.changeValue(next),
|
|
68
|
+
})
|
|
69
|
+
instanceRef.current = instance
|
|
70
|
+
return () => {
|
|
71
|
+
instanceRef.current = null
|
|
72
|
+
instance.destroy()
|
|
73
|
+
}
|
|
74
|
+
}, [node, enabled])
|
|
117
75
|
|
|
118
|
-
|
|
119
|
-
const steps = Math.round(-(y - originRef.current.y) / drag)
|
|
120
|
-
if (steps === 0) return
|
|
121
|
-
draggingRef.current = true
|
|
122
|
-
// The same pipeline the wheel and the arrow keys use. Counting from where
|
|
123
|
-
// the drag started keeps it from accumulating a rounding error.
|
|
124
|
-
changeValue(
|
|
125
|
-
applyDelta(
|
|
126
|
-
originRef.current.value,
|
|
127
|
-
steps,
|
|
128
|
-
dragOptions,
|
|
129
|
-
rawRange,
|
|
130
|
-
state.event,
|
|
131
|
-
),
|
|
132
|
-
)
|
|
133
|
-
},
|
|
134
|
-
onDragEnd: () => {
|
|
135
|
-
draggingRef.current = false
|
|
136
|
-
},
|
|
137
|
-
})
|
|
76
|
+
const composedRef = useComposedRefs<HTMLDivElement>(forwardedRef, setNode)
|
|
138
77
|
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
78
|
+
const context = useMemo(
|
|
79
|
+
() => ({ moved: () => instanceRef.current?.moved() ?? false }),
|
|
80
|
+
[],
|
|
142
81
|
)
|
|
143
82
|
|
|
144
|
-
const context = useMemo(() => ({ draggingRef }), [])
|
|
145
|
-
|
|
146
83
|
return (
|
|
147
84
|
<StepperProvider value={context}>
|
|
148
85
|
<div ref={composedRef} className={className} style={style} {...props}>
|
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
import { ComponentPropsWithoutRef,
|
|
1
|
+
import { ComponentPropsWithoutRef, ReactNode, CSSProperties } from 'react'
|
|
2
2
|
|
|
3
3
|
import { useLongPress } from '../../hooks/useLongPress'
|
|
4
4
|
|
|
5
5
|
import { useNumberInputContext, useStepperContext } from './context'
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
import type { CSSVariables } from '../../css-variables'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `--stepper-icon-size` is read by the default arrow inside, and set on the
|
|
11
|
+
* button so that it inherits there.
|
|
12
|
+
*/
|
|
13
|
+
export type StepperButtonProps = Omit<
|
|
14
|
+
ComponentPropsWithoutRef<'div'>,
|
|
15
|
+
'style'
|
|
16
|
+
> & {
|
|
17
|
+
style?: CSSProperties & CSSVariables<'stepper-icon-size'>
|
|
12
18
|
}
|
|
13
19
|
|
|
14
|
-
type Props = StepperButtonProps
|
|
15
|
-
Omit<ComponentPropsWithoutRef<'div'>, keyof StepperButtonProps>
|
|
20
|
+
type Props = StepperButtonProps
|
|
16
21
|
|
|
17
22
|
/**
|
|
18
23
|
* Shared body of `IncrementStepper` and `DecrementStepper`: the two differ only
|
|
@@ -36,13 +41,12 @@ export function StepperButton({
|
|
|
36
41
|
useNumberInputContext()
|
|
37
42
|
const stepper = useStepperContext()
|
|
38
43
|
|
|
39
|
-
// Not memoized:
|
|
40
|
-
//
|
|
41
|
-
// calls it through a ref, so a fresh identity per render costs nothing.
|
|
44
|
+
// Not memoized: `useLongPress` only ever calls it through a ref, so a fresh
|
|
45
|
+
// identity per render costs nothing.
|
|
42
46
|
const press = useLongPress(() => {
|
|
43
47
|
// Once the pointer has actually travelled, the drag on `Stepper` owns the
|
|
44
48
|
// value; repeating on top of it would move it twice.
|
|
45
|
-
if (stepper?.
|
|
49
|
+
if (stepper?.moved()) return
|
|
46
50
|
nudge(direction, ['raw', step])
|
|
47
51
|
})
|
|
48
52
|
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { createContext, RefObject, useContext } from 'react'
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
InputEventOption,
|
|
5
|
-
ModifierState,
|
|
6
|
-
ModifierValue,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from '@tremolo-ui/functions'
|
|
3
|
+
import {
|
|
4
|
+
type InputEventOption,
|
|
5
|
+
type ModifierState,
|
|
6
|
+
type ModifierValue,
|
|
7
|
+
} from '@tremolo-ui/dom'
|
|
8
|
+
import { type Scale, type ValueRange } from '@tremolo-ui/functions'
|
|
10
9
|
|
|
11
10
|
export type NumberInputContextValue = {
|
|
12
11
|
value: number
|
|
@@ -104,7 +103,8 @@ export function useNumberInputContext<T>(
|
|
|
104
103
|
|
|
105
104
|
/** Set by `Stepper` once a drag has actually moved, so the steppers stand down. */
|
|
106
105
|
export type StepperContextValue = {
|
|
107
|
-
|
|
106
|
+
/** Whether the drag in progress has moved the value. */
|
|
107
|
+
moved: () => boolean
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
const StepperContext =
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ComponentPropsWithoutRef,
|
|
3
|
-
CSSProperties,
|
|
4
3
|
forwardRef,
|
|
5
4
|
ReactNode,
|
|
6
5
|
useCallback,
|
|
@@ -11,23 +10,23 @@ import {
|
|
|
11
10
|
} from 'react'
|
|
12
11
|
|
|
13
12
|
import {
|
|
14
|
-
|
|
13
|
+
DEFAULT_DRAG_SENSITIVITY,
|
|
14
|
+
DEFAULT_KEYBOARD_OPTIONS,
|
|
15
|
+
DEFAULT_WHEEL_OPTIONS,
|
|
16
|
+
commitNumberInputText,
|
|
15
17
|
InputEventOption,
|
|
16
18
|
ModifierState,
|
|
17
19
|
type ModifierValue,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
numberInputBounds,
|
|
21
|
+
numberInputRanges,
|
|
22
|
+
nudgeNumberInput,
|
|
23
|
+
parseNumberText,
|
|
24
|
+
wheelDirection,
|
|
25
|
+
} from '@tremolo-ui/dom'
|
|
26
|
+
import { linearScale, type Scale } from '@tremolo-ui/functions'
|
|
23
27
|
|
|
24
28
|
import { useCheckSteps } from '../../hooks/_internal/useCheckSteps'
|
|
25
29
|
import { useWheel } from '../../hooks/useWheel'
|
|
26
|
-
import {
|
|
27
|
-
DEFAULT_DRAG_SENSITIVITY,
|
|
28
|
-
DEFAULT_KEYBOARD_OPTIONS,
|
|
29
|
-
DEFAULT_WHEEL_OPTIONS,
|
|
30
|
-
} from '../../input-event'
|
|
31
30
|
|
|
32
31
|
import { NumberInputProvider } from './context'
|
|
33
32
|
import { DecrementStepper } from './DecrementStepper'
|
|
@@ -86,6 +85,9 @@ export interface NumberInputProps {
|
|
|
86
85
|
* Read a value back out of the text. Has to undo `format`.
|
|
87
86
|
*
|
|
88
87
|
* Text with no number in it reads as `NaN`, which leaves the value alone.
|
|
88
|
+
* The default, `parseNumberText` from `@tremolo-ui/dom`, reads the number
|
|
89
|
+
* with the unit around it left out, and is `NaN` for one it cannot read
|
|
90
|
+
* whole: `1,000`, `1:30`, or a number with a unit in front, such as `L 30`.
|
|
89
91
|
*/
|
|
90
92
|
parse?: (text: string) => number
|
|
91
93
|
|
|
@@ -163,7 +165,7 @@ export interface NumberInputProps {
|
|
|
163
165
|
|
|
164
166
|
/**
|
|
165
167
|
* Select the text when `InputField` takes focus: `'all'` selects all of it,
|
|
166
|
-
* `'number'` only the
|
|
168
|
+
* `'number'` only the number, leaving the unit the format put around it,
|
|
167
169
|
* and `'none'` leaves the caret where the click put it.
|
|
168
170
|
* @default 'none'
|
|
169
171
|
*/
|
|
@@ -219,8 +221,6 @@ export interface NumberInputProps {
|
|
|
219
221
|
*/
|
|
220
222
|
readonly?: boolean
|
|
221
223
|
|
|
222
|
-
className?: string
|
|
223
|
-
style?: CSSProperties
|
|
224
224
|
/**
|
|
225
225
|
* Called with the new value. While the user types, it is called for every
|
|
226
226
|
* entry that reads as a number, unclamped; committing the entry calls it
|
|
@@ -249,20 +249,8 @@ export interface NumberInputMethods {
|
|
|
249
249
|
blur: () => void
|
|
250
250
|
}
|
|
251
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
252
|
const defaultFormat = (value: number) => String(value)
|
|
260
253
|
|
|
261
|
-
const defaultParse = (text: string) => {
|
|
262
|
-
const match = text.match(LEADING_NUMBER)
|
|
263
|
-
return match ? Number(match[1]) : NaN
|
|
264
|
-
}
|
|
265
|
-
|
|
266
254
|
type Props = NumberInputProps &
|
|
267
255
|
Omit<ComponentPropsWithoutRef<'div'>, keyof NumberInputProps>
|
|
268
256
|
|
|
@@ -307,32 +295,15 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
307
295
|
|
|
308
296
|
// --- interpret props ---
|
|
309
297
|
const format = formatProp ?? defaultFormat
|
|
310
|
-
const parse = parseProp ??
|
|
298
|
+
const parse = parseProp ?? parseNumberText
|
|
311
299
|
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
}),
|
|
300
|
+
const { normalized: range, raw: rawRange } = useMemo(
|
|
301
|
+
() => numberInputRanges({ min, max, step, scale, clampValue }),
|
|
322
302
|
[clampValue, min, max, step, scale],
|
|
323
303
|
)
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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],
|
|
304
|
+
const ranges = useMemo(
|
|
305
|
+
() => ({ normalized: range, raw: rawRange }),
|
|
306
|
+
[range, rawRange],
|
|
336
307
|
)
|
|
337
308
|
|
|
338
309
|
// The pipeline range stands in the safe-integer range when an end is
|
|
@@ -356,9 +327,9 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
356
327
|
|
|
357
328
|
const text = draft ?? format(value)
|
|
358
329
|
const editing = draft !== null
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
330
|
+
const bounds = numberInputBounds(value, { min, max, clampValue })
|
|
331
|
+
const { atMin, atMax } = bounds
|
|
332
|
+
const outOfRange = !editing && bounds.outOfRange
|
|
362
333
|
|
|
363
334
|
// --- internal functions ---
|
|
364
335
|
const handleDraft = useCallback(
|
|
@@ -384,18 +355,15 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
384
355
|
|
|
385
356
|
const commitDraft = useCallback(() => {
|
|
386
357
|
if (draft === null || inactive) return
|
|
387
|
-
const
|
|
358
|
+
const committed = commitNumberInputText(draft, parse, {
|
|
359
|
+
min,
|
|
360
|
+
max,
|
|
361
|
+
clampValue,
|
|
362
|
+
})
|
|
388
363
|
// Text with no number in it is not a value. Dropping the draft puts the
|
|
389
|
-
// input back to what it was showing
|
|
390
|
-
|
|
391
|
-
|
|
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)
|
|
364
|
+
// input back to what it was showing.
|
|
365
|
+
if (committed === null) setDraft(null)
|
|
366
|
+
else changeValue(committed)
|
|
399
367
|
}, [draft, inactive, parse, clampValue, min, max, changeValue])
|
|
400
368
|
|
|
401
369
|
const nudge = useCallback(
|
|
@@ -404,26 +372,20 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
404
372
|
option: ModifierValue<InputEventOption>,
|
|
405
373
|
modifiers?: ModifierState,
|
|
406
374
|
) => {
|
|
407
|
-
const [mode] = selectInputEvent(option, modifiers).option
|
|
408
375
|
changeValue(
|
|
409
|
-
|
|
410
|
-
value,
|
|
411
|
-
direction,
|
|
412
|
-
option,
|
|
413
|
-
mode === 'raw' ? rawRange : range,
|
|
414
|
-
modifiers,
|
|
415
|
-
),
|
|
376
|
+
nudgeNumberInput(value, direction, option, ranges, modifiers),
|
|
416
377
|
)
|
|
417
378
|
},
|
|
418
|
-
[changeValue, value,
|
|
379
|
+
[changeValue, value, ranges],
|
|
419
380
|
)
|
|
420
381
|
|
|
421
382
|
// --- hooks ---
|
|
422
383
|
const wheelRefCallback = useWheel<HTMLDivElement>(
|
|
423
384
|
(event) => {
|
|
424
|
-
|
|
385
|
+
const direction = wheelDirection(event)
|
|
386
|
+
if (!wheel || inactive || direction === null) return
|
|
425
387
|
event.preventDefault()
|
|
426
|
-
nudge(
|
|
388
|
+
nudge(direction, wheel, event)
|
|
427
389
|
},
|
|
428
390
|
{ requireFocus: true },
|
|
429
391
|
)
|
|
@@ -450,8 +412,8 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
450
412
|
unformatOnFocus,
|
|
451
413
|
keepCaretOnStep,
|
|
452
414
|
blurOnEnter,
|
|
453
|
-
atMin
|
|
454
|
-
atMax
|
|
415
|
+
atMin,
|
|
416
|
+
atMax,
|
|
455
417
|
drag,
|
|
456
418
|
format,
|
|
457
419
|
parse,
|
|
@@ -476,6 +438,8 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
476
438
|
text,
|
|
477
439
|
editing,
|
|
478
440
|
outOfRange,
|
|
441
|
+
atMin,
|
|
442
|
+
atMax,
|
|
479
443
|
drag,
|
|
480
444
|
dragSensitivity,
|
|
481
445
|
pointerLock,
|
|
@@ -532,7 +496,3 @@ export const NumberInput = {
|
|
|
532
496
|
}
|
|
533
497
|
|
|
534
498
|
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'
|