Skip to content

Make the sidebar, drawer and command palette keyboard-safe, and prefetch chapters on intent - #11

Merged
renrenmimi merged 2 commits into
masterfrom
fix/shell-keyboard
Oct 10, 2026
Merged

renrenmimi merged 2 commits into
masterfrom
fix/shell-keyboard

Conversation

@renrenmimi

Copy link
Copy Markdown
Owner

Branches from #9 (commit 2148ad8), so it can be merged before or after it, and independently of #10.

Summary

Fixes from the 2026-10-09 audit (04-TSer-代码与体验.md):

  • A9
    • The closed phone drawer and the collapsed desktop sidebar stayed in the Tab order: 13 of the first 18 Tab stops were invisible.
    • Opening the drawer did not move focus, Escape did not close it, and there was no skip link.
  • A10 The command palette did not trap or return focus, did not keep the selection in view, and always showed ⌘K.
  • A12 Every page load prefetched the other 10–11 chapters, about 400 KB.
  • A19 (palette) Its search field was 15.5px, so iOS zoomed in when it was focused.
  • A26 (state dots, theme button)
    • The chapter state dots put aria-label on a role-less <span>.
    • The theme button only said "Toggle theme".
  • A27 (plural, ⌘K) The English progress line said "1 tasks", and ⌘K was shown on every platform.
  • B21 (sidebar) The Chinese progress line used the gaming term 通关.

Changes

  • app/sidebar.tsx
    • <aside id="sidebar"> is inert whenever it is off screen.
    • While the phone drawer is open, .shell-main is inert, <html> does not scroll, and the first link takes focus. Escape or the scrim closes the drawer and focuses the menu button.
    • A "Skip to content" link comes first and focuses the page's <main>.
    • Chapter and brand links use prefetch={false} and prefetch on hover or focus.
    • State dots have role="img". The progress line handles singular and plural, and the Chinese line now reads 学完 N/12 章.
  • app/toolbar.tsx
    • The menu button has id="sidebar-toggle", aria-controls="sidebar" and aria-expanded, and uses the same breakpoint hook as the sidebar.
    • The palette button shows Ctrl K outside Apple platforms.
    • The theme button is named by what it will do ("Switch to the light theme").
  • app/command-palette.tsx
    • aria-modal="true". Tab stays in the search box.
    • The search box is a combobox with aria-activedescendant, and the results are a listbox of options, scrolled into view as the selection moves.
    • Page scroll is locked while open, and focus returns to the opener on close.
    • An IME confirmation is not taken as Enter. The Chinese empty-state copy is in a formal register.
  • app/globals.css: .skip-link styles next to the sidebar rules, and .cmdk-input at 16px.
  • Test: test/shell-keyboard.test.tsx.

Verification

  • npx tsc --noEmit --incremental false: clean.
  • npm test: 7 files, 48 tests pass.
  • npm run build: succeeds.
  • Headless Chrome against a local next start:
    • Collapsed sidebar (1440): 18 Tab presses from the top land in the sidebar 0 times; before the fix it was 13. Same result for the closed drawer at 390.
    • Skip link: the first Tab shows "Skip to content" at the top, and Enter moves focus into <main>.
    • Drawer (390, touch): the open drawer has focus inside it, .shell-main is inert and <html> overflow is hidden. Escape closes it and focuses #sidebar-toggle, and a scrim click does the same.
    • Palette (390×640): focus starts in the combobox, and after 11 ArrowDown presses the selection is still inside the list's visible area. Tab stays in the combobox, and Escape returns focus to the "Jump to" button.
    • Prefetch: in the 7 s after load, 0 chapter chunks or RSC payloads are requested; before the fix it was 10–11 chapters.

🤖 Generated with Claude Code

renrenmimi and others added 2 commits October 9, 2026 16:07
JSX trims each line of a text block and joins the lines with one space.
The Chinese copy is wrapped across source lines throughout the course, so
it rendered with spaces between Chinese characters and around full-width
punctuation: 1,572 of them in 36 files.

- 1,548 line breaks are joined by a script that walks the TypeScript
  syntax tree and touches only JSX text and JSX attribute strings, never
  code samples, template literals or comments. Breaks next to Latin
  letters, digits, expressions, "——" or operators keep their space.
- 24 more spaces go: 20 after full-width punctuation, 3 between Chinese
  text and a tag whose text is Chinese, and one {" "} between a
  full-width mark and Chinese text.
- test/zh-copy-spacing.test.ts, the check DataData and AlgoAlgo use,
  scans every TSX file and fails on any new stray space; CLAUDE.md states
  the rule.

Apart from the one {" "}, the diff changes nothing but whitespace. The
text every edited file renders differs from master only by deleted
spaces, each next to Chinese text, so the English copy is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on intent

- The sidebar is inert whenever it is off screen (closed drawer, collapsed on desktop), so
  its links are no longer invisible tab stops. An open drawer makes the page behind it
  inert, locks scrolling, takes focus, and closes on Escape or the scrim with focus back on
  the menu button, which now has aria-controls and aria-expanded.
- Add a "Skip to content" link before the sidebar that focuses the page's <main>.
- The command palette is a modal combobox: aria-modal, Tab stays in the search box, the
  results are a listbox announced through aria-activedescendant and scrolled into view, the
  page behind does not scroll, an IME confirmation is not taken as Enter, and closing returns
  focus to the opener. Its search field is 16px so iOS does not zoom.
- Chapter and brand links prefetch only on hover or focus instead of on every page load.
- Show Ctrl K outside Apple platforms, name the theme button by what it will do, give the
  chapter state dots role="img", count tasks and quizzes in the singular and plural, and
  replace the gaming term in the Chinese progress line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 10, 2026 01:54
@vercel

vercel Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
tser Ready Ready Preview Oct 10, 2026 1:55am UTC

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-10T01:57:46.661878Z 751c09f PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 751c09f24f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/command-palette.tsx
Comment on lines +48 to +49
return () => {
html.style.overflow = previousOverflow;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Coordinate scroll locks when closing stacked overlays

On a viewport at or below 960px, open the drawer, press Ctrl/Cmd-K, then press Escape. Both the palette's global Escape listener and the drawer listener close their overlay in the same update. The palette captured overflow: hidden from the drawer, and because its cleanup runs after the earlier Sidebar component's cleanup, this assignment restores hidden after the drawer restored the original value, leaving the page permanently unscrollable after both overlays disappear. Use a shared/reference-counted scroll lock or ensure Escape dismisses only the topmost overlay.

Useful? React with 👍 / 👎.

Comment thread app/sidebar.tsx
Comment on lines +111 to +113
<a className="skip-link" href="#main" onClick={skipToContent}>
{L({ en: "Skip to content", zh: "跳到正文" })}
</a>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the skip link inside the drawer focus boundary

When the narrow-screen drawer is open, only .shell-main is made inert, while this newly added skip link is a focusable sibling outside both .shell-main and the drawer. Tabbing forward from the drawer's last control therefore wraps to the skip link, so focus escapes the modal and activating the link attempts to focus inert page content. Make the skip link inert while the drawer is open or include it in a complete focus trap.

Useful? React with 👍 / 👎.

This branch was successfully deployed

1 active deployment
Preview — 751c09f2 Deployed Oct 10, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants