@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,7 +1,8 @@
|
|
|
1
|
-
import clsx from 'clsx'
|
|
2
1
|
import { SVGProps } from 'react'
|
|
3
2
|
|
|
4
|
-
import {
|
|
3
|
+
import { useCheckPlacement } from '../_util/Placement'
|
|
4
|
+
|
|
5
|
+
import { arcPath, arcRadius, useKnobContext } from './context'
|
|
5
6
|
|
|
6
7
|
export function ActiveLine({
|
|
7
8
|
stroke = 'currentColor',
|
|
@@ -9,17 +10,17 @@ export function ActiveLine({
|
|
|
9
10
|
className,
|
|
10
11
|
...props
|
|
11
12
|
}: Omit<SVGProps<SVGPathElement>, 'd'>) {
|
|
13
|
+
useCheckPlacement('Knob.ActiveLine', 'Knob.SVGRoot')
|
|
14
|
+
|
|
12
15
|
const r2 = useKnobContext((s) => s.r2)
|
|
13
16
|
const r3 = useKnobContext((s) => s.r3)
|
|
14
17
|
|
|
15
18
|
const radius = arcRadius(strokeWidth)
|
|
16
|
-
const start = pointOnArc(r2, radius)
|
|
17
|
-
const end = pointOnArc(r3, radius)
|
|
18
19
|
|
|
19
20
|
return (
|
|
20
21
|
<path
|
|
21
|
-
className={
|
|
22
|
-
d={
|
|
22
|
+
className={className}
|
|
23
|
+
d={arcPath(r2, r3, radius)}
|
|
23
24
|
fill="none"
|
|
24
25
|
stroke={stroke}
|
|
25
26
|
strokeWidth={strokeWidth}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import clsx from 'clsx'
|
|
2
1
|
import { SVGProps } from 'react'
|
|
3
2
|
|
|
4
|
-
import {
|
|
3
|
+
import { useCheckPlacement } from '../_util/Placement'
|
|
4
|
+
|
|
5
|
+
import { arcPath, arcRadius, useKnobContext } from './context'
|
|
5
6
|
|
|
6
7
|
export function InactiveLine({
|
|
7
8
|
stroke = 'currentColor',
|
|
@@ -9,6 +10,8 @@ export function InactiveLine({
|
|
|
9
10
|
className,
|
|
10
11
|
...props
|
|
11
12
|
}: Omit<SVGProps<SVGPathElement>, 'd'>) {
|
|
13
|
+
useCheckPlacement('Knob.InactiveLine', 'Knob.SVGRoot')
|
|
14
|
+
|
|
12
15
|
const min = useKnobContext((s) => s.min)
|
|
13
16
|
const max = useKnobContext((s) => s.max)
|
|
14
17
|
const startValue = useKnobContext((s) => s.startValue)
|
|
@@ -19,17 +22,13 @@ export function InactiveLine({
|
|
|
19
22
|
const r4 = useKnobContext((s) => s.r4)
|
|
20
23
|
|
|
21
24
|
const radius = arcRadius(strokeWidth)
|
|
22
|
-
const p1 = pointOnArc(r1, radius)
|
|
23
|
-
const p2 = pointOnArc(r2, radius)
|
|
24
|
-
const p3 = pointOnArc(r3, radius)
|
|
25
|
-
const p4 = pointOnArc(r4, radius)
|
|
26
25
|
|
|
27
26
|
return (
|
|
28
27
|
<>
|
|
29
28
|
{startValue > min && (
|
|
30
29
|
<path
|
|
31
|
-
className={
|
|
32
|
-
d={
|
|
30
|
+
className={className}
|
|
31
|
+
d={arcPath(r1, r2, radius)}
|
|
33
32
|
fill="none"
|
|
34
33
|
stroke={stroke}
|
|
35
34
|
strokeWidth={strokeWidth}
|
|
@@ -38,8 +37,8 @@ export function InactiveLine({
|
|
|
38
37
|
)}
|
|
39
38
|
{startValue < max && (
|
|
40
39
|
<path
|
|
41
|
-
className={
|
|
42
|
-
d={
|
|
40
|
+
className={className}
|
|
41
|
+
d={arcPath(r3, r4, radius)}
|
|
43
42
|
fill="none"
|
|
44
43
|
stroke={stroke}
|
|
45
44
|
strokeWidth={strokeWidth}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { ReactNode, SVGProps } from 'react'
|
|
2
2
|
|
|
3
|
+
import { Placement } from '../_util/Placement'
|
|
4
|
+
|
|
3
5
|
import { viewBoxSize } from './context'
|
|
4
6
|
|
|
5
|
-
export interface
|
|
7
|
+
export interface KnobSVGRootProps {
|
|
6
8
|
/**
|
|
7
9
|
* `<Knob.InactiveLine />`, `<Knob.ActiveLine />` and `<Knob.Thumb />` go
|
|
8
10
|
* here, in the order you want them painted.
|
|
@@ -14,7 +16,7 @@ export function SVGRoot({
|
|
|
14
16
|
children,
|
|
15
17
|
style,
|
|
16
18
|
...props
|
|
17
|
-
}:
|
|
19
|
+
}: KnobSVGRootProps & Omit<SVGProps<SVGSVGElement>, keyof KnobSVGRootProps>) {
|
|
18
20
|
return (
|
|
19
21
|
<svg
|
|
20
22
|
viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`}
|
|
@@ -24,7 +26,7 @@ export function SVGRoot({
|
|
|
24
26
|
}}
|
|
25
27
|
{...props}
|
|
26
28
|
>
|
|
27
|
-
{children}
|
|
29
|
+
<Placement name="Knob.SVGRoot">{children}</Placement>
|
|
28
30
|
</svg>
|
|
29
31
|
)
|
|
30
32
|
}
|
|
@@ -1,24 +1,44 @@
|
|
|
1
|
-
import clsx from 'clsx'
|
|
2
1
|
import { SVGProps } from 'react'
|
|
3
2
|
|
|
4
3
|
import { clamp } from '@tremolo-ui/functions'
|
|
5
4
|
|
|
5
|
+
import { useCheckPlacement } from '../_util/Placement'
|
|
6
|
+
|
|
6
7
|
import { useKnobContext, viewBoxSize } from './context'
|
|
7
8
|
|
|
8
|
-
interface
|
|
9
|
-
/**
|
|
9
|
+
export interface KnobThumbProps {
|
|
10
|
+
/**
|
|
11
|
+
* Fill colour of the circle.
|
|
12
|
+
* @default 'currentColor'
|
|
13
|
+
*/
|
|
10
14
|
thumb?: string
|
|
11
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Colour of the line that points at the value.
|
|
17
|
+
* @default 'currentColor'
|
|
18
|
+
*/
|
|
12
19
|
thumbLine?: string
|
|
13
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Diameter of the circle, as a percentage of the knob.
|
|
22
|
+
* @default 84
|
|
23
|
+
*/
|
|
14
24
|
thumbSize?: number
|
|
15
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Thickness of the line, as a percentage of the knob.
|
|
27
|
+
* @default 6
|
|
28
|
+
*/
|
|
16
29
|
thumbLineWeight?: number
|
|
17
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* How far down the line reaches, as a percentage of the knob from its top.
|
|
32
|
+
* The line starts at the edge of the circle.
|
|
33
|
+
* @default 35
|
|
34
|
+
*/
|
|
18
35
|
thumbLineLength?: number
|
|
19
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Classes for what the thumb draws inside itself: `thumbLine` is the line
|
|
39
|
+
* that points at the value.
|
|
40
|
+
*/
|
|
20
41
|
classes?: {
|
|
21
|
-
thumb?: string
|
|
22
42
|
thumbLine?: string
|
|
23
43
|
}
|
|
24
44
|
}
|
|
@@ -34,17 +54,19 @@ export function Thumb({
|
|
|
34
54
|
classes,
|
|
35
55
|
|
|
36
56
|
...props
|
|
37
|
-
}:
|
|
57
|
+
}: KnobThumbProps & Omit<SVGProps<SVGSVGElement>, 'd' | keyof KnobThumbProps>) {
|
|
58
|
+
useCheckPlacement('Knob.Thumb', 'Knob.SVGRoot')
|
|
59
|
+
|
|
38
60
|
const angleRange = useKnobContext((s) => s.angleRange)
|
|
39
61
|
|
|
40
62
|
const p = useKnobContext((s) => s.p)
|
|
41
63
|
const r1 = useKnobContext((s) => s.r1)
|
|
42
64
|
|
|
43
65
|
return (
|
|
44
|
-
<svg className={
|
|
66
|
+
<svg className={className} {...props}>
|
|
45
67
|
<circle cx="50%" cy="50%" r={`${thumbSize / 2}%`} fill={thumb} />
|
|
46
68
|
<line
|
|
47
|
-
className={
|
|
69
|
+
className={classes?.thumbLine}
|
|
48
70
|
x1="50%"
|
|
49
71
|
y1={`${(viewBoxSize - clamp(thumbSize, 0, 100)) / 2}%`}
|
|
50
72
|
x2="50%"
|
|
@@ -51,6 +51,22 @@ export function arcRadius(strokeWidth: number | string | undefined) {
|
|
|
51
51
|
return Number.isFinite(width) ? center - width / 2 : center
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/** Build an SVG path for an arc, splitting full turns into drawable segments. */
|
|
55
|
+
export function arcPath(startAngle: number, endAngle: number, radius: number) {
|
|
56
|
+
const start = pointOnArc(startAngle, radius)
|
|
57
|
+
const sweep = endAngle - startAngle
|
|
58
|
+
const segmentCount = Math.max(1, Math.ceil(Math.abs(sweep) / 180))
|
|
59
|
+
const segmentSweep = sweep / segmentCount
|
|
60
|
+
let path = `M ${start.x} ${start.y}`
|
|
61
|
+
|
|
62
|
+
for (let i = 1; i <= segmentCount; i += 1) {
|
|
63
|
+
const end = pointOnArc(startAngle + segmentSweep * i, radius)
|
|
64
|
+
path += ` A ${radius} ${radius} 0 0 ${segmentSweep >= 0 ? 1 : 0} ${end.x} ${end.y}`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return path
|
|
68
|
+
}
|
|
69
|
+
|
|
54
70
|
/** 設定から描画に必要な角度を導出する。レンダー中に呼ぶ。 */
|
|
55
71
|
export function calcAngles({
|
|
56
72
|
value,
|
|
@@ -71,7 +87,7 @@ export function calcAngles({
|
|
|
71
87
|
return { p, r1, r2, r3, r4 }
|
|
72
88
|
}
|
|
73
89
|
|
|
74
|
-
const KnobContext = createContext<KnobContextValue | null>(null)
|
|
90
|
+
const KnobContext = /* @__PURE__ */ createContext<KnobContextValue | null>(null)
|
|
75
91
|
|
|
76
92
|
export const KnobProvider = KnobContext.Provider
|
|
77
93
|
|
|
@@ -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
|
useCallback,
|
|
@@ -9,19 +9,26 @@ import {
|
|
|
9
9
|
useRef,
|
|
10
10
|
} from 'react'
|
|
11
11
|
|
|
12
|
-
import type { AxisOptions, XY } from '@tremolo-ui/dom'
|
|
13
12
|
import {
|
|
14
13
|
applyDelta,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
type
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
type AxisOptions,
|
|
15
|
+
type InputEventOption,
|
|
16
|
+
type ModifierValue,
|
|
17
|
+
selectModifier,
|
|
18
|
+
type XY,
|
|
19
|
+
} from '@tremolo-ui/dom'
|
|
20
|
+
import { linearScale, type Scale, type ValueRange } from '@tremolo-ui/functions'
|
|
20
21
|
|
|
22
|
+
import { useComposedRefs } from '../../compose-refs'
|
|
23
|
+
import { useCheckSteps } from '../../hooks/_internal/useCheckSteps'
|
|
21
24
|
import { useDragValue } from '../../hooks/useDragValue'
|
|
22
25
|
import { useWheel } from '../../hooks/useWheel'
|
|
23
|
-
import {
|
|
24
|
-
|
|
26
|
+
import {
|
|
27
|
+
DEFAULT_DRAG_SENSITIVITY,
|
|
28
|
+
DEFAULT_KEYBOARD_OPTIONS,
|
|
29
|
+
DEFAULT_WHEEL_OPTIONS,
|
|
30
|
+
} from '../../input-event'
|
|
31
|
+
import { cssLength } from '../_util/css-length'
|
|
25
32
|
|
|
26
33
|
import { ActiveLine } from './ActiveLine'
|
|
27
34
|
import { calcAngles, KnobProvider } from './context'
|
|
@@ -29,18 +36,26 @@ import { InactiveLine } from './InactiveLine'
|
|
|
29
36
|
import { SVGRoot } from './SVGRoot'
|
|
30
37
|
import { Thumb } from './Thumb'
|
|
31
38
|
|
|
39
|
+
import type { CSSVariables } from '../../css-variables'
|
|
40
|
+
|
|
32
41
|
const defaultExternalStyles: KnobProps['externalStyles'] = {
|
|
33
|
-
userSelectNone: true,
|
|
34
42
|
cursor: 'grabbing',
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
export interface KnobProps {
|
|
38
|
-
|
|
46
|
+
/** The current value. The knob shows only this, so update it from `onChange`. */
|
|
39
47
|
value: number
|
|
48
|
+
/** The value with the knob turned all the way down. */
|
|
40
49
|
min: number
|
|
50
|
+
/** The value with the knob turned all the way up. */
|
|
41
51
|
max: number
|
|
42
52
|
|
|
43
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Granularity of the value. A drag, the wheel and the arrow keys snap it to
|
|
55
|
+
* multiples of `step`.
|
|
56
|
+
*
|
|
57
|
+
* @default 1
|
|
58
|
+
*/
|
|
44
59
|
step?: number
|
|
45
60
|
/**
|
|
46
61
|
* How the value is distributed across the travel.
|
|
@@ -53,52 +68,123 @@ export interface KnobProps {
|
|
|
53
68
|
*/
|
|
54
69
|
scale?: Scale
|
|
55
70
|
/**
|
|
56
|
-
* value
|
|
57
|
-
* restriction: enableDoubleClickDefault = true
|
|
71
|
+
* The value a double click restores, while `enableDoubleClickDefault` is on.
|
|
58
72
|
* @default min
|
|
59
|
-
* @see enableDoubleClickDefault
|
|
60
73
|
*/
|
|
61
74
|
defaultValue?: number
|
|
62
75
|
|
|
63
76
|
/**
|
|
64
|
-
*
|
|
77
|
+
* Where the active arc starts. Put it at the centre of a bipolar control,
|
|
78
|
+
* such as a pan knob, so that the arc grows from there either way.
|
|
65
79
|
* @default min
|
|
66
80
|
*/
|
|
67
81
|
startValue?: number
|
|
68
82
|
|
|
69
|
-
/** width and height */
|
|
70
83
|
/**
|
|
71
|
-
* Width and height of the knob.
|
|
72
|
-
*
|
|
84
|
+
* Width and height of the knob. Sets `--knob-size`; the size the theme
|
|
85
|
+
* gives it stands when this is omitted.
|
|
73
86
|
*/
|
|
74
87
|
size?: number | string
|
|
75
88
|
|
|
76
89
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
90
|
+
* The cursor to show while dragging. It is set on the dragged element, so it
|
|
91
|
+
* stays while the pointer is outside the knob.
|
|
92
|
+
*
|
|
93
|
+
* @default { cursor: 'grabbing' }
|
|
79
94
|
*/
|
|
80
95
|
externalStyles?: {
|
|
81
|
-
|
|
82
|
-
cursor?: Cursor
|
|
96
|
+
cursor?: CSSProperties['cursor']
|
|
83
97
|
}
|
|
84
98
|
/**
|
|
85
|
-
* wheel
|
|
86
|
-
*
|
|
99
|
+
* How much one notch of the wheel moves the value. It only acts while the
|
|
100
|
+
* focus is inside, so that scrolling the page past the knob leaves it alone.
|
|
101
|
+
*
|
|
102
|
+
* `['raw', n]` moves the value by `n`, and `['normalized', n]` by `n` of the
|
|
103
|
+
* range between `min` and `max`. The result is snapped to `step`, except for
|
|
104
|
+
* an amount set on a modifier key (`{ default: …, shift: … }`). `null` turns
|
|
105
|
+
* the wheel off.
|
|
106
|
+
*
|
|
107
|
+
* @default ['raw', 1]
|
|
108
|
+
*/
|
|
109
|
+
wheel?: ModifierValue<InputEventOption> | null
|
|
110
|
+
/**
|
|
111
|
+
* How much a drag moves the value, per modifier key.
|
|
112
|
+
*
|
|
113
|
+
* `1` is the normal travel of 100px for the whole range; `0.1` makes the
|
|
114
|
+
* same movement cover a tenth of it. Shift is bound to `0.1` by default, to
|
|
115
|
+
* match what it does on the arrow keys.
|
|
116
|
+
*
|
|
117
|
+
* Pressing or releasing the key mid-drag does not disturb the value: the
|
|
118
|
+
* travel so far is kept and the new sensitivity applies from there. It takes
|
|
119
|
+
* effect on the next movement, since a key on its own produces no pointer
|
|
120
|
+
* event.
|
|
121
|
+
*
|
|
122
|
+
* @default { default: 1, shift: 0.1 }
|
|
123
|
+
*/
|
|
124
|
+
dragSensitivity?: ModifierValue<number>
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Hide the cursor while dragging and read the pointer movement directly,
|
|
128
|
+
* rather than letting it wander off across the screen.
|
|
129
|
+
*
|
|
130
|
+
* A knob does not care where the pointer is, only how far it moved, and
|
|
131
|
+
* letting it wander costs twice: the cursor ends up far from the knob it is
|
|
132
|
+
* holding, and **the drag stops at the edge of the screen**, where the
|
|
133
|
+
* operating system pins the pointer and the coordinates stop changing. A
|
|
134
|
+
* `dragSensitivity` below 1 reaches that edge quickly.
|
|
135
|
+
*
|
|
136
|
+
* Off by default because it is not free: the browser shows its own notice,
|
|
137
|
+
* Esc takes the lock back, and the request needs a user gesture and can be
|
|
138
|
+
* refused. A refused request is not an error — the drag simply carries on as
|
|
139
|
+
* an ordinary one.
|
|
140
|
+
*
|
|
141
|
+
* @default false
|
|
142
|
+
*/
|
|
143
|
+
pointerLock?: boolean
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* How much one arrow key press moves the value.
|
|
147
|
+
*
|
|
148
|
+
* `['raw', n]` moves the value by `n`, and `['normalized', n]` by `n` of the
|
|
149
|
+
* range between `min` and `max`. The result is snapped to `step`, except for
|
|
150
|
+
* an amount set on a modifier key, which is what lets shift move off the
|
|
151
|
+
* grid. `null` turns the arrow keys off.
|
|
152
|
+
*
|
|
153
|
+
* The default moves by 1, and by 0.1 with shift. With a `step` above 1, raise
|
|
154
|
+
* the amount to match: 1 would round straight back to where it started, and
|
|
155
|
+
* a development build warns about it.
|
|
156
|
+
*
|
|
157
|
+
* @default { default: ['raw', 1], shift: ['raw', 0.1] }
|
|
87
158
|
*/
|
|
88
|
-
|
|
159
|
+
keyboard?: ModifierValue<InputEventOption> | null
|
|
89
160
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
161
|
+
* Restore `defaultValue` on a double click.
|
|
162
|
+
* @default true
|
|
92
163
|
*/
|
|
93
|
-
keyboard?: InputEventOption | null
|
|
94
164
|
enableDoubleClickDefault?: boolean
|
|
95
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Make the knob unchangeable and remove it from the tab order.
|
|
168
|
+
* The parts carry `data-disabled` while it is set.
|
|
169
|
+
*/
|
|
96
170
|
disabled?: boolean
|
|
171
|
+
/**
|
|
172
|
+
* Make the knob unchangeable while leaving it focusable.
|
|
173
|
+
* The parts carry `data-readonly` while it is set.
|
|
174
|
+
*/
|
|
97
175
|
readonly?: boolean
|
|
98
176
|
|
|
99
|
-
/**
|
|
177
|
+
/**
|
|
178
|
+
* How far the knob turns from `min` to `max`, in degrees, centred on the
|
|
179
|
+
* top.
|
|
180
|
+
* @default 270
|
|
181
|
+
*/
|
|
100
182
|
angleRange?: number
|
|
101
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Called with the new value when a drag, the wheel, an arrow key or a double
|
|
186
|
+
* click moves it.
|
|
187
|
+
*/
|
|
102
188
|
onChange?: (value: number) => void
|
|
103
189
|
|
|
104
190
|
/**
|
|
@@ -115,6 +201,8 @@ export interface KnobProps {
|
|
|
115
201
|
* </Knob.Root>
|
|
116
202
|
*/
|
|
117
203
|
children: ReactNode
|
|
204
|
+
|
|
205
|
+
style?: CSSProperties & CSSVariables<'knob-size'>
|
|
118
206
|
}
|
|
119
207
|
|
|
120
208
|
export interface KnobMethods {
|
|
@@ -130,7 +218,7 @@ type Props = KnobProps & Omit<ComponentPropsWithoutRef<'div'>, keyof KnobProps>
|
|
|
130
218
|
*/
|
|
131
219
|
const WHEEL_OPTIONS = { requireFocus: true }
|
|
132
220
|
|
|
133
|
-
export const Root = forwardRef<KnobMethods, Props>(
|
|
221
|
+
export const Root = /* @__PURE__ */ forwardRef<KnobMethods, Props>(
|
|
134
222
|
(
|
|
135
223
|
{
|
|
136
224
|
value,
|
|
@@ -142,8 +230,10 @@ export const Root = forwardRef<KnobMethods, Props>(
|
|
|
142
230
|
startValue = min,
|
|
143
231
|
size,
|
|
144
232
|
externalStyles: _externalStyles,
|
|
145
|
-
wheel =
|
|
146
|
-
keyboard =
|
|
233
|
+
wheel = DEFAULT_WHEEL_OPTIONS,
|
|
234
|
+
keyboard = DEFAULT_KEYBOARD_OPTIONS,
|
|
235
|
+
dragSensitivity = DEFAULT_DRAG_SENSITIVITY,
|
|
236
|
+
pointerLock = false,
|
|
147
237
|
enableDoubleClickDefault = true,
|
|
148
238
|
disabled = false,
|
|
149
239
|
readonly = false,
|
|
@@ -162,6 +252,7 @@ export const Root = forwardRef<KnobMethods, Props>(
|
|
|
162
252
|
const elmRef = useRef<HTMLElement | SVGElement>(null)
|
|
163
253
|
|
|
164
254
|
const externalStyles = { ...defaultExternalStyles, ..._externalStyles }
|
|
255
|
+
const inactive = disabled || readonly
|
|
165
256
|
|
|
166
257
|
// --- internal functions ---
|
|
167
258
|
const range: ValueRange = useMemo(
|
|
@@ -169,17 +260,19 @@ export const Root = forwardRef<KnobMethods, Props>(
|
|
|
169
260
|
[min, max, step, scale],
|
|
170
261
|
)
|
|
171
262
|
|
|
263
|
+
useCheckSteps({ component: 'Knob', range, keyboard, wheel })
|
|
264
|
+
|
|
172
265
|
const handleKeyDown = useCallback(
|
|
173
266
|
(event: React.KeyboardEvent<HTMLOrSVGElement>) => {
|
|
174
|
-
if (!keyboard || !onChange ||
|
|
267
|
+
if (!keyboard || !onChange || inactive) return
|
|
175
268
|
const key = event.key
|
|
176
269
|
if (['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(key)) {
|
|
177
270
|
event.preventDefault()
|
|
178
|
-
const direction = key
|
|
179
|
-
onChange(applyDelta(value, direction, keyboard, range))
|
|
271
|
+
const direction = key === 'ArrowRight' || key === 'ArrowUp' ? 1 : -1
|
|
272
|
+
onChange(applyDelta(value, direction, keyboard, range, event))
|
|
180
273
|
}
|
|
181
274
|
},
|
|
182
|
-
[keyboard, onChange,
|
|
275
|
+
[keyboard, onChange, inactive, value, range],
|
|
183
276
|
)
|
|
184
277
|
|
|
185
278
|
// --- hooks ---
|
|
@@ -197,25 +290,23 @@ export const Root = forwardRef<KnobMethods, Props>(
|
|
|
197
290
|
>({
|
|
198
291
|
axis,
|
|
199
292
|
getValue: () => [value, value],
|
|
293
|
+
sensitivity: (state) =>
|
|
294
|
+
selectModifier(dragSensitivity, state.event).value,
|
|
200
295
|
threshold: 1,
|
|
201
|
-
cursor:
|
|
296
|
+
cursor: inactive ? undefined : externalStyles.cursor,
|
|
297
|
+
pointerLock: inactive ? false : pointerLock,
|
|
298
|
+
shouldStart: () => !inactive,
|
|
202
299
|
onChange: (v) => {
|
|
203
|
-
if (
|
|
300
|
+
if (inactive) return
|
|
204
301
|
onChange?.(v[1])
|
|
205
302
|
},
|
|
206
|
-
onDragStart: () => {
|
|
207
|
-
if (externalStyles.userSelectNone) addUserSelectNone()
|
|
208
|
-
},
|
|
209
|
-
onDragEnd: () => {
|
|
210
|
-
if (externalStyles.userSelectNone) removeUserSelectNone()
|
|
211
|
-
},
|
|
212
303
|
})
|
|
213
304
|
|
|
214
305
|
const wheelRefCallback = useWheel<HTMLElement>((event) => {
|
|
215
|
-
if (!wheel ||
|
|
306
|
+
if (!wheel || inactive) return
|
|
216
307
|
event.preventDefault()
|
|
217
|
-
if (!onChange || event.deltaY
|
|
218
|
-
onChange(applyDelta(value, -Math.sign(event.deltaY), wheel, range))
|
|
308
|
+
if (!onChange || event.deltaY === 0) return
|
|
309
|
+
onChange(applyDelta(value, -Math.sign(event.deltaY), wheel, range, event))
|
|
219
310
|
}, WHEEL_OPTIONS)
|
|
220
311
|
|
|
221
312
|
// Composed once, so React attaches the refs a single time instead of
|
|
@@ -234,35 +325,38 @@ export const Root = forwardRef<KnobMethods, Props>(
|
|
|
234
325
|
useImperativeHandle(forwardedRef, () => {
|
|
235
326
|
return {
|
|
236
327
|
focus() {
|
|
237
|
-
elmRef.current?.focus()
|
|
328
|
+
if (!disabled) elmRef.current?.focus()
|
|
238
329
|
},
|
|
239
330
|
blur() {
|
|
240
331
|
elmRef.current?.blur()
|
|
241
332
|
},
|
|
242
333
|
}
|
|
243
|
-
}, [])
|
|
334
|
+
}, [disabled])
|
|
244
335
|
|
|
245
336
|
return (
|
|
246
337
|
<KnobProvider value={context}>
|
|
247
338
|
<div
|
|
248
339
|
ref={rootRefCallback}
|
|
249
|
-
className={
|
|
250
|
-
tabIndex={0}
|
|
340
|
+
className={className}
|
|
341
|
+
tabIndex={disabled ? -1 : 0}
|
|
251
342
|
role="slider"
|
|
252
343
|
aria-valuenow={value}
|
|
253
344
|
aria-valuemin={min}
|
|
254
345
|
aria-valuemax={max}
|
|
255
346
|
aria-disabled={disabled}
|
|
256
347
|
aria-readonly={readonly}
|
|
257
|
-
data-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
348
|
+
data-disabled={disabled ? '' : undefined}
|
|
349
|
+
data-readonly={readonly ? '' : undefined}
|
|
350
|
+
data-dragging={dragging ? '' : undefined}
|
|
351
|
+
style={
|
|
352
|
+
{
|
|
353
|
+
'--knob-size': cssLength(size),
|
|
354
|
+
...style,
|
|
355
|
+
} as CSSProperties
|
|
356
|
+
}
|
|
263
357
|
onPointerDown={onPointerDown}
|
|
264
358
|
onDoubleClick={(event) => {
|
|
265
|
-
if (enableDoubleClickDefault && onChange) {
|
|
359
|
+
if (!inactive && enableDoubleClickDefault && onChange) {
|
|
266
360
|
onChange(defaultValue)
|
|
267
361
|
}
|
|
268
362
|
onDoubleClick?.(event)
|
|
@@ -291,4 +385,6 @@ export const Knob = {
|
|
|
291
385
|
Thumb,
|
|
292
386
|
}
|
|
293
387
|
|
|
294
|
-
export { useKnobContext } from './context'
|
|
388
|
+
export { useKnobContext, type KnobContextValue } from './context'
|
|
389
|
+
export { type KnobSVGRootProps } from './SVGRoot'
|
|
390
|
+
export { type KnobThumbProps } from './Thumb'
|
|
@@ -1,18 +1,11 @@
|
|
|
1
|
-
import { ComponentPropsWithoutRef } from 'react'
|
|
2
|
-
|
|
3
1
|
import {
|
|
4
2
|
StepperArrow,
|
|
5
3
|
StepperButton,
|
|
6
|
-
StepperButtonProps,
|
|
7
|
-
} from './
|
|
8
|
-
|
|
9
|
-
export type DecrementStepperProps = StepperButtonProps
|
|
4
|
+
type StepperButtonProps,
|
|
5
|
+
} from './StepperButton'
|
|
10
6
|
|
|
11
7
|
/** Lowers the value by one `step`, repeating while held. */
|
|
12
|
-
export function DecrementStepper(
|
|
13
|
-
props: DecrementStepperProps &
|
|
14
|
-
Omit<ComponentPropsWithoutRef<'div'>, keyof DecrementStepperProps>,
|
|
15
|
-
) {
|
|
8
|
+
export function DecrementStepper(props: StepperButtonProps) {
|
|
16
9
|
return (
|
|
17
10
|
<StepperButton
|
|
18
11
|
direction={-1}
|
|
@@ -1,18 +1,11 @@
|
|
|
1
|
-
import { ComponentPropsWithoutRef } from 'react'
|
|
2
|
-
|
|
3
1
|
import {
|
|
4
2
|
StepperArrow,
|
|
5
3
|
StepperButton,
|
|
6
|
-
StepperButtonProps,
|
|
7
|
-
} from './
|
|
8
|
-
|
|
9
|
-
export type IncrementStepperProps = StepperButtonProps
|
|
4
|
+
type StepperButtonProps,
|
|
5
|
+
} from './StepperButton'
|
|
10
6
|
|
|
11
7
|
/** Raises the value by one `step`, repeating while held. */
|
|
12
|
-
export function IncrementStepper(
|
|
13
|
-
props: IncrementStepperProps &
|
|
14
|
-
Omit<ComponentPropsWithoutRef<'div'>, keyof IncrementStepperProps>,
|
|
15
|
-
) {
|
|
8
|
+
export function IncrementStepper(props: StepperButtonProps) {
|
|
16
9
|
return (
|
|
17
10
|
<StepperButton
|
|
18
11
|
direction={1}
|