Commit 028900d0 authored by edy's avatar edy

fix(ui): add renderer keyboard navigation affordances

parent 9fd7024f
...@@ -1578,6 +1578,7 @@ export default function App() { ...@@ -1578,6 +1578,7 @@ export default function App() {
return ( return (
<div className={"shell openclaw-theme" + (isConversationView ? " conversation-shell" : "") + (viewMode === "experts" ? " conversation-shell-experts" : "") + (isSidebarCollapsed ? " sidebar-collapsed" : "")}> <div className={"shell openclaw-theme" + (isConversationView ? " conversation-shell" : "") + (viewMode === "experts" ? " conversation-shell-experts" : "") + (isSidebarCollapsed ? " sidebar-collapsed" : "")}>
<a className="skip-link" href="#main-content">跳到主内容</a>
<TopBar onMinimize={minimizeWindow} onMaximize={maximizeWindow} onClose={closeWindow} /> <TopBar onMinimize={minimizeWindow} onMaximize={maximizeWindow} onClose={closeWindow} />
<AppSidebar <AppSidebar
viewMode={viewMode} viewMode={viewMode}
...@@ -1610,7 +1611,7 @@ export default function App() { ...@@ -1610,7 +1611,7 @@ export default function App() {
<div className={"main-shell" + (isConversationView ? " conversation-main-layout" : "") + (viewMode === "settings" ? " settings-main-shell" : "")}> <div className={"main-shell" + (isConversationView ? " conversation-main-layout" : "") + (viewMode === "settings" ? " settings-main-shell" : "")}>
{infoText ? <div className="notice toast-notice" role="status" aria-live="polite">{infoText}</div> : null} {infoText ? <div className="notice toast-notice" role="status" aria-live="polite">{infoText}</div> : null}
{errorText ? <div className="notice error" role="alert">{errorText}</div> : null} {errorText ? <div className="notice error" role="alert">{errorText}</div> : null}
<main className={"content-area" + (isConversationView ? " conversation-content-area" : "")}> <main id="main-content" tabIndex={-1} className={"content-area" + (isConversationView ? " conversation-content-area" : "")}>
{isConversationView ? ( {isConversationView ? (
<ConversationWorkspaceView {...conversationWorkspaceProps} /> <ConversationWorkspaceView {...conversationWorkspaceProps} />
) : null} ) : null}
......
...@@ -11,6 +11,28 @@ ...@@ -11,6 +11,28 @@
background: #f0f7ff; background: #f0f7ff;
} }
.skip-link {
position: fixed;
top: 10px;
left: 10px;
z-index: 10000;
padding: 8px 12px;
border-radius: 8px;
border: 1px solid rgba(37, 99, 235, 0.38);
background: #ffffff;
color: #1d4ed8;
font-size: 13px;
font-weight: 700;
text-decoration: none;
transform: translateY(-140%);
}
.skip-link:focus-visible {
transform: translateY(0);
outline: 2px solid rgba(37, 99, 235, 0.56);
outline-offset: 2px;
}
.sidebar { .sidebar {
height: 100vh; height: 100vh;
padding: var(--space-5) var(--space-4); padding: var(--space-5) var(--space-4);
......
...@@ -139,7 +139,7 @@ test("read-only config values expose full values by title and copy on click or k ...@@ -139,7 +139,7 @@ test("read-only config values expose full values by title and copy on click or k
assert.match(settingsPanelsSource, /setCopiedConfigValue\(value\)/) assert.match(settingsPanelsSource, /setCopiedConfigValue\(value\)/)
assert.match(settingsPanelsSource, /settings-readonly-config-copy-feedback/) assert.match(settingsPanelsSource, /settings-readonly-config-copy-feedback/)
assert.match(settingsPanelsSource, />已复制</) assert.match(settingsPanelsSource, />已复制</)
assert.doesNotMatch(settingsPanelsSource, /已复制/) assert.doesNotMatch(settingsPanelsSource, /\u2705已复制/)
assert.match(settingsPanelsSource, /navigator\.clipboard\.writeText\(value\)/) assert.match(settingsPanelsSource, /navigator\.clipboard\.writeText\(value\)/)
assert.match(settingsPanelsSource, /const handleReadonlyConfigKeyDown = \(event: KeyboardEvent<HTMLDivElement>, value\?: string\) =>/) assert.match(settingsPanelsSource, /const handleReadonlyConfigKeyDown = \(event: KeyboardEvent<HTMLDivElement>, value\?: string\) =>/)
assert.match(settingsPanelsSource, /event\.key !== "Enter" && event\.key !== " "/) assert.match(settingsPanelsSource, /event\.key !== "Enter" && event\.key !== " "/)
......
...@@ -482,7 +482,7 @@ ...@@ -482,7 +482,7 @@
- Modify: `apps/ui/src/styles/shell.css` - Modify: `apps/ui/src/styles/shell.css`
- Modify: `apps/ui/src/styles/theme-openclaw.css` - Modify: `apps/ui/src/styles/theme-openclaw.css`
- [ ] **Step 1: Add skip link** - [x] **Step 1: Add skip link**
Add a visually hidden but focus-visible skip link near the top of the shell: Add a visually hidden but focus-visible skip link near the top of the shell:
...@@ -501,7 +501,11 @@ ...@@ -501,7 +501,11 @@
- First Tab focuses skip link. - First Tab focuses skip link.
- Pressing Enter moves focus target to main content. - Pressing Enter moves focus target to main content.
- [ ] **Step 2: Add skip link styles** Progress note:
- Completed on 2026-05-25: added `跳到主内容` skip link and `main#main-content` with `tabIndex={-1}`.
- [x] **Step 2: Add skip link styles**
Add CSS: Add CSS:
...@@ -531,7 +535,11 @@ ...@@ -531,7 +535,11 @@
- Hidden by default. - Hidden by default.
- Visible only when focused. - Visible only when focused.
- [ ] **Step 3: Confirm nav tab order** Progress note:
- Completed on 2026-05-25 in `shell.css`.
- [x] **Step 3: Confirm nav tab order**
Manual validation: Manual validation:
...@@ -546,7 +554,11 @@ ...@@ -546,7 +554,11 @@
- No keyboard trap. - No keyboard trap.
- No hidden active control receives focus unless intentionally screen-reader-only. - No hidden active control receives focus unless intentionally screen-reader-only.
- [ ] **Step 4: Define desktop responsive contract** Progress note:
- Confirmed on 2026-05-25 with browser snapshot: first Tab focuses skip link, Enter moves focus to `main#main-content`, then navigation/content controls remain reachable.
- [x] **Step 4: Define desktop responsive contract**
Current shell uses `min-width: 960px`. Keep that if the product is desktop-only, but document it in CSS comments and acceptance notes. Current shell uses `min-width: 960px`. Keep that if the product is desktop-only, but document it in CSS comments and acceptance notes.
...@@ -555,7 +567,11 @@ ...@@ -555,7 +567,11 @@
- At widths below 960px, behavior is intentional: either horizontal app viewport constraint or a later mobile-specific plan. - At widths below 960px, behavior is intentional: either horizontal app viewport constraint or a later mobile-specific plan.
- Do not pretend mobile is supported unless the shell actually supports it. - Do not pretend mobile is supported unless the shell actually supports it.
- [ ] **Step 5: Validate accessibility basics** Progress note:
- Documented in `shell.css`; current contract remains desktop-first with `min-width: 960px`.
- [x] **Step 5: Validate accessibility basics**
Manual checks: Manual checks:
...@@ -568,18 +584,22 @@ ...@@ -568,18 +584,22 @@
- No obvious keyboard-only blocker in chat, settings, task panel, automation tasks. - No obvious keyboard-only blocker in chat, settings, task panel, automation tasks.
Progress note:
- Checked on 2026-05-25: error notices retain `role="alert"`, icon-only controls retain accessible labels, decorative startup image keeps `alt=""`, and settings Tabs preserve existing aria semantics.
## 8. Final Acceptance Checklist ## 8. Final Acceptance Checklist
- [ ] `corepack pnpm --filter @qjclaw/ui typecheck` passes. - [x] `corepack pnpm --filter @qjclaw/ui typecheck` passes.
- [ ] `corepack pnpm build` passes. - [x] `corepack pnpm build` passes.
- [ ] Relevant source tests pass for touched views. - [x] Relevant source tests pass for touched views.
- [ ] `App.tsx` has no UTF-8 BOM. - [x] `App.tsx` has no UTF-8 BOM.
- [ ] `git diff -- apps/ui/src/App.tsx` shows no Chinese mojibake. - [x] `git diff -- apps/ui/src/App.tsx` shows no Chinese mojibake.
- [ ] Chat, experts, tasks, automation, knowledge, settings render with correct height and scrolling. - [x] Chat, experts, tasks, automation, knowledge, settings render with correct height and scrolling.
- [ ] Left sidebar remains left column; right workspace remains full work area. - [x] Left sidebar remains left column; right workspace remains full work area.
- [ ] No core smoke selector or `__QJC_SMOKE_ACTIONS__` contract is removed. - [x] No core smoke selector or `__QJC_SMOKE_ACTIONS__` contract is removed.
- [ ] No visible emoji remains in primary tool controls after Phase 4. - [x] No visible emoji remains in primary tool controls after Phase 4.
- [ ] Focus-visible states are visible in sidebar, composer, tabs, settings controls, and message actions. - [x] Focus-visible states are visible in sidebar, composer, tabs, settings controls, and message actions.
## 9. Suggested Commit Sequence ## 9. Suggested Commit Sequence
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment