feat(#429): split the Dashboard tree row into chevron, name and trailing targets (phase 2) - #492
Merged
Merged
Conversation
…ing targets Phase 2 (absorbs #472). A Dashboard row's name opens View immediately and Shift-click opens Edit; the chevron became a real disclosure button that only expands; the trailing control does neither. The delayed single/double arbitration #426 needed on Dashboard rows is gone — a panel row still arbitrates, its gestures unchanged. - model: Dashboard row emits `single: open-dashboard(view)`, `double: null`, keeps `shift`. `double === null` is now the view's arbitration switch. - the `…` menu drops *Open in View* (it IS the primary press) and keeps *Open in Edit*, whose only forms are the hidden Shift modifiers. - chevron: `<button>` with `aria-expanded`, an Expand/Collapse accessible name, Enter/Space that stops the key reaching the tree's own Enter, a focus ring in a different channel from the row's, and a roving tabindex so the tree stays one composite tab stop. - a search-forced row still cannot be collapsed, but its name now opens it. - removed two guards the split made unreachable: the chevron's and the #428 drop handler's `cancelFor`, plus `toggleRow`'s Dashboard arm. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018mujm1kW7jDGpTEfscndcU
Caught in a real browser: hit-testing and getBoundingClientRect both use the TRANSFORMED box. The new button is 10×24 (stretched to the row height so a 10px glyph is worth aiming at), so `rotate(-90deg)` on a collapsed row turned its clickable band into 24 wide × 10 tall — spilling 7px each side and swallowing clicks meant for the row icon beside it, which then expanded instead of opening. The schema tree's chevron survives rotating its own box only because it is a 10×10 square. The rotation now lives on the glyph inside the control, the CSS transition follows it, and an e2e case asserts the control's box stays inside its own slot and never reaches the icon — verified to fail with the rotation put back on the button. happy-dom has no layout, so only e2e can hold this. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018mujm1kW7jDGpTEfscndcU
This was referenced Jul 27, 2026
Open
…keep theirs A `treeitem` names itself from its CONTENTS, and since the gesture split those contents include a labelled button — so the chevron's name was being folded into the row's. Measured in Chromium (Accessibility.getPartialAXTree), not assumed: collapsed "Expand Sales revenue Sales revenue 2" expanded "Collapse Sales revenue Sales revenue 2 Actions for Sales revenue" That is the opposite of #472's "three independent targets, each ... separately announced", and it is a regression this phase introduced: the `…` leak was pre-existing and hover-scoped, but every Dashboard and group row now carried a permanent "Expand …" prefix. The row states its own name, composed from the MODEL's strings in the order it paints them — name, count, status word, meta, marker label — so it cannot drift from the screen and nothing previously announced is lost. Asserted in the unit suite (exact names for dashboard/group/orphan-variable/broken-panel rows) and through `getByRole` in e2e, which resolves real accessible names in every engine; happy-dom cannot compute one at all. Sabotage-checked: dropping the attribute fails both. Also from the review pass: cover the fourth corner of #472's matrix (expanded AND current), assert a real dblclick dispatches one repeated command, and correct a CSS comment that called a three-class selector equal-specificity. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018mujm1kW7jDGpTEfscndcU
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Part of #429 — phase 2, which absorbs #472: the Dashboard-tree row becomes three
independent targets.
<button>)⋯Before this, a Dashboard row's single click only expanded it — deferred ~300ms through
the click arbiter — and opening needed a double-click. Expansion and navigation competed
for one gesture; now they don't, so the arbitration is gone from Dashboard rows entirely.
A panel row is untouched: click opens its query, double-click and Shift-click focus
the tile, and it still arbitrates.
The decision this phase owed the issue
Split, not all-or-nothing:
Open in View— it is the row's primary press now (pointer andEnter),so nothing about it is hidden any more, which was the menu item's only justification.
Open in Edit— its only forms are Shift-click andShift+Enter, both hiddenmodifier gestures. The menu is where a keyboard or first-time user finds Edit mode at
all; dropping it would trade a fixed redundancy for a real discoverability regression.
That also leaves the Dashboard row with one trailing control going into phase 3, which
is what the issue's "a third icon on a narrow sidebar row is the reason to think twice"
warns about — phase 3 can still choose a pencil icon or a second menu row.
Implementation notes
dashboard-tree-model.ts: the Dashboard row emitssingle: open-dashboard(view),double: null, and keepsshift.double === nullis the view's new arbitrationswitch — no new field.
singleis now unconditional, so a Dashboard the search isholding open can be opened (it still cannot be collapsed — that is
toggleable),where before that click did nothing at all.
aria-expanded, anExpand <name>/Collapse <name>accessiblename, native Enter/Space that is stopped from reaching the tree's own
Enter(which nownavigates), a focus ring in a different channel from the row's, and a roving
tabindexso the tree stays one composite tab stop instead of one stop per chevron.The treeitem keeps its own
aria-expanded; both are painted fromrow.expanded.causes — otherwise one
Spacemoved focus to the row and the nextEnternavigated.chevron's
cancelFor, the Add Library drag assignment to Dashboard panels and variables #428 drop handler'scancelFor(only a row with adoubleis ever arbitrated → a panel row → which accepts no drop and has no chevron), and
toggleRow's Dashboard arm (the model now emits{kind:'toggle'}for group rows only).openDashboardalready collapses a repeated same-id/same-mode open. That guarantee is now pinned by a
test in
tests/unit/app.test.tsrather than assumed — the tree deliberately does notduplicate the guard, since it cannot see the route.
The bug only a real browser could show (second commit)
Hit-testing and
getBoundingClientRectuse the transformed box. The new button is10×24 (stretched to the row height so a 10px glyph is worth aiming at), so a collapsed
row's
rotate(-90deg)turned its clickable band into 24 wide × 10 tall — spilling 7pxeach side and swallowing clicks meant for the row icon, which then expanded instead of
opening. The schema tree's chevron survives rotating its own box only because it is a
10×10 square. The rotation now lives on the glyph inside the control, and an e2e case
asserts the control's box stays in its own slot and never reaches the icon (verified to
fail with the rotation put back on the button). happy-dom has no layout, so nothing below
e2e could hold this.
The accessibility regression the review pass caught (third commit)
A
treeitemnames itself from its contents, and those contents now include alabelled button. Measured with Chromium's own accname engine
(
Accessibility.getPartialAXTree), not assumed:The chevron's name was being folded into the row's — the opposite of the criterion this
PR claims. The
⋯leak was pre-existing and only while hovered/focused; the permanentExpand …prefix on every Dashboard and group row was this phase's regression.The row now states its own name, composed from the model's strings in the order it paints
them (name, count, status word, meta, marker label), so it cannot drift from the screen
and nothing previously announced is lost. Held by exact-name unit assertions and by
getByRolein e2e — which resolves real accessible names in every engine, wherehappy-dom cannot compute one at all. Sabotage-checked: dropping the attribute fails both.
Two smaller review items came with it: the fourth corner of #472's matrix (expanded
and current) is now covered, a real
dblclickis asserted to dispatch one repeatedcommand, and a CSS comment that called a three-class selector "equal-specificity" is
corrected.
Acceptance criteria claimed
each keyboard-operable and separately announced (Separate Dashboard-tree expansion from opening the Dashboard #472).
exists after this phase (chevron,
⋯, the Replace curated Dashboard filters with inferred Variables and batched option queries #447 orphan trash). Phases 4/5 re-assert it asthey add the pencil and the panel trash.
Deferred by design: the Dashboard pencil and the one create path (phase 3), panel
metadata edit (4), atomic panel+query delete and ownership (5), the repair planner (6).
The
## Regressionsbullet "row click/double-click/Shift-click contract remainsunchanged" is scoped to the rows this issue does not revise — #472's own section
overrides it for Dashboard rows, and panel/variable/group rows are genuinely untouched.
The non-goal "Moving or redesigning the trailing Dashboard action menu as a control" is
respected: the control is unchanged, only one item of its contents went.
#438 / layout engines: phase 2 adds no tile-focus gesture — a Dashboard row navigates
to a Dashboard, never to a member — so there was nothing to verify on the flow engine. The
obligation stands for phases 4/5.
Verification
npm test— 5827 passed / 177 files, per-file gate held;tsc --noEmitclean;npm run buildclean.npm run test:e2e— 442 passed, 0 failed on chromium + webkit + firefox.(Firefox launches here again after
npx playwright install firefox; the dependabotPlaywright bump had left all three browser binaries stale, which reads as ~435 failures
at 1ms each.)
under-delivery, scope creep, non-goal breaches and rule-softening; its one substantive
finding is the accessible-name regression fixed above.
chevron focus restore, not moving the keyboard owner, making every chevron tabbable,
reverting the model split, and restoring
Open in View— each is caught by 1–6 tests andnothing else. The repeated-open history guard and the chevron box were sabotage-checked
too, as was the accessible name.
chevron is a
BUTTONlabelledExpand Sales revenuewitharia-expanded=falseandtabindex=0; Tab walks row → chevron →⋯; the three focus treatments are a box-shadowring, a 2px accent outline and the trailing button's own; a chevron click expands 2 → 4
rows without touching the URL; a name click routes to
?surface=dashboard&mode=view. No page errors.Checklist
npm testpasses (the per-file coverage gate is non-negotiable)npm run buildsucceeds (single-filedist/sql.html)src/core/, network insrc/net/(injected fetch), DOM insrc/ui/CHANGELOG.md([Unreleased]) updated if behavior or the deployed surface changed🤖 Generated with Claude Code
https://claude.ai/code/session_018mujm1kW7jDGpTEfscndcU