4.2 KiB
<XYPad> — 2-axis touch surface
(no submodules)
Purpose
<XYPad> is an accessible 2-axis touch surface that emits normalised
{ x, y } coordinates as the user drags a pointer or steps with the
arrow keys. The Y axis follows musical-software convention — the top
of the pad is y = 1 and the bottom is y = 0. Both axes are
normalised to [0, 1] regardless of the pad's pixel size, so the
caller can map them onto any parameter range with a simple scale.
Common applications include morphing two modulation sources, mapping filter cutoff against resonance, controlling pan + send, or any pair of correlated parameters that benefit from a single gestural surface.
Features
- Pointer-drag emission via Pointer Events with capture for off-pad drags
- Keyboard navigation — Arrows step by
step, PageUp/PageDown bystep * 4, Home/End jump the X axis to the extremes - Full Tab focusability with
role="application" - Clamping to
[0, 1]on every emission, including out-of-bounds drags disabledremoves the element from the Tab order and silences events- CSS-variable themed background and cursor
data-x/data-ydeterministic attributes for E2E inspection
Usage: Basic
import { useState } from "react";
import { XYPad } from "@suaveplan/react-control-surface";
function Pad() {
const [v, setV] = useState({ x: 0.5, y: 0.5 });
return <XYPad value={v} onChange={setV} label="Morph" />;
}
Usage: Advanced
import { useState } from "react";
import { XYPad } from "@suaveplan/react-control-surface";
function FilterPad({
onCutoff,
onResonance,
}: {
onCutoff: (hz: number) => void;
onResonance: (q: number) => void;
}) {
const [v, setV] = useState({ x: 0.5, y: 0.5 });
return (
<XYPad
value={v}
onChange={(next) => {
setV(next);
onCutoff(40 + next.x * 19960);
onResonance(0.5 + next.y * 9.5);
}}
step={0.01}
width={320}
height={240}
label="Filter morph"
/>
);
}
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
value |
{ x: number; y: number } |
req | Current normalised value, both in [0, 1]. |
onChange |
(next: XYValue) => void |
req | Called for every drag and keyboard step. |
step |
number |
0.05 |
Step size for arrow keys. |
width |
number |
200 |
Render width in CSS pixels. |
height |
number |
200 |
Render height in CSS pixels. |
disabled |
boolean |
false |
When true, ignores all input. |
label |
string |
— | Visible + accessible label. |
ariaLabel |
string |
derived | Override the accessible label. |
className |
string |
— | Appended to rcs-xy-pad. |
The exported pointerToValue(rect, clientX, clientY) helper performs
the pure rect-to-[0, 1] conversion (including Y-inversion and
clamping) and is exported for re-use in custom pads.
Implementation Notes
Pointer events are used uniformly across mouse + touch + pen. On
pointerdown the pad calls setPointerCapture so subsequent drags
outside the bounding rect continue to fire pointermove events. The
draggingRef flag avoids spurious moves before the user has pressed.
Keyboard handling is exhaustive: every recognised key calls
preventDefault and emits a clamped value; unrecognised keys do
nothing (the event bubbles for the consumer to handle). The
aria-valuetext attribute is updated on every render with the
current x/y truncated to two decimals, providing screen-reader
support without a polling cycle.