diff --git a/AGENTS.md b/AGENTS.md index 943ce04..0e6fb14 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -236,3 +236,4 @@ Questions for the lab go to Dr Jodie Rummer (`@JodieRummer`) on GitHub: label th - Security scans (#144): use Snyk CLI >=1.1308.3 with `test --all-projects --dev` for the pnpm 12 multi-document lockfile. Remaining upstream advisories and mitigations are tracked in docs/dev/SNYK-144.md. - Leaflet treats string tooltips as HTML. Pass a DOM element populated with `textContent` for workbook-derived point and move labels, including when rebuilding overlays. +- Leaflet polylines are pointer-only by default. Keep move paths keyboard-focusable, with focus tooltips and Enter/Space popup handlers; the SVG fallback uses the shared chart `markKeys` contract. diff --git a/e2e/map-keyboard.spec.ts b/e2e/map-keyboard.spec.ts new file mode 100644 index 0000000..72d8764 --- /dev/null +++ b/e2e/map-keyboard.spec.ts @@ -0,0 +1,48 @@ +import { expect, test } from '@playwright/test'; + +test('move lines expose their details to the keyboard after overlay toggles', async ({ page }) => { + await page.route(/server\.arcgisonline\.com|tile\.openstreetmap\.org/, (route) => route.abort()); + await page.goto('/recaptures'); + const map = page.getByRole('region', { + name: "Map of recaptured sharks' moves between nurseries" + }); + const line = map.locator('.ps-line-0[role="button"]').first(); + await expect(line).toBeVisible(); + const label = await line.getAttribute('aria-label'); + await line.focus(); + await expect(map.locator('.leaflet-tooltip').last()).toHaveText(label!); + await line.press('Enter'); + await expect(map.locator('.leaflet-popup-content')).toContainText(label!); + await line.press('Escape'); + await expect(map.locator('.leaflet-popup-content')).toHaveCount(0); + // Re-adding an overlay creates another path element: it must retain the handlers. + const toggle = map.getByRole('checkbox', { name: /Blacktip.*moves/i }); + await toggle.uncheck(); + await toggle.check(); + await line.focus(); + await line.press('Space'); + await expect(map.locator('.leaflet-popup-content')).toContainText(label!); +}); + +test('SVG map fallback supports the shared arrow-key chart controls', async ({ page }) => { + // Keep the hydrated SVG fallback by preventing the lazy Leaflet module from loading. + await page.route('**/*.js', async (route) => { + const response = await route.fetch(); + const body = await response.text(); + if (body.includes('Leaflet') && body.includes('1.9.4')) await route.abort(); + else await route.fulfill({ response, body }); + }); + await page.goto('/recaptures'); + const map = page.locator('.chart-card').filter({ + has: page.getByRole('heading', { name: 'Moves between nurseries on the map', exact: true }) + }); + const svg = map.locator('svg[role="application"]'); + await expect(svg).toBeVisible(); + await svg.focus(); + await svg.press('Home'); + await expect(map.locator('.tip')).toContainText('Moves'); + await svg.press('End'); + await expect(map.locator('.tip')).toContainText('Same-nursery recaptures'); + await svg.press('Escape'); + await expect(map.locator('.tip')).toHaveCount(0); +}); diff --git a/src/lib/components/InteractiveMap.svelte b/src/lib/components/InteractiveMap.svelte index 3d53030..86afa1d 100644 --- a/src/lib/components/InteractiveMap.svelte +++ b/src/lib/components/InteractiveMap.svelte @@ -1,5 +1,12 @@ {#if ready} @@ -195,7 +330,14 @@ >

Drag to pan, use + / − to zoom. Select a circle for details.

{:else} - + g.lines.map((l) => ({ ...l, color: g.color })))} + {lineLabel} + {valueLabel} + {format} + csv={false} + /> {/if}