From 1522322d6335c402e9ad09aa3ca45793e7d885d9 Mon Sep 17 00:00:00 2001 From: BhuvanArn Date: Wed, 15 Jul 2026 18:56:17 +0200 Subject: [PATCH] fix: hide roadmap card behind its open popover (#204) --- .../roadmap-topic-card/index.spec.tsx | 44 ++++++++++++++++++- .../molecules/roadmap-topic-card/index.tsx | 26 ++++++++++- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/web/src/components/molecules/roadmap-topic-card/index.spec.tsx b/web/src/components/molecules/roadmap-topic-card/index.spec.tsx index d5fa5637..9d886551 100644 --- a/web/src/components/molecules/roadmap-topic-card/index.spec.tsx +++ b/web/src/components/molecules/roadmap-topic-card/index.spec.tsx @@ -1,8 +1,18 @@ -import { render, screen } from '@testing-library/react'; -import { describe, expect, it } from 'vitest'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { RoadmapTopicCard } from './index'; +/** + * jsdom reports every element as 0x0, so the card's clamp detection never + * fires. Force scrollHeight > clientHeight so the card renders as a button + * that opens the popover. + */ +const forceTruncation = () => { + vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(200); + vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(50); +}; + const topic = { title: 'Kubernetes fundamentals', priority: 'HIGH' as const, @@ -11,6 +21,8 @@ const topic = { }; describe('RoadmapTopicCard', () => { + afterEach(() => vi.restoreAllMocks()); + it('renders step, title, priority chip, rationale and gap marker', () => { render(); expect(screen.getByText('Step 1')).toBeInTheDocument(); @@ -45,4 +57,32 @@ describe('RoadmapTopicCard', () => { 'text-success', ); }); + + // The popover is absolutely positioned over the card but is WIDER than it + // (20rem vs the ~12rem timeline column), so the same text wraps into fewer + // lines and the popover ends up SHORTER than the card it covers. The bottom + // of the collapsed card — "Read more" and its border — then showed through + // underneath. Hiding the card while the popover is open is what stops that; + // it stays in flow (`invisible`, not `hidden`) so the timeline row keeps its + // height. + it('hides the collapsed card while the popover is open', () => { + forceTruncation(); + render(); + + const card = screen.getByRole('button', { name: /Step 3/ }); + expect(card).not.toHaveClass('invisible'); + + fireEvent.click(card); + + expect(card).toHaveClass('invisible'); + expect(card).toHaveAttribute('aria-hidden', 'true'); + expect(card).toHaveAttribute('inert'); + expect( + screen.getByRole('region', { name: /Kubernetes fundamentals/ }), + ).toBeInTheDocument(); + + fireEvent.click(screen.getByRole('button', { name: 'Close' })); + expect(card).not.toHaveClass('invisible'); + expect(card).not.toHaveAttribute('inert'); + }); }); diff --git a/web/src/components/molecules/roadmap-topic-card/index.tsx b/web/src/components/molecules/roadmap-topic-card/index.tsx index a3fe86e0..350c0ff0 100644 --- a/web/src/components/molecules/roadmap-topic-card/index.tsx +++ b/web/src/components/molecules/roadmap-topic-card/index.tsx @@ -27,6 +27,8 @@ export const RoadmapTopicCard = ({ step, topic }: RoadmapTopicCardProps) => { const rationaleRef = useRef(null); const titleRef = useRef(null); const rootRef = useRef(null); + const articleRef = useRef(null); + const panelRef = useRef(null); const panelId = useId(); const [isTruncated, setIsTruncated] = useState(false); const [isOpen, setIsOpen] = useState(false); @@ -38,6 +40,19 @@ export const RoadmapTopicCard = ({ step, topic }: RoadmapTopicCardProps) => { setIsTruncated(clamped(rationaleRef.current) || clamped(titleRef.current)); }, [topic.title, topic.rationale]); + // The card goes `inert` while the popover is open, which drops focus to + // . Hand it to the panel on open and take it back on close so keyboard + // users stay on this card. + const wasOpen = useRef(false); + useEffect(() => { + if (isOpen) { + panelRef.current?.focus(); + } else if (wasOpen.current && document.activeElement === document.body) { + articleRef.current?.focus(); + } + wasOpen.current = isOpen; + }, [isOpen]); + // Close the popover on outside-click or Escape. useEffect(() => { if (!isOpen) return; @@ -68,11 +83,16 @@ export const RoadmapTopicCard = ({ step, topic }: RoadmapTopicCardProps) => { return (
setIsOpen((v) => !v) : undefined} role={isTruncated ? 'button' : undefined} tabIndex={isTruncated ? 0 : undefined} @@ -117,9 +137,11 @@ export const RoadmapTopicCard = ({ step, topic }: RoadmapTopicCardProps) => { {isOpen && (

Step {step}