4.7 KiB
<PianoKey>
(no submodules)
Purpose
A single piano key. Two visual kinds (white / black) and three
visual states (idle / pressed / highlighted). Consumers compose
many keys to build a full keyboard component — <PianoKey> itself
deliberately renders a single rect so it stays compositional, theming
flexible, and reusable in non-piano contexts (e.g. step sequencer cells
themed as keys). The component implements BoundControlProps and
emits onChange(max) on press, onChange(min) on release.
Features
- Pointer and keyboard activation (Space, Enter).
- Pointer-leave release (no stuck keys when you glide off the key).
- Key-auto-repeat suppression via active-ref tracking.
- Explicit
stateprop overrides derived state so callers can paint a key ashighlighted(e.g. for chord-shape display) without changingvalue. - Default dimensions tuned per
kind(white: 32×140, black: 20×90). role="button"witharia-pressedreflecting the current state.- CSS-variable theming:
--cs-piano-white,--cs-piano-white-pressed,--cs-piano-white-highlight,--cs-piano-white-stroke,--cs-piano-black,--cs-piano-black-pressed,--cs-piano-black-highlight,--cs-piano-black-stroke.
Usage: Basic
import { useState } from "react";
import { PianoKey } from "@suaveplan/react-control-surface";
export function MiddleC() {
const [v, setV] = useState(0);
return (
<PianoKey
value={v}
onChange={setV}
min={0}
max={1}
note={60}
/>
);
}
Usage: Advanced
Compose an octave with black keys overlaid on whites:
import { useState } from "react";
import { PianoKey } from "@suaveplan/react-control-surface";
export function Octave() {
const [pressed, setPressed] = useState<Set<number>>(new Set());
const flip = (n: number, on: boolean) =>
setPressed((p) => {
const next = new Set(p);
if (on) next.add(n);
else next.delete(n);
return next;
});
const whites = [0, 2, 4, 5, 7, 9, 11];
const blacks = [{ off: 1, x: 22 }, { off: 3, x: 54 }, { off: 6, x: 118 }];
return (
<div style={{ position: "relative", height: 140, width: 224 }}>
{whites.map((o, i) => (
<div key={o} style={{ position: "absolute", left: i * 32 }}>
<PianoKey
value={pressed.has(60 + o) ? 1 : 0}
onChange={(v) => flip(60 + o, v > 0)}
min={0}
max={1}
note={60 + o}
/>
</div>
))}
{blacks.map(({ off, x }) => (
<div key={off} style={{ position: "absolute", left: x }}>
<PianoKey
kind="black"
value={pressed.has(60 + off) ? 1 : 0}
onChange={(v) => flip(60 + off, v > 0)}
min={0}
max={1}
note={60 + off}
/>
</div>
))}
</div>
);
}
API Reference
| Prop | Type | Default |
|---|---|---|
value |
number |
required |
onChange |
(v: number) => void |
required |
min/max |
number |
required |
disabled |
boolean |
false |
kind |
"white" | "black" |
"white" |
state |
"idle" | "pressed" | "highlighted" |
derived |
note |
number |
— |
label |
string |
— |
ariaLabel |
string |
— |
widthPx |
number |
per-kind |
heightPx |
number |
per-kind |
className |
string |
— |
Implementation Notes
The active state tracks an internal ref rather than React state so the
press / release pair is event-loop-stable and immune to auto-repeat
storms from held keys. Pointer capture is set on pointerdown and
released on the matching pointerup / pointercancel / pointerleave,
which gives correct release semantics even when the pointer drifts off
the SVG (touch glissando). The explicit state prop fully overrides
the derived state so the component can be driven from an external
signal (incoming MIDI from another player, computer-aided
visualisation) without coupling value to the visual state.