@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.
Files changed (64) hide show
  1. package/dist/index.cjs +1162 -1554
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +58 -93
  4. package/dist/index.d.cts +592 -278
  5. package/dist/index.d.cts.map +1 -1
  6. package/dist/index.d.ts +592 -278
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1163 -1555
  9. package/dist/index.js.map +1 -1
  10. package/package.json +12 -13
  11. package/src/components/AnimationCanvas/index.tsx +84 -155
  12. package/src/components/Knob/ActiveLine.tsx +6 -6
  13. package/src/components/Knob/InactiveLine.tsx +9 -11
  14. package/src/components/Knob/SVGRoot.tsx +9 -18
  15. package/src/components/Knob/context.tsx +57 -89
  16. package/src/components/Knob/index.css +6 -2
  17. package/src/components/Knob/index.tsx +95 -83
  18. package/src/components/NumberInput/DecrementStepper.tsx +17 -50
  19. package/src/components/NumberInput/IncrementStepper.tsx +17 -50
  20. package/src/components/NumberInput/InputField.tsx +123 -0
  21. package/src/components/NumberInput/Stepper.tsx +90 -12
  22. package/src/components/NumberInput/context.tsx +74 -146
  23. package/src/components/NumberInput/index.css +38 -102
  24. package/src/components/NumberInput/index.tsx +252 -98
  25. package/src/components/NumberInput/stepperButton.tsx +90 -0
  26. package/src/components/Piano/index.tsx +272 -288
  27. package/src/components/Piano/keyboardShortcuts.ts +34 -4
  28. package/src/components/PointsEditor/Background.tsx +8 -2
  29. package/src/components/PointsEditor/Container.tsx +19 -9
  30. package/src/components/PointsEditor/Point.tsx +117 -52
  31. package/src/components/PointsEditor/context.tsx +28 -74
  32. package/src/components/PointsEditor/index.css +5 -0
  33. package/src/components/PointsEditor/index.tsx +77 -43
  34. package/src/components/Slider/{Scale.tsx → Marks.tsx} +8 -8
  35. package/src/components/Slider/{ScaleOption.tsx → MarksOption.tsx} +16 -18
  36. package/src/components/Slider/Thumb.tsx +55 -68
  37. package/src/components/Slider/Track.tsx +15 -15
  38. package/src/components/Slider/context.tsx +36 -51
  39. package/src/components/Slider/index.css +18 -5
  40. package/src/components/Slider/index.tsx +136 -170
  41. package/src/components/Slider/type.ts +4 -4
  42. package/src/components/XYPad/Area.tsx +16 -7
  43. package/src/components/XYPad/Thumb.tsx +62 -74
  44. package/src/components/XYPad/context.tsx +46 -0
  45. package/src/components/XYPad/index.css +12 -0
  46. package/src/components/XYPad/index.tsx +193 -250
  47. package/src/hooks/useDrag.ts +41 -76
  48. package/src/hooks/useDragValue.ts +149 -0
  49. package/src/hooks/useEventListener.ts +1 -1
  50. package/src/hooks/useWheel.ts +51 -0
  51. package/src/index.ts +15 -13
  52. package/src/components/AnimationCanvas/canvas.ts +0 -46
  53. package/src/components/DragObserver/index.tsx +0 -58
  54. package/src/components/NumberInput/InternalInput.tsx +0 -221
  55. package/src/components/NumberInput/type.ts +0 -58
  56. package/src/components/Piano/KeyLabel.tsx +0 -51
  57. package/src/components/Piano/context.tsx +0 -73
  58. package/src/components/Piano/key.tsx +0 -205
  59. package/src/components/WheelObserver/index.tsx +0 -48
  60. package/src/components/_util/type.ts +0 -1
  61. package/src/hooks/useDragWithElement.ts +0 -72
  62. package/src/hooks/usePianoDrag.ts +0 -75
  63. package/src/hooks/useRefCallbackEvent.ts +0 -34
  64. /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
