diff --git a/src/lib/pages/TasksPage.svelte b/src/lib/pages/TasksPage.svelte index 9d137c0..de9c6af 100644 --- a/src/lib/pages/TasksPage.svelte +++ b/src/lib/pages/TasksPage.svelte @@ -110,6 +110,56 @@ } } + // ARIA radiogroup keyboard handling for the energy segmented control. + // Full W3C APG Radio Group pattern: arrow keys cycle, Home / End jump + // to ends, focus and selection move together. The options array lives + // here so the keyboard handler and the render loop share one source + // of truth — desynchronising them is the usual way these patterns + // rot over time. + type EnergyOption = { value: EnergyLevel | null; label: string }; + const energyOptions: EnergyOption[] = [ + { value: null, label: "—" }, + { value: "high", label: "High" }, + { value: "medium", label: "Med" }, + { value: "brain_dead", label: "Low" }, + ]; + let energyRadioGroupEl = $state(null); + + function selectEnergyByIndex(idx: number) { + const clamped = Math.max(0, Math.min(energyOptions.length - 1, idx)); + const opt = energyOptions[clamped]; + cycleCurrentEnergy(opt.value); + // Move focus to the newly-checked button so keyboard nav tracks + // selection, per the ARIA radio pattern. + const btn = energyRadioGroupEl?.querySelectorAll('[role="radio"]')[clamped]; + btn?.focus(); + } + + function energyRadioKeydown(e: KeyboardEvent) { + const current = energyOptions.findIndex((o) => o.value === settings.currentEnergy); + const here = current < 0 ? 0 : current; + switch (e.key) { + case "ArrowRight": + case "ArrowDown": + e.preventDefault(); + selectEnergyByIndex((here + 1) % energyOptions.length); + break; + case "ArrowLeft": + case "ArrowUp": + e.preventDefault(); + selectEnergyByIndex((here - 1 + energyOptions.length) % energyOptions.length); + break; + case "Home": + e.preventDefault(); + selectEnergyByIndex(0); + break; + case "End": + e.preventDefault(); + selectEnergyByIndex(energyOptions.length - 1); + break; + } + } + let completedTasks = $derived.by(() => { let list = tasks.filter((t) => t.done); if (activeBucket !== "all") { @@ -244,23 +294,25 @@ from any bucket / list context. -->
-
- {#each [ - { value: null, label: '—' }, - { value: 'high' as EnergyLevel, label: 'High' }, - { value: 'medium' as EnergyLevel, label: 'Med' }, - { value: 'brain_dead' as EnergyLevel, label: 'Low' }, - ] as opt} + aria-label="My current energy" + tabindex="-1" + onkeydown={energyRadioKeydown} + > + {#each energyOptions as opt} + {@const checked = settings.currentEnergy === opt.value} {/each}