@tremolo-ui/react 0.0.12 → 0.1.0-alpha.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 (57) hide show
  1. package/dist/cjs/index.js +316 -232
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/cjs/types/components/Knob/index.d.ts +12 -0
  4. package/dist/cjs/types/components/Knob/index.d.ts.map +1 -1
  5. package/dist/cjs/types/components/Piano/KeyLabel.d.ts +1 -3
  6. package/dist/cjs/types/components/Piano/KeyLabel.d.ts.map +1 -1
  7. package/dist/cjs/types/components/Piano/context.d.ts +24 -0
  8. package/dist/cjs/types/components/Piano/context.d.ts.map +1 -0
  9. package/dist/cjs/types/components/Piano/index.d.ts +9 -9
  10. package/dist/cjs/types/components/Piano/index.d.ts.map +1 -1
  11. package/dist/cjs/types/components/Piano/key.d.ts +10 -19
  12. package/dist/cjs/types/components/Piano/key.d.ts.map +1 -1
  13. package/dist/cjs/types/components/Slider/index.d.ts.map +1 -1
  14. package/dist/cjs/types/components/XYPad/index.d.ts.map +1 -1
  15. package/dist/cjs/types/hooks/useDrag.d.ts.map +1 -1
  16. package/dist/cjs/types/hooks/useDragWithElement.d.ts +16 -0
  17. package/dist/cjs/types/hooks/useDragWithElement.d.ts.map +1 -0
  18. package/dist/esm/index.js +316 -233
  19. package/dist/esm/index.js.map +1 -1
  20. package/dist/esm/types/components/Knob/index.d.ts +12 -0
  21. package/dist/esm/types/components/Knob/index.d.ts.map +1 -1
  22. package/dist/esm/types/components/Piano/KeyLabel.d.ts +1 -3
  23. package/dist/esm/types/components/Piano/KeyLabel.d.ts.map +1 -1
  24. package/dist/esm/types/components/Piano/context.d.ts +24 -0
  25. package/dist/esm/types/components/Piano/context.d.ts.map +1 -0
  26. package/dist/esm/types/components/Piano/index.d.ts +9 -9
  27. package/dist/esm/types/components/Piano/index.d.ts.map +1 -1
  28. package/dist/esm/types/components/Piano/key.d.ts +10 -19
  29. package/dist/esm/types/components/Piano/key.d.ts.map +1 -1
  30. package/dist/esm/types/components/Slider/index.d.ts.map +1 -1
  31. package/dist/esm/types/components/XYPad/index.d.ts.map +1 -1
  32. package/dist/esm/types/hooks/useDrag.d.ts.map +1 -1
  33. package/dist/esm/types/hooks/useDragWithElement.d.ts +16 -0
  34. package/dist/esm/types/hooks/useDragWithElement.d.ts.map +1 -0
  35. package/dist/styles/index.css +2 -0
  36. package/package.json +2 -1
  37. package/src/components/Knob/index.tsx +14 -0
  38. package/src/components/Piano/KeyLabel.tsx +9 -5
  39. package/src/components/Piano/context.tsx +108 -0
  40. package/src/components/Piano/index.css +2 -0
  41. package/src/components/Piano/index.tsx +186 -114
  42. package/src/components/Piano/key.tsx +215 -0
  43. package/src/components/Slider/index.tsx +40 -52
  44. package/src/components/XYPad/index.tsx +64 -54
  45. package/src/hooks/useDrag.ts +6 -7
  46. package/src/hooks/useDragWithElement.ts +76 -0
  47. package/dist/cjs/types/components/Piano/BlackKey.d.ts +0 -5
  48. package/dist/cjs/types/components/Piano/BlackKey.d.ts.map +0 -1
  49. package/dist/cjs/types/components/Piano/WhiteKey.d.ts +0 -7
  50. package/dist/cjs/types/components/Piano/WhiteKey.d.ts.map +0 -1
  51. package/dist/esm/types/components/Piano/BlackKey.d.ts +0 -5
  52. package/dist/esm/types/components/Piano/BlackKey.d.ts.map +0 -1
  53. package/dist/esm/types/components/Piano/WhiteKey.d.ts +0 -7
  54. package/dist/esm/types/components/Piano/WhiteKey.d.ts.map +0 -1
  55. package/src/components/Piano/BlackKey.tsx +0 -132
  56. package/src/components/Piano/WhiteKey.tsx +0 -134
  57. package/src/components/Piano/key.ts +0 -51
