diff --git a/scripts/smoke-test.mjs b/scripts/smoke-test.mjs index a9df592..98b1db1 100644 --- a/scripts/smoke-test.mjs +++ b/scripts/smoke-test.mjs @@ -57,6 +57,33 @@ try { assert.equal(await page.locator('.rail').count(), 0, 'explore is not a dashboard'); assert.equal(await page.locator('.list-row').count(), 7); + const issueTab = page.getByRole('tab', { name: 'Issues' }); + assert.equal(await issueTab.getAttribute('tabindex'), '0'); + assert.equal(await page.getByRole('tab', { name: 'Repositories' }).getAttribute('tabindex'), '-1'); + await issueTab.focus(); + await issueTab.press('ArrowRight'); + await page.waitForURL('**/explore/repos'); + await page.waitForFunction(() => + document.activeElement?.getAttribute('role') === 'tab' + && document.activeElement.textContent.trim() === 'Repositories' + && document.activeElement.getAttribute('aria-selected') === 'true', + ); + await page.getByRole('tab', { name: 'Repositories' }).press('End'); + await page.waitForURL('**/explore/orgs'); + await page.waitForFunction(() => + document.activeElement?.getAttribute('role') === 'tab' + && document.activeElement.textContent.trim() === 'Organizations' + && document.activeElement.getAttribute('aria-selected') === 'true', + ); + await page.getByRole('tab', { name: 'Organizations' }).press('Home'); + await page.waitForURL('**/explore'); + await page.waitForFunction(() => + document.activeElement?.getAttribute('role') === 'tab' + && document.activeElement.textContent.trim() === 'Issues' + && document.activeElement.getAttribute('aria-selected') === 'true', + ); + await settle(); + // search + tabs await page.getByLabel('Search', { exact: true }).fill('quickstart'); await settle(); diff --git a/src/components/ui.tsx b/src/components/ui.tsx index 67f29db..8eaf607 100644 --- a/src/components/ui.tsx +++ b/src/components/ui.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, type ReactNode } from 'react'; +import { useEffect, useState, type KeyboardEvent as ReactKeyboardEvent, type ReactNode } from 'react'; import { createPortal } from 'react-dom'; import { Link } from 'react-router-dom'; import { AnimatePresence, motion } from 'motion/react'; @@ -154,14 +154,37 @@ export function Segmented({ onChange: (v: T) => void; idPrefix: string; }) { + const moveFocus = (event: ReactKeyboardEvent, index: number) => { + if (!options.length) return; + let nextIndex: number; + switch (event.key) { + case 'ArrowRight': nextIndex = (index + 1) % options.length; break; + case 'ArrowLeft': nextIndex = (index - 1 + options.length) % options.length; break; + case 'Home': nextIndex = 0; break; + case 'End': nextIndex = options.length - 1; break; + default: return; + } + event.preventDefault(); + onChange(options[nextIndex].value); + window.setTimeout(() => { + const tablist = [...document.querySelectorAll('[data-segmented]')] + .find(element => element.dataset.segmented === idPrefix); + const targetValue = options[nextIndex].value; + tablist?.querySelector(`[role="tab"][data-segment-value="${targetValue}"]`)?.focus(); + }, 50); + }; + return ( -
+
{options.map(opt => (