Audit fixes: a11y, gesture correctness, math edge cases

- Keyboard focus-visible ring on all controls (WCAG 2.4.7), suppressed
  for pointer grabs; focusRing prop to customize or disable
- aria-orientation on track modes; data-dragging/-disabled/-readonly/
  -focus-visible state attributes; disabled dims, readOnly drops the
  grab cursor
- Escape cancels a drag and restores the pre-gesture value
- Controlled mode no longer desyncs when the parent rejects onChange
- Second concurrent pointer can no longer hijack a drag session;
  drags cancel when disabled/readOnly flips mid-gesture
- Wheel: onChangeStart symmetry, settle flush on unsubscribe, dominant-
  axis direction (fixes macOS Shift+scroll reversal), ignored mid-drag
- Shift+wheel and fine multipliers now work on stepped/values knobs
- Fader/LEDFader track mapping inset-aligned with handle travel (no
  value jump when grabbing the cap); LEDFader zones sorted
- roundTo no longer returns NaN for exponential-notation values;
  logTaper falls back to linear for invalid ranges; decimalsFromStep
  shaves float noise; findClosest([]) returns the input
- Double-click reset brackets with onChangeStart/End
- MetalKnob honors showValue/unit/format; VintageKnob omits them from
  its type; TickLabels extracted as a public primitive
- SegmentDisplay: letters/hex charset, dash overflow instead of 888
This commit is contained in:
Dreamodus 2026-07-12 14:38:50 -07:00
parent 60a02cb1db
commit da352d3695
13 changed files with 378 additions and 82 deletions

View file

@ -0,0 +1,53 @@
import * as React from 'react';
import { useKnobContext } from '../core/context';
import { polarToCartesian } from '../core/math';
export interface TickLabelsProps {
/** Labels distributed evenly across the travel, e.g. ['0','','5','','10']. */
labels: readonly string[];
/** Radius of the label ring. Defaults to just inside the canvas edge. */
radius?: number;
fontSize?: number;
color?: string;
fontFamily?: string;
/** Extra props per label, by index. */
getLabelProps?: (index: number) => React.SVGProps<SVGTextElement> | undefined;
}
/** Printed scale labels around the knob's travel (0…10 amp-style scales). */
export const TickLabels: React.FC<TickLabelsProps> = ({
labels,
radius,
fontSize,
color = 'currentColor',
fontFamily,
getLabelProps,
}) => {
const { size, center: c, angleOffset, angleRange } = useKnobContext();
const r = radius ?? size / 2 - size * 0.02;
return (
<g>
{labels.map((text, i) => {
if (!text) return null;
const t = labels.length === 1 ? 0 : i / (labels.length - 1);
const p = polarToCartesian(c, c, r, angleOffset + t * angleRange);
return (
<text
key={i}
x={p.x}
y={p.y}
textAnchor="middle"
dominantBaseline="central"
fontSize={fontSize ?? size * 0.085}
fontFamily={fontFamily}
fill={color}
style={{ pointerEvents: 'none' }}
{...getLabelProps?.(i)}
>
{text}
</text>
);
})}
</g>
);
};