From 410a7cd37e333356470f323c85c7aabe29bebcd6 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Mon, 25 May 2026 10:57:54 +0300 Subject: [PATCH] feat(SliderArea): keyboard accessibility for the comparison slider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../ui/SliderArea/SliderArea.svelte | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/widgets/ComparisonView/ui/SliderArea/SliderArea.svelte b/src/widgets/ComparisonView/ui/SliderArea/SliderArea.svelte index 310ed0b..cfa81ae 100644 --- a/src/widgets/ComparisonView/ui/SliderArea/SliderArea.svelte +++ b/src/widgets/ComparisonView/ui/SliderArea/SliderArea.svelte @@ -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