fix(client): reveal menu shortcuts during keyboard navigation

This commit is contained in:
Stevring committed 2026-10-20 18:57:48 +08:00
1 parent 62f78d82f2
commit 65d4afafd6
8 files changed
+61 -9

No files matched your search

+29 -2
View File
@@ -54,6 +54,7 @@ it.each([false, true])('keeps workspace and archived search actions reachable wi
const browser = await chromium.launch()
onTestFinished(() => browser.close())
const page = await browser.newPage({ viewport: { width: 1280, height: 900 }, hasTouch, locale: 'en-US' })
await page.addInitScript(() => { Object.defineProperty(navigator, 'platform', { value: 'MacIntel' }) })
onTestFailed(() => saveFailureShot(page, `row-actions-${String(hasTouch)}`))
const activate = async (target: Locator) => {
if (hasTouch) await target.tap()
@@ -86,8 +87,34 @@ it.each([false, true])('keeps workspace and archived search actions reachable wi
expect(await project.getAttribute('aria-expanded')).toBe('true')
await openFixture()
if (!hasTouch) await row.hover()
await activate(row.getByRole('button', { name: /^Session actions for / }))
await activate(page.getByRole('menuitem', { name: 'Archive session', exact: true }))
const actions = row.getByRole('button', { name: /^Session actions for / })
await activate(actions)
const archive = page.getByRole('menuitem', { name: 'Archive session', exact: true })
const shortcut = archive.locator('[aria-hidden="true"]').filter({ has: page.locator('kbd') })
await archive.waitFor()
expect(await shortcut.count()).toBe(1)
expect(await page.evaluate(() => matchMedia('(pointer: coarse)').matches)).toBe(hasTouch)
expect(await shortcut.isVisible()).toBe(!hasTouch)
for (const key of ['ArrowDown', 'Tab']) {
await page.keyboard.press(key)
expect(await page.locator('html').getAttribute('data-input-modality')).toBe('keyboard')
expect(await page.getByRole('menu').evaluate(menu => menu.contains(document.activeElement))).toBe(true)
expect(await shortcut.isVisible()).toBe(true)
if (hasTouch) {
await page.setViewportSize({ width: 390, height: 900 })
await expect.poll(() => shortcut.evaluate((element) => {
const bounds = element.getBoundingClientRect()
return bounds.left >= 0 && bounds.right <= window.innerWidth
})).toBe(true)
await page.setViewportSize({ width: 1280, height: 900 })
}
await page.keyboard.press('Escape')
if (!hasTouch) await row.hover()
await activate(actions)
await archive.waitFor()
expect(await shortcut.isVisible()).toBe(!hasTouch)
}
await activate(archive)
await row.waitFor({ state: 'detached' })
await activate(page.getByRole('button', { name: 'View options', exact: true }))
await activate(page.getByRole('menuitem', { name: 'All conversations (show archived)', exact: true }))
+5 -2
View File
@@ -9,8 +9,11 @@
en: dc8c41fbf9869179
zh: e45976a585cb9c86
/web-ui-style-reference/component-rules:
en: 1a6ee7a02cef1e1c
zh: 641bf60f7521ae12
en: 4ee4a6fd2917306a
zh: d7d68c140a698ab0
/web-ui-style-reference/input-capabilities:
en: bf2e98bce68743e2
zh: fc58dcbb60bb944a
/web-ui-style-reference/corner-radii-and-settings-cards:
en: 118972f6c2bd6ce4
zh: 85f8416b03e4dd8b
+11
View File
@@ -30,6 +30,17 @@ Global style sheets belong in `ui-theme/src/styles/`. Component styles live besi
- Flat borders and separators that use a neutral `--dsw-alias-border-*` token draw at `0.5px` — buttons, inputs, cards, row dividers, and separators drawn as filled boxes (menu separators, the conversation header seam, markdown `hr`, vertical rails) share the hairline weight, which Chromium paints as one device pixel. Dashed affordances and state-colored borders keep 1px; spinner ring tracks keep their width through the spec's explicit allowlist. The ui-theme elevation spec rejects wider neutral solid borders.
- Clickable artifact links (markdown anchors, prose file mentions, web source and fetch links, produced-file chips, workflow member links) color through `--dsw-alias-link` at `font-weight: 500`, with no underline at rest and a dotted 3px-offset underline on hover/focus. Compact Thinking Markdown keeps tertiary text color and a resting dotted underline ([compact presentation](../packages/client/ui-primitives/README.md#rendering-agent-output)). Text-leading anchors also lead with the ui-primitives `LinkIconMedium` category glyph riding `currentColor`, which for a well-known external host is that site's own mark instead of the globe; workflow member links and image-only anchors carry no glyph, and tool-row file links keep their grey dotted affordance.
<a id="input-capabilities"></a>
## Input capabilities
`pointer` and `hover` describe the primary pointing device chosen by the browser; `any-hover` considers every available pointing device. None detects a physical keyboard.
- Use `(pointer: coarse)` for presentation that depends on pointing accuracy, such as touch-sized controls and text fields.
- Keep CSS hover effects under `(hover: hover)` so a touch-first layout remains usable without hover.
- JavaScript hover previews use `(any-hover: hover)` when accepting mouse entry and ignore touch entry. A secondary mouse can therefore open a preview even when the touchscreen remains the primary pointer. Enabling all CSS hover styles through `any-hover` would also enable them for touch-generated `:hover` states.
On a mixed-input device, a mouse preview can appear while the primary-pointer CSS hover style stays inactive. Keyboard focus and shortcut hints follow actual keyboard interaction independently of these capability queries; [Menu and input modality](../packages/client/ui-primitives/README.md#input-modality) define the shared behavior.
<a id="corner-radii-and-settings-cards"></a>
## Corner radii and settings cards
+11
View File
@@ -30,6 +30,17 @@
- 使用中性 `--dsw-alias-border-*` token 的平面边框与分割线一律 `0.5px`——按钮、输入框、卡片、行分割线,以及以填充盒绘制的分隔线(菜单分隔、对话标题栏接缝、markdown `hr`、竖向轨道线)共用发丝线粗细,Chromium 将其绘制为一个设备像素。dashed 记号与状态色 border 保持 1px;spinner 圆环经 spec 的显式豁免保留原宽度。更宽的中性 solid border 会被 ui-theme elevation spec 拒绝。
- 可点击产物链接(Markdown 锚点、正文文件引用、网页来源与抓取链接、产物 chips、workflow 成员链接)经 `--dsw-alias-link` 着色、`font-weight: 500`,默认无下划线,hover/focus 时为 3px offset 的点状下划线。紧凑 Thinking Markdown 保持 tertiary 文字色和默认点状下划线([紧凑展示](../packages/client/ui-primitives/README.zh.md#rendering-agent-output))。带文字的锚点另以 ui-primitives 的 `LinkIconMedium` 分类图形(随 `currentColor`)作前置;目的地是已知外部站点时改用该站点自己的标记而非地球;workflow 成员链接与只包图片的锚点不带图形,工具行文件链接保持其灰色点线示能。
<a id="input-capabilities"></a>
## 输入能力
`pointer` 与 `hover` 描述浏览器选定的主指针,`any-hover` 则考虑所有可用指针。三者都不检测物理键盘。
- `(pointer: coarse)` 用于依赖指针精度的呈现,例如适合触摸的控件和文本框尺寸。
- CSS 悬停效果保留在 `(hover: hover)` 下,使以触屏为主的布局无需悬停也能使用。
- JavaScript 悬停预览在接受 mouse 进入事件时使用 `(any-hover: hover)`,并忽略 touch 进入事件。因此,即使触屏仍是主指针,外接鼠标也能打开预览。如果通过 `any-hover` 启用全部 CSS 悬停样式,触摸产生的 `:hover` 状态也会应用这些样式。
混合输入设备上,鼠标预览可以出现,而基于主指针的 CSS 悬停样式保持关闭。键盘焦点和快捷键提示根据实际键盘交互显示,不依赖这些能力查询;共享行为见 [Menu 与输入方式](../packages/client/ui-primitives/README.zh.md#input-modality)。
<a id="corner-radii-and-settings-cards"></a>
## 圆角与设置卡片
@@ -15,8 +15,8 @@
en: d152484eb41ac6b4
zh: 09388d293f9be9cb
/deepseek-ai-dsh-client-ui-primitives/use-this-package:
en: 86bf820c4e52f41e
zh: 33565e9238b554b3
en: d2f5701274be5110
zh: d60e2617136a6f26
/deepseek-ai-dsh-client-ui-primitives/use-this-package/component-catalog:
en: cb874a0ca53a6747
zh: 31179193dd1d6e12
+1 -1
View File
@@ -25,7 +25,7 @@ Use `dsh-client-ui-primitives` to build web-client controls and render agent out
<a id="use-this-package"></a>
## Use this package
`Toast` uses the theme’s toast background and label colors in both light and dark modes. `Menu` data rows and `MenuItemButton` component rows accept owner-provided effective shortcuts and align their keys at the trailing edge as muted text without a background, including submenus. Menus hide these hints when the primary pointer is coarse (`pointer: coarse`); keyboard bindings remain active. `ShortcutKeys` defaults to unboxed keys for menu and inline hints. Its `tooltip` variant uses lighter keycaps over the dark bubble and groups plus-separated combinations into one filled block. `Tooltip.shortcutKeys` vertically centers keycaps beside optional localized action text, or shows only keys when the label is empty. The optional `className` supplies owner interaction styles. `Modal` uses the frame’s shared overlay inset, with a minimum 24px viewport margin. Its mask leaves the Windows caption unpainted while the full-viewport layer blocks background clicks. `Modal` and the settings shell share top-layer Escape and Tab handling through `useModalLayer`, restoring the previous focus on close. `closeTopModal(document)` requests the foreground modal’s current close action; newer menus or unregistered dialogs block dismissal of the modal behind them. Components use `isBehindModal` to avoid moving focus behind a nested dialog. Menus consume their local Escape before modal dismissal. `observeComposition` supplies the same composition-end and legacy IME guard to local modal and recording handlers; callers dispose its document listeners with their interaction lifetime.
`Toast` uses the theme’s toast background and label colors in both light and dark modes. `Menu` data rows and `MenuItemButton` component rows accept owner-provided effective shortcuts and align their keys at the trailing edge as muted text without a background, including submenus. Menus hide these hints when the primary pointer is coarse (`pointer: coarse`) and [keyboard navigation](#input-modality) is inactive. Keyboard navigation reveals them until the next pointer press; keyboard bindings remain active. `ShortcutKeys` defaults to unboxed keys for menu and inline hints. Its `tooltip` variant uses lighter keycaps over the dark bubble and groups plus-separated combinations into one filled block. `Tooltip.shortcutKeys` vertically centers keycaps beside optional localized action text, or shows only keys when the label is empty. The optional `className` supplies owner interaction styles. `Modal` uses the frame’s shared overlay inset, with a minimum 24px viewport margin. Its mask leaves the Windows caption unpainted while the full-viewport layer blocks background clicks. `Modal` and the settings shell share top-layer Escape and Tab handling through `useModalLayer`, restoring the previous focus on close. `closeTopModal(document)` requests the foreground modal’s current close action; newer menus or unregistered dialogs block dismissal of the modal behind them. Components use `isBehindModal` to avoid moving focus behind a nested dialog. Menus consume their local Escape before modal dismissal. `observeComposition` supplies the same composition-end and legacy IME guard to local modal and recording handlers; callers dispose its document listeners with their interaction lifetime.
Automatic modal entry and modal/menu return focus, including after Escape and application close shortcuts, use `focusWithoutRing(element, options?)` to suppress the outline until Tab or directional navigation resumes normal focus styling. Dialog containers keep their outline-free styling. Existing borders, shadows, and error states remain intact. Mark a dialog's initial control with `data-modal-autofocus` so the modal captures its invoking control before moving focus. Controls mounted with the dialog must not use React `autoFocus`, which runs before that capture. Tab and Shift+Tab from the dialog container enter its first and last focusable controls.
+1 -1
View File
@@ -25,7 +25,7 @@ kind: "package-library"
<a id="use-this-package"></a>
## 使用本包
`Toast` 在浅色和深色模式下均使用主题的系统提示背景与文字颜色。`Menu` 数据条目与 `MenuItemButton` 组件条目接收功能 owner 提供的有效快捷键,并在末端以无背景的浅灰色文字对齐显示,子菜单也采用同一呈现。主指针为粗指针(`pointer: coarse`)时,菜单隐藏这些提示,快捷键绑定仍然生效。`ShortcutKeys` 默认以无方块的按键文字显示菜单和行内提示。`tooltip` 变体在深色气泡上使用稍浅的键帽,以加号连接的组合则共用一个填充色块。`Tooltip.shortcutKeys` 将键帽与可选的本地化操作文本垂直居中排列,标签为空时只显示按键。可选的 `className` 供调用方设置交互状态样式。`Modal` 使用框架共享的浮层起点,视口边距至少为 24px。其遮罩不绘制 Windows 顶栏区域,覆盖整个视口的模态层仍阻挡背景点击。`Modal` 与设置外壳通过 `useModalLayer` 共用顶层 Esc 和 Tab 处理,关闭时恢复先前焦点。`closeTopModal(document)` 请求前台弹窗当前的关闭操作;上层菜单或未注册的对话框会阻止关闭其后方弹窗。组件通过 `isBehindModal` 避免将焦点移到嵌套弹窗后方。菜单先消费自己的 Esc,再由模态层处理关闭。 `observeComposition` 为局部弹层和录键处理提供相同的 composition-end 与旧版 IME 保护;调用方随交互生命周期释放其 document 监听。
`Toast` 在浅色和深色模式下均使用主题的系统提示背景与文字颜色。`Menu` 数据条目与 `MenuItemButton` 组件条目接收功能 owner 提供的有效快捷键,并在末端以无背景的浅灰色文字对齐显示,子菜单也采用同一呈现。主指针为粗指针(`pointer: coarse`)且未使用[键盘导航](#input-modality)时,菜单隐藏这些提示。键盘导航会显示提示,直到下一次指针按下;快捷键绑定始终生效。`ShortcutKeys` 默认以无方块的按键文字显示菜单和行内提示。`tooltip` 变体在深色气泡上使用稍浅的键帽,以加号连接的组合则共用一个填充色块。`Tooltip.shortcutKeys` 将键帽与可选的本地化操作文本垂直居中排列,标签为空时只显示按键。可选的 `className` 供调用方设置交互状态样式。`Modal` 使用框架共享的浮层起点,视口边距至少为 24px。其遮罩不绘制 Windows 顶栏区域,覆盖整个视口的模态层仍阻挡背景点击。`Modal` 与设置外壳通过 `useModalLayer` 共用顶层 Esc 和 Tab 处理,关闭时恢复先前焦点。`closeTopModal(document)` 请求前台弹窗当前的关闭操作;上层菜单或未注册的对话框会阻止关闭其后方弹窗。组件通过 `isBehindModal` 避免将焦点移到嵌套弹窗后方。菜单先消费自己的 Esc,再由模态层处理关闭。 `observeComposition` 为局部弹层和录键处理提供相同的 composition-end 与旧版 IME 保护;调用方随交互生命周期释放其 document 监听。
弹窗自动进入及弹窗、菜单回焦,包括通过 Esc 和应用关闭快捷键触发的回焦,均使用 `focusWithoutRing(element, options?)`,在 Tab 或方向键导航恢复正常焦点样式前抑制外轮廓线。弹窗容器仍不绘制焦点外框。原有边框、阴影和错误状态保持不变。用 `data-modal-autofocus` 标记弹窗的初始控件,让模态层先保存触发控件,再移动焦点。随弹窗挂载的控件不得使用 React `autoFocus`,因为它会在保存触发控件前执行。弹窗容器获得焦点时,Tab 和 Shift+Tab 分别进入第一个和最后一个可聚焦控件。
@@ -209,7 +209,7 @@
}
@media (pointer: coarse) {
.shortcut {
:global(html:not([data-input-modality='keyboard'])) .shortcut {
display: none;
}
}