fix(web): complete plugin tab accessibility
This commit is contained in:
@@ -58,7 +58,9 @@
|
||||
}
|
||||
|
||||
.tab:focus-visible {
|
||||
outline: none;
|
||||
outline: 2px solid var(--dsw-alias-state-business-primary);
|
||||
outline-offset: 2px;
|
||||
border-radius: 2px;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** Plugins settings section: localized tabs around feature-owned pages. */
|
||||
|
||||
import { useEffect, useId, useState } from 'react'
|
||||
import { useEffect, useId, useRef, useState } from 'react'
|
||||
import type {
|
||||
HostObservable, InjectFace, PropsLocale, PropsRenderSlots, PropsRuntime,
|
||||
} from '@deepseek-ai/dsh-client-ui-slots'
|
||||
@@ -32,6 +32,7 @@ export type PluginConfigSectionProps =
|
||||
/** Render one Plugins page whose contents arrive from feature-owned tabs. */
|
||||
export function PluginConfigSection({ t, renderSlot, useTabs }: PluginConfigSectionProps) {
|
||||
const tabsId = useId()
|
||||
const tabRefs = useRef<Array<HTMLButtonElement | null>>([])
|
||||
const rows = useTabs(value => value)
|
||||
const [activeId, setActiveId] = useState<string>()
|
||||
const [visitedIds, setVisitedIds] = useState<ReadonlySet<string>>(() => new Set())
|
||||
@@ -55,11 +56,12 @@ export function PluginConfigSection({ t, renderSlot, useTabs }: PluginConfigSect
|
||||
{rows.length === 0 ? <p className={css.empty}>{t('empty')}</p> : (
|
||||
<>
|
||||
<div className={css.tabs} role="tablist" aria-label={t('tabs')}>
|
||||
{rows.map((row) => {
|
||||
{rows.map((row, index) => {
|
||||
const selected = row.id === active
|
||||
return (
|
||||
<button
|
||||
key={row.id}
|
||||
ref={(element) => { tabRefs.current[index] = element }}
|
||||
id={`${tabsId}-tab-${row.id}`}
|
||||
type="button"
|
||||
role="tab"
|
||||
@@ -67,7 +69,23 @@ export function PluginConfigSection({ t, renderSlot, useTabs }: PluginConfigSect
|
||||
aria-selected={selected}
|
||||
aria-controls={`${tabsId}-panel-${row.id}`}
|
||||
data-active={selected ? 'true' : undefined}
|
||||
tabIndex={selected ? 0 : -1}
|
||||
onClick={() => { setActiveId(row.id) }}
|
||||
onKeyDown={(event) => {
|
||||
let nextIndex: number
|
||||
switch (event.key) {
|
||||
case 'ArrowRight': nextIndex = (index + 1) % rows.length; break
|
||||
case 'ArrowLeft': nextIndex = (index - 1 + rows.length) % rows.length; break
|
||||
case 'Home': nextIndex = 0; break
|
||||
case 'End': nextIndex = rows.length - 1; break
|
||||
default: return
|
||||
}
|
||||
event.preventDefault()
|
||||
const nextRow = rows[nextIndex] as PluginSettingsTabRow
|
||||
const nextTab = tabRefs.current[nextIndex] as HTMLButtonElement
|
||||
setActiveId(nextRow.id)
|
||||
nextTab.focus()
|
||||
}}
|
||||
>
|
||||
{row.label}
|
||||
</button>
|
||||
|
||||
@@ -117,6 +117,38 @@ describe('PluginConfigSection', () => {
|
||||
expect(screen.getByRole('heading', { name: en.title })).toBeTruthy()
|
||||
expect(screen.getByText(en.intro)).toBeTruthy()
|
||||
})
|
||||
|
||||
it('moves focus and selection with standard horizontal tab keys', () => {
|
||||
renderSection([
|
||||
{ id: 'configurable', order: 0, label: en.configurableTab },
|
||||
{ id: 'all', order: 10, label: 'Plugin list' },
|
||||
{ id: 'diagnostics', order: 20, label: 'Diagnostics' },
|
||||
])
|
||||
|
||||
const configurable = screen.getByRole('tab', { name: en.configurableTab })
|
||||
const all = screen.getByRole('tab', { name: 'Plugin list' })
|
||||
const diagnostics = screen.getByRole('tab', { name: 'Diagnostics' })
|
||||
expect(configurable.getAttribute('tabindex')).toBe('0')
|
||||
expect(all.getAttribute('tabindex')).toBe('-1')
|
||||
|
||||
configurable.focus()
|
||||
fireEvent.keyDown(configurable, { key: 'ArrowRight' })
|
||||
expect(document.activeElement).toBe(all)
|
||||
expect(all.getAttribute('aria-selected')).toBe('true')
|
||||
|
||||
fireEvent.keyDown(all, { key: 'End' })
|
||||
expect(document.activeElement).toBe(diagnostics)
|
||||
fireEvent.keyDown(diagnostics, { key: 'ArrowRight' })
|
||||
expect(document.activeElement).toBe(configurable)
|
||||
fireEvent.keyDown(configurable, { key: 'ArrowLeft' })
|
||||
expect(document.activeElement).toBe(diagnostics)
|
||||
fireEvent.keyDown(diagnostics, { key: 'Home' })
|
||||
expect(document.activeElement).toBe(configurable)
|
||||
|
||||
fireEvent.keyDown(configurable, { key: 'Escape' })
|
||||
expect(document.activeElement).toBe(configurable)
|
||||
expect(configurable.getAttribute('aria-selected')).toBe('true')
|
||||
})
|
||||
})
|
||||
|
||||
describe('ConfigurablePluginsTab', () => {
|
||||
|
||||
Reference in New Issue
Block a user