@tremolo-ui/react 0.2.0 → 0.3.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 +1245 -1083
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +176 -159
- package/dist/index.d.cts +244 -253
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +244 -253
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1218 -1067
- package/dist/index.js.map +1 -1
- package/package.json +18 -19
- package/src/components/AnimationCanvas/index.tsx +0 -7
- package/src/components/DragObserver/index.tsx +9 -7
- package/src/components/Knob/ActiveLine.tsx +29 -0
- package/src/components/Knob/InactiveLine.tsx +53 -0
- package/src/components/Knob/SVGRoot.tsx +39 -0
- package/src/components/Knob/Thumb.tsx +64 -0
- package/src/components/Knob/context.tsx +120 -0
- package/src/components/Knob/index.css +5 -1
- package/src/components/Knob/index.tsx +110 -164
- package/src/components/NumberInput/DecrementStepper.tsx +0 -2
- package/src/components/NumberInput/IncrementStepper.tsx +0 -2
- package/src/components/NumberInput/InternalInput.tsx +3 -1
- package/src/components/NumberInput/Stepper.tsx +0 -2
- package/src/components/NumberInput/context.tsx +0 -1
- package/src/components/NumberInput/index.tsx +6 -8
- package/src/components/Piano/KeyLabel.tsx +0 -2
- package/src/components/Piano/context.tsx +0 -2
- package/src/components/Piano/index.tsx +7 -11
- package/src/components/Piano/key.tsx +0 -10
- package/src/components/Piano/keyboardShortcuts.ts +0 -2
- package/src/components/PointsEditor/Background.tsx +17 -0
- package/src/components/PointsEditor/Container.tsx +26 -0
- package/src/components/PointsEditor/Point.tsx +137 -0
- package/src/components/PointsEditor/context.tsx +91 -0
- package/src/components/PointsEditor/index.css +30 -0
- package/src/components/PointsEditor/index.tsx +129 -0
- package/src/components/Slider/Scale.tsx +0 -2
- package/src/components/Slider/ScaleOption.tsx +0 -2
- package/src/components/Slider/Thumb.tsx +0 -4
- package/src/components/Slider/Track.tsx +0 -2
- package/src/components/Slider/context.tsx +0 -1
- package/src/components/Slider/index.tsx +42 -14
- package/src/components/WheelObserver/index.tsx +17 -19
- package/src/components/XYPad/Area.tsx +0 -2
- package/src/components/XYPad/Thumb.tsx +0 -3
- package/src/components/XYPad/index.tsx +52 -25
- package/src/components/_util/composeRefs.tsx +63 -0
- package/src/components/_util/index.ts +23 -6
- package/src/components/_util/type.ts +1 -0
- package/src/hooks/useAnimationFrame.ts +0 -3
- package/src/hooks/useCallbackRef.ts +2 -2
- package/src/hooks/useDrag.ts +12 -11
- package/src/hooks/useDragWithElement.ts +12 -17
- package/src/hooks/useEventListener.ts +6 -9
- package/src/hooks/useInterval.ts +0 -3
- package/src/hooks/useLongPress.ts +0 -3
- package/src/hooks/useMIDIAccess.ts +20 -28
- package/src/hooks/useMIDIInput.ts +10 -38
- package/src/hooks/useMIDIMessage.ts +4 -12
- package/src/hooks/usePianoDrag.ts +75 -0
- package/src/index.ts +8 -5
- package/src/styles/global.css +17 -1
- package/dist/index.css.map +0 -1
- package/src/components/AnimationKnob/index.tsx +0 -314
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import clsx from 'clsx'
|
|
2
|
+
import { ComponentPropsWithoutRef, useRef } from 'react'
|
|
3
|
+
|
|
4
|
+
import { usePointsEditorContext } from './context'
|
|
5
|
+
|
|
6
|
+
export function Container({
|
|
7
|
+
className,
|
|
8
|
+
children,
|
|
9
|
+
...props
|
|
10
|
+
}: ComponentPropsWithoutRef<'div'>) {
|
|
11
|
+
const containerElementRef = useRef<HTMLDivElement>(null)
|
|
12
|
+
const setContainerElementRef = usePointsEditorContext(
|
|
13
|
+
(s) => s.setContainerElementRef,
|
|
14
|
+
)
|
|
15
|
+
setContainerElementRef(containerElementRef)
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
ref={containerElementRef}
|
|
20
|
+
className={clsx('tremolo-points-editor-container', className)}
|
|
21
|
+
{...props}
|
|
22
|
+
>
|
|
23
|
+
{children}
|
|
24
|
+
</div>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import clsx from 'clsx'
|
|
2
|
+
import { ComponentPropsWithoutRef } from 'react'
|
|
3
|
+
|
|
4
|
+
import { clamp } from '@tremolo-ui/functions'
|
|
5
|
+
|
|
6
|
+
import { useDragWithElement } from '../../hooks/useDragWithElement'
|
|
7
|
+
import {
|
|
8
|
+
addUserSelectNone,
|
|
9
|
+
removeUserSelectNone,
|
|
10
|
+
resetCursorStyle,
|
|
11
|
+
setCursorStyle,
|
|
12
|
+
} from '../_util'
|
|
13
|
+
|
|
14
|
+
import { usePointsEditorContext } from './context'
|
|
15
|
+
|
|
16
|
+
export type PointBaseType = { x: number; y: number }
|
|
17
|
+
|
|
18
|
+
export function clampPoint(
|
|
19
|
+
point: PointBaseType,
|
|
20
|
+
min?: Partial<PointBaseType>,
|
|
21
|
+
max?: Partial<PointBaseType>,
|
|
22
|
+
) {
|
|
23
|
+
const { x, y } = point
|
|
24
|
+
const newX = clamp(x, min?.x ?? 0, max?.x ?? 1)
|
|
25
|
+
const newY = clamp(y, min?.y ?? 0, max?.y ?? 1)
|
|
26
|
+
return { x: newX, y: newY }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PointProps<T extends PointBaseType> {
|
|
30
|
+
value: T
|
|
31
|
+
min?: Partial<PointBaseType>
|
|
32
|
+
max?: Partial<PointBaseType>
|
|
33
|
+
|
|
34
|
+
size?: number | string
|
|
35
|
+
width?: number | string
|
|
36
|
+
height?: number | string
|
|
37
|
+
color?: string
|
|
38
|
+
|
|
39
|
+
disabled?: boolean
|
|
40
|
+
readonly?: boolean
|
|
41
|
+
|
|
42
|
+
onChange?: (value: PointBaseType) => void
|
|
43
|
+
onDragStart?: (value: PointBaseType) => void
|
|
44
|
+
onDragEnd?: (value: PointBaseType) => void
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function Point<T extends PointBaseType>({
|
|
48
|
+
value,
|
|
49
|
+
min,
|
|
50
|
+
max,
|
|
51
|
+
size,
|
|
52
|
+
width = 16,
|
|
53
|
+
height = 16,
|
|
54
|
+
color,
|
|
55
|
+
|
|
56
|
+
disabled,
|
|
57
|
+
readonly,
|
|
58
|
+
|
|
59
|
+
onChange,
|
|
60
|
+
onDragStart,
|
|
61
|
+
onDragEnd,
|
|
62
|
+
|
|
63
|
+
className,
|
|
64
|
+
style,
|
|
65
|
+
onPointerDown,
|
|
66
|
+
...props
|
|
67
|
+
}: 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
|
+
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))
|
|
89
|
+
},
|
|
90
|
+
onDragStart: (x, y) => {
|
|
91
|
+
if (readonly) return
|
|
92
|
+
if (externalStyles.userSelectNone) addUserSelectNone()
|
|
93
|
+
if (externalStyles.cursor) setCursorStyle(externalStyles.cursor)
|
|
94
|
+
|
|
95
|
+
onDragStart?.(clampPoint({ x, y }, min, max))
|
|
96
|
+
},
|
|
97
|
+
onDragEnd: (x, y) => {
|
|
98
|
+
if (readonly) return
|
|
99
|
+
if (externalStyles.userSelectNone) removeUserSelectNone()
|
|
100
|
+
if (externalStyles.cursor) resetCursorStyle()
|
|
101
|
+
|
|
102
|
+
onDragEnd?.(clampPoint({ x, y }, min, max))
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const colors: Record<string, string | undefined> = {
|
|
107
|
+
'--color': color,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div
|
|
112
|
+
ref={(div) => {
|
|
113
|
+
// wheelRefCallback(div)
|
|
114
|
+
touchMoveRefCallback(div)
|
|
115
|
+
}}
|
|
116
|
+
className={clsx('tremolo-points-editor-point', className)}
|
|
117
|
+
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
|
118
|
+
tabIndex={0}
|
|
119
|
+
aria-disabled={disabled != undefined ? disabled : __disabled}
|
|
120
|
+
aria-readonly={readonly != undefined ? readonly : __readonly}
|
|
121
|
+
data-dragging={dragging}
|
|
122
|
+
style={{
|
|
123
|
+
...colors,
|
|
124
|
+
width: size ?? width,
|
|
125
|
+
height: size ?? height,
|
|
126
|
+
left: `${value.x * 100}%`,
|
|
127
|
+
top: `${value.y * 100}%`,
|
|
128
|
+
...style,
|
|
129
|
+
}}
|
|
130
|
+
onPointerDown={(event) => {
|
|
131
|
+
pointerDownHandler(event)
|
|
132
|
+
onPointerDown?.(event)
|
|
133
|
+
}}
|
|
134
|
+
{...props}
|
|
135
|
+
/>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
createRef,
|
|
4
|
+
RefObject,
|
|
5
|
+
useContext,
|
|
6
|
+
useEffect,
|
|
7
|
+
useRef,
|
|
8
|
+
} from 'react'
|
|
9
|
+
import { createStore, useStore } from 'zustand'
|
|
10
|
+
|
|
11
|
+
import { Cursor } from '../_util'
|
|
12
|
+
|
|
13
|
+
type State = {
|
|
14
|
+
containerElementRef: RefObject<HTMLDivElement | null>
|
|
15
|
+
disabled: boolean
|
|
16
|
+
readonly: boolean
|
|
17
|
+
externalStyles: {
|
|
18
|
+
userSelectNone?: boolean
|
|
19
|
+
cursor?: Cursor
|
|
20
|
+
}
|
|
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
|
+
|
|
43
|
+
return createStore<State & Action>()((set) => ({
|
|
44
|
+
...DEFAULT_PROPS,
|
|
45
|
+
...initProps,
|
|
46
|
+
setContainerElementRef: (containerElementRef) => {
|
|
47
|
+
set(() => ({
|
|
48
|
+
containerElementRef,
|
|
49
|
+
}))
|
|
50
|
+
},
|
|
51
|
+
}))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const PointsEditorContext = createContext<PointsEditorStore | null>(null)
|
|
55
|
+
|
|
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
|
+
}
|
|
83
|
+
|
|
84
|
+
export function usePointsEditorContext<T>(
|
|
85
|
+
selector: (state: State & Action) => T,
|
|
86
|
+
): T {
|
|
87
|
+
const store = useContext(PointsEditorContext)
|
|
88
|
+
if (!store)
|
|
89
|
+
throw new Error('Missing PointsEditorContext.Provider in the tree')
|
|
90
|
+
return useStore(store, selector)
|
|
91
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
.tremolo-points-editor {
|
|
2
|
+
position: relative;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.tremolo-points-editor-background {
|
|
6
|
+
position: absolute;
|
|
7
|
+
inset: 0;
|
|
8
|
+
z-index: 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.tremolo-points-editor-container {
|
|
12
|
+
position: absolute;
|
|
13
|
+
inset: 0;
|
|
14
|
+
z-index: 10;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.tremolo-points-editor-point {
|
|
18
|
+
--color: oklch(59.5% 0.17548 266.373);
|
|
19
|
+
|
|
20
|
+
position: absolute;
|
|
21
|
+
border-radius: 50%;
|
|
22
|
+
outline: none;
|
|
23
|
+
cursor: grab;
|
|
24
|
+
background-color: var(--color);
|
|
25
|
+
translate: -50% -50%;
|
|
26
|
+
|
|
27
|
+
&[data-dragging='true'] {
|
|
28
|
+
cursor: inherit;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import clsx from 'clsx'
|
|
2
|
+
import { ComponentPropsWithoutRef, forwardRef } from 'react'
|
|
3
|
+
|
|
4
|
+
import { InputEventOption } from '@tremolo-ui/functions'
|
|
5
|
+
|
|
6
|
+
import { Cursor } from '../_util'
|
|
7
|
+
|
|
8
|
+
import { Background } from './Background'
|
|
9
|
+
import { Container } from './Container'
|
|
10
|
+
import { PointsEditorProvider } from './context'
|
|
11
|
+
import { Point, PointBaseType } from './Point'
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
TODO:
|
|
15
|
+
|
|
16
|
+
- 複数選択
|
|
17
|
+
- modifier
|
|
18
|
+
*/
|
|
19
|
+
|
|
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
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface PointsEditorProps {
|
|
47
|
+
width?: number | string
|
|
48
|
+
height?: number | string
|
|
49
|
+
|
|
50
|
+
// TODO
|
|
51
|
+
grid?: number | PointBaseType
|
|
52
|
+
// modifier?:
|
|
53
|
+
|
|
54
|
+
disabled?: boolean
|
|
55
|
+
readonly?: boolean
|
|
56
|
+
|
|
57
|
+
externalStyles?: {
|
|
58
|
+
userSelectNone?: boolean
|
|
59
|
+
cursor?: Cursor
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// TODO
|
|
63
|
+
/**
|
|
64
|
+
* wheel control option
|
|
65
|
+
* If null, no event will be triggered
|
|
66
|
+
*/
|
|
67
|
+
wheel?: InputEventOption | null
|
|
68
|
+
/**
|
|
69
|
+
* keyboard control option
|
|
70
|
+
* If null, no event will be triggered
|
|
71
|
+
*/
|
|
72
|
+
keyboard?: InputEventOption | null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type Props = PointsEditorProps &
|
|
76
|
+
Omit<ComponentPropsWithoutRef<'div'>, keyof PointsEditorProps>
|
|
77
|
+
|
|
78
|
+
const Root = forwardRef<HTMLDivElement, Props>(
|
|
79
|
+
(
|
|
80
|
+
{
|
|
81
|
+
width = 200,
|
|
82
|
+
height = 100,
|
|
83
|
+
disabled = false,
|
|
84
|
+
readonly = false,
|
|
85
|
+
externalStyles: _externalStyles,
|
|
86
|
+
style,
|
|
87
|
+
className,
|
|
88
|
+
children,
|
|
89
|
+
...props
|
|
90
|
+
},
|
|
91
|
+
forwardedRef,
|
|
92
|
+
) => {
|
|
93
|
+
const externalStyles = { ...defaultExternalStyles, ..._externalStyles }
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<PointsEditorProvider
|
|
97
|
+
disabled={disabled}
|
|
98
|
+
readonly={readonly}
|
|
99
|
+
externalStyles={externalStyles}
|
|
100
|
+
>
|
|
101
|
+
<div
|
|
102
|
+
ref={forwardedRef}
|
|
103
|
+
className={clsx('tremolo-points-editor', className)}
|
|
104
|
+
style={{
|
|
105
|
+
width,
|
|
106
|
+
height,
|
|
107
|
+
...style,
|
|
108
|
+
}}
|
|
109
|
+
{...props}
|
|
110
|
+
>
|
|
111
|
+
{children}
|
|
112
|
+
</div>
|
|
113
|
+
</PointsEditorProvider>
|
|
114
|
+
)
|
|
115
|
+
},
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Multiple Point Controller
|
|
120
|
+
*/
|
|
121
|
+
export const PointsEditor = {
|
|
122
|
+
Root,
|
|
123
|
+
Background,
|
|
124
|
+
Container,
|
|
125
|
+
Point,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export { usePointsEditorContext } from './context'
|
|
129
|
+
export { clampPoint, type PointBaseType, type PointProps } from './Point'
|
|
@@ -7,14 +7,12 @@ import { useSliderContext } from './context'
|
|
|
7
7
|
import { ScaleOption } from './ScaleOption'
|
|
8
8
|
import { generateOptionsList, ScaleOptions } from './type'
|
|
9
9
|
|
|
10
|
-
/** @category Slider */
|
|
11
10
|
export interface ScaleProps {
|
|
12
11
|
gap?: number | string
|
|
13
12
|
options?: ScaleOptions
|
|
14
13
|
children?: ReactNode
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
/** @category Slider */
|
|
18
16
|
export function Scale({
|
|
19
17
|
gap = 6,
|
|
20
18
|
options,
|
|
@@ -6,7 +6,6 @@ import { toFixed, xor, normalizeValue } from '@tremolo-ui/functions'
|
|
|
6
6
|
import { useSliderContext } from './context'
|
|
7
7
|
import { ScaleType } from './type'
|
|
8
8
|
|
|
9
|
-
/** @category Slider */
|
|
10
9
|
export interface ScaleOptionProps {
|
|
11
10
|
// required
|
|
12
11
|
value: number
|
|
@@ -32,7 +31,6 @@ export interface ScaleOptionProps {
|
|
|
32
31
|
}
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
/** @category Slider */
|
|
36
34
|
export function ScaleOption({
|
|
37
35
|
value,
|
|
38
36
|
type = 'mark-number',
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
|
|
10
10
|
import { useSliderContext } from './context'
|
|
11
11
|
|
|
12
|
-
/** @category Slider */
|
|
13
12
|
export interface SliderThumbProps {
|
|
14
13
|
size?: number | string
|
|
15
14
|
width?: number | string
|
|
@@ -25,16 +24,13 @@ export interface SliderThumbProps {
|
|
|
25
24
|
__percent?: number
|
|
26
25
|
}
|
|
27
26
|
|
|
28
|
-
/** @category Slider */
|
|
29
27
|
export interface SliderThumbMethods {
|
|
30
28
|
focus: () => void
|
|
31
29
|
blur: () => void
|
|
32
30
|
}
|
|
33
31
|
|
|
34
|
-
/** @category Slider */
|
|
35
32
|
export const defaultThumbSize = 22
|
|
36
33
|
|
|
37
|
-
/** @category Slider */
|
|
38
34
|
export const Thumb = forwardRef<SliderThumbMethods, SliderThumbProps>(
|
|
39
35
|
(
|
|
40
36
|
{
|
|
@@ -8,7 +8,6 @@ import { useSliderContext } from './context'
|
|
|
8
8
|
export const defaultLength = 140
|
|
9
9
|
export const defaultThickness = 10
|
|
10
10
|
|
|
11
|
-
/** @category Slider */
|
|
12
11
|
export interface SliderTrackProps {
|
|
13
12
|
length?: number | string
|
|
14
13
|
thickness?: number | string
|
|
@@ -28,7 +27,6 @@ export interface SliderTrackProps {
|
|
|
28
27
|
__percent?: number
|
|
29
28
|
}
|
|
30
29
|
|
|
31
|
-
/** @category Slider */
|
|
32
30
|
export function Track({
|
|
33
31
|
length = defaultLength,
|
|
34
32
|
thickness = defaultThickness,
|
|
@@ -57,7 +57,6 @@ export function SliderProvider({ children, ...props }: SliderProviderProps) {
|
|
|
57
57
|
)
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
/** @category Slider */
|
|
61
60
|
export function useSliderContext<T>(selector: (state: State) => T): T {
|
|
62
61
|
const store = useContext(SliderContext)
|
|
63
62
|
if (!store) throw new Error('Missing SliderContext.Provider in the tree')
|
|
@@ -23,7 +23,13 @@ import {
|
|
|
23
23
|
|
|
24
24
|
import { useDragWithElement } from '../../hooks/useDragWithElement'
|
|
25
25
|
import { useRefCallbackEvent } from '../../hooks/useRefCallbackEvent'
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
addUserSelectNone,
|
|
28
|
+
Cursor,
|
|
29
|
+
removeUserSelectNone,
|
|
30
|
+
resetCursorStyle,
|
|
31
|
+
setCursorStyle,
|
|
32
|
+
} from '../_util'
|
|
27
33
|
|
|
28
34
|
import { SliderProvider } from './context'
|
|
29
35
|
import { Scale } from './Scale'
|
|
@@ -31,7 +37,11 @@ import { ScaleOption } from './ScaleOption'
|
|
|
31
37
|
import { Thumb, SliderThumbMethods, SliderThumbProps } from './Thumb'
|
|
32
38
|
import { Track, SliderTrackProps } from './Track'
|
|
33
39
|
|
|
34
|
-
|
|
40
|
+
const defaultExternalStyles: SliderProps['externalStyles'] = {
|
|
41
|
+
userSelectNone: true,
|
|
42
|
+
cursor: 'pointer',
|
|
43
|
+
}
|
|
44
|
+
|
|
35
45
|
export interface SliderProps {
|
|
36
46
|
// required
|
|
37
47
|
value: number
|
|
@@ -41,24 +51,38 @@ export interface SliderProps {
|
|
|
41
51
|
// optional
|
|
42
52
|
step?: number
|
|
43
53
|
skew?: number // TODO | SkewFunction
|
|
54
|
+
/**
|
|
55
|
+
* slider orientation
|
|
56
|
+
* aria-orientation property is also applied.
|
|
57
|
+
*/
|
|
44
58
|
vertical?: boolean
|
|
45
59
|
reverse?: boolean
|
|
46
|
-
|
|
60
|
+
|
|
61
|
+
/** Global style to apply when dragged */
|
|
62
|
+
externalStyles?: {
|
|
63
|
+
userSelectNone?: boolean
|
|
64
|
+
cursor?: Cursor
|
|
65
|
+
}
|
|
47
66
|
/**
|
|
48
67
|
* wheel control option
|
|
68
|
+
* If null, no event will be triggered
|
|
49
69
|
*/
|
|
50
70
|
wheel?: InputEventOption | null
|
|
51
71
|
/**
|
|
52
72
|
* keyboard control option
|
|
73
|
+
* If null, no event will be triggered
|
|
53
74
|
*/
|
|
54
75
|
keyboard?: InputEventOption | null
|
|
76
|
+
|
|
55
77
|
/**
|
|
56
78
|
* Only the appearance will change.
|
|
57
79
|
* Please consider using with readonly.
|
|
80
|
+
* aria-disabled property is also applied.
|
|
58
81
|
*/
|
|
59
82
|
disabled?: boolean
|
|
60
83
|
/**
|
|
61
84
|
* Make the value unchangeable.
|
|
85
|
+
* aria-readonly property is also applied.
|
|
62
86
|
*/
|
|
63
87
|
readonly?: boolean
|
|
64
88
|
className?: string
|
|
@@ -71,9 +95,6 @@ export interface SliderProps {
|
|
|
71
95
|
// ReactElement<typeof SliderThumb, typeof SliderTrack>[]
|
|
72
96
|
}
|
|
73
97
|
|
|
74
|
-
/**
|
|
75
|
-
* @category Slider
|
|
76
|
-
*/
|
|
77
98
|
export interface SliderMethods {
|
|
78
99
|
focus: () => void
|
|
79
100
|
blur: () => void
|
|
@@ -82,7 +103,7 @@ export interface SliderMethods {
|
|
|
82
103
|
type Props = SliderProps &
|
|
83
104
|
Omit<ComponentPropsWithoutRef<'div'>, keyof SliderProps>
|
|
84
105
|
|
|
85
|
-
const
|
|
106
|
+
const Root = forwardRef<SliderMethods, Props>(
|
|
86
107
|
(
|
|
87
108
|
{
|
|
88
109
|
value,
|
|
@@ -92,7 +113,7 @@ const SliderImpl = forwardRef<SliderMethods, Props>(
|
|
|
92
113
|
skew = 1,
|
|
93
114
|
vertical = false,
|
|
94
115
|
reverse = false,
|
|
95
|
-
|
|
116
|
+
externalStyles: _externalStyles,
|
|
96
117
|
wheel = ['raw', 1],
|
|
97
118
|
keyboard = ['raw', 1],
|
|
98
119
|
disabled = false,
|
|
@@ -116,6 +137,8 @@ const SliderImpl = forwardRef<SliderMethods, Props>(
|
|
|
116
137
|
const thumbRef = useRef<SliderThumbMethods>(null)
|
|
117
138
|
|
|
118
139
|
// --- interpret props ---
|
|
140
|
+
const externalStyles = { ...defaultExternalStyles, ..._externalStyles }
|
|
141
|
+
|
|
119
142
|
const p = toFixed(normalizeValue(value, min, max, skew) * 100)
|
|
120
143
|
const rev = toFixed(100 - p)
|
|
121
144
|
// NOTE
|
|
@@ -192,13 +215,15 @@ const SliderImpl = forwardRef<SliderMethods, Props>(
|
|
|
192
215
|
)
|
|
193
216
|
|
|
194
217
|
// --- hooks ---
|
|
195
|
-
const
|
|
218
|
+
const { refHandler: touchMoveRefCallback, pointerDownHandler } =
|
|
196
219
|
useDragWithElement<HTMLDivElement>({
|
|
197
220
|
baseElementRef: trackElementRef,
|
|
198
221
|
onDrag: onDrag,
|
|
199
222
|
onDragStart: (nx, ny) => {
|
|
200
223
|
if (readonly) return
|
|
201
|
-
if (
|
|
224
|
+
if (externalStyles.userSelectNone) addUserSelectNone()
|
|
225
|
+
if (externalStyles.cursor) setCursorStyle(externalStyles.cursor)
|
|
226
|
+
|
|
202
227
|
thumbRef.current?.focus()
|
|
203
228
|
onDragStart?.(
|
|
204
229
|
clamp(
|
|
@@ -210,7 +235,10 @@ const SliderImpl = forwardRef<SliderMethods, Props>(
|
|
|
210
235
|
},
|
|
211
236
|
onDragEnd: (nx, ny) => {
|
|
212
237
|
if (readonly) return
|
|
213
|
-
|
|
238
|
+
|
|
239
|
+
if (externalStyles.userSelectNone) removeUserSelectNone()
|
|
240
|
+
if (externalStyles.cursor) resetCursorStyle()
|
|
241
|
+
|
|
214
242
|
onDragEnd?.(
|
|
215
243
|
clamp(
|
|
216
244
|
stepValue(rawValue(vertical ? ny : nx, min, max, skew), step),
|
|
@@ -327,14 +355,14 @@ const SliderImpl = forwardRef<SliderMethods, Props>(
|
|
|
327
355
|
|
|
328
356
|
/**
|
|
329
357
|
* Customizable slider
|
|
330
|
-
* @category Slider
|
|
331
358
|
*/
|
|
332
|
-
export const Slider =
|
|
359
|
+
export const Slider = {
|
|
360
|
+
Root,
|
|
333
361
|
Thumb,
|
|
334
362
|
Track,
|
|
335
363
|
Scale,
|
|
336
364
|
ScaleOption,
|
|
337
|
-
}
|
|
365
|
+
}
|
|
338
366
|
|
|
339
367
|
export { useSliderContext } from './context'
|
|
340
368
|
export { type SliderThumbMethods, type SliderThumbProps } from './Thumb'
|
|
@@ -1,20 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ComponentProps, ElementType, ReactNode } from 'react'
|
|
2
2
|
|
|
3
3
|
import { useRefCallbackEvent } from '../../hooks/useRefCallbackEvent'
|
|
4
|
+
import { composeRefs } from '../_util/composeRefs'
|
|
5
|
+
import { Override } from '../_util/type'
|
|
4
6
|
|
|
5
|
-
/**
|
|
6
|
-
* @example
|
|
7
|
-
* <WheelObserver
|
|
8
|
-
* onWheel={(event) => {
|
|
9
|
-
* event.preventDefault()
|
|
10
|
-
* console.log(event.deltaY)
|
|
11
|
-
* }
|
|
12
|
-
* >
|
|
13
|
-
* <div>Wheel here</div>
|
|
14
|
-
* </WheelObserver>
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/** @category WheelObserver */
|
|
18
7
|
export interface WheelObserverProps<T extends ElementType> {
|
|
19
8
|
/**
|
|
20
9
|
* React.ElementType
|
|
@@ -26,12 +15,21 @@ export interface WheelObserverProps<T extends ElementType> {
|
|
|
26
15
|
onWheel?: (event: WheelEvent) => void
|
|
27
16
|
}
|
|
28
17
|
|
|
29
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* @example
|
|
20
|
+
* <WheelObserver
|
|
21
|
+
* onWheel={(event) => {
|
|
22
|
+
* event.preventDefault()
|
|
23
|
+
* console.log(event.deltaY)
|
|
24
|
+
* }
|
|
25
|
+
* >
|
|
26
|
+
* <div>Wheel here</div>
|
|
27
|
+
* </WheelObserver>
|
|
28
|
+
*/
|
|
30
29
|
export function WheelObserver<T extends ElementType = 'div'>(
|
|
31
|
-
props: WheelObserverProps<T
|
|
32
|
-
Omit<ComponentPropsWithoutRef<T>, keyof WheelObserverProps<T>>,
|
|
30
|
+
props: Override<WheelObserverProps<T>, ComponentProps<T>>,
|
|
33
31
|
) {
|
|
34
|
-
const {
|
|
32
|
+
const { as: Component = 'div', children, onWheel, ref, ...attributes } = props
|
|
35
33
|
|
|
36
34
|
const wheelRefCallback = useRefCallbackEvent(
|
|
37
35
|
'wheel',
|
|
@@ -43,7 +41,7 @@ export function WheelObserver<T extends ElementType = 'div'>(
|
|
|
43
41
|
)
|
|
44
42
|
|
|
45
43
|
return (
|
|
46
|
-
<Component ref={wheelRefCallback} {...attributes}>
|
|
44
|
+
<Component ref={composeRefs(ref, wheelRefCallback)} {...attributes}>
|
|
47
45
|
{children}
|
|
48
46
|
</Component>
|
|
49
47
|
)
|