Skip to content

Commit

Permalink
feat: add custom label support
Browse files Browse the repository at this point in the history
  • Loading branch information
tigranpetrossian committed Apr 27, 2024
1 parent a8e0199 commit 2168d69
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 25 deletions.
28 changes: 14 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,21 @@ const App = () => {

## Props

| Prop | Default value | Description |
|-----------------------|------------------|--------------------------------------------------------------|
| `noteRange` | `[21, 108]` | The lowest and the highest notes of the piano in MIDI numbers (0-127). |
| `defaultActiveNotes` | `[]` | Notes that are pressed by default. Subsequent updates are ignored. Cleared when the user begins playing. |
| `activeNotes` | | Currently pressed notes. Puts component into controlled mode; active notes must be managed externally via callbacks. |
| `onPlayNote` | | Fired when a note is played. |
| `onStopNote` | | Fired when a note is stopped. |
| `onChange` | | Fired when active notes are changed via user input. |
| `interactive` | `true` | Enable interaction with the piano via keyboard, mouse, or touch. |
| Prop | Default value | Description |
|-----------------------|------------------|-------------------------------------------------------------------------------------------------------------------------|
| `noteRange` | `[21, 108]` | The lowest and the highest notes of the piano in MIDI numbers (0-127). |
| `defaultActiveNotes` | `[]` | Notes that are pressed by default. Subsequent updates are ignored. Cleared when the user begins playing. |
| `activeNotes` | | Currently pressed notes. Puts component into controlled mode; active notes must be managed externally via callbacks. |
| `onPlayNote` | | Fired when a note is played. |
| `onStopNote` | | Fired when a note is stopped. |
| `onChange` | | Fired when active notes are changed via user input. |
| `interactive` | `true` | Enable interaction with the piano via keyboard, mouse, or touch. |
| `keymap` | `DEFAULT_KEYMAP` | Mapping of computer keys to MIDI note numbers, e.g. `[{ key: 'q', midiNumber: 60 }, ..., { key: 'i', midiNumber: 72 }]` |
| `width` | `"auto"` | Width of the piano. Accepts any valid CSS value. When unspecified, the piano fills it's container and is responsive. |
| `height` | `"auto"` | Height of the piano. Accepts any valid CSS value. |
| `whiteKeyAspectRatio` | `"24 / 150"` | Aspect ratio of the white key in CSS format. Ignored when `height` is specified. |
| `blackKeyHeight` | `"67.5%"` | Height of the black key. Allows tweaking the appearance of black keys in relation to white keys. |
| `components` | | Allows replacing default components for black and white keys. |
| `width` | `"auto"` | Width of the piano. Accepts any valid CSS value. When unspecified, the piano fills it's container and is responsive. |
| `height` | `"auto"` | Height of the piano. Accepts any valid CSS value. |
| `whiteKeyAspectRatio` | `"24 / 150"` | Aspect ratio of the white key in CSS format. Ignored when `height` is specified. |
| `blackKeyHeight` | `"67.5%"` | Height of the black key. Allows tweaking the appearance of black keys in relation to white keys. |
| `components` | | Allows replacing default components for black and white keys and adding a custom label to each key. |


## Styling
Expand Down
26 changes: 18 additions & 8 deletions src/components/Key/Key.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import React, { useMemo } from 'react';
import { midiToNote } from 'lib/midi';
import type { CustomKeyComponent, KeyColor } from 'types';
import { DEFAULT_BLACK_KEY_HEIGHT, DEFAULT_WHITE_KEY_ASPECT_RATIO } from 'lib/constants';
import type { CustomKeyComponent, CustomLabelComponent, KeyColor, Keymap } from 'types';
import { DEFAULT_BLACK_KEY_HEIGHT, DEFAULT_WHITE_KEY_ASPECT_RATIO, MIDI_NUMBER_C0 } from 'lib/constants';
import { defaultKeyComponents } from 'components/Key/defaultKeyComponents';

type KeyProps = {
midiNumber: number;
firstNoteMidiNumber: number;
active: boolean;
whiteKeyAspectRatio?: React.CSSProperties['aspectRatio'];
blackKeyHeight?: React.CSSProperties['height'];
isFixedHeight: boolean;
components?: {
blackKey?: CustomKeyComponent;
whiteKey?: CustomKeyComponent;
label?: CustomLabelComponent;
};
whiteKeyAspectRatio?: React.CSSProperties['aspectRatio'];
blackKeyHeight?: React.CSSProperties['height'];
isFixedHeight: boolean;
active: boolean;
keymap: Keymap | undefined;
} & React.HTMLAttributes<HTMLDivElement>;

const Key = React.memo((props: KeyProps) => {
Expand All @@ -26,18 +28,22 @@ const Key = React.memo((props: KeyProps) => {
blackKeyHeight,
isFixedHeight,
components,
keymap,
...htmlAttributes
} = props;
const note = midiToNote(midiNumber);
const Component = getKeyComponent(components, note.keyColor);
const KeyComponent = getKeyComponent(components, note.keyColor);
const Label = components?.label;
const style = useMemo(
() => getKeyStyles(midiNumber, firstNoteMidiNumber, isFixedHeight, whiteKeyAspectRatio, blackKeyHeight),
[midiNumber, firstNoteMidiNumber, isFixedHeight, whiteKeyAspectRatio, blackKeyHeight]
);
const keyboardShortcut = useMemo(() => getKeyboardShortcut(midiNumber, keymap), [midiNumber, keymap]);

return (
<div style={style} {...htmlAttributes} data-midi-number={midiNumber}>
<Component active={active} note={note} />
<KeyComponent active={active} note={note} />
{Label ? <Label active={active} note={note} keyboardShortcut={keyboardShortcut} midiC0={MIDI_NUMBER_C0} /> : null}
</div>
);
});
Expand Down Expand Up @@ -78,6 +84,10 @@ function getKeyComponent(components: KeyProps['components'], color: KeyColor) {
return components?.[`${color}Key`] ?? defaultKeyComponents[`${color}Key`];
}

function getKeyboardShortcut(midiNumber: number, keymap: KeyProps['keymap']) {
return keymap?.find((item) => item.midiNumber === midiNumber)?.key;
}

// The keyboard is laid out on a horizontal CSS grid.
// Position represents a starting column: `grid-column-start` in CSS terms.
// White keys span over 12 columns each, making the octave length 84 columns total.
Expand Down
7 changes: 5 additions & 2 deletions src/components/Klavier.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useMemo, useRef } from 'react';
import type { Keymap, CustomKeyComponent } from 'types';
import type { Keymap, CustomKeyComponent, CustomLabelComponent } from 'types';
import { DEFAULT_KEYMAP } from 'keymap';
import { DEFAULT_NOTE_RANGE } from 'lib/constants';
import { range } from 'lib/range';
Expand Down Expand Up @@ -85,11 +85,13 @@ interface KlavierProps {
* @example:
* const CustomBlackKey = ({ active, note }) => { return <div /> }
* const CustomWhiteKey = ({ active, note }) => { return <div /> }
* <Klavier components={{ blackKey: CustomBlackKey, whiteKey: CustomWhiteKey }} />
* const CustomLabel = ({ active, note, midiC0, keyboardShortcut }) => { return <div/> }
* <Klavier components={{ blackKey: CustomBlackKey, whiteKey: CustomWhiteKey, label: CustomLabel }} />
*/
components?: {
blackKey?: CustomKeyComponent;
whiteKey?: CustomKeyComponent;
label?: CustomLabelComponent;
};
}

Expand Down Expand Up @@ -152,6 +154,7 @@ const Klavier = (props: KlavierProps) => {
whiteKeyAspectRatio={whiteKeyAspectRatio}
blackKeyHeight={blackKeyHeight}
components={components}
keymap={keyMap}
/>
))}
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { Klavier } from 'components/Klavier';
export type { KlavierProps } from 'components/Klavier';
export type { CustomKeyProps, Keymap, KeymapItem } from 'types';
export type { CustomKeyProps, CustomLabelProps, Keymap, KeymapItem } from 'types';
9 changes: 9 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,12 @@ export type CustomKeyProps = {
};

export type CustomKeyComponent = React.ComponentType<CustomKeyProps>;

export type CustomLabelProps = {
note: Note;
midiC0: number;
keyboardShortcut: string | undefined;
active: boolean;
};

export type CustomLabelComponent = React.ComponentType<CustomLabelProps>;

0 comments on commit 2168d69

Please sign in to comment.