@@ -6,19 +6,33 @@ import React, {
6
6
  ReactElement,
7
7
  ReactNode,
8
8
  RefObject,
9
+ useCallback,
9
10
  useEffect,
11
+ useMemo,
10
12
  useRef,
11
13
  useState,
12
14
  } from 'react'
13
15
 
14
- import { isWhiteKey, NoteKey, noteKeys, noteKey } from '@tremolo-ui/functions'
16
+ import {
17
+ isBlackKey,
18
+ isWhiteKey,
19
+ noteKey,
20
+ NoteKey,
21
+ noteKeys,
22
+ } from '@tremolo-ui/functions'
15
23
 
24
+ import { useDragWithElement } from '../../hooks/useDragWithElement'
16
25
  import { useEventListener } from '../../hooks/useEventListener'
17
26
 
18
- import { BlackKey } from './BlackKey'
19
- import { KeyMethods, KeyProps } from './key'
27
+ import { NoteRange, PianoProvider } from './context'
28
+ import {
29
+ BlackKey,
30
+ defaultBlackKeyWidth,
31
+ defaultWhiteKeyWidth,
32
+ KeyMethods,
33
+ WhiteKey,
34
+ } from './key'
20
35
  import { KeyboardShortcuts } from './keyboardShortcuts'
21
- import { defaultWhiteKeyWidth, WhiteKey } from './WhiteKey'
22
36
 
23
37
  /**
24
38
  * Piano component
@@ -27,18 +41,27 @@ import { defaultWhiteKeyWidth, WhiteKey } from './WhiteKey'
27
41
  * - add scale highlight
28
42
  */
29
43
 
44
+ /** @category Piano */
45
+ export function getNoteRangeArray(noteRange: NoteRange) {
46
+ return Array.from(
47
+ { length: noteRange.last - noteRange.first + 1 },
48
+ (_, i) => i + noteRange.first,
49
+ )
50
+ }
51
+
30
52
  /** @category Piano */
