diff --git a/docs/testing/HEALTH_METRICS_TABS_TESTS.md b/docs/testing/HEALTH_METRICS_TABS_TESTS.md
new file mode 100644
index 00000000..faab4427
--- /dev/null
+++ b/docs/testing/HEALTH_METRICS_TABS_TESTS.md
@@ -0,0 +1,13 @@
+# Health Metrics Tabs Tests
+
+`src/components/dashboard/CommitmentHealthMetrics.test.tsx` covers the tab behavior for the health metrics card.
+
+## Coverage
+
+- Default Value History tab selection.
+- `role="tablist"`, `role="tab"`, `aria-selected`, and tabpanel labelling.
+- Click switching across Value History, Drawdown, Fee Generation, and Compliance.
+- Data handoff from each tab to its corresponding chart component.
+- Arrow key, Home, and End keyboard navigation.
+
+The chart components are mocked in the test so the suite can focus on tab state, accessibility, and prop routing without rendering Recharts internals.
diff --git a/src/components/dashboard/CommitmentHealthMetrics.test.tsx b/src/components/dashboard/CommitmentHealthMetrics.test.tsx
new file mode 100644
index 00000000..ac227f15
--- /dev/null
+++ b/src/components/dashboard/CommitmentHealthMetrics.test.tsx
@@ -0,0 +1,141 @@
+// @vitest-environment happy-dom
+
+import { fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { describe, expect, it, vi } from 'vitest'
+import CommitmentHealthMetrics from './CommitmentHealthMetrics'
+
+vi.mock('./HealthMetricsValueHistoryChart', () => ({
+ HealthMetricsValueHistoryChart: ({
+ data,
+ volatilityPercent,
+ }: {
+ data: Array<{ date: string; currentValue: number; initialAmount?: number }>
+ volatilityPercent?: number
+ }) => (
+
+ {JSON.stringify({ data, volatilityPercent })}
+
+ ),
+}))
+
+vi.mock('./HealthMetricsDrawdownChart', () => ({
+ HealthMetricsDrawdownChart: ({
+ data,
+ thresholdPercent,
+ volatilityPercent,
+ }: {
+ data: Array<{ date: string; drawdownPercent: number }>
+ thresholdPercent?: number
+ volatilityPercent?: number
+ }) => (
+
+ {JSON.stringify({ data, thresholdPercent, volatilityPercent })}
+
+ ),
+}))
+
+vi.mock('./HealthMetricsFeeGenerationChart', () => ({
+ HealthMetricsFeeGenerationChart: ({
+ data,
+ volatilityPercent,
+ }: {
+ data: Array<{ date: string; feeAmount: number }>
+ volatilityPercent?: number
+ }) => (
+
+ {JSON.stringify({ data, volatilityPercent })}
+
+ ),
+}))
+
+vi.mock('./HealthMetricsComplianceChart', () => ({
+ HealthMetricsComplianceChart: ({
+ data,
+ }: {
+ data: Array<{ date: string; complianceScore: number }>
+ }) => (
+
+ {JSON.stringify({ data })}
+
+ ),
+}))
+
+const props = {
+ complianceData: [{ date: 'Jun 3', complianceScore: 98 }],
+ drawdownData: [{ date: 'Jun 2', drawdownPercent: 0.12 }],
+ valueHistoryData: [{ date: 'Jun 1', currentValue: 1500, initialAmount: 1000 }],
+ feeGenerationData: [{ date: 'Jun 4', feeAmount: 42 }],
+ thresholdPercent: 0.2,
+ volatilityPercent: 17,
+}
+
+function getTabs() {
+ return {
+ value: screen.getByRole('tab', { name: /value history/i }),
+ drawdown: screen.getByRole('tab', { name: /drawdown/i }),
+ fee: screen.getByRole('tab', { name: /fee generation/i }),
+ compliance: screen.getByRole('tab', { name: /compliance/i }),
+ }
+}
+
+describe('CommitmentHealthMetrics', () => {
+ it('renders the value tab by default with tab semantics and value data', () => {
+ render( )
+
+ const tabs = getTabs()
+
+ expect(screen.getByRole('tablist', { name: 'Health metric charts' })).toBeInTheDocument()
+ expect(tabs.value).toHaveAttribute('aria-selected', 'true')
+ expect(tabs.drawdown).toHaveAttribute('aria-selected', 'false')
+ expect(screen.getByRole('tabpanel')).toHaveAttribute('aria-labelledby', 'health-metric-tab-value')
+ expect(screen.getByTestId('value-chart')).toHaveTextContent('"currentValue":1500')
+ expect(screen.getByTestId('value-chart')).toHaveTextContent('"volatilityPercent":17')
+ })
+
+ it('switches to each chart tab and passes the matching data props', () => {
+ render( )
+
+ fireEvent.click(getTabs().drawdown)
+ expect(getTabs().drawdown).toHaveAttribute('aria-selected', 'true')
+ expect(screen.getByRole('tabpanel')).toHaveAttribute('aria-labelledby', 'health-metric-tab-drawdown')
+ expect(screen.getByTestId('drawdown-chart')).toHaveTextContent('"drawdownPercent":0.12')
+ expect(screen.getByTestId('drawdown-chart')).toHaveTextContent('"thresholdPercent":0.2')
+
+ fireEvent.click(getTabs().fee)
+ expect(getTabs().fee).toHaveAttribute('aria-selected', 'true')
+ expect(screen.getByRole('tabpanel')).toHaveAttribute('aria-labelledby', 'health-metric-tab-fee')
+ expect(screen.getByTestId('fee-chart')).toHaveTextContent('"feeAmount":42')
+
+ fireEvent.click(getTabs().compliance)
+ expect(getTabs().compliance).toHaveAttribute('aria-selected', 'true')
+ expect(screen.getByRole('tabpanel')).toHaveAttribute('aria-labelledby', 'health-metric-tab-compliance')
+ expect(screen.getByTestId('compliance-chart')).toHaveTextContent('"complianceScore":98')
+ })
+
+ it('supports arrow, Home, and End keyboard navigation across tabs', async () => {
+ render( )
+
+ const tabs = getTabs()
+
+ tabs.value.focus()
+ fireEvent.keyDown(tabs.value, { key: 'ArrowRight' })
+
+ await waitFor(() => expect(getTabs().drawdown).toHaveAttribute('aria-selected', 'true'))
+ expect(getTabs().drawdown).toHaveFocus()
+
+ fireEvent.keyDown(getTabs().drawdown, { key: 'ArrowLeft' })
+
+ await waitFor(() => expect(getTabs().value).toHaveAttribute('aria-selected', 'true'))
+ expect(getTabs().value).toHaveFocus()
+
+ fireEvent.keyDown(getTabs().value, { key: 'End' })
+
+ await waitFor(() => expect(getTabs().compliance).toHaveAttribute('aria-selected', 'true'))
+ expect(getTabs().compliance).toHaveFocus()
+
+ fireEvent.keyDown(getTabs().compliance, { key: 'Home' })
+
+ await waitFor(() => expect(getTabs().value).toHaveAttribute('aria-selected', 'true'))
+ expect(getTabs().value).toHaveFocus()
+ })
+})
diff --git a/src/components/dashboard/CommitmentHealthMetrics.tsx b/src/components/dashboard/CommitmentHealthMetrics.tsx
index 416ef633..6148155a 100644
--- a/src/components/dashboard/CommitmentHealthMetrics.tsx
+++ b/src/components/dashboard/CommitmentHealthMetrics.tsx
@@ -15,6 +15,13 @@ function cn(...inputs: ClassValue[]) {
type TabType = 'value' | 'drawdown' | 'fee' | 'compliance';
+const tabs: { id: TabType; label: string }[] = [
+ { id: 'value', label: 'Value History' },
+ { id: 'drawdown', label: 'Drawdown' },
+ { id: 'fee', label: 'Fee Generation' },
+ { id: 'compliance', label: 'Compliance' },
+];
+
const tabIcons: Record = {
value: ,
drawdown: ,
@@ -41,23 +48,50 @@ export default function CommitmentHealthMetrics({
}: CommitmentHealthMetricsProps) {
const [activeTab, setActiveTab] = useState('value');
- const tabs: { id: TabType; label: string }[] = [
- { id: 'value', label: 'Value History' },
- { id: 'drawdown', label: 'Drawdown' },
- { id: 'fee', label: 'Fee Generation' },
- { id: 'compliance', label: 'Compliance' },
- ];
+ const handleTabKeyDown = (event: React.KeyboardEvent, index: number) => {
+ const lastIndex = tabs.length - 1;
+ let nextIndex: number | null = null;
+
+ if (event.key === 'ArrowRight') {
+ nextIndex = index === lastIndex ? 0 : index + 1;
+ } else if (event.key === 'ArrowLeft') {
+ nextIndex = index === 0 ? lastIndex : index - 1;
+ } else if (event.key === 'Home') {
+ nextIndex = 0;
+ } else if (event.key === 'End') {
+ nextIndex = lastIndex;
+ }
+
+ if (nextIndex !== null) {
+ event.preventDefault();
+ setActiveTab(tabs[nextIndex].id);
+ event.currentTarget
+ .closest('[role="tablist"]')
+ ?.querySelectorAll('[role="tab"]')
+ [nextIndex]?.focus();
+ }
+ };
return (
Health Metrics
-
- {tabs.map((tab) => (
+
+ {tabs.map((tab, index) => (
setActiveTab(tab.id)}
+ onKeyDown={(event) => handleTabKeyDown(event, index)}
+ role="tab"
+ id={`health-metric-tab-${tab.id}`}
+ aria-selected={activeTab === tab.id}
+ aria-controls={`health-metric-panel-${tab.id}`}
+ tabIndex={activeTab === tab.id ? 0 : -1}
className={cn(
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200',
activeTab === tab.id
@@ -72,7 +106,12 @@ export default function CommitmentHealthMetrics({
-
+
{activeTab === 'value' && (