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