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(/