@tremolo-ui/react 0.8.0 → 0.9.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 +416 -252
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +256 -110
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +256 -110
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +368 -203
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
- package/src/components/AnimationCanvas/index.tsx +4 -18
- package/src/components/FileInput/index.tsx +4 -1
- package/src/components/Knob/Thumb.tsx +19 -19
- package/src/components/Knob/index.tsx +85 -41
- package/src/components/NumberInput/InputField.tsx +9 -7
- package/src/components/NumberInput/Stepper.tsx +30 -6
- package/src/components/NumberInput/StepperButton.tsx +12 -4
- package/src/components/NumberInput/context.tsx +24 -2
- package/src/components/NumberInput/index.tsx +142 -27
- package/src/components/Piano/index.tsx +24 -7
- package/src/components/PointsEditor/Container.tsx +1 -1
- package/src/components/PointsEditor/Point.tsx +61 -30
- package/src/components/PointsEditor/context.tsx +3 -4
- package/src/components/PointsEditor/index.tsx +14 -15
- package/src/components/Slider/Marks.tsx +3 -2
- package/src/components/Slider/MarksOption.tsx +3 -2
- package/src/components/Slider/Thumb.tsx +20 -11
- package/src/components/Slider/Track.tsx +15 -13
- package/src/components/Slider/context.tsx +3 -3
- package/src/components/Slider/index.tsx +85 -48
- package/src/components/XYPad/Thumb.tsx +18 -8
- package/src/components/XYPad/context.tsx +1 -1
- package/src/components/XYPad/index.tsx +74 -42
- package/src/components/_util/VisuallyHiddenRangeInput.tsx +1 -1
- package/src/hooks/_internal/useChangeGesture.ts +93 -0
- package/src/hooks/_internal/useCheckSteps.ts +1 -1
- package/src/hooks/useAnimationFrame.ts +37 -15
- package/src/hooks/useEventListener.ts +93 -22
- package/src/hooks/useWheel.ts +4 -4
- package/src/index.ts +9 -5
- package/src/hooks/useInterval.ts +0 -27
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tremolo-ui/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "React component library for audio app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -22,18 +22,18 @@
|
|
|
22
22
|
"clean:sb": "rimraf ./storybook-static"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@storybook/addon-a11y": "10.6.
|
|
26
|
-
"@storybook/addon-docs": "10.6.
|
|
27
|
-
"@storybook/addon-links": "10.6.
|
|
28
|
-
"@storybook/react-vite": "10.6.
|
|
25
|
+
"@storybook/addon-a11y": "10.6.1",
|
|
26
|
+
"@storybook/addon-docs": "10.6.1",
|
|
27
|
+
"@storybook/addon-links": "10.6.1",
|
|
28
|
+
"@storybook/react-vite": "10.6.1",
|
|
29
29
|
"@testing-library/jest-dom": "7.0.1",
|
|
30
30
|
"@testing-library/react": "16.3.3",
|
|
31
31
|
"@types/react": "19.3.0",
|
|
32
32
|
"@types/react-dom": "19.3.0",
|
|
33
|
-
"jotai": "3.0.
|
|
33
|
+
"jotai": "3.0.1",
|
|
34
34
|
"react": "19.3.0",
|
|
35
35
|
"react-dom": "19.3.0",
|
|
36
|
-
"storybook": "10.6.
|
|
36
|
+
"storybook": "10.6.1",
|
|
37
37
|
"tone": "15.1.22"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"react-dom": "^18 || ^19"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@tremolo-ui/dom": "
|
|
45
|
-
"@tremolo-ui/functions": "^0.
|
|
44
|
+
"@tremolo-ui/dom": "0.9.0",
|
|
45
|
+
"@tremolo-ui/functions": "^0.9.0"
|
|
46
46
|
},
|
|
47
47
|
"files": [
|
|
48
48
|
"dist",
|
|
@@ -9,26 +9,12 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
createAnimationCanvas,
|
|
11
11
|
type AnimationCanvasInstance,
|
|
12
|
-
type
|
|
12
|
+
type CanvasDrawFunction,
|
|
13
|
+
type CanvasInitFunction,
|
|
13
14
|
} from '@tremolo-ui/dom'
|
|
14
15
|
|
|
15
16
|
import { useComposedRefs } from '../../compose-refs'
|
|
16
17
|
|
|
17
|
-
export type InitFunction = (
|
|
18
|
-
context: CanvasRenderingContext2D,
|
|
19
|
-
option: {
|
|
20
|
-
/** current canvas width */
|
|
21
|
-
width: number
|
|
22
|
-
/** current canvas height */
|
|
23
|
-
height: number
|
|
24
|
-
},
|
|
25
|
-
) => void
|
|
26
|
-
|
|
27
|
-
export type DrawFunction = (
|
|
28
|
-
context: CanvasRenderingContext2D,
|
|
29
|
-
option: AnimationFrame,
|
|
30
|
-
) => void
|
|
31
|
-
|
|
32
18
|
export interface AnimationCanvasCommonProps {
|
|
33
19
|
/**
|
|
34
20
|
* Draw one frame. It is given the 2D context and the frame: the size in CSS
|
|
@@ -37,12 +23,12 @@ export interface AnimationCanvasCommonProps {
|
|
|
37
23
|
* A new function replaces the old one in place, so writing it inline does
|
|
38
24
|
* not restart anything.
|
|
39
25
|
*/
|
|
40
|
-
draw:
|
|
26
|
+
draw: CanvasDrawFunction
|
|
41
27
|
/**
|
|
42
28
|
* Called once before the first frame, with the context and the size in CSS
|
|
43
29
|
* pixels. Set up what every frame shares here.
|
|
44
30
|
*/
|
|
45
|
-
init?:
|
|
31
|
+
init?: CanvasInitFunction
|
|
46
32
|
/**
|
|
47
33
|
* Redraw on every animation frame. Turn it off to draw only when there is a
|
|
48
34
|
* reason to: when the canvas mounts, when it is resized, and when the
|
|
@@ -7,7 +7,10 @@ import {
|
|
|
7
7
|
useMemo,
|
|
8
8
|
} from 'react'
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
partitionByAccept,
|
|
12
|
+
visuallyHiddenStyle,
|
|
13
|
+
} from '@tremolo-ui/dom/internal'
|
|
11
14
|
|
|
12
15
|
import { FileInputProvider } from './context'
|
|
13
16
|
import { Trigger } from './Trigger'
|
|
@@ -12,46 +12,46 @@ export interface KnobThumbProps {
|
|
|
12
12
|
* Fill colour of the circle.
|
|
13
13
|
* @default 'currentColor'
|
|
14
14
|
*/
|
|
15
|
-
|
|
15
|
+
color?: string
|
|
16
16
|
/**
|
|
17
17
|
* Colour of the line that points at the value.
|
|
18
18
|
* @default 'currentColor'
|
|
19
19
|
*/
|
|
20
|
-
|
|
20
|
+
lineColor?: string
|
|
21
21
|
/**
|
|
22
22
|
* Diameter of the circle, as a percentage of the knob.
|
|
23
23
|
* @default 84
|
|
24
24
|
*/
|
|
25
|
-
|
|
25
|
+
size?: number
|
|
26
26
|
/**
|
|
27
27
|
* Thickness of the line, as a percentage of the knob.
|
|
28
28
|
* @default 6
|
|
29
29
|
*/
|
|
30
|
-
|
|
30
|
+
lineWeight?: number
|
|
31
31
|
/**
|
|
32
32
|
* How far down the line reaches, as a percentage of the knob from its top.
|
|
33
33
|
* The line starts at the edge of the circle.
|
|
34
34
|
* @default 35
|
|
35
35
|
*/
|
|
36
|
-
|
|
36
|
+
lineLength?: number
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
* Classes for what the thumb draws inside itself: `
|
|
40
|
-
*
|
|
39
|
+
* Classes for what the thumb draws inside itself: `line` is the line that
|
|
40
|
+
* points at the value.
|
|
41
41
|
*/
|
|
42
42
|
classes?: {
|
|
43
|
-
|
|
43
|
+
line?: string
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export function Thumb({
|
|
48
48
|
className,
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
color = 'currentColor',
|
|
51
|
+
lineColor = 'currentColor',
|
|
52
|
+
size = 84,
|
|
53
|
+
lineWeight = 6,
|
|
54
|
+
lineLength = 35,
|
|
55
55
|
classes,
|
|
56
56
|
|
|
57
57
|
...props
|
|
@@ -65,15 +65,15 @@ export function Thumb({
|
|
|
65
65
|
|
|
66
66
|
return (
|
|
67
67
|
<svg className={className} {...props}>
|
|
68
|
-
<circle cx="50%" cy="50%" r={`${
|
|
68
|
+
<circle cx="50%" cy="50%" r={`${size / 2}%`} fill={color} />
|
|
69
69
|
<line
|
|
70
|
-
className={classes?.
|
|
70
|
+
className={classes?.line}
|
|
71
71
|
x1="50%"
|
|
72
|
-
y1={`${(KNOB_VIEWBOX_SIZE - clamp(
|
|
72
|
+
y1={`${(KNOB_VIEWBOX_SIZE - clamp(size, 0, 100)) / 2}%`}
|
|
73
73
|
x2="50%"
|
|
74
|
-
y2={`${
|
|
75
|
-
stroke={
|
|
76
|
-
strokeWidth={`${
|
|
74
|
+
y2={`${lineLength}%`}
|
|
75
|
+
stroke={lineColor}
|
|
76
|
+
strokeWidth={`${lineWeight}%`}
|
|
77
77
|
// NOTE
|
|
78
78
|
// https://bugs.webkit.org/show_bug.cgi?id=201854
|
|
79
79
|
// https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/transform-origin#browser_compatibility
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
CSSProperties,
|
|
4
4
|
forwardRef,
|
|
5
5
|
ReactNode,
|
|
6
|
+
Ref,
|
|
6
7
|
useCallback,
|
|
7
8
|
useImperativeHandle,
|
|
8
9
|
useMemo,
|
|
@@ -10,23 +11,27 @@ import {
|
|
|
10
11
|
} from 'react'
|
|
11
12
|
|
|
12
13
|
import {
|
|
13
|
-
applyDelta,
|
|
14
|
-
arrowKeyDirection,
|
|
15
14
|
type AxisOptions,
|
|
16
|
-
|
|
15
|
+
type ChangeSource,
|
|
17
16
|
DEFAULT_DRAG_SENSITIVITY,
|
|
18
17
|
DEFAULT_KEYBOARD_OPTIONS,
|
|
19
18
|
DEFAULT_WHEEL_OPTIONS,
|
|
20
19
|
type InputEventOption,
|
|
21
20
|
knobAngles,
|
|
22
21
|
type ModifierValue,
|
|
23
|
-
selectModifier,
|
|
24
|
-
wheelDirection,
|
|
25
22
|
type XY,
|
|
26
23
|
} from '@tremolo-ui/dom'
|
|
24
|
+
import {
|
|
25
|
+
applyDelta,
|
|
26
|
+
arrowKeyDirection,
|
|
27
|
+
cssLength,
|
|
28
|
+
selectModifier,
|
|
29
|
+
wheelDirection,
|
|
30
|
+
} from '@tremolo-ui/dom/internal'
|
|
27
31
|
import { linearScale, type Scale, type ValueRange } from '@tremolo-ui/functions'
|
|
28
32
|
|
|
29
33
|
import { useComposedRefs } from '../../compose-refs'
|
|
34
|
+
import { useChangeGesture } from '../../hooks/_internal/useChangeGesture'
|
|
30
35
|
import { useCheckSteps } from '../../hooks/_internal/useCheckSteps'
|
|
31
36
|
import { useDragValue } from '../../hooks/useDragValue'
|
|
32
37
|
import { useWheel } from '../../hooks/useWheel'
|
|
@@ -39,10 +44,6 @@ import { Thumb } from './Thumb'
|
|
|
39
44
|
|
|
40
45
|
import type { CSSVariables } from '../../css-variables'
|
|
41
46
|
|
|
42
|
-
const defaultExternalStyles: KnobProps['externalStyles'] = {
|
|
43
|
-
cursor: 'grabbing',
|
|
44
|
-
}
|
|
45
|
-
|
|
46
47
|
export interface KnobProps {
|
|
47
48
|
/** The current value. The knob shows only this, so update it from `onChange`. */
|
|
48
49
|
value: number
|
|
@@ -69,10 +70,10 @@ export interface KnobProps {
|
|
|
69
70
|
*/
|
|
70
71
|
scale?: Scale
|
|
71
72
|
/**
|
|
72
|
-
* The value a double click restores
|
|
73
|
-
* @default
|
|
73
|
+
* The value a double click restores. `null` turns the double click off.
|
|
74
|
+
* @default startValue
|
|
74
75
|
*/
|
|
75
|
-
|
|
76
|
+
resetValue?: number | null
|
|
76
77
|
|
|
77
78
|
/**
|
|
78
79
|
* Where the active arc starts. Put it at the centre of a bipolar control,
|
|
@@ -91,11 +92,9 @@ export interface KnobProps {
|
|
|
91
92
|
* The cursor to show while dragging. It is set on the dragged element, so it
|
|
92
93
|
* stays while the pointer is outside the knob.
|
|
93
94
|
*
|
|
94
|
-
* @default
|
|
95
|
+
* @default 'grabbing'
|
|
95
96
|
*/
|
|
96
|
-
|
|
97
|
-
cursor?: CSSProperties['cursor']
|
|
98
|
-
}
|
|
97
|
+
dragCursor?: CSSProperties['cursor']
|
|
99
98
|
/**
|
|
100
99
|
* How much one notch of the wheel moves the value. It only acts while the
|
|
101
100
|
* focus is inside, so that scrolling the page past the knob leaves it alone.
|
|
@@ -158,11 +157,6 @@ export interface KnobProps {
|
|
|
158
157
|
* @default { default: ['raw', 1], shift: ['raw', 0.1] }
|
|
159
158
|
*/
|
|
160
159
|
keyboard?: ModifierValue<InputEventOption> | null
|
|
161
|
-
/**
|
|
162
|
-
* Restore `defaultValue` on a double click.
|
|
163
|
-
* @default true
|
|
164
|
-
*/
|
|
165
|
-
enableDoubleClickDefault?: boolean
|
|
166
160
|
|
|
167
161
|
/**
|
|
168
162
|
* Make the knob unchangeable and remove it from the tab order.
|
|
@@ -173,7 +167,7 @@ export interface KnobProps {
|
|
|
173
167
|
* Make the knob unchangeable while leaving it focusable.
|
|
174
168
|
* The parts carry `data-readonly` while it is set.
|
|
175
169
|
*/
|
|
176
|
-
|
|
170
|
+
readOnly?: boolean
|
|
177
171
|
|
|
178
172
|
/**
|
|
179
173
|
* How far the knob turns from `min` to `max`, in degrees, centred on the
|
|
@@ -187,6 +181,25 @@ export interface KnobProps {
|
|
|
187
181
|
* click moves it.
|
|
188
182
|
*/
|
|
189
183
|
onChange?: (value: number) => void
|
|
184
|
+
/**
|
|
185
|
+
* Called when a change of the value starts — a drag, a double click, the first wheel notch
|
|
186
|
+
* or arrow key — with the value
|
|
187
|
+
* before it and what it is made with. A host recording automation can treat
|
|
188
|
+
* the control as touched from here until `onChangeEnd`.
|
|
189
|
+
*/
|
|
190
|
+
onChangeStart?: (value: number, source: ChangeSource) => void
|
|
191
|
+
/**
|
|
192
|
+
* Called when the change ends, with the value it ended on: on release, or
|
|
193
|
+
* `changeEndDelay` after the last wheel notch or arrow key.
|
|
194
|
+
*/
|
|
195
|
+
onChangeEnd?: (value: number, source: ChangeSource) => void
|
|
196
|
+
/**
|
|
197
|
+
* How long after the last wheel notch or arrow key the change counts as
|
|
198
|
+
* over, in milliseconds. Neither has an event that says it is done.
|
|
199
|
+
*
|
|
200
|
+
* @default 500
|
|
201
|
+
*/
|
|
202
|
+
changeEndDelay?: number
|
|
190
203
|
|
|
191
204
|
/**
|
|
192
205
|
* The knob renders exactly what you compose here; there is no default
|
|
@@ -204,6 +217,13 @@ export interface KnobProps {
|
|
|
204
217
|
children: ReactNode
|
|
205
218
|
|
|
206
219
|
style?: CSSProperties & CSSVariables<'knob-size'>
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Receives `focus` and `blur`. They do nothing while the knob is disabled,
|
|
223
|
+
* where the element's own `focus()` would still focus it. `ref` reaches the
|
|
224
|
+
* root element itself.
|
|
225
|
+
*/
|
|
226
|
+
actionsRef?: Ref<KnobMethods>
|
|
207
227
|
}
|
|
208
228
|
|
|
209
229
|
export interface KnobMethods {
|
|
@@ -219,7 +239,7 @@ type Props = KnobProps & Omit<ComponentPropsWithoutRef<'div'>, keyof KnobProps>
|
|
|
219
239
|
*/
|
|
220
240
|
const WHEEL_OPTIONS = { requireFocus: true }
|
|
221
241
|
|
|
222
|
-
export const Root = /* @__PURE__ */ forwardRef<
|
|
242
|
+
export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
223
243
|
(
|
|
224
244
|
{
|
|
225
245
|
value,
|
|
@@ -227,33 +247,35 @@ export const Root = /* @__PURE__ */ forwardRef<KnobMethods, Props>(
|
|
|
227
247
|
max,
|
|
228
248
|
step = 1,
|
|
229
249
|
scale = linearScale,
|
|
230
|
-
|
|
250
|
+
resetValue,
|
|
231
251
|
startValue = min,
|
|
232
252
|
size,
|
|
233
|
-
|
|
253
|
+
dragCursor = 'grabbing',
|
|
234
254
|
wheel = DEFAULT_WHEEL_OPTIONS,
|
|
235
255
|
keyboard = DEFAULT_KEYBOARD_OPTIONS,
|
|
236
256
|
dragSensitivity = DEFAULT_DRAG_SENSITIVITY,
|
|
237
257
|
pointerLock = false,
|
|
238
|
-
enableDoubleClickDefault = true,
|
|
239
258
|
disabled = false,
|
|
240
|
-
|
|
259
|
+
readOnly = false,
|
|
241
260
|
angleRange = 270,
|
|
242
261
|
onChange,
|
|
262
|
+
onChangeStart,
|
|
263
|
+
onChangeEnd,
|
|
264
|
+
changeEndDelay,
|
|
243
265
|
onKeyDown,
|
|
244
266
|
onPointerDown,
|
|
245
267
|
onDoubleClick,
|
|
246
268
|
className,
|
|
247
269
|
style,
|
|
248
270
|
children,
|
|
271
|
+
actionsRef,
|
|
249
272
|
...props
|
|
250
273
|
}: Props,
|
|
251
274
|
forwardedRef,
|
|
252
275
|
) => {
|
|
253
|
-
const elmRef = useRef<
|
|
276
|
+
const elmRef = useRef<HTMLDivElement>(null)
|
|
254
277
|
|
|
255
|
-
const
|
|
256
|
-
const inactive = disabled || readonly
|
|
278
|
+
const inactive = disabled || readOnly
|
|
257
279
|
|
|
258
280
|
// --- internal functions ---
|
|
259
281
|
const range: ValueRange = useMemo(
|
|
@@ -263,15 +285,29 @@ export const Root = /* @__PURE__ */ forwardRef<KnobMethods, Props>(
|
|
|
263
285
|
|
|
264
286
|
useCheckSteps({ component: 'Knob', range, keyboard, wheel })
|
|
265
287
|
|
|
288
|
+
const gesture = useChangeGesture(
|
|
289
|
+
value,
|
|
290
|
+
{ onChangeStart, onChangeEnd, changeEndDelay },
|
|
291
|
+
inactive,
|
|
292
|
+
)
|
|
293
|
+
const change = useCallback(
|
|
294
|
+
(next: number) => {
|
|
295
|
+
gesture.changed(next)
|
|
296
|
+
onChange?.(next)
|
|
297
|
+
},
|
|
298
|
+
[gesture, onChange],
|
|
299
|
+
)
|
|
300
|
+
|
|
266
301
|
const handleKeyDown = useCallback(
|
|
267
302
|
(event: React.KeyboardEvent<HTMLOrSVGElement>) => {
|
|
268
303
|
if (!keyboard || !onChange || inactive) return
|
|
269
304
|
const direction = arrowKeyDirection(event.key)
|
|
270
305
|
if (direction === null) return
|
|
271
306
|
event.preventDefault()
|
|
272
|
-
|
|
307
|
+
gesture.pulse('keyboard')
|
|
308
|
+
change(applyDelta(value, direction, keyboard, range, event))
|
|
273
309
|
},
|
|
274
|
-
[keyboard, onChange, inactive, value, range],
|
|
310
|
+
[keyboard, onChange, inactive, value, range, gesture, change],
|
|
275
311
|
)
|
|
276
312
|
|
|
277
313
|
// --- hooks ---
|
|
@@ -292,13 +328,17 @@ export const Root = /* @__PURE__ */ forwardRef<KnobMethods, Props>(
|
|
|
292
328
|
sensitivity: (state) =>
|
|
293
329
|
selectModifier(dragSensitivity, state.event).value,
|
|
294
330
|
threshold: 1,
|
|
295
|
-
cursor: inactive ? undefined :
|
|
331
|
+
cursor: inactive ? undefined : dragCursor,
|
|
296
332
|
pointerLock: inactive ? false : pointerLock,
|
|
297
333
|
shouldStart: () => !inactive,
|
|
298
334
|
onChange: (v) => {
|
|
299
335
|
if (inactive) return
|
|
300
|
-
|
|
336
|
+
change(v[1])
|
|
337
|
+
},
|
|
338
|
+
onDragStart: () => {
|
|
339
|
+
if (!inactive) gesture.hold('pointer')
|
|
301
340
|
},
|
|
341
|
+
onDragEnd: () => gesture.end(),
|
|
302
342
|
})
|
|
303
343
|
|
|
304
344
|
const wheelRefCallback = useWheel<HTMLElement>((event) => {
|
|
@@ -309,12 +349,14 @@ export const Root = /* @__PURE__ */ forwardRef<KnobMethods, Props>(
|
|
|
309
349
|
if (direction === null) return
|
|
310
350
|
event.preventDefault()
|
|
311
351
|
if (!onChange) return
|
|
312
|
-
|
|
352
|
+
gesture.pulse('wheel')
|
|
353
|
+
change(applyDelta(value, direction, wheel, range, event))
|
|
313
354
|
}, WHEEL_OPTIONS)
|
|
314
355
|
|
|
315
356
|
// Composed once, so React attaches the refs a single time instead of
|
|
316
357
|
// detaching and re-attaching on every render.
|
|
317
|
-
const rootRefCallback = useComposedRefs<
|
|
358
|
+
const rootRefCallback = useComposedRefs<HTMLDivElement>(
|
|
359
|
+
forwardedRef,
|
|
318
360
|
elmRef,
|
|
319
361
|
dragRefCallback,
|
|
320
362
|
wheelRefCallback,
|
|
@@ -325,7 +367,7 @@ export const Root = /* @__PURE__ */ forwardRef<KnobMethods, Props>(
|
|
|
325
367
|
return { ...config, ...knobAngles(config) }
|
|
326
368
|
}, [value, min, max, step, scale, startValue, angleRange])
|
|
327
369
|
|
|
328
|
-
useImperativeHandle(
|
|
370
|
+
useImperativeHandle(actionsRef, () => {
|
|
329
371
|
return {
|
|
330
372
|
focus() {
|
|
331
373
|
if (!disabled) elmRef.current?.focus()
|
|
@@ -347,9 +389,9 @@ export const Root = /* @__PURE__ */ forwardRef<KnobMethods, Props>(
|
|
|
347
389
|
aria-valuemin={min}
|
|
348
390
|
aria-valuemax={max}
|
|
349
391
|
aria-disabled={disabled}
|
|
350
|
-
aria-readonly={
|
|
392
|
+
aria-readonly={readOnly}
|
|
351
393
|
data-disabled={disabled ? '' : undefined}
|
|
352
|
-
data-readonly={
|
|
394
|
+
data-readonly={readOnly ? '' : undefined}
|
|
353
395
|
data-dragging={dragging ? '' : undefined}
|
|
354
396
|
style={
|
|
355
397
|
{
|
|
@@ -359,8 +401,10 @@ export const Root = /* @__PURE__ */ forwardRef<KnobMethods, Props>(
|
|
|
359
401
|
}
|
|
360
402
|
onPointerDown={onPointerDown}
|
|
361
403
|
onDoubleClick={(event) => {
|
|
362
|
-
if (!inactive &&
|
|
363
|
-
|
|
404
|
+
if (!inactive && resetValue !== null && onChange) {
|
|
405
|
+
gesture.instant('doubleClick', () =>
|
|
406
|
+
change(resetValue ?? startValue),
|
|
407
|
+
)
|
|
364
408
|
}
|
|
365
409
|
onDoubleClick?.(event)
|
|
366
410
|
}}
|
|
@@ -11,11 +11,11 @@ import {
|
|
|
11
11
|
caretAtDecimalOffset,
|
|
12
12
|
caretDecimalOffset,
|
|
13
13
|
numberSpan,
|
|
14
|
-
} from '@tremolo-ui/dom'
|
|
14
|
+
} from '@tremolo-ui/dom/internal'
|
|
15
15
|
|
|
16
16
|
import { useComposedRefs } from '../../compose-refs'
|
|
17
17
|
|
|
18
|
-
import { useNumberInputContext } from './context'
|
|
18
|
+
import { useNumberInputContext, useNumberInputGesture } from './context'
|
|
19
19
|
|
|
20
20
|
// The value belongs to `NumberInput.Root`, and the field is always text.
|
|
21
21
|
type Props = Omit<
|
|
@@ -43,7 +43,7 @@ export const InputField = /* @__PURE__ */ forwardRef<HTMLInputElement, Props>(
|
|
|
43
43
|
max,
|
|
44
44
|
step,
|
|
45
45
|
disabled,
|
|
46
|
-
|
|
46
|
+
readOnly,
|
|
47
47
|
keyboard,
|
|
48
48
|
selectOnFocus,
|
|
49
49
|
unformatOnFocus,
|
|
@@ -58,6 +58,7 @@ export const InputField = /* @__PURE__ */ forwardRef<HTMLInputElement, Props>(
|
|
|
58
58
|
inputRef,
|
|
59
59
|
} = useNumberInputContext()
|
|
60
60
|
|
|
61
|
+
const gesture = useNumberInputGesture()
|
|
61
62
|
const [focused, setFocused] = useState(false)
|
|
62
63
|
|
|
63
64
|
// Only until the first keystroke: from then on the draft is the user's own
|
|
@@ -119,11 +120,11 @@ export const InputField = /* @__PURE__ */ forwardRef<HTMLInputElement, Props>(
|
|
|
119
120
|
role="spinbutton"
|
|
120
121
|
value={shown}
|
|
121
122
|
disabled={disabled}
|
|
122
|
-
readOnly={
|
|
123
|
+
readOnly={readOnly}
|
|
123
124
|
aria-disabled={disabled}
|
|
124
|
-
aria-readonly={
|
|
125
|
+
aria-readonly={readOnly}
|
|
125
126
|
data-disabled={disabled ? '' : undefined}
|
|
126
|
-
data-readonly={
|
|
127
|
+
data-readonly={readOnly ? '' : undefined}
|
|
127
128
|
aria-valuenow={value}
|
|
128
129
|
aria-valuemin={min}
|
|
129
130
|
aria-valuemax={max}
|
|
@@ -159,7 +160,7 @@ export const InputField = /* @__PURE__ */ forwardRef<HTMLInputElement, Props>(
|
|
|
159
160
|
} else if (
|
|
160
161
|
keyboard &&
|
|
161
162
|
!disabled &&
|
|
162
|
-
!
|
|
163
|
+
!readOnly &&
|
|
163
164
|
(key === 'ArrowUp' || key === 'ArrowDown') &&
|
|
164
165
|
// Arrow keys pick a candidate while an IME is converting. Stepping
|
|
165
166
|
// the value there would fight the conversion, and moving the caret
|
|
@@ -174,6 +175,7 @@ export const InputField = /* @__PURE__ */ forwardRef<HTMLInputElement, Props>(
|
|
|
174
175
|
from: input.value,
|
|
175
176
|
}
|
|
176
177
|
}
|
|
178
|
+
gesture?.key()
|
|
177
179
|
nudge(key === 'ArrowUp' ? 1 : -1, keyboard, event)
|
|
178
180
|
}
|
|
179
181
|
onKeyDown?.(event)
|
|
@@ -7,11 +7,18 @@ import {
|
|
|
7
7
|
useState,
|
|
8
8
|
} from 'react'
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
createStepperDrag,
|
|
12
|
+
type StepperDragInstance,
|
|
13
|
+
} from '@tremolo-ui/dom/internal'
|
|
11
14
|
|
|
12
15
|
import { useComposedRefs } from '../../compose-refs'
|
|
13
16
|
|
|
14
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
StepperProvider,
|
|
19
|
+
useNumberInputContext,
|
|
20
|
+
useNumberInputGesture,
|
|
21
|
+
} from './context'
|
|
15
22
|
|
|
16
23
|
type Props = ComponentPropsWithoutRef<'div'>
|
|
17
24
|
|
|
@@ -24,21 +31,25 @@ type Props = ComponentPropsWithoutRef<'div'>
|
|
|
24
31
|
* off text selection on whatever element it is attached to.
|
|
25
32
|
*/
|
|
26
33
|
export const Stepper = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
27
|
-
function Stepper(
|
|
34
|
+
function Stepper(
|
|
35
|
+
{ className, style, children, onPointerDown, ...props },
|
|
36
|
+
forwardedRef,
|
|
37
|
+
) {
|
|
28
38
|
const {
|
|
29
39
|
value,
|
|
30
40
|
disabled,
|
|
31
|
-
|
|
41
|
+
readOnly,
|
|
32
42
|
drag,
|
|
33
43
|
dragSensitivity,
|
|
34
44
|
pointerLock,
|
|
45
|
+
dragCursor,
|
|
35
46
|
rawRange,
|
|
36
47
|
changeValue,
|
|
37
48
|
} = useNumberInputContext()
|
|
38
49
|
// Only attached while it can do something: `createDrag` puts
|
|
39
50
|
// `touch-action: none` on the element, and a stepper that cannot be
|
|
40
51
|
// dragged has no reason to stop the page scrolling under a finger.
|
|
41
|
-
const enabled = drag !== null && !disabled && !
|
|
52
|
+
const enabled = drag !== null && !disabled && !readOnly
|
|
42
53
|
|
|
43
54
|
// See useDrag for why the node is held in state rather than a ref.
|
|
44
55
|
const [node, setNode] = useState<HTMLDivElement | null>(null)
|
|
@@ -49,6 +60,7 @@ export const Stepper = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
|
49
60
|
pixels: drag ?? 1,
|
|
50
61
|
sensitivity: dragSensitivity,
|
|
51
62
|
pointerLock,
|
|
63
|
+
cursor: dragCursor,
|
|
52
64
|
}
|
|
53
65
|
const optionsRef = useRef(options)
|
|
54
66
|
|
|
@@ -73,6 +85,7 @@ export const Stepper = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
|
73
85
|
}
|
|
74
86
|
}, [node, enabled])
|
|
75
87
|
|
|
88
|
+
const gesture = useNumberInputGesture()
|
|
76
89
|
const composedRef = useComposedRefs<HTMLDivElement>(forwardedRef, setNode)
|
|
77
90
|
|
|
78
91
|
const context = useMemo(
|
|
@@ -82,7 +95,18 @@ export const Stepper = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
|
82
95
|
|
|
83
96
|
return (
|
|
84
97
|
<StepperProvider value={context}>
|
|
85
|
-
<div
|
|
98
|
+
<div
|
|
99
|
+
ref={composedRef}
|
|
100
|
+
className={className}
|
|
101
|
+
style={style}
|
|
102
|
+
onPointerDown={(event) => {
|
|
103
|
+
// A drag on the stepper is a press like any other: held from here
|
|
104
|
+
// until the pointer is released.
|
|
105
|
+
if (enabled) gesture?.press()
|
|
106
|
+
onPointerDown?.(event)
|
|
107
|
+
}}
|
|
108
|
+
{...props}
|
|
109
|
+
>
|
|
86
110
|
{children}
|
|
87
111
|
</div>
|
|
88
112
|
</StepperProvider>
|
|
@@ -2,7 +2,11 @@ import { ComponentPropsWithoutRef, ReactNode, CSSProperties } from 'react'
|
|
|
2
2
|
|
|
3
3
|
import { useLongPress } from '../../hooks/useLongPress'
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
useNumberInputContext,
|
|
7
|
+
useNumberInputGesture,
|
|
8
|
+
useStepperContext,
|
|
9
|
+
} from './context'
|
|
6
10
|
|
|
7
11
|
import type { CSSVariables } from '../../css-variables'
|
|
8
12
|
|
|
@@ -37,9 +41,10 @@ export function StepperButton({
|
|
|
37
41
|
variant: 'increment' | 'decrement'
|
|
38
42
|
icon: ReactNode
|
|
39
43
|
}) {
|
|
40
|
-
const { step, disabled,
|
|
44
|
+
const { step, disabled, readOnly, atMin, atMax, nudge } =
|
|
41
45
|
useNumberInputContext()
|
|
42
46
|
const stepper = useStepperContext()
|
|
47
|
+
const gesture = useNumberInputGesture()
|
|
43
48
|
|
|
44
49
|
// Not memoized: `useLongPress` only ever calls it through a ref, so a fresh
|
|
45
50
|
// identity per render costs nothing.
|
|
@@ -64,10 +69,13 @@ export function StepperButton({
|
|
|
64
69
|
data-disabled={
|
|
65
70
|
disabled || (direction > 0 ? atMax : atMin) ? '' : undefined
|
|
66
71
|
}
|
|
67
|
-
data-readonly={
|
|
72
|
+
data-readonly={readOnly ? '' : undefined}
|
|
68
73
|
style={style}
|
|
69
74
|
onPointerDown={(event) => {
|
|
70
|
-
if (!disabled && !
|
|
75
|
+
if (!disabled && !readOnly) {
|
|
76
|
+
gesture?.press()
|
|
77
|
+
press(event)
|
|
78
|
+
}
|
|
71
79
|
onPointerDown?.(event)
|
|
72
80
|
}}
|
|
73
81
|
{...props}
|