@tremolo-ui/react 0.5.0 → 0.7.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 +1669 -541
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1571 -486
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +1571 -486
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1640 -538
- package/dist/index.js.map +1 -1
- package/package.json +29 -27
- package/src/components/AnimationCanvas/index.tsx +115 -76
- package/src/components/DropZone/index.tsx +100 -0
- package/src/components/FileInput/Trigger.tsx +39 -0
- package/src/components/FileInput/context.tsx +24 -0
- package/src/components/FileInput/index.tsx +144 -0
- package/src/components/Knob/ActiveLine.tsx +7 -6
- package/src/components/Knob/InactiveLine.tsx +9 -10
- package/src/components/Knob/SVGRoot.tsx +5 -3
- package/src/components/Knob/Thumb.tsx +33 -11
- package/src/components/Knob/context.tsx +17 -1
- package/src/components/Knob/index.tsx +156 -60
- package/src/components/NumberInput/DecrementStepper.tsx +3 -10
- package/src/components/NumberInput/IncrementStepper.tsx +3 -10
- package/src/components/NumberInput/InputField.tsx +185 -108
- package/src/components/NumberInput/Stepper.tsx +120 -84
- package/src/components/NumberInput/StepperButton.tsx +100 -0
- package/src/components/NumberInput/context.tsx +34 -8
- package/src/components/NumberInput/index.tsx +270 -82
- package/src/components/Piano/index.tsx +363 -200
- package/src/components/PointsEditor/Background.tsx +13 -10
- package/src/components/PointsEditor/Container.tsx +83 -29
- package/src/components/PointsEditor/Point.tsx +241 -79
- package/src/components/PointsEditor/SelectionBox.tsx +48 -0
- package/src/components/PointsEditor/context.tsx +78 -10
- package/src/components/PointsEditor/index.tsx +440 -41
- package/src/components/Slider/Marks.tsx +48 -19
- package/src/components/Slider/MarksOption.tsx +64 -45
- package/src/components/Slider/Thumb.tsx +107 -55
- package/src/components/Slider/Track.tsx +66 -62
- package/src/components/Slider/context.tsx +4 -2
- package/src/components/Slider/index.tsx +136 -77
- package/src/components/Slider/type.ts +29 -9
- package/src/components/XYPad/Area.tsx +46 -41
- package/src/components/XYPad/Thumb.tsx +131 -64
- package/src/components/XYPad/context.tsx +4 -1
- package/src/components/XYPad/index.tsx +183 -53
- package/src/components/_util/Placement.tsx +68 -0
- package/src/components/_util/VisuallyHiddenRangeInput.tsx +12 -0
- package/src/components/_util/css-length.ts +13 -0
- package/src/components/_util/decimal-digits.ts +23 -0
- package/src/components/_util/visually-hidden.ts +25 -0
- package/src/{components/_util/composeRefs.tsx → compose-refs/index.tsx} +33 -5
- package/src/css-variables.ts +23 -0
- package/src/hooks/_internal/useCallbackRef.ts +3 -1
- package/src/hooks/_internal/useCheckSteps.ts +169 -0
- package/src/hooks/useAnimationFrame.ts +17 -7
- package/src/hooks/useDrag.ts +51 -12
- package/src/hooks/useDragValue.ts +71 -11
- package/src/hooks/useDropZone.ts +78 -0
- package/src/hooks/useEventListener.ts +10 -6
- 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 +4 -1
- package/src/index.ts +45 -25
- package/src/input-event.ts +32 -0
- package/dist/index.css +0 -501
- package/src/Globals.d.ts +0 -3
- package/src/components/Knob/index.css +0 -81
- package/src/components/NumberInput/index.css +0 -121
- package/src/components/NumberInput/stepperButton.tsx +0 -90
- package/src/components/Piano/index.css +0 -90
- package/src/components/PointsEditor/index.css +0 -35
- package/src/components/Slider/index.css +0 -125
- package/src/components/XYPad/index.css +0 -60
- package/src/components/_util/index.ts +0 -28
- package/src/styles/global.css +0 -19
- /package/src/components/Piano/{keyboardShortcuts.ts → keyboard-shortcuts.ts} +0 -0
|
@@ -1,27 +1,44 @@
|
|
|
1
|
-
import clsx from 'clsx'
|
|
2
1
|
import {
|
|
2
|
+
AriaAttributes,
|
|
3
3
|
ComponentPropsWithoutRef,
|
|
4
|
-
|
|
5
|
-
ReactNode,
|
|
6
|
-
Ref,
|
|
4
|
+
forwardRef,
|
|
7
5
|
useImperativeHandle,
|
|
8
6
|
useRef,
|
|
7
|
+
CSSProperties,
|
|
9
8
|
} from 'react'
|
|
10
9
|
|
|
11
|
-
import {
|
|
10
|
+
import { useCheckPlacement } from '../_util/Placement'
|
|
11
|
+
import { VisuallyHiddenRangeInput } from '../_util/VisuallyHiddenRangeInput'
|
|
12
|
+
|
|
13
|
+
import { toXY, useXYPadContext, type XYInput } from './context'
|
|
14
|
+
|
|
15
|
+
import type { CSSVariables } from '../../css-variables'
|
|
12
16
|
|
|
13
17
|
export interface XYPadThumbProps {
|
|
14
18
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
19
|
+
* Sets `--color`, for the theme to colour the thumb with. The size is not
|
|
20
|
+
* set here but by `--thumb-size` on `XYPad.Root`, so that the root can leave
|
|
21
|
+
* the matching room around the area.
|
|
17
22
|
*/
|
|
18
23
|
color?: string
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
/**
|
|
26
|
+
* The accessible name of each axis, as `[x, y]`. There are two range inputs
|
|
27
|
+
* inside the thumb, so each takes a name of its own; a single string names
|
|
28
|
+
* them both, which is rarely what you want.
|
|
29
|
+
*/
|
|
30
|
+
'aria-label'?: XYInput<AriaAttributes['aria-label']>
|
|
31
|
+
/** The ids of what labels each axis, as `[x, y]` or one for both. */
|
|
32
|
+
'aria-labelledby'?: XYInput<AriaAttributes['aria-labelledby']>
|
|
33
|
+
/** The ids of what describes each axis, as `[x, y]` or one for both. */
|
|
34
|
+
'aria-describedby'?: XYInput<AriaAttributes['aria-describedby']>
|
|
35
|
+
/**
|
|
36
|
+
* What the value of each axis means, when the number does not say it, as
|
|
37
|
+
* `[x, y]` or one for both.
|
|
38
|
+
*/
|
|
39
|
+
'aria-valuetext'?: XYInput<AriaAttributes['aria-valuetext']>
|
|
22
40
|
|
|
23
|
-
|
|
24
|
-
ref?: Ref<XYPadThumbMethods>
|
|
41
|
+
style?: CSSProperties & CSSVariables<'color' | 'translate'>
|
|
25
42
|
}
|
|
26
43
|
|
|
27
44
|
export interface XYPadThumbMethods {
|
|
@@ -32,59 +49,109 @@ export interface XYPadThumbMethods {
|
|
|
32
49
|
type Props = XYPadThumbProps &
|
|
33
50
|
Omit<ComponentPropsWithoutRef<'div'>, keyof XYPadThumbProps>
|
|
34
51
|
|
|
35
|
-
export
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const methods = () => ({
|
|
49
|
-
focus() {
|
|
50
|
-
elementRef.current?.focus()
|
|
52
|
+
export const Thumb = /* @__PURE__ */ forwardRef<XYPadThumbMethods, Props>(
|
|
53
|
+
function Thumb(
|
|
54
|
+
{
|
|
55
|
+
color,
|
|
56
|
+
children,
|
|
57
|
+
className,
|
|
58
|
+
style,
|
|
59
|
+
'aria-label': ariaLabel,
|
|
60
|
+
'aria-labelledby': ariaLabelledby,
|
|
61
|
+
'aria-describedby': ariaDescribedby,
|
|
62
|
+
'aria-valuetext': ariaValuetext,
|
|
63
|
+
...props
|
|
51
64
|
},
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
forwardedRef,
|
|
66
|
+
) {
|
|
67
|
+
const xInputRef = useRef<HTMLInputElement>(null)
|
|
68
|
+
const yInputRef = useRef<HTMLInputElement>(null)
|
|
69
|
+
const {
|
|
70
|
+
value,
|
|
71
|
+
min,
|
|
72
|
+
max,
|
|
73
|
+
step,
|
|
74
|
+
disabled,
|
|
75
|
+
readonly,
|
|
76
|
+
onChange,
|
|
77
|
+
percent,
|
|
78
|
+
thumbRef,
|
|
79
|
+
} = useXYPadContext()
|
|
56
80
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
useImperativeHandle(thumbRef, methods, [])
|
|
81
|
+
// The thumb is positioned against the area.
|
|
82
|
+
useCheckPlacement('XYPad.Thumb', 'XYPad.Area')
|
|
60
83
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
{
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
84
|
+
// Per axis, since the thumb holds one input for each.
|
|
85
|
+
const labels = toXY(ariaLabel)
|
|
86
|
+
const labelledby = toXY(ariaLabelledby)
|
|
87
|
+
const describedby = toXY(ariaDescribedby)
|
|
88
|
+
const valueText = toXY(ariaValuetext)
|
|
89
|
+
|
|
90
|
+
const methods = () => ({
|
|
91
|
+
focus() {
|
|
92
|
+
if (!disabled) xInputRef.current?.focus()
|
|
93
|
+
},
|
|
94
|
+
blur() {
|
|
95
|
+
xInputRef.current?.blur()
|
|
96
|
+
yInputRef.current?.blur()
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
useImperativeHandle(forwardedRef, methods, [disabled])
|
|
101
|
+
// Root focuses the thumb when a drag starts, wherever the user placed it.
|
|
102
|
+
useImperativeHandle(thumbRef, methods, [disabled])
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div
|
|
106
|
+
className={className}
|
|
107
|
+
data-disabled={disabled ? '' : undefined}
|
|
108
|
+
data-readonly={readonly ? '' : undefined}
|
|
109
|
+
{...props}
|
|
110
|
+
style={{
|
|
111
|
+
...{ '--color': color },
|
|
112
|
+
// Not a style but the mechanics of the position below: the thumb is
|
|
113
|
+
// placed by a percentage of the area, which needs it out of flow and
|
|
114
|
+
// measured from its own centre.
|
|
115
|
+
position: 'absolute',
|
|
116
|
+
translate: 'var(--translate, -50% -50%)',
|
|
117
|
+
zIndex: 100,
|
|
118
|
+
...style,
|
|
119
|
+
// Where the thumb sits is the component's decision, not a style: a
|
|
120
|
+
// `left` from the caller would take it off the area, so it is written
|
|
121
|
+
// after theirs.
|
|
122
|
+
left: `${percent[0]}%`,
|
|
123
|
+
top: `${percent[1]}%`,
|
|
124
|
+
}}
|
|
125
|
+
>
|
|
126
|
+
{([0, 1] as const).map((axis) => (
|
|
127
|
+
<VisuallyHiddenRangeInput
|
|
128
|
+
key={axis}
|
|
129
|
+
ref={axis === 0 ? xInputRef : yInputRef}
|
|
130
|
+
data-axis={axis === 0 ? 'x' : 'y'}
|
|
131
|
+
value={value[axis]}
|
|
132
|
+
min={min[axis]}
|
|
133
|
+
max={max[axis]}
|
|
134
|
+
step={step[axis]}
|
|
135
|
+
disabled={disabled}
|
|
136
|
+
aria-readonly={readonly}
|
|
137
|
+
aria-orientation={axis === 0 ? 'horizontal' : 'vertical'}
|
|
138
|
+
aria-label={labels[axis] ?? (axis === 0 ? 'x' : 'y')}
|
|
139
|
+
aria-labelledby={labelledby[axis]}
|
|
140
|
+
aria-describedby={describedby[axis]}
|
|
141
|
+
aria-valuetext={valueText[axis]}
|
|
142
|
+
onChange={(event) => {
|
|
143
|
+
if (readonly) {
|
|
144
|
+
event.currentTarget.value = String(value[axis])
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
const next = [...value] as [number, number]
|
|
148
|
+
next[axis] = event.currentTarget.valueAsNumber
|
|
149
|
+
onChange?.(next)
|
|
150
|
+
}}
|
|
151
|
+
/>
|
|
152
|
+
))}
|
|
153
|
+
{children}
|
|
154
|
+
</div>
|
|
155
|
+
)
|
|
156
|
+
},
|
|
157
|
+
)
|
|
@@ -17,6 +17,7 @@ export type XYPadContextValue = {
|
|
|
17
17
|
reverse: XY<boolean>
|
|
18
18
|
disabled: boolean
|
|
19
19
|
readonly: boolean
|
|
20
|
+
onChange?: (value: XY<number>) => void
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Position of the thumb, 0-100 per axis, already accounting for `reverse`.
|
|
@@ -29,7 +30,9 @@ export type XYPadContextValue = {
|
|
|
29
30
|
thumbRef: RefObject<XYPadThumbMethods | null>
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
const XYPadContext = createContext<XYPadContextValue | null>(
|
|
33
|
+
const XYPadContext = /* @__PURE__ */ createContext<XYPadContextValue | null>(
|
|
34
|
+
null,
|
|
35
|
+
)
|
|
33
36
|
|
|
34
37
|
export const XYPadProvider = XYPadContext.Provider
|
|
35
38
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import clsx from 'clsx'
|
|
2
1
|
import {
|
|
3
2
|
ComponentPropsWithoutRef,
|
|
3
|
+
CSSProperties,
|
|
4
4
|
forwardRef,
|
|
5
5
|
ReactNode,
|
|
6
6
|
Ref,
|
|
@@ -10,26 +10,31 @@ import {
|
|
|
10
10
|
useRef,
|
|
11
11
|
} from 'react'
|
|
12
12
|
|
|
13
|
-
import type { AxisOptions } from '@tremolo-ui/dom'
|
|
14
13
|
import {
|
|
15
14
|
applyDelta,
|
|
16
|
-
|
|
17
|
-
toFixed,
|
|
15
|
+
type AxisOptions,
|
|
18
16
|
InputEventOption,
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
ModifierState,
|
|
18
|
+
type ModifierValue,
|
|
19
|
+
selectModifier,
|
|
20
|
+
} from '@tremolo-ui/dom'
|
|
21
|
+
import { linearScale, type Scale, toFixed } from '@tremolo-ui/functions'
|
|
21
22
|
|
|
23
|
+
import { useComposedRefs } from '../../compose-refs'
|
|
24
|
+
import { useCheckSteps } from '../../hooks/_internal/useCheckSteps'
|
|
22
25
|
import { useDragValue } from '../../hooks/useDragValue'
|
|
23
26
|
import { useWheel } from '../../hooks/useWheel'
|
|
24
|
-
import {
|
|
25
|
-
|
|
27
|
+
import {
|
|
28
|
+
DEFAULT_DRAG_SENSITIVITY,
|
|
29
|
+
DEFAULT_KEYBOARD_OPTIONS,
|
|
30
|
+
DEFAULT_WHEEL_OPTIONS,
|
|
31
|
+
} from '../../input-event'
|
|
26
32
|
|
|
27
33
|
import { Area } from './Area'
|
|
28
34
|
import { toXY, XY, XYInput, XYPadProvider } from './context'
|
|
29
35
|
import { Thumb, XYPadThumbMethods } from './Thumb'
|
|
30
36
|
|
|
31
37
|
const defaultExternalStyles: XYPadProps['externalStyles'] = {
|
|
32
|
-
userSelectNone: true,
|
|
33
38
|
cursor: 'pointer',
|
|
34
39
|
}
|
|
35
40
|
|
|
@@ -40,13 +45,23 @@ const defaultExternalStyles: XYPadProps['externalStyles'] = {
|
|
|
40
45
|
* value applies to both axes.
|
|
41
46
|
*/
|
|
42
47
|
export interface XYPadProps {
|
|
48
|
+
/** The current value as `[x, y]`. The pad shows only this, so update it from `onChange`. */
|
|
43
49
|
value: XY<number>
|
|
50
|
+
/** The value at the start of each axis, as `[x, y]` or one number for both. */
|
|
44
51
|
min: XYInput<number>
|
|
52
|
+
/** The value at the end of each axis, as `[x, y]` or one number for both. */
|
|
45
53
|
max: XYInput<number>
|
|
46
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Granularity of each axis, as `[x, y]` or one number for both. A drag, the
|
|
57
|
+
* wheel and the arrow keys snap the value to multiples of it.
|
|
58
|
+
*
|
|
59
|
+
* @default 1
|
|
60
|
+
*/
|
|
47
61
|
step?: XYInput<number>
|
|
48
62
|
/**
|
|
49
|
-
* How the value of each axis is distributed across the travel
|
|
63
|
+
* How the value of each axis is distributed across the travel, as `[x, y]`
|
|
64
|
+
* or one scale for both.
|
|
50
65
|
*
|
|
51
66
|
* Pick one of the scales from `@tremolo-ui/functions`: `linearScale`,
|
|
52
67
|
* `exponentialScale`, `curveScale(n)`, `symmetricSkewScale(n)`, or
|
|
@@ -55,29 +70,88 @@ export interface XYPadProps {
|
|
|
55
70
|
* @default linearScale
|
|
56
71
|
*/
|
|
57
72
|
scale?: XYInput<Scale>
|
|
73
|
+
/**
|
|
74
|
+
* Grow an axis the other way, as `[x, y]` or one for both. By default x grows
|
|
75
|
+
* rightwards and y downwards. The arrow keys follow the direction on screen.
|
|
76
|
+
*
|
|
77
|
+
* @default false
|
|
78
|
+
*/
|
|
58
79
|
reverse?: XYInput<boolean>
|
|
59
80
|
|
|
60
81
|
/**
|
|
61
|
-
* wheel
|
|
62
|
-
*
|
|
82
|
+
* How much one notch of the wheel moves the value. It only acts while the
|
|
83
|
+
* focus is inside, so that scrolling the page past the pad leaves it alone.
|
|
84
|
+
* Scrolling sideways, or with shift held, moves x; otherwise it moves y.
|
|
85
|
+
*
|
|
86
|
+
* `['raw', n]` moves the value by `n`, and `['normalized', n]` by `n` of the
|
|
87
|
+
* range of that axis. The result is snapped to `step`, except for an amount
|
|
88
|
+
* set on a modifier key (`{ default: …, alt: … }`). `null` turns the wheel
|
|
89
|
+
* off.
|
|
90
|
+
*
|
|
91
|
+
* @default ['raw', 1]
|
|
63
92
|
*/
|
|
64
|
-
wheel?: InputEventOption | null
|
|
93
|
+
wheel?: ModifierValue<InputEventOption> | null
|
|
65
94
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
95
|
+
* How much a drag moves the value, per modifier key.
|
|
96
|
+
*
|
|
97
|
+
* `1` is the pointer position itself, which is what a drag normally is here.
|
|
98
|
+
* **Anything else turns the drag relative**: `0.1` makes the same movement
|
|
99
|
+
* cover a tenth of the travel, so the value stops following the pointer and
|
|
100
|
+
* starts moving a tenth as fast. Shift is bound to `0.1` by default, to
|
|
101
|
+
* match what it does on the arrow keys.
|
|
102
|
+
*
|
|
103
|
+
* Pressing or releasing the key mid-drag does not disturb the value: the
|
|
104
|
+
* travel so far is kept and the new sensitivity applies from there. **The
|
|
105
|
+
* pointer and the value stay apart for the rest of the drag** — snapping
|
|
106
|
+
* them back together on release would move the value nobody asked to move.
|
|
107
|
+
*
|
|
108
|
+
* @default { default: 1, shift: 0.1 }
|
|
68
109
|
*/
|
|
69
|
-
|
|
110
|
+
dragSensitivity?: ModifierValue<number>
|
|
70
111
|
|
|
112
|
+
/**
|
|
113
|
+
* How much one arrow key press moves the value. Left and right move x, up
|
|
114
|
+
* and down move y.
|
|
115
|
+
*
|
|
116
|
+
* `['raw', n]` moves the value by `n`, and `['normalized', n]` by `n` of the
|
|
117
|
+
* range of that axis. The result is snapped to `step`, except for an amount
|
|
118
|
+
* set on a modifier key, which is what lets shift move off the grid. `null`
|
|
119
|
+
* turns the arrow keys off.
|
|
120
|
+
*
|
|
121
|
+
* The default moves by 1, and by 0.1 with shift. With a `step` above 1, raise
|
|
122
|
+
* the amount to match: 1 would round straight back to where it started, and
|
|
123
|
+
* a development build warns about it.
|
|
124
|
+
*
|
|
125
|
+
* @default { default: ['raw', 1], shift: ['raw', 0.1] }
|
|
126
|
+
*/
|
|
127
|
+
keyboard?: ModifierValue<InputEventOption> | null
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The cursor to show while dragging. It is set on the dragged element, so it
|
|
131
|
+
* stays while the pointer is outside the pad.
|
|
132
|
+
*
|
|
133
|
+
* @default { cursor: 'pointer' }
|
|
134
|
+
*/
|
|
71
135
|
externalStyles?: {
|
|
72
|
-
|
|
73
|
-
cursor?: Cursor
|
|
136
|
+
cursor?: CSSProperties['cursor']
|
|
74
137
|
}
|
|
75
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Make the pad unchangeable and remove its thumb from the tab order.
|
|
141
|
+
* The parts carry `data-disabled` while it is set.
|
|
142
|
+
*/
|
|
76
143
|
disabled?: boolean
|
|
144
|
+
/**
|
|
145
|
+
* Make the pad unchangeable while leaving its thumb focusable.
|
|
146
|
+
* The parts carry `data-readonly` while it is set.
|
|
147
|
+
*/
|
|
77
148
|
readonly?: boolean
|
|
78
149
|
|
|
150
|
+
/** Called with the new value when a drag, the wheel or an arrow key moves it. */
|
|
79
151
|
onChange?: (value: XY<number>) => void
|
|
152
|
+
/** Called when a drag starts, with the value where the area was pressed. */
|
|
80
153
|
onDragStart?: (value: XY<number>) => void
|
|
154
|
+
/** Called when the drag ends, with the value it ended on. */
|
|
81
155
|
onDragEnd?: (value: XY<number>) => void
|
|
82
156
|
|
|
83
157
|
/**
|
|
@@ -109,7 +183,7 @@ type Props = XYPadProps &
|
|
|
109
183
|
*/
|
|
110
184
|
const WHEEL_OPTIONS = { requireFocus: true }
|
|
111
185
|
|
|
112
|
-
export const Root = forwardRef<XYPadMethods, Props>(
|
|
186
|
+
export const Root = /* @__PURE__ */ forwardRef<XYPadMethods, Props>(
|
|
113
187
|
(
|
|
114
188
|
{
|
|
115
189
|
value,
|
|
@@ -118,8 +192,9 @@ export const Root = forwardRef<XYPadMethods, Props>(
|
|
|
118
192
|
step: _step = 1,
|
|
119
193
|
scale: _scale = linearScale,
|
|
120
194
|
reverse: _reverse = false,
|
|
121
|
-
wheel =
|
|
122
|
-
keyboard =
|
|
195
|
+
wheel = DEFAULT_WHEEL_OPTIONS,
|
|
196
|
+
keyboard = DEFAULT_KEYBOARD_OPTIONS,
|
|
197
|
+
dragSensitivity = DEFAULT_DRAG_SENSITIVITY,
|
|
123
198
|
className,
|
|
124
199
|
style,
|
|
125
200
|
externalStyles: _externalStyles,
|
|
@@ -144,6 +219,7 @@ export const Root = forwardRef<XYPadMethods, Props>(
|
|
|
144
219
|
|
|
145
220
|
// --- interpret props ---
|
|
146
221
|
const externalStyles = { ...defaultExternalStyles, ..._externalStyles }
|
|
222
|
+
const inactive = disabled || readonly
|
|
147
223
|
|
|
148
224
|
const min = useMemo(() => toXY(_min), [_min])
|
|
149
225
|
const max = useMemo(() => toXY(_max), [_max])
|
|
@@ -177,33 +253,61 @@ export const Root = forwardRef<XYPadMethods, Props>(
|
|
|
177
253
|
[min, max, step, scale, reverse],
|
|
178
254
|
)
|
|
179
255
|
|
|
256
|
+
useCheckSteps({
|
|
257
|
+
component: 'XYPad',
|
|
258
|
+
axis: 'x',
|
|
259
|
+
range: axis[0],
|
|
260
|
+
keyboard,
|
|
261
|
+
wheel,
|
|
262
|
+
})
|
|
263
|
+
useCheckSteps({
|
|
264
|
+
component: 'XYPad',
|
|
265
|
+
axis: 'y',
|
|
266
|
+
range: axis[1],
|
|
267
|
+
keyboard,
|
|
268
|
+
wheel,
|
|
269
|
+
})
|
|
270
|
+
|
|
180
271
|
const withAxis = useCallback(
|
|
181
272
|
(axis: 0 | 1, next: number): XY<number> =>
|
|
182
|
-
axis
|
|
273
|
+
axis === 0 ? [next, value[1]] : [value[0], next],
|
|
183
274
|
[value],
|
|
184
275
|
)
|
|
185
276
|
|
|
186
277
|
/** @param i 0 = x, 1 = y */
|
|
187
278
|
const nudge = useCallback(
|
|
188
|
-
(
|
|
189
|
-
|
|
279
|
+
(
|
|
280
|
+
i: 0 | 1,
|
|
281
|
+
direction: number,
|
|
282
|
+
option: ModifierValue<InputEventOption>,
|
|
283
|
+
modifiers: ModifierState,
|
|
284
|
+
): XY<number> =>
|
|
285
|
+
withAxis(
|
|
286
|
+
i,
|
|
287
|
+
applyDelta(value[i], direction, option, axis[i], modifiers),
|
|
288
|
+
),
|
|
190
289
|
[value, axis, withAxis],
|
|
191
290
|
)
|
|
192
291
|
|
|
193
292
|
const handleKeyDown = useCallback(
|
|
194
293
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
195
|
-
if (!onChange || readonly || !keyboard) return
|
|
196
294
|
const key = event.key
|
|
197
|
-
if (['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(key))
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
295
|
+
if (!['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(key))
|
|
296
|
+
return
|
|
297
|
+
|
|
298
|
+
// The key picks the axis, whichever of the two inputs holds the
|
|
299
|
+
// focus: the pad is one control to the person moving it, and the focus
|
|
300
|
+
// lands on the x input, so reading the axis off the input would leave
|
|
301
|
+
// the y axis with no keys at all.
|
|
302
|
+
const i: 0 | 1 = key === 'ArrowRight' || key === 'ArrowLeft' ? 0 : 1
|
|
303
|
+
event.preventDefault()
|
|
304
|
+
if (!onChange || inactive || !keyboard) return
|
|
305
|
+
let direction = 1
|
|
306
|
+
if (key === 'ArrowLeft' || key === 'ArrowUp') direction *= -1
|
|
307
|
+
if (reverse[i]) direction *= -1
|
|
308
|
+
onChange(nudge(i, direction, keyboard, event))
|
|
205
309
|
},
|
|
206
|
-
[onChange,
|
|
310
|
+
[onChange, inactive, keyboard, reverse, nudge],
|
|
207
311
|
)
|
|
208
312
|
|
|
209
313
|
// --- hooks ---
|
|
@@ -211,33 +315,41 @@ export const Root = forwardRef<XYPadMethods, Props>(
|
|
|
211
315
|
const { refCallback: dragRefCallback } = useDragValue<HTMLDivElement>({
|
|
212
316
|
axis,
|
|
213
317
|
baseElementRef: areaRef,
|
|
318
|
+
sensitivity: (state) =>
|
|
319
|
+
selectModifier(dragSensitivity, state.event).value,
|
|
214
320
|
updateOnPointerDown: true,
|
|
215
|
-
cursor:
|
|
321
|
+
cursor: inactive ? undefined : externalStyles.cursor,
|
|
322
|
+
shouldStart: () => !inactive,
|
|
216
323
|
onChange: (v) => {
|
|
217
|
-
if (
|
|
324
|
+
if (inactive) return
|
|
218
325
|
onChange?.(v)
|
|
219
326
|
},
|
|
220
327
|
onDragStart: (v) => {
|
|
221
|
-
if (
|
|
222
|
-
if (externalStyles.userSelectNone) addUserSelectNone()
|
|
328
|
+
if (inactive) return
|
|
223
329
|
thumbRef.current?.focus()
|
|
224
330
|
onDragStart?.(v)
|
|
225
331
|
},
|
|
226
332
|
onDragEnd: (v) => {
|
|
227
|
-
if (
|
|
228
|
-
if (externalStyles.userSelectNone) removeUserSelectNone()
|
|
333
|
+
if (inactive) return
|
|
229
334
|
onDragEnd?.(v)
|
|
230
335
|
},
|
|
231
336
|
})
|
|
232
337
|
|
|
233
338
|
const wheelRefCallback = useWheel<HTMLDivElement>((event) => {
|
|
234
|
-
if (!onChange ||
|
|
235
|
-
|
|
339
|
+
if (!onChange || inactive || !wheel) return
|
|
340
|
+
// Browsers turn shift+wheel into horizontal scrolling: `deltaY` comes
|
|
341
|
+
// out empty and `deltaX` carries the movement. Reading whichever axis
|
|
342
|
+
// moved keeps shift working as the x-axis modifier — and picks up a
|
|
343
|
+
// trackpad's own horizontal gesture, which never had a modifier.
|
|
344
|
+
const horizontal = event.deltaX !== 0
|
|
345
|
+
const delta = horizontal ? event.deltaX : event.deltaY
|
|
346
|
+
if (delta === 0) return
|
|
347
|
+
const i: 0 | 1 = horizontal || event.shiftKey ? 0 : 1
|
|
236
348
|
event.preventDefault()
|
|
237
349
|
let direction = 1
|
|
238
|
-
if (
|
|
350
|
+
if (delta < 0) direction *= -1
|
|
239
351
|
if (reverse[i]) direction *= -1
|
|
240
|
-
onChange(nudge(i, direction, wheel))
|
|
352
|
+
onChange(nudge(i, direction, wheel, event))
|
|
241
353
|
}, WHEEL_OPTIONS)
|
|
242
354
|
|
|
243
355
|
// Composed once, so React attaches the refs a single time instead of
|
|
@@ -258,34 +370,47 @@ export const Root = forwardRef<XYPadMethods, Props>(
|
|
|
258
370
|
reverse,
|
|
259
371
|
disabled,
|
|
260
372
|
readonly,
|
|
373
|
+
onChange,
|
|
261
374
|
percent,
|
|
262
375
|
areaRef,
|
|
263
376
|
thumbRef,
|
|
264
377
|
}),
|
|
265
|
-
[
|
|
378
|
+
[
|
|
379
|
+
value,
|
|
380
|
+
min,
|
|
381
|
+
max,
|
|
382
|
+
step,
|
|
383
|
+
scale,
|
|
384
|
+
reverse,
|
|
385
|
+
disabled,
|
|
386
|
+
readonly,
|
|
387
|
+
onChange,
|
|
388
|
+
percent,
|
|
389
|
+
],
|
|
266
390
|
)
|
|
267
391
|
|
|
268
392
|
useImperativeHandle(forwardedRef, () => {
|
|
269
393
|
return {
|
|
270
394
|
focus() {
|
|
271
|
-
thumbRef.current?.focus()
|
|
395
|
+
if (!disabled) thumbRef.current?.focus()
|
|
272
396
|
},
|
|
273
397
|
blur() {
|
|
274
398
|
thumbRef.current?.blur()
|
|
275
399
|
},
|
|
276
400
|
original: rootRef,
|
|
277
401
|
}
|
|
278
|
-
}, [])
|
|
402
|
+
}, [disabled])
|
|
279
403
|
|
|
280
404
|
return (
|
|
281
405
|
<XYPadProvider value={context}>
|
|
282
|
-
{/*
|
|
406
|
+
{/* oxlint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- the group handles pointer and keyboard input shared by its two range controls */}
|
|
283
407
|
<div
|
|
284
|
-
className={
|
|
408
|
+
className={className}
|
|
285
409
|
ref={rootRefCallback}
|
|
410
|
+
role="group"
|
|
286
411
|
tabIndex={-1}
|
|
287
|
-
|
|
288
|
-
|
|
412
|
+
data-disabled={disabled ? '' : undefined}
|
|
413
|
+
data-readonly={readonly ? '' : undefined}
|
|
289
414
|
style={style}
|
|
290
415
|
onPointerDown={onPointerDown}
|
|
291
416
|
onKeyDown={(event) => {
|
|
@@ -293,7 +418,7 @@ export const Root = forwardRef<XYPadMethods, Props>(
|
|
|
293
418
|
onKeyDown?.(event)
|
|
294
419
|
}}
|
|
295
420
|
onFocus={(event) => {
|
|
296
|
-
thumbRef.current?.focus()
|
|
421
|
+
if (!disabled) thumbRef.current?.focus()
|
|
297
422
|
onFocus?.(event)
|
|
298
423
|
}}
|
|
299
424
|
onBlur={(event) => {
|
|
@@ -320,4 +445,9 @@ export const XYPad = {
|
|
|
320
445
|
|
|
321
446
|
export { type XYPadThumbProps, type XYPadThumbMethods } from './Thumb'
|
|
322
447
|
export { type XYPadAreaProps } from './Area'
|
|
323
|
-
export {
|
|
448
|
+
export {
|
|
449
|
+
useXYPadContext,
|
|
450
|
+
type XYPadContextValue,
|
|
451
|
+
type XY,
|
|
452
|
+
type XYInput,
|
|
453
|
+
} from './context'
|