31
53
  export interface PianoProps {
32
54
  // required
33
- noteRange: { first: number; last: number }
55
+ noteRange: NoteRange
34
56
 
35
57
  // optional
36
- height?: number | string
37
-
38
58
  glissando?: boolean
39
59
  midiMax?: number
40
60
  keyboardShortcuts?: KeyboardShortcuts
41
61
  fill?: boolean
62
+ whiteNoteWidth?: number
63
+ blackNoteWidth?: number
64
+ height?: number | string
42
65
 
43
66
  style?: CSSProperties
44
67
 
@@ -64,76 +87,130 @@ export function Piano({
64
87
  keyboardShortcuts,
65
88
  fill = false,
66
89
  height = fill ? '100%' : 160,
90
+ whiteNoteWidth: _whiteNoteWidth = defaultWhiteKeyWidth,
67
91
  style,
68
92
  className,
69
93
  onPlayNote,
70
94
  onStopNote,
71
95
  label,
72
96
  children,
73
- onPointerUp,
74
97
  onPointerDown,
75
98
  ...props
76
99
  }: PianoProps & Omit<ComponentPropsWithoutRef<'div'>, keyof PianoProps>) {
77
100
  // -- state and ref ---
78
- const [pressed, setPressed] = useState(false)
79
- const [whiteNoteWidth, setWhiteNoteWidth] = useState(-1)
101
+ const [whiteNoteWidth, setWhiteNoteWidth] = useState(_whiteNoteWidth)
80
102
  const keyRefs = useRef<RefObject<KeyMethods | null>[]>([])
81
103
  for (let i = 0; i < noteRange.last - noteRange.first + 1; i++) {
82
104
  keyRefs.current[i] = createRef<KeyMethods>()
83
105
  }
84
106
  const pianoRef = useRef<HTMLDivElement>(null)
107
+ const hitKeyIndex = useRef(-1)
85
108
 
86
109
  // --- interpret props ---
87
- const noteRangeArray = Array.from(
88
- { length: noteRange.last - noteRange.first + 1 },
89
- (_, i) => i + noteRange.first,
110
+ const noteRangeArray = getNoteRangeArray(noteRange)
111
+ const whiteNotes = noteRangeArray.filter((v) => isWhiteKey(v))
112
+ const blackNotes = noteRangeArray.filter((v) => isBlackKey(v))
113
+ const whiteNoteCount = whiteNotes.length
114
+ const padding = 1
115
+ const staticWidth = (whiteNoteWidth + padding) * whiteNoteCount
116
+ // const blackNoteShiftPercent = 0.3
117
+
118
+ // TODO: if fill setWhiteNoteWidth
119
+ // useMemo
120
+ // __width
121
+ const childrenWithProps = useMemo(() => {
122
+ return (
123
+ children &&
124
+ React.Children.map(children, (child, index) => {
125
+ if (React.isValidElement(child)) {
126
+ const props = { ref: keyRefs.current[index] }
127
+ return React.cloneElement(child, props)
128
+ }
129
+ return child
130
+ })
131
+ )
132
+ }, [children])
133
+
134
+ // --- internal functions ---
135
+ const notePosition = useCallback(
136
+ (note: number) => {
137
+ const pitchPositions: Record<NoteKey, number> = {
138
+ C: 0,
139
+ 'C#': 1,
140
+ D: 1,
141
+ 'D#': 2,
142
+ E: 2,
143
+ F: 3,
144
+ 'F#': 4,
145
+ G: 4,
146
+ 'G#': 5,
147
+ A: 5,
148
+ 'A#': 6,
149
+ B: 6,
150
+ }
151
+
152
+ const whiteNoteWidth = defaultWhiteKeyWidth
153
+ const blackNoteWidth = defaultBlackKeyWidth
154
+ const padding = 1
155
+
156
+ const targetNoteKey = noteKey(note)
157
+ const firstNoteKey = noteKey(noteRange.first)
158
+ const octave = Math.floor((note - noteRange.first) / 12)
159
+ const octaveOffset =
160
+ noteKeys.indexOf(firstNoteKey) > noteKeys.indexOf(targetNoteKey) ? 1 : 0
161
+ const w = whiteNoteWidth + padding
162
+ const pos = pitchPositions[targetNoteKey] - pitchPositions[firstNoteKey]
163
+ const blackKeyOffset = isBlackKey(note) ? blackNoteWidth / 2 : 0
164
+ return pos * w + (octave + octaveOffset) * 7 * w - blackKeyOffset
165
+ },
166
+ [noteRange.first],
90
167
  )
91
- const whiteNoteCount = noteRangeArray.filter((v) => isWhiteKey(v)).length
92
-
93
- let whiteKeyProps: KeyProps = {},
94
- blackKeyProps: KeyProps = {}
95
- if (children != undefined) {
96
- React.Children.forEach(children, (child) => {
97
- if (React.isValidElement(child)) {
98
- if (child.type == WhiteKey) {
99
- whiteKeyProps = child.props as KeyProps
100
- } else if (child.type == BlackKey) {
101
- blackKeyProps = child.props as KeyProps
102
- } else {
103
- throw new Error('only <WhiteKey> or <BlackKey>')
168
+
169
+ const getHitKeyIndex = useCallback(
170
+ (x: number, y: number) => {
171
+ if (!pianoRef.current) return -1
172
+ const containerHeight = pianoRef.current.clientHeight
173
+ const notes = [...blackNotes, ...whiteNotes]
174
+ for (let i = 0; i < notes.length; i++) {
175
+ const note = notes[i]
176
+ const pos = notePosition(note)
177
+ const w = isWhiteKey(note) ? defaultWhiteKeyWidth : defaultBlackKeyWidth
178
+ const h = isWhiteKey(note) ? containerHeight : containerHeight * 0.6
179
+ if (pos <= x && x < pos + w && 0 <= y && y < h) {
180
+ return note
104
181
  }
105
- } else {
106
- throw new Error('children is an invalid element.')
107
182
  }
108
- })
109
- }
183
+ return -1
184
+ },
185
+ [blackNotes, notePosition, whiteNotes],
186
+ )
110
187
 
111
- // --- internal functions ---
112
- function notePosition(note: number) {
113
- const pitchPositions: Record<NoteKey, number> = {
114
- C: 0,
115
- 'C#': 0.55,
116
- D: 1,
117
- 'D#': 1.8,
118
- E: 2,
119
- F: 3,
120
- 'F#': 3.5,
121
- G: 4,
122
- 'G#': 4.7,
123
- A: 5,
124
- 'A#': 5.85,
125
- B: 6,
126
- }
188
+ // TODO: 単一のポインターに対しては、useDragで対応可能だが、
189
+ // マルチタッチに対しては、TouchEventを使う必要がありそう
190
+ // 取り敢えず、シングルタッチだけ対応
191
+ const onDrag = useCallback(
192
+ (perX: number, perY: number) => {
193
+ if (!pianoRef.current) return
194
+ const x = perX * staticWidth
195
+ const y = perY * pianoRef.current.clientHeight
196
+ const note = getHitKeyIndex(x, y)
197
+ const index = noteRangeArray.indexOf(note)
198
+ if (index == -1) return
199
+ if (hitKeyIndex.current != index) {
200
+ keyRefs.current[hitKeyIndex.current]?.current?.stop()
201
+ keyRefs.current[index]?.current?.play()
202
+ hitKeyIndex.current = index
203
+ }
204
+ },
205
+ [getHitKeyIndex, noteRangeArray, staticWidth],
206
+ )
127
207
 
128
- const targetNoteKey = noteKey(note)
129
- const firstNoteKey = noteKey(noteRange.first)
130
- const pos = pitchPositions[targetNoteKey] - pitchPositions[firstNoteKey]
131
- const octave = Math.floor((note - noteRange.first) / 12)
132
- const octaveOffset =
133
- noteKeys.indexOf(firstNoteKey) > noteKeys.indexOf(targetNoteKey) ? 1 : 0
134
- const length = whiteNoteWidth + 3
135
- return pos * length + (octave + octaveOffset) * 7 * length
136
- }
208
+ const onDragEnd = useCallback(() => {
209
+ if (keyRefs.current[hitKeyIndex.current]?.current?.played()) {
210
+ keyRefs.current[hitKeyIndex.current]?.current?.stop()
211
+ }
212
+ hitKeyIndex.current = -1
213
+ }, [keyRefs])
137
214
 
138
215
  // --- hooks ---
139
216
  useEffect(() => {
@@ -142,16 +219,23 @@ export function Piano({
142
219
  if (!parent) throw new Error("doesn't have a parent element.")
143
220
  const resizeObserver = new ResizeObserver(() => {
144
221
  const w = pianoRef.current!.clientWidth
145
- setWhiteNoteWidth(w / whiteNoteCount - 3)
222
+ setWhiteNoteWidth(w / whiteNoteCount - padding)
146
223
  })
147
224
  resizeObserver.observe(parent)
148
225
  return () => {
149
226
  resizeObserver.unobserve(parent)
150
227
  }
151
228
  } else {
152
- setWhiteNoteWidth(whiteKeyProps?.width || defaultWhiteKeyWidth)
229
+ setWhiteNoteWidth(_whiteNoteWidth)
153
230
  }
154
- }, [fill, whiteKeyProps?.width, whiteNoteCount])
231
+ }, [fill, _whiteNoteWidth, whiteNoteCount])
232
+
233
+ const [touchMoveRefCallback, pointerDownHandler] =
234
+ useDragWithElement<HTMLDivElement>({
235
+ baseElementRef: pianoRef,
236
+ onDrag: onDrag,
237
+ onDragEnd: onDragEnd,
238
+ })
155
239
 
156
240
  useEventListener(globalThis.window, 'keydown', (e) => {
157
241
  if (e.repeat) return
@@ -174,66 +258,54 @@ export function Piano({
174
258
  })
175
259
 
176
260
  return (
177
- <div
178
- ref={pianoRef}
179
- className={clsx('tremolo-piano', className)}
180
- style={{
181
- width: fill ? '100%' : (whiteNoteWidth + 3) * whiteNoteCount,
182
- height: height,
183
- ...style,
184
- }}
185
- onPointerDown={(e) => {
186
- setPressed(true)
187
- onPointerDown?.(e)
188
- }}
189
- onPointerUp={(e) => {
190
- setPressed(false)
191
- onPointerUp?.(e)
192
- }}
193
- {...props}
261
+ <PianoProvider
262
+ noteRange={noteRange}
263
+ glissando={glissando}
264
+ midiMax={midiMax}
265
+ fill={fill}
266
+ onPlayNote={onPlayNote}
267
+ onStopNote={onStopNote}
268
+ label={label}
194
269
  >
195
- {noteRangeArray.map((note, index) => {
196
- return isWhiteKey(note) ? (
197
- <WhiteKey
198
- key={note}
199
- ref={keyRefs.current[index]}
200
- __glissando={glissando && pressed}
201
- __position={notePosition(note)}
202
- __note={note}
203
- __disabled={note > midiMax}
204
- __index={index}
205
- __fill={fill}
206
- __width={whiteNoteWidth}
207
- __onPlayNote={onPlayNote}
208
- __onStopNote={onStopNote}
209
- __label={label}
210
- {...whiteKeyProps}
211
- />
212
- ) : (
213
- <BlackKey
214
- key={note}
215
- ref={keyRefs.current[index]}
216
- __glissando={glissando && pressed}
217
- __position={notePosition(note)}
218
- __note={note}
219
- __disabled={note > midiMax}
220
- __index={index}
221
- __fill={fill}
222
- __width={whiteNoteWidth * 0.65}
223
- __onPlayNote={onPlayNote}
224
- __onStopNote={onStopNote}
225
- __label={label}
226
- {...blackKeyProps}
227
- />
228
- )
229
- })}
230
- </div>
270
+ <div
271
+ ref={(div) => {
272
+ pianoRef.current = div
273
+ touchMoveRefCallback(div)
274
+ }}
275
+ className={clsx('tremolo-piano', className)}
276
+ style={{
277
+ width: fill ? '100%' : staticWidth,
278
+ height: height,
279
+ ...style,
280
+ }}
281
+ onPointerDown={(event) => {
282
+ pointerDownHandler(event)
283
+ onPointerDown?.(event)
284
+ }}
285
+ {...props}
286
+ >
287
+ {childrenWithProps ||
288
+ getNoteRangeArray(noteRange).map((note, index) =>
289
+ isWhiteKey(note) ? (
290
+ <WhiteKey
291
+ ref={keyRefs.current[index]}
292
+ key={note}
293
+ noteNumber={note}
294
+ />
295
+ ) : (
296
+ <BlackKey
297
+ ref={keyRefs.current[index]}
298
+ key={note}
299
+ noteNumber={note}
300
+ />
301
+ ),
302
+ )}
303
+ </div>
304
+ </PianoProvider>
231
305
  )
232
306
  }
233
307
 
234
- export * from './key'
235
308
  export * from './keyboardShortcuts'
236
309
 
237
- export { WhiteKey } from './WhiteKey'
238
- export { BlackKey } from './BlackKey'
310
+ export { WhiteKey, BlackKey, type KeyProps, type KeyMethods } from './key'
239
311
  export { KeyLabel, type KeyLabelProps } from './KeyLabel'
@@ -0,0 +1,215 @@
1
+ import clsx from 'clsx'
2
+ import React, {
3
+ CSSProperties,
4
+ ComponentPropsWithoutRef,
5
+ forwardRef,
6
+ useImperativeHandle,
7
+ useState,
8
+ ReactElement,
9
+ } from 'react'
10
+
11
+ import { usePianoContext } from './context'
12
+ import { KeyLabel, KeyLabelProps } from './KeyLabel'
13
+
14
+ /**
15
+ * @category Piano
16
+ */
17
+ export interface KeyProps {
18
+ noteNumber: number
19
+
20
+ width?: number
21
+ height?: number | `${number}%`
22
+
23
+ bg?: string
24
+ color?: string
25
+ activeBg?: string
26
+ activeColor?: string
27
+
28
+ style?: CSSProperties
29
+
30
+ /**
31
+ * \<KeyLabel />
32
+ */
33
+ children?: ReactElement
34
+
35
+ /** @internal */
36
+ __width?: number
37
+ }
38
+
39
+ /**
40
+ * @category Piano
41
+ */
42
+ export interface KeyMethods {
43
+ play: () => void
44
+ stop: () => void
45
+ played: () => boolean
46
+ }
47
+
48
+ type Props = KeyProps & Omit<ComponentPropsWithoutRef<'div'>, keyof KeyProps>
49
+ type ImplProps = Props & { keyType: 'black' | 'white' }
50
+
51
+ /** @category Piano */
52
+ export const defaultWhiteKeyWidth = 40
53
+ /** @category Piano */
54
+ export const defaultBlackKeyWidth = 26
55
+
56
+ const KeyImpl = forwardRef<KeyMethods, ImplProps>(
57
+ (
58
+ {
59
+ keyType,
60
+ noteNumber,
61
+ width,
62
+ height,
63
+ bg,
64
+ color,
65
+ activeBg,
66
+ activeColor,
67
+ className,
68
+ style,
69
+ children,
70
+ __width,
71
+ ...props
72
+ }: ImplProps,
73
+ ref,
74
+ ) => {
75
+ const [played, setPlayed] = useState(false)
76
+ const midiMax = usePianoContext((s) => s.midiMax)
77
+ const fill = usePianoContext((s) => s.fill)
78
+ // const glissando = usePianoContext(s => s.glissando)
79
+ const onPlayNote = usePianoContext((s) => s.onPlayNote)
80
+ const onStopNote = usePianoContext((s) => s.onStopNote)
81
+ const label = usePianoContext((s) => s.label)
82
+ const notePosition = usePianoContext((s) => s.notePosition)
83
+ const position = notePosition(noteNumber)
84
+
85
+ const disabled = noteNumber > midiMax
86
+ const colors = {
87
+ '--bg': bg,
88
+ '--color': color,
89
+ '--active-bg': activeBg,
90
+ '--active-color': activeColor,
91
+ }
92
+
93
+ useImperativeHandle(ref, () => {
94
+ return {
95
+ play() {
96
+ if (disabled) return
97
+ setPlayed(true)
98
+ if (onPlayNote) onPlayNote(noteNumber)
99
+ },
100
+ stop() {
101
+ setPlayed(false)
102
+ if (onStopNote) onStopNote(noteNumber)
103
+ },
104
+ played() {
105
+ return played
106
+ },
107
+ }
108
+ }, [disabled, noteNumber, onPlayNote, onStopNote, played])
109
+
110
+ let keyLabelProps: KeyLabelProps = {}
111
+ if (children != undefined) {
112
+ React.Children.forEach(children, (child) => {
113
+ if (React.isValidElement(child)) {
114
+ if (child.type == KeyLabel) {
115
+ keyLabelProps = child.props as KeyLabelProps
116
+ } else {
117
+ throw new Error('only <KeyLabel>')
118
+ }
119
+ } else {
120
+ throw new Error('children is an invalid element.')
121
+ }
122
+ })
123
+ }
124
+
125
+ return (
126
+ <div
127
+ className={clsx(`tremolo-piano-${keyType}-key`, className)}
128
+ aria-disabled={disabled}
129
+ data-active={played}
130
+ style={{
131
+ ...colors,
132
+ left: position,
133
+ width: fill ? __width : width,
134
+ height: height,
135
+ ...style,
136
+ }}
137
+ // onPointerDown={() => {
138
+ // if (disabled) return
139
+ // setPlayed(true)
140
+ // if (onPlayNote) onPlayNote(noteNumber)
141
+ // }}
142
+ // onPointerEnter={() => {
143
+ // if (disabled) return
144
+ // if (glissando) {
145
+ // setPlayed(true)
146
+ // if (onPlayNote) onPlayNote(noteNumber)
147
+ // }
148
+ // }}
149
+ // onPointerUp={() => {
150
+ // if (disabled) return
151
+ // setPlayed(false)
152
+ // if (onStopNote) onStopNote(noteNumber)
153
+ // }}
154
+ // onPointerLeave={() => {
155
+ // if (disabled) return
156
+ // if (glissando) {
157
+ // setPlayed(false)
158
+ // if (onStopNote) onStopNote(noteNumber)
159
+ // }
160
+ // }}
161
+ {...props}
162
+ >
163
+ <KeyLabel label={label} __note={noteNumber} {...keyLabelProps} />
164
+ </div>
165
+ )
166
+ },
167
+ )
168
+
169
+ /** @category Piano */
170
+ export const WhiteKey = forwardRef<KeyMethods, Props>(
171
+ (
172
+ {
173
+ width = defaultWhiteKeyWidth,
174
+ height = '100%',
175
+ onContextMenu = (e) => e.preventDefault(),
176
+ ...props
177
+ }: Props,
178
+ ref,
179
+ ) => {
180
+ return (
181
+ <KeyImpl
182
+ ref={ref}
183
+ keyType="white"
184
+ width={width}
185
+ height={height}
186
+ onContextMenu={onContextMenu}
187
+ {...props}
188
+ />
189
+ )
190
+ },
191
+ )
192
+
193
+ /** @category Piano */
194
+ export const BlackKey = forwardRef<KeyMethods, Props>(
195
+ (
196
+ {
197
+ width = defaultBlackKeyWidth,
198
+ height = '60%',
199
+ onContextMenu = (e) => e.preventDefault(),
200
+ ...props
201
+ }: Props,
202
+ ref,
203
+ ) => {
204
+ return (
205
+ <KeyImpl
206
+ ref={ref}
207
+ keyType="black"
208
+ width={width}
209
+ height={height}
210
+ onContextMenu={onContextMenu}
211
+ {...props}
212
+ />
213
+ )
214
+ },
215
+ )