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
20 changes: 13 additions & 7 deletions components/roadmap/RoadmapItem.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useId, useState } from 'react';

// Since a roadmap item can contain nested roadmap lists, we need to import RoadmapList to display them.
/* eslint-disable import/no-cycle*/
Expand Down Expand Up @@ -26,7 +26,8 @@ export interface IRoadmapItemProps {
*/
export default function RoadmapItem({ item, colorClass, showConnector = true, collapsed = true }: IRoadmapItemProps) {
const [isCollapsed, setIsCollapsed] = useState(collapsed);
const isCollapsible = item.solutions !== undefined;
const isCollapsible = item.solutions !== undefined || item.implementations !== undefined;
const collapsibleContentId = useId();
Comment on lines +29 to +30
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Base collapsibility on child count, not just field presence.

solutions: [] or implementations: [] currently marks the item as collapsible, but the child lists only render when .length is truthy. That leaves an expand/collapse control wired to an empty container.

Suggested fix
-  const isCollapsible = item.solutions !== undefined || item.implementations !== undefined;
+  const hasSolutions = (item.solutions?.length ?? 0) > 0;
+  const hasImplementations = (item.implementations?.length ?? 0) > 0;
+  const isCollapsible = hasSolutions || hasImplementations;
   const collapsibleContentId = useId();
@@
-          {!isCollapsed && item?.solutions?.length && (
+          {!isCollapsed && hasSolutions && (
             <RoadmapList className='ml-2 pt-3' colorClass='bg-blue-400' items={item.solutions} collapsed={false} />
           )}
 
-          {!isCollapsed && item?.implementations?.length && (
+          {!isCollapsed && hasImplementations && (
             <RoadmapList className='ml-9 pt-3' colorClass='bg-black' items={item.implementations} collapsed={false} />
           )}

Also applies to: 53-61

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/roadmap/RoadmapItem.tsx` around lines 29 - 30, The collapsible
control is enabled when solutions or implementations fields exist even if they
are empty; change the logic in the isCollapsible computation to check actual
child counts (e.g., use item.solutions?.length and item.implementations?.length)
so isCollapsible is true only when the total number of children > 0, and apply
the same count-based check where the collapse control and content rendering are
handled (referencing isCollapsible, collapsibleContentId and the render block
around lines 53-61) so the toggle is not shown for empty lists.


const connectorClasses = 'border-l-2 border-dashed border-gray-300';
const classNames = `pt-2 ${showConnector && connectorClasses}`;
Expand All @@ -45,15 +46,20 @@ export default function RoadmapItem({ item, colorClass, showConnector = true, co
isCollapsed={isCollapsed}
isCollapsible={isCollapsible}
onClickCollapse={() => setIsCollapsed(!isCollapsed)}
collapsibleContentId={collapsibleContentId}
/>
</div>

{!isCollapsed && item?.solutions?.length && (
<RoadmapList className='ml-2 pt-3' colorClass='bg-blue-400' items={item.solutions} collapsed={false} />
)}
{isCollapsible && (
<div id={collapsibleContentId} hidden={isCollapsed}>
{!isCollapsed && item?.solutions?.length && (
<RoadmapList className='ml-2 pt-3' colorClass='bg-blue-400' items={item.solutions} collapsed={false} />
)}

{!isCollapsed && item?.implementations?.length && (
<RoadmapList className='ml-9 pt-3' colorClass='bg-black' items={item.implementations} collapsed={false} />
{!isCollapsed && item?.implementations?.length && (
<RoadmapList className='ml-9 pt-3' colorClass='bg-black' items={item.implementations} collapsed={false} />
)}
</div>
)}
</li>
);
Expand Down
65 changes: 48 additions & 17 deletions components/roadmap/RoadmapPill.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ interface IPillProps {
isCollapsible?: boolean;
isCollapsed?: boolean;
onClickCollapse?: () => void;
collapsibleContentId?: string;
}

/**
Expand All @@ -55,9 +56,45 @@ export default function Pill({
colorClass = '',
isCollapsible = false,
isCollapsed = false,
onClickCollapse = () => {}
onClickCollapse = () => {},
collapsibleContentId
}: IPillProps) {
const [isDescriptionVisible, setIsDescriptionVisible] = useState(false);
const isDescriptionTrigger = !item.url && Boolean(item.description);
const interactiveClassName =
'block rounded-md text-left font-medium text-gray-900 transition-colors hover:text-gray-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet focus-visible:ring-offset-2';
const titleContent = (
<>
{item.done && (
<span title='Done!'>
<DoneIcon />
</span>
)}
<span>{item.title}</span>
</>
);

let titleElement = <span className='block text-left font-medium text-gray-900'>{item.title}</span>;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep the done icon in the non-interactive title path.

The fallback branch renders only item.title, so completed pills without a URL or description no longer show the checkmark that titleContent already builds. That’s a visible regression for done roadmap items.

Suggested fix
-  let titleElement = <span className='block text-left font-medium text-gray-900'>{item.title}</span>;
+  let titleElement = <span className='block text-left font-medium text-gray-900'>{titleContent}</span>;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/roadmap/RoadmapPill.tsx` at line 77, The non-interactive fallback
assigns titleElement to a plain span with item.title, which loses the
completed/checkmark rendering built by titleContent; update the fallback so it
uses the same construction as titleContent (or delegate to titleContent) when
item.done is true so the done icon/checkmark remains visible for completed
items—look for titleElement, titleContent and the item.done logic in the
RoadmapPill component and ensure the fallback path composes the done state the
same way as the primary titleContent.


if (item.url) {
titleElement = (
<a href={item.url} rel='noopener noreferrer' className={`${interactiveClassName} cursor-pointer`}>
{titleContent}
</a>
);
} else if (isDescriptionTrigger) {
titleElement = (
<button
type='button'
onClick={() => setIsDescriptionVisible(true)}
aria-label={`Open details for ${item.title}`}
aria-haspopup='dialog'
className={`${interactiveClassName} cursor-pointer`}
>
{titleContent}
</button>
);
}

return (
<>
Expand All @@ -71,23 +108,17 @@ export default function Pill({
'flex flex-1 items-center justify-between rounded-r-md border-y border-r border-gray-200 bg-white'
}
>
<div className='px-4 py-2 text-sm'>
<a
href={item.url}
rel='noopener noreferrer'
onClick={() => !item.url && item.description && setIsDescriptionVisible(true)}
className={`block text-left font-medium text-gray-900 ${item.description || item.url ? 'cursor-pointer hover:text-gray-600' : 'cursor-default'}`}
>
{item.done && (
<span title='Done!'>
<DoneIcon />
</span>
)}
<span>{item.title}</span>
</a>
</div>
<div className='px-4 py-2 text-sm'>{titleElement}</div>
{isCollapsible && (
<button className='mr-2' onClick={onClickCollapse} data-testid='RoadmapItem-button'>
<button
type='button'
className='mr-2 rounded-md p-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet focus-visible:ring-offset-2'
onClick={onClickCollapse}
data-testid='RoadmapItem-button'
aria-expanded={!isCollapsed}
aria-controls={collapsibleContentId}
aria-label={`${isCollapsed ? 'Expand' : 'Collapse'} ${item.title}`}
>
<IconArrowRight className={`h-4 ${isCollapsed ? 'rotate-90' : '-rotate-90'}`} />
</button>
)}
Expand Down
4 changes: 2 additions & 2 deletions config/tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -899,7 +899,7 @@
"borderColor": "border-[#CA1A33]"
},
{
"name": "Liquid",
"name": "AsyncAPI CLI",
"color": "bg-[#61d0f2]",
"borderColor": "border-[#40ccf7]"
},
Expand Down Expand Up @@ -1622,7 +1622,7 @@
"borderColor": "border-[#CA1A33]"
},
{
"name": "Liquid",
"name": "AsyncAPI CLI",
"color": "bg-[#61d0f2]",
"borderColor": "border-[#40ccf7]"
},
Expand Down
6 changes: 5 additions & 1 deletion tests/babel.test.config.cts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
*/

module.exports = {
presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'],
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
['@babel/preset-react', { runtime: 'automatic' }],
'@babel/preset-typescript'
],
plugins: ['babel-plugin-transform-import-meta']
};
62 changes: 62 additions & 0 deletions tests/roadmap/accessibility.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';

import RoadmapItem from '../../components/roadmap/RoadmapItem';
import RoadmapPill from '../../components/roadmap/RoadmapPill';

describe('roadmap accessibility', () => {
test('renders description-only roadmap pills as accessible buttons', () => {
const markup = renderToStaticMarkup(
<RoadmapPill
item={{
title: 'Accessible roadmap item',
description: 'Extra details'
}}
/>
);

expect(markup).toContain('<button');
expect(markup).toContain('type="button"');
expect(markup).toContain('aria-label="Open details for Accessible roadmap item"');
expect(markup).toContain('aria-haspopup="dialog"');
expect(markup).toContain('focus-visible:ring-2');
expect(markup).not.toContain('<a');
});

test('connects collapsible roadmap items to controlled content', () => {
const markup = renderToStaticMarkup(
<RoadmapItem
item={{
title: 'Expandable roadmap item',
solutions: [{ title: 'Nested solution' }]
}}
colorClass='bg-blue-400'
/>
);

const controlsMatch = markup.match(/aria-controls="([^"]+)"/);

expect(controlsMatch).not.toBeNull();
expect(markup).toContain('aria-expanded="false"');
expect(markup).toContain(`id="${controlsMatch?.[1]}"`);
expect(markup).toContain('aria-label="Expand Expandable roadmap item"');
expect(markup).toContain('hidden=""');
});

test('marks collapsible roadmap items as expanded when children are visible', () => {
const markup = renderToStaticMarkup(
<RoadmapItem
item={{
title: 'Expanded roadmap item',
solutions: [{ title: 'Visible child' }]
}}
colorClass='bg-blue-400'
collapsed={false}
/>
);

expect(markup).toContain('aria-expanded="true"');
expect(markup).toContain('aria-label="Collapse Expanded roadmap item"');
expect(markup).toContain('Visible child');
});
});
Loading