diff --git a/README.md b/README.md
index 6dbf601..9b40f5c 100644
--- a/README.md
+++ b/README.md
@@ -43,6 +43,8 @@ window.document.body.appendChild(container)
ReactDOM.render( , container)
```
+### Interactivity
+
The library also supports keyboard maps for handling mouse, touch, and keyboard events:
```jsx harmony
@@ -60,12 +62,8 @@ const App = () => {
-
-
+ onChange={handleKeysChange}
+ />
)
}
@@ -77,6 +75,10 @@ window.document.body.appendChild(container)
ReactDOM.render( , container)
```
+It is capable of server-side rendering support, falling back to making the keys behave like links, checkboxes or radio buttons. Simply supply the `behavior` prop.
+
+### Customization
+
The component is stylable, just supply custom components for the keys:
```jsx harmony
@@ -108,7 +110,9 @@ window.document.body.appendChild(container)
ReactDOM.render( , container)
```
-Custom keys should accept a `keyChannels` prop for active keys. For instance, in the custom key components imported above:
+Components get their styles from CSS. The custom property `--opacity-highlight` is responsible for toggling the active, or "pressed" state of the key, simply assign it to the `opacity` style of the component you want to show for active keys.
+
+The library also exposes other custom properties: `--color-natural-key`, `--color-accidental-key`, and `--color-active-key` for basic coloring of the keys. You may expose your own properties for your custom key components.
```jsx harmony
// ./my-styled-keys/NaturalKey.js
@@ -121,11 +125,9 @@ const NaturalKey = ({
keyChannels = []
}) => {
return (
-
-
- {keyChannels.map(k => (
-
- ))}
+
+
+
)
}
diff --git a/example/controllers/Channel.ts b/example/controllers/Channel.ts
index 8b06c56..246d484 100644
--- a/example/controllers/Channel.ts
+++ b/example/controllers/Channel.ts
@@ -24,9 +24,10 @@ type KeyChannelCallback = (oldKeys: KeyChannel[]) => KeyChannel[]
type HandleProps = {
setKeyChannels(callback: KeyChannelCallback | KeyChannel[]): void,
generator?: SoundGenerator,
+ channel: number,
}
type Handle = (props: HandleProps) => (newKeys: KeyChannel[]) => void
-export const handle: Handle = ({ setKeyChannels, generator, }) => newKeys => {
+export const handle: Handle = ({ setKeyChannels, generator, channel, }) => newKeys => {
setKeyChannels((oldKeys) => {
if (generator! !== undefined) {
const oldKeysKeys = oldKeys.map((k) => k.key)
@@ -35,11 +36,11 @@ export const handle: Handle = ({ setKeyChannels, generator, }) => newKeys => {
const keysOn = newKeys.filter((nk) => !oldKeysKeys.includes(nk.key))
keysOn.forEach((k) => {
- generator.noteOn(k.channel, k.key, Math.floor(k.velocity * 127))
+ generator.noteOn(channel, k.key, Math.floor(k.velocity * 127))
})
keysOff.forEach((k) => {
- generator.noteOff(k.channel, k.key, Math.floor(k.velocity * 127))
+ generator.noteOff(channel, k.key, Math.floor(k.velocity * 127))
})
}
diff --git a/example/index.tsx b/example/index.tsx
index dbb1f0f..f052df9 100644
--- a/example/index.tsx
+++ b/example/index.tsx
@@ -1,7 +1,7 @@
import * as React from 'react'
import ReactDOM from 'react-dom'
-import Keyboard, { KeyboardMap } from '../src'
+import Keyboard from '../src'
import * as Channel from './controllers/Channel'
import * as Instrument from './controllers/Instrument'
import * as Generator from './controllers/Generator'
@@ -75,13 +75,9 @@ const App = () => {
endKey={127}
keyChannels={keyChannels}
height="100%"
- >
-
-
+ onChange={Channel.handle({ setKeyChannels, generator: generator.current!, channel, })}
+ keyboardMapping={keyboardMapping}
+ />
diff --git a/package.json b/package.json
index a2dc3e3..3312c0e 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,5 @@
{
- "version": "1.0.13",
+ "version": "1.1.0",
"license": "MIT",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
diff --git a/src/components/AccidentalKey/AccidentalKey.tsx b/src/components/AccidentalKey/AccidentalKey.tsx
index fc59ca2..3a66835 100644
--- a/src/components/AccidentalKey/AccidentalKey.tsx
+++ b/src/components/AccidentalKey/AccidentalKey.tsx
@@ -1,38 +1,28 @@
import * as React from 'react'
-import * as PropTypes from 'prop-types'
-import keyPropTypes from '../../services/keyPropTypes'
-type Props = PropTypes.InferProps
-
-const AccidentalKey: React.FC = ({ keyChannels }) => (
+const AccidentalKey: React.FC = () => (
- {Array.isArray(keyChannels!) &&
- keyChannels.map((c) => (
-
- ))}
+
)
-AccidentalKey.propTypes = keyPropTypes
-
export default AccidentalKey
diff --git a/src/components/Keyboard/Keyboard.stories.tsx b/src/components/Keyboard/Keyboard.stories.tsx
index 9428649..9c2bf99 100644
--- a/src/components/Keyboard/Keyboard.stories.tsx
+++ b/src/components/Keyboard/Keyboard.stories.tsx
@@ -3,7 +3,6 @@ import * as PropTypes from 'prop-types'
import StyledAccidentalKey from '../StyledAccidentalKey/StyledAccidentalKey'
import StyledNaturalKey from '../StyledNaturalKey/StyledNaturalKey'
import Keyboard, { propTypes } from './Keyboard'
-import KeyboardMap from '../KeyboardMap/KeyboardMap'
const Wrapper: React.FC = (props) => (
) => (
endKey={108}
keyChannels={[
{
- channel: 0,
key: 60,
velocity: 1,
},
{
- channel: 0,
key: 64,
velocity: 1,
},
{
- channel: 0,
key: 67,
velocity: 1,
},
@@ -78,34 +74,39 @@ export const WithDifferentKeyRange = (props?: Partial
) => (
)
export const Styled = (props?: Partial) => (
-
-
-
+
+
+
+
+
)
export const AnotherStyled = (props?: Partial) => (
@@ -113,6 +114,46 @@ export const AnotherStyled = (props?: Partial) => (
style={{
// @ts-ignore
'--size-scale-factor': 2,
+ '--color-accidental-key': '#35313b',
+ '--color-natural-key': '#e3e3e5',
+ }}
+ >
+
+
+
+
+)
+
+export const DarkStyled = (props?: Partial) => (
+
@@ -122,17 +163,14 @@ export const AnotherStyled = (props?: Partial) => (
endKey={108}
keyChannels={[
{
- channel: 0,
key: 60,
velocity: 1,
},
{
- channel: 0,
key: 63,
velocity: 1,
},
{
- channel: 0,
key: 67,
velocity: 1,
},
@@ -156,13 +194,14 @@ const HasMapComponent = () => {
const newKeysKeys = newKeys.map((k) => k.key)
const keysOff = oldKeys.filter((ok) => !newKeysKeys.includes(ok.key))
const keysOn = newKeys.filter((nk) => !oldKeysKeys.includes(nk.key))
+ const channel = 0
keysOn.forEach((k) => {
- midiAccess.current?.send([0b10010000 + k.channel, k.key, Math.floor(k.velocity * 127)])
+ midiAccess.current?.send([0b10010000 + channel, k.key, Math.floor(k.velocity * 127)])
})
keysOff.forEach((k) => {
- midiAccess.current?.send([0b10000000 + k.channel, k.key, Math.floor(k.velocity * 127)])
+ midiAccess.current?.send([0b10000000 + channel, k.key, Math.floor(k.velocity * 127)])
})
return newKeys
@@ -183,54 +222,73 @@ const HasMapComponent = () => {
return (
-
-
-
+
)
}
export const HasMap = () =>
+
+export const Checkbox = (props?: Partial) => (
+
+
+
+)
+
+export const Radio = (props?: Partial) => (
+
+
+
+)
+
+export const Link = (props?: Partial) => (
+
+ `?key=${key}`} />
+
+)
diff --git a/src/components/Keyboard/Keyboard.tsx b/src/components/Keyboard/Keyboard.tsx
index dec3b47..738734c 100644
--- a/src/components/Keyboard/Keyboard.tsx
+++ b/src/components/Keyboard/Keyboard.tsx
@@ -6,6 +6,10 @@ import getKeyLeftUnmemoized from '../../services/getKeyLeft'
import generateKeys from '../../services/generateKeys'
import DefaultAccidentalKey from '../AccidentalKey/AccidentalKey'
import DefaultNaturalKey from '../NaturalKey/NaturalKey'
+import KeyboardMap from '../KeyboardMap/KeyboardMap'
+import getKeyBounds from '../../services/getKeyBounds'
+
+const BEHAVIOR = ['link', 'checkbox', 'radio'] as const
export const propTypes = {
/**
@@ -35,7 +39,6 @@ export const propTypes = {
*/
keyChannels: PropTypes.arrayOf(
PropTypes.shape({
- channel: PropTypes.number.isRequired,
key: PropTypes.number.isRequired,
velocity: PropTypes.number.isRequired,
}),
@@ -58,6 +61,26 @@ export const propTypes = {
* Height of the component.
*/
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ /**
+ * Event handler triggered upon change in activated keys in the component.
+ */
+ onChange: PropTypes.func,
+ /**
+ * Map from key code to key number.
+ */
+ keyboardMapping: PropTypes.object,
+ /**
+ * Behavior of the component when clicking.
+ */
+ behavior: PropTypes.oneOf(BEHAVIOR),
+ /**
+ * Name of the component used for forms.
+ */
+ name: PropTypes.string,
+ /**
+ * Destination of the component upon clicking a key, if behavior is set to 'link'.
+ */
+ href: PropTypes.func,
}
type Props = PropTypes.InferProps
@@ -72,6 +95,9 @@ type Props = PropTypes.InferProps
* @param width - Width of the component.
* @param keyComponents - Components to use for each kind of key.
* @param height - Height of the component.
+ * @param name - Name of the component used for forms.
+ * @param href - Destination of the component upon clicking a key, if behavior is set to 'link'.
+ * @param behavior - Behavior of the component when clicking.
*/
const Keyboard: React.FC = ({
startKey,
@@ -82,7 +108,11 @@ const Keyboard: React.FC = ({
width = '100%',
keyComponents = {},
height = 80,
- children,
+ onChange,
+ keyboardMapping,
+ behavior,
+ name,
+ href,
}) => {
const [clientSide, setClientSide] = React.useState(false)
const [clientSideKeys, setClientSideKeys] = React.useState([])
@@ -105,89 +135,100 @@ const Keyboard: React.FC = ({
const keys = clientSide ? clientSideKeys : generateKeys(startKey, endKey)
return (
-
- {keys.map((key) => {
- const isNatural = isNaturalKey(key)
- const Component: any = isNatural ? NaturalKey! : AccidentalKey!
- const currentKeyChannels = Array.isArray(keyChannels!) ? keyChannels.filter((kc) => kc!.key === key) : null
-
- const width = getKeyWidth(key)
- const left = getKeyLeft(key)
-
- let leftBounds: number
- let rightBounds: number
-
- switch (key % 12) {
- case 0:
- case 5:
- leftBounds = left
- rightBounds = key + 1 > endKey! ? left + width : getKeyLeft(key + 1)
- break
- case 4:
- case 11:
- leftBounds = key - 1 < startKey! ? left : getKeyLeft(key - 1) + getKeyWidth(key - 1)
- rightBounds = left + width
- break
- case 2:
- case 7:
- case 9:
- leftBounds = key - 1 < startKey! ? left : getKeyLeft(key - 1) + getKeyWidth(key - 1)
- rightBounds = key + 1 > endKey! ? left + width : getKeyLeft(key + 1)
- break
- default:
- leftBounds = left
- rightBounds = left + width
- break
+
+
+
+ {keys.map((key) => {
+ const isNatural = isNaturalKey(key)
+ const Component: any = isNatural ? NaturalKey! : AccidentalKey!
+ const [currentKey = null] = Array.isArray(keyChannels!) ? keyChannels.filter((kc) => kc!.key === key) : []
+ const width = getKeyWidth(key)
+ const left = getKeyLeft(key)
+ const { left: leftBounds, right: rightBounds } = getKeyBounds(
+ startKey,
+ endKey,
+ getKeyLeft,
+ getKeyWidth,
+ )(key, left, width)
+ const octaveStart = Math.floor(key / 12) * 12
+ const octaveEnd = octaveStart + 11
+ const octaveLeftBounds = getKeyLeft(octaveStart)
+ const octaveRightBounds = getKeyLeft(octaveEnd) + getKeyWidth(octaveEnd)
+ const components: Record = {
+ link: 'a',
+ checkbox: 'label',
+ radio: 'label',
+ }
+
+ const { [behavior!]: component = 'div' } = components
+
+ const KeyComponent = component as React.ElementType
+
+ return (
+
+ {(behavior! === 'checkbox' || behavior === 'radio') && (
+
+ )}
+
+
+ )
})}
-
+ {clientSide && (
+
+ )}
+
+
)
}
diff --git a/src/components/KeyboardMap/KeyboardMap.tsx b/src/components/KeyboardMap/KeyboardMap.tsx
index 1b8bacc..2fe616d 100644
--- a/src/components/KeyboardMap/KeyboardMap.tsx
+++ b/src/components/KeyboardMap/KeyboardMap.tsx
@@ -3,6 +3,10 @@ import * as PropTypes from 'prop-types'
import reverseGetKeyFromPoint from '../../services/reverseGetKeyFromPoint'
const propTypes = {
+ /**
+ * Ratio of the length of the accidental keys to the natural keys.
+ */
+ accidentalKeyLengthRatio: PropTypes.number,
/**
* Event handler triggered upon change in activated keys in the component.
*/
@@ -11,22 +15,17 @@ const propTypes = {
* Map from key code to key number.
*/
keyboardMapping: PropTypes.object,
- /**
- * Active MIDI channel for registering keys.
- */
- channel: PropTypes.number.isRequired,
}
-type Props = PropTypes.InferProps & { accidentalKeyLengthRatio?: number }
+type Props = PropTypes.InferProps
/**
* Keyboard map for allowing interactivity with the keyboard.
- * @param channel - Active MIDI channel for registering keys.
- * @param accidentalKeyLengthRatio - Ratio of the length of the accidental keys to the natural keys. This is set by the Keyboard component.
+ * @param accidentalKeyLengthRatio - Ratio of the length of the accidental keys to the natural keys.
* @param onChange - Event handler triggered upon change in activated keys in the component.
* @param keyboardMapping - Map from key code to key number.
*/
-const KeyboardMap: React.FC = ({ channel, accidentalKeyLengthRatio, onChange, keyboardMapping = {} }) => {
+const KeyboardMap: React.FC = ({ accidentalKeyLengthRatio, onChange, keyboardMapping = {} }) => {
const baseRef = React.useRef(null)
const keysOnRef = React.useRef([])
const lastVelocity = React.useRef(undefined)
@@ -62,7 +61,7 @@ const KeyboardMap: React.FC = ({ channel, accidentalKeyLengthRatio, onCha
if (lastVelocity.current === undefined) {
lastVelocity.current = keyData.velocity > 1 ? 1 : keyData.velocity < 0 ? 0 : keyData.velocity
}
- keysOnRef.current = [...keysOnRef.current, { ...keyData, velocity: lastVelocity.current, channel, id: -1 }]
+ keysOnRef.current = [...keysOnRef.current, { ...keyData, velocity: lastVelocity.current, id: -1 }]
if (typeof onChange! === 'function') {
onChange(keysOnRef.current)
}
@@ -89,10 +88,7 @@ const KeyboardMap: React.FC = ({ channel, accidentalKeyLengthRatio, onCha
if (lastVelocity.current === undefined) {
lastVelocity.current = keyData.velocity > 1 ? 1 : keyData.velocity < 0 ? 0 : keyData.velocity
}
- keysOnRef.current = [
- ...keysOnRef.current,
- { ...keyData, velocity: lastVelocity.current, channel, id: t.identifier },
- ]
+ keysOnRef.current = [...keysOnRef.current, { ...keyData, velocity: lastVelocity.current, id: t.identifier }]
if (typeof onChange! === 'function') {
onChange(keysOnRef.current)
}
@@ -136,7 +132,6 @@ const KeyboardMap: React.FC = ({ channel, accidentalKeyLengthRatio, onCha
...keysOnRef.current.filter((k) => k.id !== t.identifier),
{
...keyData,
- channel,
velocity: lastVelocity.current,
id: t.identifier,
},
@@ -152,7 +147,7 @@ const KeyboardMap: React.FC = ({ channel, accidentalKeyLengthRatio, onCha
return () => {
window.removeEventListener('touchmove', handleTouchMove)
}
- }, [accidentalKeyLengthRatio, channel, onChange])
+ }, [accidentalKeyLengthRatio, onChange])
React.useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
@@ -188,7 +183,7 @@ const KeyboardMap: React.FC = ({ channel, accidentalKeyLengthRatio, onCha
if (mouseKey.key !== keyData.key) {
keysOnRef.current = [
...keysOnRef.current.filter((k) => k.id !== -1),
- { ...keyData, velocity: lastVelocity.current, channel, id: -1 },
+ { ...keyData, velocity: lastVelocity.current, id: -1 },
]
if (typeof onChange! === 'function') {
onChange(keysOnRef.current)
@@ -201,7 +196,7 @@ const KeyboardMap: React.FC = ({ channel, accidentalKeyLengthRatio, onCha
return () => {
window.removeEventListener('mousemove', handleMouseMove)
}
- }, [accidentalKeyLengthRatio, channel, onChange])
+ }, [accidentalKeyLengthRatio, onChange])
React.useEffect(() => {
const handleTouchEnd = (e: TouchEvent) => {
@@ -245,7 +240,7 @@ const KeyboardMap: React.FC = ({ channel, accidentalKeyLengthRatio, onCha
return () => {
window.removeEventListener('mouseup', handleMouseUp)
}
- }, [accidentalKeyLengthRatio, channel, onChange])
+ }, [accidentalKeyLengthRatio, onChange])
React.useEffect(() => {
const baseRefComponent = baseRef.current
@@ -267,7 +262,7 @@ const KeyboardMap: React.FC = ({ channel, accidentalKeyLengthRatio, onCha
if (keysOnRef.current.some((k) => k.key === key && k.id === -2)) {
return
}
- keysOnRef.current = [...keysOnRef.current, { key, velocity: 0.75, channel, id: -2 }]
+ keysOnRef.current = [...keysOnRef.current, { key, velocity: 0.75, id: -2 }]
if (typeof onChange! === 'function') {
onChange(keysOnRef.current)
}
@@ -322,6 +317,7 @@ const KeyboardMap: React.FC = ({ channel, accidentalKeyLengthRatio, onCha
height: '100%',
zIndex: 4,
outline: 0,
+ cursor: 'pointer',
}}
onContextMenu={handleContextMenu}
onDragStart={handleDragStart}
diff --git a/src/components/NaturalKey/NaturalKey.tsx b/src/components/NaturalKey/NaturalKey.tsx
index b568d6e..ce7c7be 100644
--- a/src/components/NaturalKey/NaturalKey.tsx
+++ b/src/components/NaturalKey/NaturalKey.tsx
@@ -1,10 +1,6 @@
import * as React from 'react'
-import * as PropTypes from 'prop-types'
-import keyPropTypes from '../../services/keyPropTypes'
-type Props = PropTypes.InferProps
-
-const NaturalKey: React.FC = ({ keyChannels }) => (
+const NaturalKey: React.FC = () => (
= ({ keyChannels }) => (
position: 'relative',
}}
>
- {Array.isArray(keyChannels!) &&
- keyChannels.map((c) => (
-
- ))}
+
)
-NaturalKey.propTypes = keyPropTypes
-
export default NaturalKey
diff --git a/src/components/StyledAccidentalKey/StyledAccidentalKey.tsx b/src/components/StyledAccidentalKey/StyledAccidentalKey.tsx
index 6347818..61eaaad 100644
--- a/src/components/StyledAccidentalKey/StyledAccidentalKey.tsx
+++ b/src/components/StyledAccidentalKey/StyledAccidentalKey.tsx
@@ -1,230 +1,452 @@
import * as React from 'react'
-import * as PropTypes from 'prop-types'
-import keyPropTypes from '../../services/keyPropTypes'
-const DEFAULT_COLOR = '#35313b'
const LIGHT_COLOR = 'white'
-type Props = PropTypes.InferProps
-
-const StyledAccidentalKey: React.FC = ({ keyChannels }) => {
- const hasKeyChannels = Array.isArray(keyChannels!) && keyChannels.length > 0
+const StyledAccidentalKey: React.FC = () => {
return (
-
-
+ >
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+ >
+
+
+
+
+
+
+
+
+
+
+
)
}
-StyledAccidentalKey.propTypes = keyPropTypes
-
export default StyledAccidentalKey
diff --git a/src/components/StyledNaturalKey/StyledNaturalKey.tsx b/src/components/StyledNaturalKey/StyledNaturalKey.tsx
index 50f0862..a9e021f 100644
--- a/src/components/StyledNaturalKey/StyledNaturalKey.tsx
+++ b/src/components/StyledNaturalKey/StyledNaturalKey.tsx
@@ -1,202 +1,399 @@
import * as React from 'react'
-import * as PropTypes from 'prop-types'
-import keyPropTypes from '../../services/keyPropTypes'
-const DEFAULT_COLOR = '#e3e3e5'
const LIGHT_COLOR = 'white'
-type Props = PropTypes.InferProps
-
-const StyledNaturalKey: React.FC = ({ keyChannels }) => {
- const hasKeyChannels = Array.isArray(keyChannels!) && keyChannels.length > 0
+const StyledNaturalKey: React.FC = () => {
return (
-
-
)
}
-StyledNaturalKey.propTypes = keyPropTypes
-
export default StyledNaturalKey
diff --git a/src/index.ts b/src/index.ts
index c001724..373e3cc 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,8 +1,7 @@
import Keyboard from './components/Keyboard/Keyboard'
-import KeyboardMap from './components/KeyboardMap/KeyboardMap'
import StyledNaturalKey from './components/StyledNaturalKey/StyledNaturalKey'
import StyledAccidentalKey from './components/StyledAccidentalKey/StyledAccidentalKey'
export default Keyboard
-export { StyledNaturalKey, StyledAccidentalKey, KeyboardMap }
+export { StyledNaturalKey, StyledAccidentalKey }
diff --git a/src/services/getKeyBounds.ts b/src/services/getKeyBounds.ts
new file mode 100644
index 0000000..b51a0a7
--- /dev/null
+++ b/src/services/getKeyBounds.ts
@@ -0,0 +1,43 @@
+type Bounds = {
+ left: number
+ right: number
+}
+
+type GetKeyBounds = (
+ startKey: number,
+ endKey: number,
+ getKeyLeft: (key: number) => number,
+ getKeyWidth: (key: number) => number,
+) => (key: number, left: number, width: number) => Bounds
+
+const getKeyBounds: GetKeyBounds = (startKey, endKey, getKeyLeft, getKeyWidth) => (key, left, width) => {
+ switch (key % 12) {
+ case 0:
+ case 5:
+ return {
+ left,
+ right: key + 1 > endKey! ? left + width : getKeyLeft(key + 1),
+ }
+ case 4:
+ case 11:
+ return {
+ left: key - 1 < startKey! ? left : getKeyLeft(key - 1) + getKeyWidth(key - 1),
+ right: left + width,
+ }
+ case 2:
+ case 7:
+ case 9:
+ return {
+ left: key - 1 < startKey! ? left : getKeyLeft(key - 1) + getKeyWidth(key - 1),
+ right: key + 1 > endKey! ? left + width : getKeyLeft(key + 1),
+ }
+ default:
+ break
+ }
+ return {
+ left,
+ right: left + width,
+ }
+}
+
+export default getKeyBounds
diff --git a/src/services/keyPropTypes.ts b/src/services/keyPropTypes.ts
deleted file mode 100644
index a5d7e14..0000000
--- a/src/services/keyPropTypes.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import * as PropTypes from 'prop-types'
-
-export default {
- keyChannels: PropTypes.arrayOf(
- PropTypes.shape({
- channel: PropTypes.number.isRequired,
- key: PropTypes.number.isRequired,
- velocity: PropTypes.number.isRequired,
- }),
- ),
-}