feat(SliderArea): keyboard accessibility for the comparison slider
The slider element had role="slider" and tabindex="0" but no keyboard handler — the focus ring appeared but the slider could not be moved. Add a keydown handler implementing the standard ARIA slider contract: - ArrowLeft / ArrowDown — step left by 1 percent - ArrowRight / ArrowUp — step right by 1 percent - Shift + arrow — coarse step (10 percent) - PageUp / PageDown — coarse step (10 percent) - Home — jump to 0 - End — jump to 100 Bounds and step sizes extracted as named constants (SLIDER_MIN, SLIDER_MAX, SLIDER_STEP_FINE, SLIDER_STEP_COARSE). Position updates go through sliderSpring.target so keyboard moves animate the same way as pointer drags. Also adds the missing ARIA attributes that screen readers need: - aria-valuemin / aria-valuemax (bounds) - aria-orientation (horizontal)
This commit is contained in:
@@ -70,6 +70,19 @@ const SLIDER_PERSIST_DEBOUNCE_MS = 100;
|
||||
const SLIDER_PADDING_MOBILE_PX = 48;
|
||||
const SLIDER_PADDING_DESKTOP_PX = 96;
|
||||
|
||||
/**
|
||||
* Position bounds (percent of container width).
|
||||
*/
|
||||
const SLIDER_MIN = 0;
|
||||
const SLIDER_MAX = 100;
|
||||
|
||||
/**
|
||||
* Fine and coarse keyboard step sizes. Shift / Page keys use the coarse
|
||||
* step; bare arrow keys use the fine step.
|
||||
*/
|
||||
const SLIDER_STEP_FINE = 1;
|
||||
const SLIDER_STEP_COARSE = 10;
|
||||
|
||||
const fontA = $derived(comparisonStore.fontA);
|
||||
const fontB = $derived(comparisonStore.fontB);
|
||||
const isLoading = $derived(comparisonStore.isLoading || !comparisonStore.isReady);
|
||||
@@ -130,6 +143,46 @@ function startDragging(e: PointerEvent) {
|
||||
handleMove(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Keyboard control for the comparison slider. Implements the standard
|
||||
* ARIA slider keyboard contract: arrows step the position, Shift+arrow
|
||||
* and PageUp/PageDown jump by the coarse step, Home/End snap to bounds.
|
||||
*/
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
const coarse = e.shiftKey;
|
||||
const step = coarse ? SLIDER_STEP_COARSE : SLIDER_STEP_FINE;
|
||||
const current = sliderSpring.target;
|
||||
let next = current;
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowLeft':
|
||||
case 'ArrowDown':
|
||||
next = current - step;
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
case 'ArrowUp':
|
||||
next = current + step;
|
||||
break;
|
||||
case 'PageDown':
|
||||
next = current - SLIDER_STEP_COARSE;
|
||||
break;
|
||||
case 'PageUp':
|
||||
next = current + SLIDER_STEP_COARSE;
|
||||
break;
|
||||
case 'Home':
|
||||
next = SLIDER_MIN;
|
||||
break;
|
||||
case 'End':
|
||||
next = SLIDER_MAX;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
sliderSpring.target = Math.max(SLIDER_MIN, Math.min(SLIDER_MAX, next));
|
||||
}
|
||||
|
||||
const storeSliderPosition = debounce((value: number) => {
|
||||
comparisonStore.sliderPosition = value;
|
||||
}, SLIDER_PERSIST_DEBOUNCE_MS);
|
||||
@@ -271,8 +324,12 @@ const paddingClass = $derived(
|
||||
role="slider"
|
||||
tabindex="0"
|
||||
aria-valuenow={Math.round(sliderPos)}
|
||||
aria-valuemin={SLIDER_MIN}
|
||||
aria-valuemax={SLIDER_MAX}
|
||||
aria-orientation="horizontal"
|
||||
aria-label="Font comparison slider"
|
||||
onpointerdown={startDragging}
|
||||
onkeydown={handleKeydown}
|
||||
class="
|
||||
relative w-full max-w-6xl h-full
|
||||
flex flex-col justify-center
|
||||
|
||||
Reference in New Issue
Block a user