From eb55f8424074dc0c6105728c155526c3bc747de5 Mon Sep 17 00:00:00 2001 From: Jannik Agethen Date: Fri, 31 Jul 2026 07:05:17 +0200 Subject: [PATCH] fix(a11y): make sample cards keyboard accessible --- components/CardFan.tsx | 21 ++++++++++++++++++--- tests/cardFan.test.ts | 23 +++++++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 tests/cardFan.test.ts diff --git a/components/CardFan.tsx b/components/CardFan.tsx index ba58604..2a8e8e5 100644 --- a/components/CardFan.tsx +++ b/components/CardFan.tsx @@ -48,22 +48,37 @@ export default function CardFan({ cards, onPick }: Props) { const ty = hovered ? -36 : open ? -4 : Math.abs(off) * 14; const sc = hovered ? 1.05 : 1; return ( -
onPick(card.login)} + onFocus={() => { + setHover(i); + setOpen(true); + }} + onBlur={(event) => { + const fan = event.currentTarget.parentElement; + const next = event.relatedTarget; + + if (!(next instanceof Node) || !fan?.contains(next)) { + setOpen(false); + setHover(null); + } + }} onMouseEnter={() => { setHover(i); setOpen(true); }} onMouseLeave={() => setHover(null)} - className="absolute left-1/2 top-[18px] w-[184px] origin-bottom cursor-pointer transition-transform duration-[450ms] ease-[cubic-bezier(.2,.8,.2,1)] max-[860px]:static max-[860px]:w-[min(230px,66vw)] max-[860px]:!transform-none max-[860px]:!z-auto" + className="absolute left-1/2 top-[18px] w-[184px] origin-bottom cursor-pointer appearance-none border-0 bg-transparent p-0 text-inherit transition-transform duration-[450ms] ease-[cubic-bezier(.2,.8,.2,1)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[#39d353] max-[860px]:static max-[860px]:w-[min(230px,66vw)] max-[860px]:!transform-none max-[860px]:!z-auto" style={{ transform: `translateX(-50%) translate(${tx}px, ${ty}px) rotate(${rot}deg) scale(${sc})`, zIndex: hovered ? 60 : 40 - i * 5, }} > -
+ ); })} diff --git a/tests/cardFan.test.ts b/tests/cardFan.test.ts new file mode 100644 index 0000000..cff7946 --- /dev/null +++ b/tests/cardFan.test.ts @@ -0,0 +1,23 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { createElement } from "react"; +import { describe, expect, it, vi } from "vitest"; +import CardFan from "@/components/CardFan"; +import { SAMPLE_CARDS } from "@/lib/github/samples"; + +describe("CardFan", () => { + it("renders every sample card as a keyboard-focusable button", () => { + const cards = SAMPLE_CARDS.slice(0, 2); + const html = renderToStaticMarkup( + createElement(CardFan, { cards, onPick: vi.fn() }), + ); + + expect(html.match(/