@tremolo-ui/react 0.3.0 → 0.5.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 +1162 -1554
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +58 -93
- package/dist/index.d.cts +592 -278
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +592 -278
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1163 -1555
- package/dist/index.js.map +1 -1
- package/package.json +12 -13
- package/src/components/AnimationCanvas/index.tsx +84 -155
- package/src/components/Knob/ActiveLine.tsx +6 -6
- package/src/components/Knob/InactiveLine.tsx +9 -11
- package/src/components/Knob/SVGRoot.tsx +9 -18
- package/src/components/Knob/context.tsx +57 -89
- package/src/components/Knob/index.css +6 -2
- package/src/components/Knob/index.tsx +95 -83
- package/src/components/NumberInput/DecrementStepper.tsx +17 -50
- package/src/components/NumberInput/IncrementStepper.tsx +17 -50
- package/src/components/NumberInput/InputField.tsx +123 -0
- package/src/components/NumberInput/Stepper.tsx +90 -12
- package/src/components/NumberInput/context.tsx +74 -146
- package/src/components/NumberInput/index.css +38 -102
- package/src/components/NumberInput/index.tsx +252 -98
- package/src/components/NumberInput/stepperButton.tsx +90 -0
- package/src/components/Piano/index.tsx +272 -288
- package/src/components/Piano/keyboardShortcuts.ts +34 -4
- package/src/components/PointsEditor/Background.tsx +8 -2
- package/src/components/PointsEditor/Container.tsx +19 -9
- package/src/components/PointsEditor/Point.tsx +117 -52
- package/src/components/PointsEditor/context.tsx +28 -74
- package/src/components/PointsEditor/index.css +5 -0
- package/src/components/PointsEditor/index.tsx +77 -43
- package/src/components/Slider/{Scale.tsx → Marks.tsx} +8 -8
- package/src/components/Slider/{ScaleOption.tsx → MarksOption.tsx} +16 -18
- package/src/components/Slider/Thumb.tsx +55 -68
- package/src/components/Slider/Track.tsx +15 -15
- package/src/components/Slider/context.tsx +36 -51
- package/src/components/Slider/index.css +18 -5
- package/src/components/Slider/index.tsx +136 -170
- package/src/components/Slider/type.ts +4 -4
- package/src/components/XYPad/Area.tsx +16 -7
- package/src/components/XYPad/Thumb.tsx +62 -74
- package/src/components/XYPad/context.tsx +46 -0
- package/src/components/XYPad/index.css +12 -0
- package/src/components/XYPad/index.tsx +193 -250
- package/src/hooks/useDrag.ts +41 -76
- package/src/hooks/useDragValue.ts +149 -0
- package/src/hooks/useEventListener.ts +1 -1
- package/src/hooks/useWheel.ts +51 -0
- package/src/index.ts +15 -13
- package/src/components/AnimationCanvas/canvas.ts +0 -46
- package/src/components/DragObserver/index.tsx +0 -58
- package/src/components/NumberInput/InternalInput.tsx +0 -221
- package/src/components/NumberInput/type.ts +0 -58
- package/src/components/Piano/KeyLabel.tsx +0 -51
- package/src/components/Piano/context.tsx +0 -73
- package/src/components/Piano/key.tsx +0 -205
- package/src/components/WheelObserver/index.tsx +0 -48
- package/src/components/_util/type.ts +0 -1
- package/src/hooks/useDragWithElement.ts +0 -72
- package/src/hooks/usePianoDrag.ts +0 -75
- package/src/hooks/useRefCallbackEvent.ts +0 -34
- /package/src/hooks/{useCallbackRef.ts → _internal/useCallbackRef.ts} +0 -0
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import clsx from 'clsx'
|
|
2
|
-
import { ComponentPropsWithoutRef } from 'react'
|
|
2
|
+
import { ComponentPropsWithoutRef, ReactNode } from 'react'
|
|
3
|
+
|
|
4
|
+
export interface PointsEditorBackgroundProps {
|
|
5
|
+
/** Whatever the points are placed over: a graph, a canvas, an image. */
|
|
6
|
+
children?: ReactNode
|
|
7
|
+
}
|
|
3
8
|
|
|
4
9
|
export function Background({
|
|
5
10
|
className,
|
|
6
11
|
children,
|
|
7
12
|
...props
|
|
8
|
-
}:
|
|
13
|
+
}: PointsEditorBackgroundProps &
|
|
14
|
+
Omit<ComponentPropsWithoutRef<'div'>, keyof PointsEditorBackgroundProps>) {
|
|
9
15
|
return (
|
|
10
16
|
<div
|
|
11
17
|
className={clsx('tremolo-points-editor-background', className)}
|
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
import clsx from 'clsx'
|
|
2
|
-
import { ComponentPropsWithoutRef,
|
|
2
|
+
import { ComponentPropsWithoutRef, ReactNode, Ref } from 'react'
|
|
3
|
+
|
|
4
|
+
import { useComposedRefs } from '../_util/composeRefs'
|
|
3
5
|
|
|
4
6
|
import { usePointsEditorContext } from './context'
|
|
5
7
|
|
|
8
|
+
export interface PointsEditorContainerProps {
|
|
9
|
+
/** `<PointsEditor.Point />` goes here. */
|
|
10
|
+
children?: ReactNode
|
|
11
|
+
ref?: Ref<HTMLDivElement>
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
export function Container({
|
|
7
|
-
className,
|
|
8
15
|
children,
|
|
16
|
+
className,
|
|
17
|
+
ref,
|
|
9
18
|
...props
|
|
10
|
-
}:
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
}: PointsEditorContainerProps &
|
|
20
|
+
Omit<ComponentPropsWithoutRef<'div'>, keyof PointsEditorContainerProps>) {
|
|
21
|
+
const containerRef = usePointsEditorContext((s) => s.containerRef)
|
|
22
|
+
|
|
23
|
+
// The container is what the pointer position is normalized against, so the
|
|
24
|
+
// context ref is composed with any ref the caller passed.
|
|
25
|
+
const composedRef = useComposedRefs<HTMLDivElement>(ref, containerRef)
|
|
16
26
|
|
|
17
27
|
return (
|
|
18
28
|
<div
|
|
19
|
-
ref={
|
|
29
|
+
ref={composedRef}
|
|
20
30
|
className={clsx('tremolo-points-editor-container', className)}
|
|
21
31
|
{...props}
|
|
22
32
|
>
|
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import clsx from 'clsx'
|
|
2
|
-
import { ComponentPropsWithoutRef } from 'react'
|
|
2
|
+
import { ComponentPropsWithoutRef, useCallback, useState } from 'react'
|
|
3
3
|
|
|
4
|
-
import { clamp } from '@tremolo-ui/functions'
|
|
4
|
+
import { applyDelta, clamp, InputEventOption } from '@tremolo-ui/functions'
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
resetCursorStyle,
|
|
11
|
-
setCursorStyle,
|
|
12
|
-
} from '../_util'
|
|
6
|
+
import { useDragValue } from '../../hooks/useDragValue'
|
|
7
|
+
import { useWheel } from '../../hooks/useWheel'
|
|
8
|
+
import { addUserSelectNone, removeUserSelectNone } from '../_util'
|
|
9
|
+
import { useComposedRefs } from '../_util/composeRefs'
|
|
13
10
|
|
|
14
11
|
import { usePointsEditorContext } from './context'
|
|
15
12
|
|
|
@@ -36,14 +33,27 @@ export interface PointProps<T extends PointBaseType> {
|
|
|
36
33
|
height?: number | string
|
|
37
34
|
color?: string
|
|
38
35
|
|
|
36
|
+
/** Overrides the `disabled` of `PointsEditor.Root`. */
|
|
39
37
|
disabled?: boolean
|
|
38
|
+
/** Overrides the `readonly` of `PointsEditor.Root`. */
|
|
40
39
|
readonly?: boolean
|
|
41
40
|
|
|
41
|
+
/** Overrides the `wheel` of `PointsEditor.Root`. */
|
|
42
|
+
wheel?: InputEventOption | null
|
|
43
|
+
/** Overrides the `keyboard` of `PointsEditor.Root`. */
|
|
44
|
+
keyboard?: InputEventOption | null
|
|
45
|
+
|
|
42
46
|
onChange?: (value: PointBaseType) => void
|
|
43
47
|
onDragStart?: (value: PointBaseType) => void
|
|
44
48
|
onDragEnd?: (value: PointBaseType) => void
|
|
45
49
|
}
|
|
46
50
|
|
|
51
|
+
/**
|
|
52
|
+
* A point is placed by its position within the container, so its value is a
|
|
53
|
+
* position: 0..1 on each axis, with y growing downwards.
|
|
54
|
+
*/
|
|
55
|
+
const AXIS = { min: 0, max: 1 }
|
|
56
|
+
|
|
47
57
|
export function Point<T extends PointBaseType>({
|
|
48
58
|
value,
|
|
49
59
|
min,
|
|
@@ -53,8 +63,10 @@ export function Point<T extends PointBaseType>({
|
|
|
53
63
|
height = 16,
|
|
54
64
|
color,
|
|
55
65
|
|
|
56
|
-
disabled,
|
|
57
|
-
readonly,
|
|
66
|
+
disabled: _disabled,
|
|
67
|
+
readonly: _readonly,
|
|
68
|
+
wheel: _wheel,
|
|
69
|
+
keyboard: _keyboard,
|
|
58
70
|
|
|
59
71
|
onChange,
|
|
60
72
|
onDragStart,
|
|
@@ -63,61 +75,113 @@ export function Point<T extends PointBaseType>({
|
|
|
63
75
|
className,
|
|
64
76
|
style,
|
|
65
77
|
onPointerDown,
|
|
78
|
+
onKeyDown,
|
|
66
79
|
...props
|
|
67
80
|
}: PointProps<T> & Omit<ComponentPropsWithoutRef<'div'>, keyof PointProps<T>>) {
|
|
68
|
-
const containerElementRef = usePointsEditorContext(
|
|
69
|
-
(s) => s.containerElementRef,
|
|
70
|
-
)
|
|
71
|
-
const externalStyles = usePointsEditorContext((s) => s.externalStyles)
|
|
72
|
-
const __disabled = usePointsEditorContext((s) => s.disabled)
|
|
73
|
-
const __readonly = usePointsEditorContext((s) => s.readonly)
|
|
74
|
-
|
|
75
|
-
// console.log(value.x, value.y)
|
|
76
|
-
|
|
77
|
-
// Piano: 値が更新されることで、イベントハンドラがリセットされる
|
|
78
|
-
|
|
79
81
|
const {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
82
|
+
containerRef,
|
|
83
|
+
externalStyles,
|
|
84
|
+
disabled: rootDisabled,
|
|
85
|
+
readonly: rootReadonly,
|
|
86
|
+
wheel: rootWheel,
|
|
87
|
+
keyboard: rootKeyboard,
|
|
88
|
+
} = usePointsEditorContext()
|
|
89
|
+
|
|
90
|
+
const disabled = _disabled ?? rootDisabled
|
|
91
|
+
const readonly = _readonly ?? rootReadonly
|
|
92
|
+
// `null` means "no event" and has to survive the fallback, so `??` is not
|
|
93
|
+
// enough: only an omitted prop inherits from the root.
|
|
94
|
+
const wheel = _wheel === undefined ? rootWheel : _wheel
|
|
95
|
+
const keyboard = _keyboard === undefined ? rootKeyboard : _keyboard
|
|
96
|
+
|
|
97
|
+
// Compared against the focus below, so the point needs its own element.
|
|
98
|
+
const [element, setElement] = useState<HTMLDivElement | null>(null)
|
|
99
|
+
|
|
100
|
+
// The value is the position itself: no scaling, and no rounding to a step.
|
|
101
|
+
const { refCallback: dragRefCallback, dragging } =
|
|
102
|
+
useDragValue<HTMLDivElement>({
|
|
103
|
+
axis: AXIS,
|
|
104
|
+
baseElementRef: containerRef,
|
|
105
|
+
cursor: readonly ? undefined : externalStyles.cursor,
|
|
106
|
+
onChange: ([x, y]) => {
|
|
107
|
+
if (readonly) return
|
|
108
|
+
|
|
109
|
+
onChange?.(clampPoint({ x, y }, min, max))
|
|
110
|
+
},
|
|
111
|
+
onDragStart: ([x, y]) => {
|
|
112
|
+
if (readonly) return
|
|
113
|
+
if (externalStyles.userSelectNone) addUserSelectNone()
|
|
114
|
+
|
|
115
|
+
onDragStart?.(clampPoint({ x, y }, min, max))
|
|
116
|
+
},
|
|
117
|
+
onDragEnd: ([x, y]) => {
|
|
118
|
+
if (readonly) return
|
|
119
|
+
if (externalStyles.userSelectNone) removeUserSelectNone()
|
|
120
|
+
|
|
121
|
+
onDragEnd?.(clampPoint({ x, y }, min, max))
|
|
122
|
+
},
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const nudge = useCallback(
|
|
126
|
+
(axis: 'x' | 'y', direction: number, option: InputEventOption) => {
|
|
127
|
+
const next = applyDelta(value[axis], direction, option, AXIS)
|
|
128
|
+
onChange?.(clampPoint({ ...value, [axis]: next }, min, max))
|
|
89
129
|
},
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (externalStyles.userSelectNone) addUserSelectNone()
|
|
93
|
-
if (externalStyles.cursor) setCursorStyle(externalStyles.cursor)
|
|
130
|
+
[value, min, max, onChange],
|
|
131
|
+
)
|
|
94
132
|
|
|
95
|
-
|
|
133
|
+
// The listener sits on the container rather than on the point: a wheel event
|
|
134
|
+
// only reaches what the cursor is over, and a point is a 16px target. Every
|
|
135
|
+
// point sees the event and the focused one acts, so the wheel works anywhere
|
|
136
|
+
// over the editor, the way it does for Slider and XYPad.
|
|
137
|
+
//
|
|
138
|
+
// The focus test is an identity check, not `contains`: with `contains` every
|
|
139
|
+
// point would match the container's focus and they would all move at once.
|
|
140
|
+
useWheel(
|
|
141
|
+
(event) => {
|
|
142
|
+
if (!onChange || readonly || !wheel) return
|
|
143
|
+
if (!element || element.ownerDocument.activeElement !== element) return
|
|
144
|
+
event.preventDefault()
|
|
145
|
+
// Scrolling up moves the point towards y = 0; shift switches to x.
|
|
146
|
+
const axis = event.shiftKey ? 'x' : 'y'
|
|
147
|
+
const direction = event.deltaY < 0 ? -1 : 1
|
|
148
|
+
nudge(axis, direction, wheel)
|
|
96
149
|
},
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (externalStyles.userSelectNone) removeUserSelectNone()
|
|
100
|
-
if (externalStyles.cursor) resetCursorStyle()
|
|
150
|
+
{ target: containerRef },
|
|
151
|
+
)
|
|
101
152
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
153
|
+
const refCallback = useComposedRefs<HTMLDivElement>(
|
|
154
|
+
dragRefCallback,
|
|
155
|
+
setElement,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
159
|
+
if (!onChange || readonly || !keyboard) return
|
|
160
|
+
const key = event.key
|
|
161
|
+
if (['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(key)) {
|
|
162
|
+
event.preventDefault()
|
|
163
|
+
// y grows downwards, so ArrowUp moves the point towards 0.
|
|
164
|
+
const axis = key === 'ArrowRight' || key === 'ArrowLeft' ? 'x' : 'y'
|
|
165
|
+
const direction = key === 'ArrowLeft' || key === 'ArrowUp' ? -1 : 1
|
|
166
|
+
nudge(axis, direction, keyboard)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
105
169
|
|
|
106
170
|
const colors: Record<string, string | undefined> = {
|
|
107
171
|
'--color': color,
|
|
108
172
|
}
|
|
109
173
|
|
|
110
174
|
return (
|
|
175
|
+
// The point is a drag handle rather than a control of a known kind: it has
|
|
176
|
+
// no single value to announce, so there is no role that fits it.
|
|
177
|
+
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
|
111
178
|
<div
|
|
112
|
-
ref={
|
|
113
|
-
// wheelRefCallback(div)
|
|
114
|
-
touchMoveRefCallback(div)
|
|
115
|
-
}}
|
|
179
|
+
ref={refCallback}
|
|
116
180
|
className={clsx('tremolo-points-editor-point', className)}
|
|
117
181
|
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
|
118
182
|
tabIndex={0}
|
|
119
|
-
aria-disabled={disabled
|
|
120
|
-
aria-readonly={readonly
|
|
183
|
+
aria-disabled={disabled}
|
|
184
|
+
aria-readonly={readonly}
|
|
121
185
|
data-dragging={dragging}
|
|
122
186
|
style={{
|
|
123
187
|
...colors,
|
|
@@ -127,9 +191,10 @@ export function Point<T extends PointBaseType>({
|
|
|
127
191
|
top: `${value.y * 100}%`,
|
|
128
192
|
...style,
|
|
129
193
|
}}
|
|
130
|
-
onPointerDown={
|
|
131
|
-
|
|
132
|
-
|
|
194
|
+
onPointerDown={onPointerDown}
|
|
195
|
+
onKeyDown={(event) => {
|
|
196
|
+
handleKeyDown(event)
|
|
197
|
+
onKeyDown?.(event)
|
|
133
198
|
}}
|
|
134
199
|
{...props}
|
|
135
200
|
/>
|
|
@@ -1,91 +1,45 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
RefObject,
|
|
5
|
-
useContext,
|
|
6
|
-
useEffect,
|
|
7
|
-
useRef,
|
|
8
|
-
} from 'react'
|
|
9
|
-
import { createStore, useStore } from 'zustand'
|
|
1
|
+
import { createContext, RefObject, useContext } from 'react'
|
|
2
|
+
|
|
3
|
+
import type { InputEventOption } from '@tremolo-ui/functions'
|
|
10
4
|
|
|
11
5
|
import { Cursor } from '../_util'
|
|
12
6
|
|
|
13
|
-
type
|
|
14
|
-
containerElementRef: RefObject<HTMLDivElement | null>
|
|
7
|
+
export type PointsEditorContextValue = {
|
|
15
8
|
disabled: boolean
|
|
16
9
|
readonly: boolean
|
|
10
|
+
/** Inherited by every `Point`; `null` turns the wheel off. */
|
|
11
|
+
wheel: InputEventOption | null
|
|
12
|
+
/** Inherited by every `Point`; `null` turns the keyboard off. */
|
|
13
|
+
keyboard: InputEventOption | null
|
|
17
14
|
externalStyles: {
|
|
18
15
|
userSelectNone?: boolean
|
|
19
16
|
cursor?: Cursor
|
|
20
17
|
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
type Action = {
|
|
24
|
-
setContainerElementRef: (
|
|
25
|
-
containerElementRef: RefObject<HTMLDivElement | null>,
|
|
26
|
-
) => void
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
type PointsEditorStore = ReturnType<typeof createPointsEditorStore>
|
|
30
|
-
|
|
31
|
-
const createPointsEditorStore = (initProps: Partial<State & Action>) => {
|
|
32
|
-
// const noteRange = initProps.noteRange || { first: 0, last: 127 }
|
|
33
|
-
const DEFAULT_PROPS: State = {
|
|
34
|
-
containerElementRef: createRef(),
|
|
35
|
-
disabled: false,
|
|
36
|
-
readonly: false,
|
|
37
|
-
externalStyles: {
|
|
38
|
-
userSelectNone: true,
|
|
39
|
-
cursor: 'grabbing',
|
|
40
|
-
},
|
|
41
|
-
}
|
|
42
18
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
containerElementRef,
|
|
49
|
-
}))
|
|
50
|
-
},
|
|
51
|
-
}))
|
|
19
|
+
/**
|
|
20
|
+
* `Container` registers its element here; `Point` normalizes the pointer
|
|
21
|
+
* against it, so a point is placed by its position within the container.
|
|
22
|
+
*/
|
|
23
|
+
containerRef: RefObject<HTMLDivElement | null>
|
|
52
24
|
}
|
|
53
25
|
|
|
54
|
-
const PointsEditorContext = createContext<
|
|
26
|
+
const PointsEditorContext = createContext<PointsEditorContextValue | null>(null)
|
|
55
27
|
|
|
56
|
-
|
|
57
|
-
Partial<State & Action>
|
|
58
|
-
>
|
|
59
|
-
|
|
60
|
-
export function PointsEditorProvider({
|
|
61
|
-
children,
|
|
62
|
-
...props
|
|
63
|
-
}: PointsEditorProviderProps) {
|
|
64
|
-
const storeRef = useRef<PointsEditorStore>(null)
|
|
65
|
-
if (!storeRef.current) {
|
|
66
|
-
storeRef.current = createPointsEditorStore(props)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
useEffect(() => {
|
|
70
|
-
if (storeRef.current) {
|
|
71
|
-
storeRef.current.setState(props)
|
|
72
|
-
} else {
|
|
73
|
-
storeRef.current = createPointsEditorStore(props)
|
|
74
|
-
}
|
|
75
|
-
}, [props])
|
|
76
|
-
|
|
77
|
-
return (
|
|
78
|
-
<PointsEditorContext.Provider value={storeRef.current}>
|
|
79
|
-
{children}
|
|
80
|
-
</PointsEditorContext.Provider>
|
|
81
|
-
)
|
|
82
|
-
}
|
|
28
|
+
export const PointsEditorProvider = PointsEditorContext.Provider
|
|
83
29
|
|
|
30
|
+
/**
|
|
31
|
+
* The settings `Root` was given, for the subcomponents to read. There is no
|
|
32
|
+
* state to keep in sync: a point's value belongs to the `Point` that draws it.
|
|
33
|
+
*/
|
|
34
|
+
export function usePointsEditorContext(): PointsEditorContextValue
|
|
35
|
+
export function usePointsEditorContext<T>(
|
|
36
|
+
selector: (state: PointsEditorContextValue) => T,
|
|
37
|
+
): T
|
|
84
38
|
export function usePointsEditorContext<T>(
|
|
85
|
-
selector
|
|
86
|
-
)
|
|
87
|
-
const
|
|
88
|
-
if (!
|
|
39
|
+
selector?: (state: PointsEditorContextValue) => T,
|
|
40
|
+
) {
|
|
41
|
+
const context = useContext(PointsEditorContext)
|
|
42
|
+
if (!context)
|
|
89
43
|
throw new Error('Missing PointsEditorContext.Provider in the tree')
|
|
90
|
-
return
|
|
44
|
+
return selector ? selector(context) : context
|
|
91
45
|
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import clsx from 'clsx'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ComponentPropsWithoutRef,
|
|
4
|
+
forwardRef,
|
|
5
|
+
ReactNode,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
} from 'react'
|
|
3
9
|
|
|
4
10
|
import { InputEventOption } from '@tremolo-ui/functions'
|
|
5
11
|
|
|
@@ -8,50 +14,36 @@ import { Cursor } from '../_util'
|
|
|
8
14
|
import { Background } from './Background'
|
|
9
15
|
import { Container } from './Container'
|
|
10
16
|
import { PointsEditorProvider } from './context'
|
|
11
|
-
import { Point
|
|
17
|
+
import { Point } from './Point'
|
|
12
18
|
|
|
13
19
|
/*
|
|
14
20
|
TODO:
|
|
15
21
|
|
|
16
22
|
- 複数選択
|
|
17
23
|
- modifier
|
|
24
|
+
- grid
|
|
18
25
|
*/
|
|
19
26
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<PointsEditor.Root
|
|
28
|
-
onDoubleClick={({ x, y }, _e) => {
|
|
29
|
-
const newPoints = [...points, { x, y }]
|
|
30
|
-
setPoints(newPoints)
|
|
31
|
-
}}
|
|
32
|
-
>
|
|
33
|
-
{points.map((point) => (
|
|
34
|
-
<PointsEditor.Point key={`${point.x}, ${point.y}`} value={point} />
|
|
35
|
-
))}
|
|
36
|
-
</PointsEditor.Root>
|
|
37
|
-
)
|
|
38
|
-
}
|
|
39
|
-
*/
|
|
40
|
-
|
|
41
|
-
const defaultExternalStyles: PointsEditorProps['externalStyles'] = {
|
|
42
|
-
userSelectNone: true,
|
|
43
|
-
cursor: 'grabbing',
|
|
44
|
-
}
|
|
27
|
+
/**
|
|
28
|
+
* A point moves over 0..1 in both axes, so a nudge of 0.01 crosses the editor
|
|
29
|
+
* in a hundred steps whatever its pixel size.
|
|
30
|
+
*/
|
|
31
|
+
const DEFAULT_INPUT_EVENT_OPTION: InputEventOption = ['normalized', 0.01]
|
|
45
32
|
|
|
46
33
|
export interface PointsEditorProps {
|
|
47
34
|
width?: number | string
|
|
48
35
|
height?: number | string
|
|
49
36
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Only the appearance will change.
|
|
39
|
+
* Please consider using with readonly.
|
|
40
|
+
* aria-disabled property is also applied.
|
|
41
|
+
*/
|
|
54
42
|
disabled?: boolean
|
|
43
|
+
/**
|
|
44
|
+
* Make the points unmovable.
|
|
45
|
+
* aria-readonly property is also applied.
|
|
46
|
+
*/
|
|
55
47
|
readonly?: boolean
|
|
56
48
|
|
|
57
49
|
externalStyles?: {
|
|
@@ -59,30 +51,53 @@ export interface PointsEditorProps {
|
|
|
59
51
|
cursor?: Cursor
|
|
60
52
|
}
|
|
61
53
|
|
|
62
|
-
// TODO
|
|
63
54
|
/**
|
|
64
|
-
* wheel control option
|
|
55
|
+
* wheel control option for every `Point`. Shift selects the x axis.
|
|
65
56
|
* If null, no event will be triggered
|
|
57
|
+
*
|
|
58
|
+
* A `Point` can override it with a `wheel` of its own.
|
|
66
59
|
*/
|
|
67
60
|
wheel?: InputEventOption | null
|
|
68
61
|
/**
|
|
69
|
-
* keyboard control option
|
|
62
|
+
* keyboard control option for every `Point`, on the arrow keys.
|
|
70
63
|
* If null, no event will be triggered
|
|
64
|
+
*
|
|
65
|
+
* A `Point` can override it with a `keyboard` of its own.
|
|
71
66
|
*/
|
|
72
67
|
keyboard?: InputEventOption | null
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The editor renders exactly what you compose here; there is no default
|
|
71
|
+
* markup to fall back to.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* <PointsEditor.Root>
|
|
75
|
+
* <PointsEditor.Background>
|
|
76
|
+
* <svg viewBox="0 0 200 100">...</svg>
|
|
77
|
+
* </PointsEditor.Background>
|
|
78
|
+
* <PointsEditor.Container>
|
|
79
|
+
* {points.map((point, i) => (
|
|
80
|
+
* <PointsEditor.Point key={i} value={point} onChange={...} />
|
|
81
|
+
* ))}
|
|
82
|
+
* </PointsEditor.Container>
|
|
83
|
+
* </PointsEditor.Root>
|
|
84
|
+
*/
|
|
85
|
+
children: ReactNode
|
|
73
86
|
}
|
|
74
87
|
|
|
75
88
|
type Props = PointsEditorProps &
|
|
76
89
|
Omit<ComponentPropsWithoutRef<'div'>, keyof PointsEditorProps>
|
|
77
90
|
|
|
78
|
-
const Root = forwardRef<HTMLDivElement, Props>(
|
|
91
|
+
export const Root = forwardRef<HTMLDivElement, Props>(
|
|
79
92
|
(
|
|
80
93
|
{
|
|
81
94
|
width = 200,
|
|
82
95
|
height = 100,
|
|
83
96
|
disabled = false,
|
|
84
97
|
readonly = false,
|
|
85
|
-
|
|
98
|
+
wheel = DEFAULT_INPUT_EVENT_OPTION,
|
|
99
|
+
keyboard = DEFAULT_INPUT_EVENT_OPTION,
|
|
100
|
+
externalStyles,
|
|
86
101
|
style,
|
|
87
102
|
className,
|
|
88
103
|
children,
|
|
@@ -90,17 +105,31 @@ const Root = forwardRef<HTMLDivElement, Props>(
|
|
|
90
105
|
},
|
|
91
106
|
forwardedRef,
|
|
92
107
|
) => {
|
|
93
|
-
const
|
|
108
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
109
|
+
|
|
110
|
+
// Picked apart so that the memo below depends on values rather than on the
|
|
111
|
+
// object literal a caller writes inline, which is new on every render.
|
|
112
|
+
const { userSelectNone = true, cursor = 'grabbing' } = externalStyles ?? {}
|
|
113
|
+
|
|
114
|
+
const context = useMemo(
|
|
115
|
+
() => ({
|
|
116
|
+
disabled,
|
|
117
|
+
readonly,
|
|
118
|
+
wheel,
|
|
119
|
+
keyboard,
|
|
120
|
+
externalStyles: { userSelectNone, cursor },
|
|
121
|
+
containerRef,
|
|
122
|
+
}),
|
|
123
|
+
[disabled, readonly, wheel, keyboard, userSelectNone, cursor],
|
|
124
|
+
)
|
|
94
125
|
|
|
95
126
|
return (
|
|
96
|
-
<PointsEditorProvider
|
|
97
|
-
disabled={disabled}
|
|
98
|
-
readonly={readonly}
|
|
99
|
-
externalStyles={externalStyles}
|
|
100
|
-
>
|
|
127
|
+
<PointsEditorProvider value={context}>
|
|
101
128
|
<div
|
|
102
129
|
ref={forwardedRef}
|
|
103
130
|
className={clsx('tremolo-points-editor', className)}
|
|
131
|
+
aria-disabled={disabled}
|
|
132
|
+
aria-readonly={readonly}
|
|
104
133
|
style={{
|
|
105
134
|
width,
|
|
106
135
|
height,
|
|
@@ -125,5 +154,10 @@ export const PointsEditor = {
|
|
|
125
154
|
Point,
|
|
126
155
|
}
|
|
127
156
|
|
|
128
|
-
export {
|
|
157
|
+
export {
|
|
158
|
+
usePointsEditorContext,
|
|
159
|
+
type PointsEditorContextValue,
|
|
160
|
+
} from './context'
|
|
161
|
+
export { type PointsEditorBackgroundProps } from './Background'
|
|
162
|
+
export { type PointsEditorContainerProps } from './Container'
|
|
129
163
|
export { clampPoint, type PointBaseType, type PointProps } from './Point'
|
|
@@ -4,23 +4,23 @@ import { ComponentPropsWithoutRef, ReactNode } from 'react'
|
|
|
4
4
|
import { xor } from '@tremolo-ui/functions'
|
|
5
5
|
|
|
6
6
|
import { useSliderContext } from './context'
|
|
7
|
-
import {
|
|
8
|
-
import { generateOptionsList,
|
|
7
|
+
import { MarksOption } from './MarksOption'
|
|
8
|
+
import { generateOptionsList, MarksOptions } from './type'
|
|
9
9
|
|
|
10
|
-
export interface
|
|
10
|
+
export interface MarksProps {
|
|
11
11
|
gap?: number | string
|
|
12
|
-
options?:
|
|
12
|
+
options?: MarksOptions
|
|
13
13
|
children?: ReactNode
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export function
|
|
16
|
+
export function Marks({
|
|
17
17
|
gap = 6,
|
|
18
18
|
options,
|
|
19
19
|
children,
|
|
20
20
|
className,
|
|
21
21
|
style,
|
|
22
22
|
...props
|
|
23
|
-
}:
|
|
23
|
+
}: MarksProps & Omit<ComponentPropsWithoutRef<'div'>, keyof MarksProps>) {
|
|
24
24
|
const min = useSliderContext((s) => s.min)
|
|
25
25
|
const max = useSliderContext((s) => s.max)
|
|
26
26
|
const step = useSliderContext((s) => s.step)
|
|
@@ -34,7 +34,7 @@ export function Scale({
|
|
|
34
34
|
|
|
35
35
|
return (
|
|
36
36
|
<div
|
|
37
|
-
className={clsx('tremolo-slider-
|
|
37
|
+
className={clsx('tremolo-slider-marks', className)}
|
|
38
38
|
style={{
|
|
39
39
|
marginLeft: vertical ? gap : undefined,
|
|
40
40
|
marginTop: !vertical ? gap : undefined,
|
|
@@ -44,7 +44,7 @@ export function Scale({
|
|
|
44
44
|
>
|
|
45
45
|
{options
|
|
46
46
|
? optionsList.map(({ value, type }, index) => {
|
|
47
|
-
return <
|
|
47
|
+
return <MarksOption key={index} value={value} type={type} />
|
|
48
48
|
})
|
|
49
49
|
: children}
|
|
50
50
|
</div>
|