feat(roadmap): add expand/collapse functionality for phase features (#1796)

* feat(roadmap): add expand/collapse functionality for phase features

Previously, only 5 features were displayed per phase with a non-clickable
"+X more features" text. This commit adds:
- useState hook to track expanded/collapsed state per phase
- Clickable "Show X more features" / "Show less" toggle button
- ChevronDown/ChevronUp icons for visual feedback
- i18n translations for expand/collapse labels (EN/FR)

* fix(roadmap): use Button component for expand/collapse toggle

Replace raw <button> with Button component for styling consistency.
Add aria-expanded attribute for keyboard and screen reader accessibility.

* fix(i18n): add pluralization for showMoreFeatures key

* fix(roadmap): improve accessibility with functional setState and button elements

- Use functional setState for isExpanded toggle
- Change feature item from div to button for keyboard accessibility
- Add type='button' and w-full text-left classes for proper layout

* fix(roadmap): avoid nested buttons for accessibility

Use div with role='button', tabIndex, and onKeyDown instead of button
to avoid invalid nested interactive elements with inner Button components.

* fix(roadmap): restructure feature row to avoid nested interactive elements

- Remove role/button attributes from outer container div
- Make the title/label area a semantic button for feature selection
- Keep action buttons (View Task, Build) as independent clickable elements

* fix(roadmap): add focus-visible styles for keyboard accessibility

---------

Co-authored-by: Andy <119136210+AndyMik90@users.noreply.github.com>
This commit is contained in:
Burak
2026-02-13 14:59:19 +01:00
committed by GitHub
parent a4870fa0c3
commit f735f0b49b
3 changed files with 46 additions and 12 deletions
@@ -1,4 +1,6 @@
import { CheckCircle2, Circle, ExternalLink, Play, TrendingUp } from 'lucide-react';
import { useState } from 'react';
import { CheckCircle2, ChevronDown, ChevronUp, Circle, ExternalLink, Play, TrendingUp } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { TaskOutcomeBadge } from './TaskOutcomeBadge';
import { Badge } from '../ui/badge';
import { Button } from '../ui/button';
@@ -7,6 +9,8 @@ import { Progress } from '../ui/progress';
import { ROADMAP_PRIORITY_COLORS } from '../../../shared/constants';
import type { PhaseCardProps } from './types';
const INITIAL_VISIBLE_COUNT = 5;
export function PhaseCard({
phase,
features,
@@ -15,8 +19,13 @@ export function PhaseCard({
onConvertToSpec,
onGoToTask,
}: PhaseCardProps) {
const { t } = useTranslation('common');
const [isExpanded, setIsExpanded] = useState(false);
const completedCount = features.filter((f) => f.status === 'done').length;
const progress = features.length > 0 ? (completedCount / features.length) * 100 : 0;
const visibleFeatures = isExpanded ? features : features.slice(0, INITIAL_VISIBLE_COUNT);
const hiddenCount = features.length - INITIAL_VISIBLE_COUNT;
const hasMoreFeatures = hiddenCount > 0;
return (
<Card className="p-4">
@@ -87,13 +96,16 @@ export function PhaseCard({
<div>
<h4 className="text-sm font-medium mb-2">Features ({features.length})</h4>
<div className="grid gap-2">
{features.slice(0, 5).map((feature) => (
{visibleFeatures.map((feature) => (
<div
key={feature.id}
className="flex items-center justify-between p-2 rounded-md bg-muted/50 hover:bg-muted cursor-pointer transition-colors"
onClick={() => onFeatureSelect(feature)}
className="flex items-center justify-between p-2 rounded-md bg-muted/50 hover:bg-muted transition-colors"
>
<div className="flex items-center gap-2 flex-1 min-w-0">
<button
type="button"
className="flex items-center gap-2 flex-1 min-w-0 text-left cursor-pointer rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
onClick={() => onFeatureSelect(feature)}
>
<Badge
variant="outline"
className={`text-xs ${ROADMAP_PRIORITY_COLORS[feature.priority]}`}
@@ -104,7 +116,7 @@ export function PhaseCard({
{feature.competitorInsightIds && feature.competitorInsightIds.length > 0 && (
<TrendingUp className="h-3 w-3 text-primary flex-shrink-0" />
)}
</div>
</button>
{feature.taskOutcome ? (
<span className="flex-shrink-0">
<TaskOutcomeBadge outcome={feature.taskOutcome} size="lg" showLabel={false} />
@@ -140,10 +152,26 @@ export function PhaseCard({
)}
</div>
))}
{features.length > 5 && (
<div className="text-sm text-muted-foreground text-center py-1">
+{features.length - 5} more features
</div>
{hasMoreFeatures && (
<Button
type="button"
variant="ghost"
onClick={() => setIsExpanded((prev) => !prev)}
aria-expanded={isExpanded}
className="flex items-center justify-center gap-1 text-sm text-muted-foreground hover:text-foreground w-full"
>
{isExpanded ? (
<>
<ChevronUp className="h-4 w-4" />
{t('roadmap.showLessFeatures')}
</>
) : (
<>
<ChevronDown className="h-4 w-4" />
{t('roadmap.showMoreFeatures', { count: hiddenCount })}
</>
)}
</Button>
)}
</div>
</div>
@@ -599,7 +599,10 @@
"roadmap": {
"taskCompleted": "Completed",
"taskDeleted": "Deleted",
"taskArchived": "Archived"
"taskArchived": "Archived",
"showMoreFeatures": "Show {{count}} more feature",
"showMoreFeatures_plural": "Show {{count}} more features",
"showLessFeatures": "Show less"
},
"roadmapGeneration": {
"progress": "Progress",
@@ -599,7 +599,10 @@
"roadmap": {
"taskCompleted": "Terminé",
"taskDeleted": "Supprimé",
"taskArchived": "Archivé"
"taskArchived": "Archivé",
"showMoreFeatures": "Afficher {{count}} fonctionnalité supplémentaire",
"showMoreFeatures_plural": "Afficher {{count}} fonctionnalités supplémentaires",
"showLessFeatures": "Afficher moins"
},
"roadmapGeneration": {
"progress": "Progression",