diff --git a/.Jules/palette.md b/.Jules/palette.md index a413d87a..3ad7fbe2 100644 --- a/.Jules/palette.md +++ b/.Jules/palette.md @@ -5,3 +5,7 @@ ## 2025-05-15 - [Inclusive Controls for Screen Readers] **Learning:** Hidden range inputs used for rotary knobs and sliders require explicit `aria-label` attributes to be useful for assistive technologies, as the visual label might not be programmatically associated with the input. **Action:** Ensure all `input[type="range"]` elements, especially those styled to look like knobs, have descriptive `aria-label` or `aria-labelledby`. + +## 2025-05-15 - [ARIA Roles Require Focus Management] +**Learning:** Assigning ARIA roles like `switch`, `button`, or `slider` to non-native elements is insufficient for accessibility if not paired with `tabIndex` and keyboard event listeners (Space/Enter for buttons/switches, Arrow keys for sliders). Without these, the UI remains unusable for keyboard-only users despite being "labeled" for screen readers. +**Action:** Always pair interactive ARIA roles with `tabIndex={0}` and appropriate `onKeyDown` handlers to ensure full keyboard operability. diff --git a/src/components/HUD/BassScreen.tsx b/src/components/HUD/BassScreen.tsx index 679d2e28..8194c223 100644 --- a/src/components/HUD/BassScreen.tsx +++ b/src/components/HUD/BassScreen.tsx @@ -126,8 +126,9 @@ export const BassScreen: React.FC = () => { {/* Harmony Section */}
- +
- + handleParamChange(selectedDrum, 'steps', parseInt(e.target.value))} /> - + { key={i} className={`step-button ${active ? 'active' : ''}`} onClick={() => store.toggleStep(selectedDrum as any, i)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + store.toggleStep(selectedDrum as any, i) + } + }} + role="switch" + tabIndex={0} + aria-checked={active} + aria-label={`Step ${i + 1}`} /> ) })} diff --git a/src/components/Knob.tsx b/src/components/Knob.tsx index a0be630a..076146cf 100644 --- a/src/components/Knob.tsx +++ b/src/components/Knob.tsx @@ -64,6 +64,41 @@ export function Knob({ label, value, min = 0, max = 1, step = 0.01, defaultValue e.currentTarget.releasePointerCapture(e.pointerId) } + const handleKeyDown = (e: React.KeyboardEvent) => { + let newValue = value + const range = max - min + const smallStep = step || range / 100 + const largeStep = range / 10 + + if (e.key === 'ArrowUp' || e.key === 'ArrowRight') { + newValue = Math.min(max, value + smallStep) + } else if (e.key === 'ArrowDown' || e.key === 'ArrowLeft') { + newValue = Math.max(min, value - smallStep) + } else if (e.key === 'PageUp') { + newValue = Math.min(max, value + largeStep) + } else if (e.key === 'PageDown') { + newValue = Math.max(min, value - largeStep) + } else if (e.key === 'Home') { + newValue = min + } else if (e.key === 'End') { + newValue = max + } else { + return + } + + e.preventDefault() + const steppedValue = Math.round(newValue / smallStep) * smallStep + // Use a more dynamic precision based on the step value + const precision = step < 0.01 ? 3 : 2 + const finalValue = Number(steppedValue.toFixed(precision)) + if (finalValue !== value) { + onChange(finalValue) + if (window.Telegram?.WebApp?.HapticFeedback) { + window.Telegram.WebApp.HapticFeedback.selectionChanged() + } + } + } + const handleDoubleClick = () => { try { const resetValue = defaultValue !== undefined ? defaultValue : (max + min) / 2 @@ -100,6 +135,14 @@ export function Knob({ label, value, min = 0, max = 1, step = 0.01, defaultValue onPointerMove={handlePointerMove} onPointerUp={handlePointerUp} onDoubleClick={handleDoubleClick} + onKeyDown={handleKeyDown} + role="slider" + tabIndex={0} + aria-label={label} + aria-valuemin={min} + aria-valuemax={max} + aria-valuenow={value} + aria-valuetext={step >= 1 ? Math.round(value).toString() : value.toFixed(2)} style={{ width: size, height: size,