- }: ComponentPropsWithoutRef<'div'>) {
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, useRef } from 'react'
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
- }: ComponentPropsWithoutRef<'div'>) {
11
- const containerElementRef = useRef<HTMLDivElement>(null)
12
- const setContainerElementRef = usePointsEditorContext(
13
- (s) => s.setContainerElementRef,
14
- )
15
- setContainerElementRef(containerElementRef)
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={containerElementRef}
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 { useDragWithElement } from '../../hooks/useDragWithElement'
7
- import {
8
- addUserSelectNone,
9
- removeUserSelectNone,
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
- refHandler: touchMoveRefCallback,
81
- pointerDownHandler,
82
- dragging,
83
- } = useDragWithElement<HTMLDivElement>({
84
- baseElementRef: containerElementRef,
85
- onDrag: (x, y) => {
86
- if (readonly) return
87
-
88
- onChange?.(clampPoint({ x, y }, min, max))
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
- onDragStart: (x, y) => {
91
- if (readonly) return
92
- if (externalStyles.userSelectNone) addUserSelectNone()
93
- if (externalStyles.cursor) setCursorStyle(externalStyles.cursor)
130
+ [value, min, max, onChange],
131
+ )
94
132
 
95
- onDragStart?.(clampPoint({ x, y }, min, max))
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
- onDragEnd: (x, y) => {
98
- if (readonly) return
99
- if (externalStyles.userSelectNone) removeUserSelectNone()
100
- if (externalStyles.cursor) resetCursorStyle()
150
+ { target: containerRef },
151
+ )
101
152
 
102
- onDragEnd?.(clampPoint({ x, y }, min, max))
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={(div) => {
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 != undefined ? disabled : __disabled}
120
- aria-readonly={readonly != undefined ? 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={(event) => {
131
- pointerDownHandler(event)
132
- onPointerDown?.(event)
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
- createContext,
3
- createRef,
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 State = {
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
- return createStore<State & Action>()((set) => ({
44
- ...DEFAULT_PROPS,
45
- ...initProps,
46
- setContainerElementRef: (containerElementRef) => {
47
- set(() => ({
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<PointsEditorStore | null>(null)
26
+ const PointsEditorContext = createContext<PointsEditorContextValue | null>(null)
55
27
 
56
- type PointsEditorProviderProps = React.PropsWithChildren<
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: (state: State & Action) => T,
86
- ): T {
87
- const store = useContext(PointsEditorContext)
88
- if (!store)
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 useStore(store, selector)
44
+ return selector ? selector(context) : context
91
45
  }
@@ -27,4 +27,9 @@
27
27
  &[data-dragging='true'] {
28
28
  cursor: inherit;
29
29
  }
30
+
31
+ /* Dragging does nothing while readonly, so do not offer to. */
32
+ &[aria-readonly='true'] {
33
+ cursor: default;
34
+ }
30
35
  }
@@ -1,5 +1,11 @@
1
1
  import clsx from 'clsx'
2
- import { ComponentPropsWithoutRef, forwardRef } from 'react'
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, PointBaseType } from './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
- import { useState } from 'react'
22
-
23
- export default function App() {
24
- const [points, setPoints] = useState(initialPoints)
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
- // TODO
51
- grid?: number | PointBaseType
52
- // modifier?:
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
- externalStyles: _externalStyles,
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 externalStyles = { ...defaultExternalStyles, ..._externalStyles }
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 { usePointsEditorContext } from './context'
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 { ScaleOption } from './ScaleOption'
8
- import { generateOptionsList, ScaleOptions } from './type'
7
+ import { MarksOption } from './MarksOption'
8
+ import { generateOptionsList, MarksOptions } from './type'
9
9
 
10
- export interface ScaleProps {
10
+ export interface MarksProps {
11
11
  gap?: number | string
12
- options?: ScaleOptions
12
+ options?: MarksOptions
13
13
  children?: ReactNode
14
14
  }
15
15
 
16
- export function Scale({
16
+ export function Marks({
17
17
  gap = 6,
18
18
  options,
19
19
  children,
20
20
  className,
21
21
  style,
22
22
  ...props
23
- }: ScaleProps & Omit<ComponentPropsWithoutRef<'div'>, keyof ScaleProps>) {
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-scale', className)}
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 <ScaleOption key={index} value={value} type={type} />
47
+ return <MarksOption key={index} value={value} type={type} />
48
48
  })
49
49
  : children}
50
50
  </div>