fix(config): announce tab validation state

This commit is contained in:
Supra4E8C
2026-08-06 03:30:50 +08:00
parent c7fe030cd3
commit 125cdbd843
2 changed files with 39 additions and 4 deletions
+12 -4
View File
@@ -74,6 +74,15 @@ export function ConfigTabs({
const Icon = CONFIG_TAB_ICONS[id];
const isActive = active === id;
const errorCount = errorCounts[id] ?? 0;
const isDirty = dirtyTabs.has(id);
const tabLabel = t(`config_management.visual.sections.${id}.title`);
const accessibleLabel = [
tabLabel,
errorCount > 0 ? t('config_management.meta_errors', { count: errorCount }) : null,
isDirty ? t('config_management.status_dirty_short') : null,
]
.filter(Boolean)
.join(', ');
return (
<button
@@ -86,6 +95,7 @@ export function ConfigTabs({
id={configTabDomId(id)}
aria-selected={isActive}
aria-controls={configPanelDomId(id)}
aria-label={accessibleLabel}
tabIndex={isActive ? 0 : -1}
className={`${styles.tab} ${isActive ? styles.tabActive : ''}`}
disabled={disabled}
@@ -93,15 +103,13 @@ export function ConfigTabs({
onKeyDown={handleKeyDown}
>
<Icon size={15} className={styles.tabGlyph} />
<span className={styles.tabLabel}>
{t(`config_management.visual.sections.${id}.title`)}
</span>
<span className={styles.tabLabel}>{tabLabel}</span>
{errorCount > 0 ? (
<span className={styles.tabBadge} aria-hidden="true">
{errorCount}
</span>
) : null}
{dirtyTabs.has(id) ? <span className={styles.tabDirtyDot} aria-hidden="true" /> : null}
{isDirty ? <span className={styles.tabDirtyDot} aria-hidden="true" /> : null}
</button>
);
})}
+27
View File
@@ -0,0 +1,27 @@
import { describe, expect, test } from 'bun:test';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import i18n from '@/i18n';
import { ConfigTabs } from '@/features/config/components/ConfigTabs';
const noop = () => {};
describe('ConfigTabs accessibility', () => {
test('announces validation errors and unsaved changes in the tab name', () => {
const markup = renderToStaticMarkup(
createElement(ConfigTabs, {
active: 'common',
errorCounts: { streaming: 2 },
dirtyTabs: new Set(['streaming']),
onChange: noop,
})
);
const accessibleLabel = [
i18n.t('config_management.visual.sections.streaming.title'),
i18n.t('config_management.meta_errors', { count: 2 }),
i18n.t('config_management.status_dirty_short'),
].join(', ');
expect(markup).toContain(`aria-label="${accessibleLabel}"`);
});
});