fix(web): address model search accessibility review
Signed-off-by: Yao Meng <[email protected]>
This commit is contained in:
1 parent
4ff33be0bd
commit
b3d6da9083
9 files changed
+44
-28
No files matched your search
@@ -80,7 +80,7 @@ describe.skipIf(webSnapshotMode() === 'record')('web e2e: DeepSeek Messages sett
|
||||
await page.getByRole('menuitem', { name: /模型/ }).click()
|
||||
await page.getByRole('menuitemradio', { name: 'Messages Flash', exact: true }).waitFor()
|
||||
await compareOrRefreshGolden(join(EXPECTED, 'picker.expected.md'),
|
||||
await captureStableAria(page, '[role="menu"]', scaffold.workspaceCwd), webSnapshotMode())
|
||||
await captureStableAria(page, '[role="group"][aria-label="模型与推理等级"]', scaffold.workspaceCwd), webSnapshotMode())
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
|
||||
@@ -115,10 +115,12 @@ describe('web e2e: the composer model switch is the default for later sessions',
|
||||
expect(await page.getByRole('group', { name: 'Origin Gateway', exact: true }).count()).toBe(0)
|
||||
await compareOrRefreshGolden(
|
||||
fileURLToPath(new URL('./expected/default-model/search.expected.md', import.meta.url)),
|
||||
await captureStableAria(page, '[role="menu"]', scaffold.workspaceCwd),
|
||||
await captureStableAria(page, '[role="group"][aria-label="模型与推理等级"]', scaffold.workspaceCwd),
|
||||
webSnapshotMode(),
|
||||
)
|
||||
await search.press('ArrowDown')
|
||||
await expect.poll(() => page.getByRole('menuitemradio', { name: 'Acme Large' })
|
||||
.evaluate(row => row === row.ownerDocument.activeElement)).toBe(true)
|
||||
const entered = Promise.withResolvers<undefined>()
|
||||
const release = Promise.withResolvers<undefined>()
|
||||
const blocked = scaffold.ctx.hmr.runExclusive(async () => {
|
||||
@@ -127,7 +129,7 @@ describe('web e2e: the composer model switch is the default for later sessions',
|
||||
})
|
||||
try {
|
||||
await entered.promise
|
||||
await page.getByRole('menuitemradio', { name: 'Acme Large' }).press('Enter')
|
||||
await page.keyboard.press('Enter')
|
||||
await expect.poll(() => trigger.getAttribute('aria-busy')).toBe('false')
|
||||
await expect.poll(() => trigger.textContent()).toContain('Acme Large')
|
||||
expect(scaffold.ctx.agentDefaultModel.currentSelection()).toEqual({ provider: START_ROUTE, model: START_MODEL })
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
- menu "模型与推理等级":
|
||||
- group "模型与推理等级":
|
||||
- searchbox "搜索模型…"
|
||||
- group "DeepSeek":
|
||||
- text: DeepSeek
|
||||
- menuitemradio "Messages Flash" [checked]
|
||||
- menuitemradio "DeepSeek-V4-Pro"
|
||||
- menu "模型":
|
||||
- group "DeepSeek":
|
||||
- text: DeepSeek
|
||||
- menuitemradio "Messages Flash" [checked]
|
||||
- menuitemradio "DeepSeek-V4-Pro"
|
||||
@@ -1,5 +1,6 @@
|
||||
- menu "模型与推理等级":
|
||||
- group "模型与推理等级":
|
||||
- searchbox "搜索模型…": ACMLG
|
||||
- group "Acme Gateway":
|
||||
- text: Acme Gateway
|
||||
- menuitemradio "Acme Large"
|
||||
- menu "模型":
|
||||
- group "Acme Gateway":
|
||||
- text: Acme Gateway
|
||||
- menuitemradio "Acme Large"
|
||||
@@ -18,8 +18,8 @@
|
||||
en: 292d778a6bfb4130
|
||||
zh: 15009b57f706cfb4
|
||||
/deepseek-ai-dsh-client-ui-model-selection/use-this-package/model-and-effort:
|
||||
en: 89daf261ce28f29a
|
||||
zh: c72833dfb8345daf
|
||||
en: 4cf17d5a20e43800
|
||||
zh: 2e865599fbf6c591
|
||||
/deepseek-ai-dsh-client-ui-model-selection/use-this-package/unroutable-sessions:
|
||||
en: 7f20428c747ff052
|
||||
zh: 975e6de728e02fa0
|
||||
|
||||
@@ -37,7 +37,7 @@ Mouse selection uses native browser clicks, including their cancellation behavio
|
||||
|
||||
### Model and effort
|
||||
|
||||
Models stay grouped by provider. The search field at the top of the composer model list matches model names case-insensitively, including nonconsecutive characters in order; leading and trailing spaces are ignored. Empty groups are hidden. Tab or an arrow key moves from search into the results; reopening the model pane clears the query. The composer menu shows model and effort names only, with DeepSeek Account first and DeepSeek second; third-party providers retain their catalog order. Navigation chevrons use `--dsw-alias-menu-icon`. The `/model` popup shows provider names and catalog descriptions; it localizes the two built-in DeepSeek descriptions and leaves external provider descriptions verbatim. The popup applies the selected model's default effort; the composer can then choose any advertised effort. An adapter without reasoning metadata leaves the Effort row absent; there is no arbitrary effort input.
|
||||
Models stay grouped by provider. The search field at the top of the composer model list matches model names case-insensitively, including nonconsecutive characters in order; leading and trailing spaces are ignored. Within each provider, prefix matches come first, followed by alignment score and then catalog order. Empty groups are hidden. Tab or an arrow key moves from search into the results; reopening the model pane clears the query. The composer menu shows model and effort names only, with DeepSeek Account first and DeepSeek second; third-party providers retain their catalog order. Navigation chevrons use `--dsw-alias-menu-icon`. The `/model` popup shows provider names and catalog descriptions; it localizes the two built-in DeepSeek descriptions and leaves external provider descriptions verbatim. The popup applies the selected model's default effort; the composer can then choose any advertised effort. An adapter without reasoning metadata leaves the Effort row absent; there is no arbitrary effort input.
|
||||
|
||||
The composer replaces the model and effort text with the Models icon when the expanded controls cannot share one line, and restores the text when space permits. The full selection remains available in the trigger's accessible name, tooltip, and menu.
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ DeepSeek 账号和 API Key 路由显示为独立提供方分组,各自展示
|
||||
|
||||
### 模型与推理强度
|
||||
|
||||
模型按提供方分组。composer 模型列表顶部的搜索框按名称进行不区分大小写的匹配,支持按顺序输入不连续字符,并忽略首尾空格。没有匹配项的分组会隐藏。Tab 或方向键从搜索框进入结果列表;重新打开模型面板会清空搜索内容。composer 菜单只显示模型与推理强度名称,DeepSeek 账号排第一,DeepSeek 排第二,第三方提供方保持目录原有顺序。导航箭头使用 `--dsw-alias-menu-icon` 文本色。`/model` 弹窗显示提供方名称与目录说明;其中两个内置 DeepSeek 模型的说明使用当前语言,外部提供方说明保持原文。弹窗应用所选模型的默认推理强度;composer 随后可以选择任一已公布的推理强度。适配器没有推理元数据时不显示 Effort 行;不存在任意推理强度输入。
|
||||
模型按提供方分组。composer 模型列表顶部的搜索框按名称进行不区分大小写的匹配,支持按顺序输入不连续字符,并忽略首尾空格。每个提供方内的结果优先按前缀匹配、其次按匹配得分、最后按目录顺序排列。没有匹配项的分组会隐藏。Tab 或方向键从搜索框进入结果列表;重新打开模型面板会清空搜索内容。composer 菜单只显示模型与推理强度名称,DeepSeek 账号排第一,DeepSeek 排第二,第三方提供方保持目录原有顺序。导航箭头使用 `--dsw-alias-menu-icon` 文本色。`/model` 弹窗显示提供方名称与目录说明;其中两个内置 DeepSeek 模型的说明使用当前语言,外部提供方说明保持原文。弹窗应用所选模型的默认推理强度;composer 随后可以选择任一已公布的推理强度。适配器没有推理元数据时不显示 Effort 行;不存在任意推理强度输入。
|
||||
|
||||
展开的控件无法排在同一行时,composer 将模型与推理强度文字替换为模型图标;空间足够后恢复文字。触发器的无障碍名称、提示和菜单仍提供完整选择。
|
||||
|
||||
|
||||
@@ -10,15 +10,17 @@
|
||||
* settles like Enter, and Escape and Shift+Tab leave a drilled pane first and
|
||||
* otherwise close back to the trigger. A drilled pane hands focus to the row
|
||||
* of the effort in use or the model search field. Model names match a
|
||||
* case-insensitive ordered subsequence within each provider group. Returning
|
||||
* to the root pane hands focus back to the cell that opened it. Data and
|
||||
* submission ride the SAME per-session ModelDirectory as the /model popup; exact-model reasoning metadata and the
|
||||
* selected effort come from the Host rather than a client-owned vocabulary. A
|
||||
* rejected selection announces through the shared transient Toast anchored to
|
||||
* the composer card; the in-menu strip with Retry remains the catalog-load
|
||||
* surface. While the directory's pending selection is unsettled, the trigger
|
||||
* shows a spinner in place of its chevron, and each row whose value that
|
||||
* selection carries shows one in place of its check mark.
|
||||
* case-insensitive ordered subsequence within each provider group, ranked by
|
||||
* prefix, alignment score, then catalog order. Returning to the root pane
|
||||
* hands focus back to the cell that opened it. Data and submission ride the
|
||||
* same per-session ModelDirectory as the /model popup; exact-model reasoning
|
||||
* metadata and the selected effort come from the Host rather than a
|
||||
* client-owned vocabulary. A rejected selection announces through the shared
|
||||
* transient Toast anchored to the composer card; the in-menu strip with
|
||||
* Retry remains the catalog-load surface. While the directory's pending
|
||||
* selection is unsettled, the trigger shows a spinner in place of its
|
||||
* chevron, and each row whose value that selection carries shows one in place
|
||||
* of its check mark.
|
||||
*/
|
||||
import { MenuSurface } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import {
|
||||
@@ -409,7 +411,7 @@ export function ModelSelect(
|
||||
id={`${id}-menu`}
|
||||
className={css.menu}
|
||||
style={menuPos ?? MEASURE_STYLE}
|
||||
role="menu"
|
||||
role={pane === 'model' ? 'group' : 'menu'}
|
||||
aria-label={t('menu.aria')}
|
||||
aria-busy={state.status === 'loading' || busy}
|
||||
>
|
||||
@@ -456,7 +458,12 @@ export function ModelSelect(
|
||||
<button type="button" className={css.retry} onClick={reload}>{t('retry')}</button>
|
||||
</div>
|
||||
))}
|
||||
<div className={clsx(css.groups, 'scrollable')}>
|
||||
<div
|
||||
className={clsx(css.groups, 'scrollable')}
|
||||
role="menu"
|
||||
aria-label={t('menu.model')}
|
||||
hidden={filteredGroups.length === 0}
|
||||
>
|
||||
{filteredGroups.map((group) => {
|
||||
const headingId = `${id}-${group.id}`
|
||||
return (
|
||||
|
||||
@@ -549,6 +549,7 @@ describe('ModelSelect search', () => {
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: /模型/ }))
|
||||
const search = screen.getByRole('searchbox')
|
||||
expect(document.activeElement).toBe(search)
|
||||
expect(search.closest('[role="menu"]')).toBeNull()
|
||||
expect(fireEvent.keyDown(search, { key: 'ArrowDown', isComposing: true })).toBe(true)
|
||||
expect(document.activeElement).toBe(search)
|
||||
fireEvent.change(search, { target: { value: ' GMFL ' } })
|
||||
@@ -556,7 +557,10 @@ describe('ModelSelect search', () => {
|
||||
expect(screen.queryByRole('group', { name: 'DeepSeek' })).toBeNull()
|
||||
expect(trigger.textContent).toContain('DeepSeek-V4-Flash')
|
||||
fireEvent.change(search, { target: { value: 'zzzz' } })
|
||||
expect(screen.getByRole('status').textContent).toBe('没有匹配的模型。')
|
||||
const status = screen.getByRole('status')
|
||||
expect(status.textContent).toBe('没有匹配的模型。')
|
||||
expect(status.closest('[role="menu"]')).toBeNull()
|
||||
expect(screen.queryByRole('menu')).toBeNull()
|
||||
expect(fireEvent.keyDown(search, { key: 'ArrowDown' })).toBe(false)
|
||||
expect(document.activeElement).toBe(search)
|
||||
fireEvent.change(search, { target: { value: '' } })
|
||||
@@ -569,6 +573,7 @@ describe('ModelSelect search', () => {
|
||||
fireEvent.change(reopened, { target: { value: 'gmfl' } })
|
||||
fireEvent.keyDown(reopened, { key: 'Tab' })
|
||||
const row = screen.getByRole('menuitemradio', { name: 'Gemini Flash' })
|
||||
expect(screen.getByRole('menu', { name: '模型' }).contains(row)).toBe(true)
|
||||
expect(document.activeElement).toBe(row)
|
||||
fireEvent.keyDown(row, { key: 'Tab' })
|
||||
await waitFor(() => { expect(screen.queryByRole('menu')).toBeNull() })
|
||||
|
||||
Reference in new issue
Block a user