feat(Board): add keyboard and swipe focal cycling
This commit is contained in:
@@ -0,0 +1,23 @@
|
|||||||
|
import {
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
it,
|
||||||
|
} from 'vitest';
|
||||||
|
import { swipeDirection } from './cycleGestures';
|
||||||
|
|
||||||
|
describe('swipeDirection', () => {
|
||||||
|
it('maps a leftward swipe past threshold to next', () => {
|
||||||
|
expect(swipeDirection(-80, 50)).toBe(1);
|
||||||
|
});
|
||||||
|
it('maps a rightward swipe past threshold to previous', () => {
|
||||||
|
expect(swipeDirection(80, 50)).toBe(-1);
|
||||||
|
});
|
||||||
|
it('ignores sub-threshold movement', () => {
|
||||||
|
expect(swipeDirection(20, 50)).toBe(0);
|
||||||
|
expect(swipeDirection(-20, 50)).toBe(0);
|
||||||
|
});
|
||||||
|
it('treats the exact threshold as a swipe (inclusive)', () => {
|
||||||
|
expect(swipeDirection(-50, 50)).toBe(1);
|
||||||
|
expect(swipeDirection(50, 50)).toBe(-1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* Maps a horizontal swipe delta to a cycle direction. A leftward swipe (negative
|
||||||
|
* dx) advances to the next pairing (+1); a rightward swipe (positive dx) goes to
|
||||||
|
* the previous (-1). Movement below the threshold is ignored (0).
|
||||||
|
*
|
||||||
|
* @param dx - Horizontal travel in px (end minus start).
|
||||||
|
* @param threshold - Minimum absolute travel in px to count as a swipe.
|
||||||
|
* @returns +1 (next), -1 (previous), or 0 (no cycle).
|
||||||
|
*/
|
||||||
|
export function swipeDirection(dx: number, threshold: number): -1 | 0 | 1 {
|
||||||
|
if (dx <= -threshold) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (dx >= threshold) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<!--
|
||||||
|
Component: Board
|
||||||
|
Shell of the pairing board widget. Reads the board singleton, renders the
|
||||||
|
focal frame, and wires focal cycling (keyboard arrows + touch swipe). Cycling
|
||||||
|
swaps the focal in place (no remount) so the reserved frame height holds — the
|
||||||
|
zero-shift invariant. Rail / sidebar / side-by-side compose in here later.
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import { getBoard } from '$features/CompareBoard';
|
||||||
|
import { swipeDirection } from '../../lib/cycleGestures/cycleGestures';
|
||||||
|
import FocalFrame from '../FocalFrame/FocalFrame.svelte';
|
||||||
|
|
||||||
|
const board = getBoard();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimum horizontal travel (px) to register a swipe as a cycle.
|
||||||
|
*/
|
||||||
|
const SWIPE_THRESHOLD = 50;
|
||||||
|
|
||||||
|
// Arrow-key cycling, suppressed while a specimen field is being edited.
|
||||||
|
$effect(() => {
|
||||||
|
function onKeydown(event: KeyboardEvent) {
|
||||||
|
const active = document.activeElement;
|
||||||
|
if (active instanceof HTMLElement && active.isContentEditable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key === 'ArrowRight') {
|
||||||
|
board.cycle(1);
|
||||||
|
} else if (event.key === 'ArrowLeft') {
|
||||||
|
board.cycle(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('keydown', onKeydown);
|
||||||
|
return () => window.removeEventListener('keydown', onKeydown);
|
||||||
|
});
|
||||||
|
|
||||||
|
let touchStartX = 0;
|
||||||
|
|
||||||
|
function onTouchStart(event: TouchEvent) {
|
||||||
|
touchStartX = event.touches[0]?.clientX ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTouchEnd(event: TouchEvent) {
|
||||||
|
const endX = event.changedTouches[0]?.clientX ?? touchStartX;
|
||||||
|
const direction = swipeDirection(endX - touchStartX, SWIPE_THRESHOLD);
|
||||||
|
if (direction !== 0) {
|
||||||
|
board.cycle(direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="w-full"
|
||||||
|
role="group"
|
||||||
|
aria-label="Pairing board — swipe or use arrow keys to cycle"
|
||||||
|
ontouchstart={onTouchStart}
|
||||||
|
ontouchend={onTouchEnd}
|
||||||
|
>
|
||||||
|
<FocalFrame />
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user