Repository navigation
feat: "Zooming and Moving" info button and keyboard access for the Timeline tile (CLUE-720) - #3030
Conversation
An info button beside the tile title opens a modal explaining how to navigate the Timeline: the zoom and pan buttons, click and shift+click to zoom, drag to pan, and the Full Timeline. Focus starts on OK, and the modal's text is the dialog's accessible description, so screen readers announce it on open. The Timeline tile now joins the tile focus trap, so the title, the info button and the Prev/Next buttons are reachable by keyboard. The plot isn't yet. - BasicEditableTileTitle renders children in the title area, so a tile can add a control beside its title. - useCustomModal takes an opt-in describeContent prop that points aria-describedby at the content. - The tile toolbar's own Tab handling now visits the topbar slot, which Shift+Tab from the toolbar used to skip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #3030 +/- ##
==========================================
+ Coverage 87.14% 87.17% +0.02%
==========================================
Files 1063 1064 +1
Lines 60939 61003 +64
Branches 16272 16294 +22
==========================================
+ Hits 53103 53177 +74
+ Misses 7812 7802 -10
Partials 24 24
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
collaborative-learning
|
||||||||||||||||||||||||||||
| Project |
collaborative-learning
|
| Branch Review |
CLUE-720-timeline-info-modal
|
| Run status |
|
| Run duration | 03m 44s |
| Commit |
|
| Committer | Kirk Swenson |
| View all properties for this run ↗︎ | |
| Test results | |
|---|---|
|
|
0
|
|
|
0
|
|
|
0
|
|
|
0
|
|
|
4
|
| View all changes introduced in this branch ↗︎ | |
…LUE-720] Tab now walks Prev, Next and the scrollbar thumb, which already pans the view with the arrow keys, Page Up/Down, Home and End. The thumb keeps the keys it handles from reaching the tile, whose ArrowUp would otherwise move focus out of the tile. The graph's event and time-marker labels stay out of the Tab order until the graph has a keyboard design. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟢 Approval recommended
The accessibility behavior is coherent, localized, and supported by comprehensive targeted tests.
0 open findings
What changed in this PR
Adds Timeline help content and keyboard navigation while extending shared modal and focus-trap infrastructure.
Changes:
- Adds the “Zooming and Moving” information modal.
- Integrates Timeline controls into tile keyboard navigation.
- Improves modal descriptions and scrollbar key handling.
| File | Description |
|---|---|
src/plugins/timeline/components/timeline-tile.tsx |
Registers Timeline focus slots and info button. |
src/plugins/timeline/components/timeline-tile.test.tsx |
Tests focus-slot registration. |
src/plugins/timeline/components/timeline-info-button.tsx |
Implements the information modal trigger. |
src/plugins/timeline/components/timeline-info-button.test.tsx |
Tests modal behavior and accessibility. |
src/plugins/timeline/components/timeline-info-button.scss |
Styles the button and modal. |
src/plugins/timeline/components/time-marker-overlay.tsx |
Excludes marker controls from Tab order. |
src/plugins/timeline/components/time-marker-overlay.test.tsx |
Verifies marker tab behavior. |
src/plugins/timeline/components/event-overlay.tsx |
Excludes graph labels from Tab order. |
src/plugins/timeline/components/event-overlay.test.tsx |
Verifies event-label tab behavior. |
src/plugins/timeline/assets/info-icon.svg |
Adds the styleable information icon. |
src/hooks/use-custom-modal.tsx |
Adds opt-in dialog content descriptions. |
src/hooks/use-custom-modal.test.tsx |
Tests aria-describedby behavior. |
src/components/ui/dynamic-scrollbar.tsx |
Stops handled keyboard events from bubbling. |
src/components/ui/dynamic-scrollbar.test.tsx |
Tests scrollbar event propagation. |
src/components/toolbar/tile-toolbar.tsx |
Routes Tab navigation through topbar controls. |
src/components/toolbar/tile-toolbar.test.tsx |
Tests topbar focus routing. |
src/components/tiles/basic-editable-tile-title.tsx |
Supports controls beside tile titles. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Forward Tab into the Timeline's contents now starts at Prev, not the scrollbar thumb. Left to itself, the focus trap enters a slot at its first tabindex="0" element, which skipped Prev and Next. The tile now picks the first or last enabled control itself, and decides whether its contents have anything to visit from the controls' disabled state rather than their tabindex, which the trap holds at -1 until the trap is entered. - Prev or Next, on reaching the first or last event, disables itself and would drop focus out of the tile. Focus moves to the control still enabled. - While the title is being edited, the info button moves clear of the wider edit box instead of sitting on top of it. - The graph's event and time-marker labels stay in the Tab order of a read-only tile, which has no focus trap. - A test drives the real focus trap through the cycle in both directions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
emcelroy
left a comment
There was a problem hiding this comment.
Looks good 👍 A Claude-generated review raised the issues below to consider, none of which are major.
1. The hadFocus check has no test
In handleSelectEvent (timeline-tile.tsx:48), the refocus runs only if the clicked button had focus. I removed that check and all tests still passed.
The test is cheap to write. In jsdom (the fake browser Jest uses), a click does not move focus. So a test can click Prev or Next without focusing it first, and then check that focus did not move.
2. Does the "before the trap is entered" test describe a real state?
The test "keeps the contents in the focus cycle before the trap is entered" works like this:
- It turns the trap off, which sets the tile's controls to
tabindex="-1". - It asks the tile for its slots.
- It calls
focusContentdirectly.
I checked how the library enters the trap. On Enter, on Tab on the tile, and when focus arrives from outside, the trap first restores the controls' tabindex. Only then does it ask for the slots and call focusContent. So I found no path where the tile is asked for its slots while its controls are still at -1. If no such path exists, two things follow:
- The test checks a state the app never reaches.
- This line in the PR description has no real case behind it: "judged from the controls' disabled state, not their tabindex, which the trap holds at -1 until it is entered".
Limit: TileComponent also reads the slots in other places (for example, at tile-component.tsx:532, when it builds the trap). I did not trace whether any of those reads happen while the controls are at -1.
If there is a real path where the tile reports its slots while its controls are at -1, add a comment to the test that names it. If not, rename or remove the test, and update the PR description line.
Optional
Commit the full keyboard-walk test
This is worth doing only if this PR is meant to give end-to-end keyboard coverage. The Jest Tab walk covers the Timeline's own slots in both directions. It has no toolbar, drag handle or resize handle, and the toolbar tests use stand-in elements. The PR says the full walk was checked in a Cypress spec that was not committed. I confirmed most of that walk in the browser.
Remove the unreachable fallback to the info button
At timeline-tile.tsx:43-44, the ?? .timeline-info-button fallback cannot run:
- When Prev or Next disables itself, another content control is still enabled.
- With two or more events, the other button is enabled.
- With a seismogram, the thumb is enabled. In an editable tile it is always enabled, because
full-timeline.tsxpassesdisabled={readOnly}.
Removing the fallback has two other effects:
- The
.timeline-info-buttonselector no longer appears twice in the file (it is also on line 116). - The comment above the effect ("the first control still enabled") becomes exact.
Wording in tests and comments
- "puts the scrollbar thumb in the focus cycle whenever there's seismogram data" checks only that the container is reported and holds the thumb. It does not Tab to the thumb. Also, "whenever" is too strong, because a read-only tile has no focus cycle.
- "A focus trap wired to the tile's API as TileComponent wires it" — the helper copies only the title, topbar, content and
focusContentparts. "As TileComponent wires these slots" would be exact. - The
tryFocusTopbarcomment in tile-toolbar.tsx says "or the slot element itself when it is the only control". The code falls back to the slot element when the slot has no focusable children. "When it has no focusable children" would be exact.
PR description
- "
info-icon.svggains a class on its inner fill" suggests that the file already existed. It is new on this branch (added in74a4aacd9). Suggested wording: "info-icon.svghas a class on its inner fill so CSS can set the button-state colors."
Follow-up (separate ticket)
Modal IDs are not unique
use-custom-modal.tsx:107 builds contentId from dataTestId or className. That is the same way the existing titleId is built. So every Timeline's modal gets the same ids.
react-modal-hookcan render more than one modal. If two ever render together,aria-describedbyandaria-labelledbycan point to the wrong dialog.- In normal use the open modal covers the page, so a second Timeline modal cannot be opened.
- The id scheme existed before this branch.
An id from React's useId would fix both ids.
Automatic findings that I checked and dismissed
| Finding | Result |
|---|---|
| Shift+Tab from the toolbar lands on an event label when the thumb is disabled | Cannot happen. The thumb is disabled only in a read-only tile, and a read-only tile has no focus trap. In the browser, Shift+Tab went to the thumb. |
| Focus is lost when Prev/Next disables itself and no fallback exists | Cannot happen. Another control is always enabled. |
Focus drops to body for a moment before the refocus |
Not seen in Chrome. Focus went straight from Next to Prev. |
tryFocusTopbar breaks Dataflow's roving tabindex |
Wrong premise. Dataflow's topbar has no roving tabindex. Dataflow also has a title and a palette, which the toolbar's Tab code tries before the topbar. (Checked in the code only. The Dataflow demo document did not load in /editor/.) |
The labels read read-only from context, but the tile uses its readOnly input |
Existing pattern. full-timeline.tsx and timeline-plot.tsx already read read-only from the context. Canvas is the only provider in the app. |
| The thumb blocks arrow, Page and Home/End keys even with Cmd, Ctrl or Alt held | No effect today. No shortcut uses those key combinations. |
- Test that Prev/Next move focus only when the clicked button had it. - Drop the test and comment for the controls' tabindex before the trap is entered: the trap restores their tabindex before it reads the slots, so the tile is never asked in that state. - Drop the info-button fallback after Prev/Next disables itself; the other content control is always enabled then. - Give each custom modal unique title and content ids with useId, so two open modals of the same class can't label or describe each other. - Tighten a test name and two comments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@emcelroy Addressed in e82529f:
Optional:
|
CLUE-720
The Timeline tile now has an ⓘ button beside its title that opens a "Zooming and Moving" modal. The modal explains how to get around the graph: the zoom and pan buttons, click and shift+click to zoom, click+hold+drag to pan, and the Full Timeline. The text and button states follow the Zeplin spec.
The Timeline tile is also reachable by keyboard for the first time. Select it and press Enter, and Tab moves through the title, the info button, Prev, Next, the Full Timeline's scrollbar thumb, the toolbar, the drag handle and the resize handle, skipping any that are disabled; Shift+Tab walks the same cycle backwards. The thumb already pans the view with the arrow keys, Page Up/Down, Home and End (CLUE-719); this makes it reachable.
Departures from the spec
custom-modal.scssalways fills teal.:focusrather than:focus-visible.Implementation
TimelineInfoButtoncomponent, built onuseCustomModal.BasicEditableTileTitlerenderschildreninside the title area, so a tile can add a control beside its title. The button is positioned absolutely, so the title stays centered.useCustomModaltakes an opt-indescribeContentprop that pointsaria-describedbyat the modal content. It's opt-in because a form dialog's controls are announced as they're reached.ClueTileAccessibilityBridge, as dataflow does:.timeline-containeris the content slot, holding Prev, Next and the scrollbar thumb. It is reported only while one of them is enabled, since an empty slot would make Tab stop and go nowhere.focusContententers the slot at its first (or, going backwards, last) enabled control. Left to itself, the trap enters at the slot's firsttabindex="0"element, which is the thumb, and skips Prev and Next.tabIndex={-1}, so they stay out of the Tab cycle until the graph has a keyboard design; Prev and Next select events by keyboard. A read-only tile has no focus trap, so they stay in its natural Tab order.DynamicScrollbarstops the keys it handles from propagating. Otherwise ArrowUp on the thumb would also trigger the tile's ArrowUp exit and send focus out of the tile.info-icon.svghas a class on its inner fill so CSS can set the button-state colors.useCustomModalbuilds its title and content ids withuseId, so each modal'saria-labelledbyandaria-describedbypoint at its own elements. They were built fromdataTestIdorclassName, so every modal of a class shared them.Not in this PR
Testing
TimelineInfoButton: opening, the modal text and its accessible description, initial focus on OK, closing with OK, the close button and Escape, and focus returning to the info button.FocusTrapControllerwalking title → info → Prev → Next → thumb and back; focus moving to the other button when Prev or Next disables itself, and left alone when an unfocused Next is clicked; no registration when read-only.DynamicScrollbarkeeps the keys it handles from its container and passes the rest on; the graph's labels are out of the Tab order in an editable tile and in it in a read-only one.useCustomModal:aria-describedbyabsent by default, and pointing at the content withdescribeContent; two open modals of the same class each labelled and described by their own elements./editor/with a one-tile Timeline document without data: the button's position and states, the modal against Zeplin, and focus returning to the button on close.🤖 Generated with Claude Code