Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions components/CardFan.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div
<button
type="button"
key={card.login}
aria-label={`Scout ${card.name ?? card.login}`}
onClick={() => 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,
}}
>
<PlayerCard card={card} />
</div>
</button>
);
})}
</div>
Expand Down
23 changes: 23 additions & 0 deletions tests/cardFan.test.ts
Original file line number Diff line number Diff line change
@@ -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(/<button/g)).toHaveLength(cards.length);
expect(html).toContain('type="button"');

for (const card of cards) {
expect(html).toContain(
`aria-label="Scout ${card.name ?? card.login}"`,
);
}
});
});