You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
For ordinary tiles and grafana-grid tiles this works: the tile is scrolled into view, receives programmatic focus, and receives the temporary navigation highlight.
For a KPI tile rendered by the flow layout, the navigation request focuses and scrolls a display: contents element. That element has no physical box, so the Dashboard reports success while the visible KPI remains off-screen and has no persistent focus indication.
This is a functional and accessibility regression in #425's completed navigation contract. Fix it before #426 starts using tile-focus navigation from the Dashboard tree.
Current implementation
src/ui/dashboard.ts resolves a tile focus target as follows:
This is required by the current KPI-band composition model. One KPI query may produce several .kpi-card elements, and the cards from consecutive KPI tiles participate directly in one wrapping .dash-kpi-stream. The host remains in the DOM for tile identity, accessibility grouping, drag wiring, and lifecycle ownership, but it generates no layout box.
Measured in Chromium against tests/e2e/dashboard-kpi-move.html, the host's getBoundingClientRect() is { x: 0, y: 0, width: 0, height: 0 }.
Consequences:
scrollIntoView() has no target geometry and does not bring the KPI into view;
focus() can make the host document.activeElement, but the host has no box on which to paint a focus indicator;
the navigation call is treated as successful, so no diagnostic explains the miss;
keyboard and assistive-technology users are left with focus on an element that has no visible focus location.
Existing partial fix
Current main already contains the CSS reach-through added with #433:
This makes the temporary navigation highlight visible on the physical KPI cards. Keep this behaviour.
It does not fix scrolling or normal focus indication because JavaScript still scrolls and focuses the zero-box host as though it were a normal tile card.
Required design
Do not force one DOM element to serve as the semantic focus target, scroll geometry, and highlight owner.
Introduce an internal resolved-navigation target with separate roles. The exact name is implementation-defined; the following shape illustrates the required contract:
interfaceDashboardNavigationTarget{/** Receives tabindex=-1 and programmatic focus. */focusNode: HTMLElement;/** Has real browser geometry and receives scrollIntoView(). */scrollNode: HTMLElement;/** Owns the temporary is-nav-target class. */highlightNode: HTMLElement;}
Resolution rules:
Target kind
focusNode
scrollNode
highlightNode
Ordinary/grafana-grid tile
tile card
tile card
tile card
Flow-layout KPI tile
.dash-kpi-member host
first rendered physical child card/state card
.dash-kpi-member host
Curated filter
filter field
filter field
filter field
This preserves the correct semantics for a flow KPI tile:
the host remains the focused tile-level group and already has role="group" plus the tile title as its accessible name;
the leading physical child provides deterministic, non-zero scroll geometry;
the host owns the temporary class, so the existing descendant selector highlights every card produced by that tile;
a multi-card KPI remains one tile semantically without changing the flattened wrapping layout.
Why the first physical child is the scroll anchor
A KPI query may render multiple cards. Because the host uses display: contents, those cards can wrap across lines and do not have one honest rectangular tile box. Creating a physical wrapper would change the current stream layout and grouping behaviour.
Use the first rendered child as the stable leading scroll anchor. This is sufficient to bring the start of the requested tile into view while the tile-level host remains the semantic focus owner and all of its children receive the navigation highlight.
Do not:
change .dash-kpi-member to a normal flex/grid box;
wrap each KPI tile's cards in a new physical layout container;
focus only an arbitrary KPI metric and thereby replace the tile-level accessible name with one field's label;
calculate and maintain an absolutely positioned synthetic union rectangle;
add a timeout to retry focus.
Focus indication
After the temporary navigation highlight clears, focus remains on .dash-kpi-member. A visible normal focus indicator must therefore be painted on its physical children while the host is focused.
Add a descendant focus rule consistent with the existing focus vocabulary, for example:
.dash-kpi-member:focus-visible>* {
/* normal focus treatment, distinct from --ring-nav */
}
The exact property may follow the repository's existing focus convention, but these requirements are fixed:
focus must remain visible after .is-nav-target is removed;
the normal focus indication and temporary navigation highlight must be distinguishable;
the temporary highlight must remain additive rather than permanently replacing focus styling;
every physical child belonging to a multi-card KPI tile must show the tile-level navigation/focus state consistently;
no new persistent Tab stop is added: the host receives tabindex="-1" only for programmatic focus.
Do not move tabindex onto every .kpi-card; that would introduce extra stops and expose metric cards as separate navigated tiles.
Empty and transitional host handling
The presence of a .dash-kpi-member host proves the tile exists. A temporarily empty host must not be reported as:
That panel is no longer on this dashboard.
Normally renderKpiInto() supplies at least one child for every state:
Still guard the resolver against a host with no physical child during a render transition.
Required behaviour:
do not classify an existing empty host as a missing tile;
deliver tile focus only after the flow reconciliation has populated KPI content through the existing deterministic render-complete path;
if the implementation extracts a resolver that can be called before content exists, return a pending/unready result or defer to the next existing render publication—do not use an arbitrary timer;
apply and later clear the temporary class on highlightNode;
use the existing generation and user-interaction guards.
Make highlightNavigationTarget() operate on the highlight owner without assuming it is also the active or scrolled element.
Preserve teardown of the highlight timer and document listeners.
src/styles.css
Keep .dash-kpi-member { display: contents; }.
Keep the existing .dash-kpi-member.is-nav-target > * reach-through.
Add persistent descendant focus styling for a focused host.
Ensure the normal focus indication remains visible after the temporary class clears.
Do not change KPI stream sizing, wrapping, drag geometry, card widths, or mobile layout.
Tests
Use the existing #425 navigation-focus suite and the real-browser KPI fixture rather than creating unrelated infrastructure.
Unit tests
Extend describe('renderDashboard — navigation focus (#425)') in tests/unit/dashboard.test.ts.
Required assertions:
Flow KPI target separation
render a flow-layout KPI tile;
focus by tile ID;
assert document.activeElement is .dash-kpi-member;
assert the host receives tabindex="-1" and .is-nav-target;
assert the recorded scrollIntoView call targets the first physical child, not the host.
Multi-card KPI
render a KPI query producing at least three cards;
assert all cards remain children of one .dash-kpi-member;
assert navigation does not add a physical wrapper or alter card count/order;
assert the host—not an individual metric card—is the active element.
State-card target
cover at least one non-ready state;
assert the .dash-kpi-state-card is used as the scroll node while the host receives focus/highlight.
No false missing diagnostic
exercise an existing flow host before/while its physical content is populated through an extracted resolver or deterministic publication seam;
assert the missing-panel toast is not shown merely because the host temporarily has no child.
Missing ID remains non-destructive
an unknown tile ID opens the Dashboard, focuses nothing, and shows the existing diagnostic.
Stale generation
the new split-node flow path remains suppressed when the renderer generation is stale.
Highlight cleanup
clearing the temporary highlight removes it from the host and descendants without moving focus.
Happy DOM cannot prove geometry or actual scrolling; unit tests should assert node selection and lifecycle only.
Playwright regression test
Add real-browser coverage, preferably by extending the existing KPI movement fixture or adding a focused sibling fixture/spec.
The fixture must:
use a flow layout;
place the target KPI below the initial .dash-page viewport so scrolling is required;
include one KPI tile that produces multiple cards (the existing first fixture query already produces three);
expose a deterministic way to request focus after the initial render, without a timeout;
keep the Dashboard's actual .dash-page as the scroll container.
Required browser assertions:
reset .dash-page.scrollTop to 0 and invoke tile navigation;
.dash-page.scrollTop becomes greater than 0, or otherwise changes enough to bring the target into view;
document.activeElement is the requested .dash-kpi-member host;
the host's own rectangle may remain zero—this is expected and must not be used as the success condition;
the first physical card/state-card has non-zero geometry and intersects the visible Dashboard viewport after navigation;
a multi-card tile's physical children receive the temporary navigation styling;
after the temporary highlight is cleared by the next user interaction, the host remains focused and a normal visible focus indication remains on its children;
no unrelated KPI tile is marked or focused;
ordinary tile focus remains unchanged.
Run this in Chromium at minimum because the bug depends on real layout geometry. It may run in the repository's full browser matrix where stable.
Regression boundaries
The change must not alter:
KPI stream flattening or wrapping;
multi-card KPI visual ordering;
KPI drag/reorder behaviour, including Command/Ctrl-drag of the complete member;
ordinary text selection inside KPI cards;
grafana-grid tile focus;
filter focus;
tile-ID versus query-ID resolution;
Dashboard execution, refresh, layout, persistence, or revisions;
View/Edit mode semantics;
mobile Dashboard layout;
the bounded navigation-highlight duration and interaction-based cleanup.
Acceptance criteria
Navigating to a flow-layout KPI scrolls a real physical child into the Dashboard viewport.
The tile-level .dash-kpi-member receives programmatic focus and retains its accessible group name.
A visible normal focus indication remains after the temporary navigation highlight clears.
Single-card, multi-card, loading/unfilled/error/zero-data KPI states are handled without changing layout semantics.
An existing but temporarily empty KPI host is not misreported as a missing panel.
Missing tile IDs still produce the existing non-destructive diagnostic.
Stale render/workspace/surface focus work remains suppressed.
Ordinary tile and curated-filter navigation are unchanged.
Unit coverage verifies target separation and lifecycle behaviour.
Playwright coverage proves real scrolling and visible focus in a browser.
KPI movement/reorder and wrapping tests remain green.
Verification
The implementation PR must report results for:
npm test
npx tsc --noEmit
npm run build
npx playwright test tests/e2e/dashboard-kpi-move.spec.js <new-or-extended-focus-spec>
Run the complete Playwright matrix when the local/CI environment supports it.
Summary
Dashboard navigation can open a Dashboard with an optional tile focus target:
For ordinary tiles and
grafana-gridtiles this works: the tile is scrolled into view, receives programmatic focus, and receives the temporary navigation highlight.For a KPI tile rendered by the
flowlayout, the navigation request focuses and scrolls adisplay: contentselement. That element has no physical box, so the Dashboard reports success while the visible KPI remains off-screen and has no persistent focus indication.This is a functional and accessibility regression in #425's completed navigation contract. Fix it before #426 starts using tile-focus navigation from the Dashboard tree.
Current implementation
src/ui/dashboard.tsresolves a tile focus target as follows:applyNavigationFocus()then treats that one element as all three concepts:A flow KPI entry in
flowKpiHostsis.dash-kpi-member:The stylesheet deliberately defines:
This is required by the current KPI-band composition model. One KPI query may produce several
.kpi-cardelements, and the cards from consecutive KPI tiles participate directly in one wrapping.dash-kpi-stream. The host remains in the DOM for tile identity, accessibility grouping, drag wiring, and lifecycle ownership, but it generates no layout box.Measured in Chromium against
tests/e2e/dashboard-kpi-move.html, the host'sgetBoundingClientRect()is{ x: 0, y: 0, width: 0, height: 0 }.Consequences:
scrollIntoView()has no target geometry and does not bring the KPI into view;focus()can make the hostdocument.activeElement, but the host has no box on which to paint a focus indicator;Existing partial fix
Current
mainalready contains the CSS reach-through added with #433:This makes the temporary navigation highlight visible on the physical KPI cards. Keep this behaviour.
It does not fix scrolling or normal focus indication because JavaScript still scrolls and focuses the zero-box host as though it were a normal tile card.
Required design
Do not force one DOM element to serve as the semantic focus target, scroll geometry, and highlight owner.
Introduce an internal resolved-navigation target with separate roles. The exact name is implementation-defined; the following shape illustrates the required contract:
Resolution rules:
focusNodescrollNodehighlightNodegrafana-gridtile.dash-kpi-memberhost.dash-kpi-memberhostThis preserves the correct semantics for a flow KPI tile:
role="group"plus the tile title as its accessible name;Why the first physical child is the scroll anchor
A KPI query may render multiple cards. Because the host uses
display: contents, those cards can wrap across lines and do not have one honest rectangular tile box. Creating a physical wrapper would change the current stream layout and grouping behaviour.Use the first rendered child as the stable leading scroll anchor. This is sufficient to bring the start of the requested tile into view while the tile-level host remains the semantic focus owner and all of its children receive the navigation highlight.
Do not:
.dash-kpi-memberto a normal flex/grid box;Focus indication
After the temporary navigation highlight clears, focus remains on
.dash-kpi-member. A visible normal focus indicator must therefore be painted on its physical children while the host is focused.Add a descendant focus rule consistent with the existing focus vocabulary, for example:
The exact property may follow the repository's existing focus convention, but these requirements are fixed:
.is-nav-targetis removed;tabindex="-1"only for programmatic focus.Do not move
tabindexonto every.kpi-card; that would introduce extra stops and expose metric cards as separate navigated tiles.Empty and transitional host handling
The presence of a
.dash-kpi-memberhost proves the tile exists. A temporarily empty host must not be reported as:Normally
renderKpiInto()supplies at least one child for every state:.dash-kpi-state-card;.kpi-cardelements.Still guard the resolver against a host with no physical child during a render transition.
Required behaviour:
Behaviour by KPI state
The fix must work in both View and Edit modes for:
For state cards, the state card is the physical scroll anchor while the host remains the focus/highlight owner.
Accessibility contract
For a successfully focused flow KPI tile:
document.activeElementis the.dash-kpi-member[data-tile="…"]host;tabindex="-1";role="group";Lifecycle and stale-state requirements
Preserve all #425 safeguards:
The new separation of
focusNode,scrollNode, andhighlightNodemust not weaken these generation and teardown checks.Suggested implementation steps
src/ui/dashboard.tsflowKpiHosts.get(tileId);focusNodeandhighlightNode;HTMLElementchild asscrollNodeafterrenderKpiInto()has populated it;applyNavigationFocus()to:tabindex="-1"tofocusNodewhen absent;scrollNode.scrollIntoView({ block: 'nearest' });focusNode.focus();highlightNode;highlightNavigationTarget()operate on the highlight owner without assuming it is also the active or scrolled element.src/styles.css.dash-kpi-member { display: contents; }..dash-kpi-member.is-nav-target > *reach-through.Tests
Use the existing #425 navigation-focus suite and the real-browser KPI fixture rather than creating unrelated infrastructure.
Unit tests
Extend
describe('renderDashboard — navigation focus (#425)')intests/unit/dashboard.test.ts.Required assertions:
Flow KPI target separation
document.activeElementis.dash-kpi-member;tabindex="-1"and.is-nav-target;scrollIntoViewcall targets the first physical child, not the host.Multi-card KPI
.dash-kpi-member;State-card target
.dash-kpi-state-cardis used as the scroll node while the host receives focus/highlight.No false missing diagnostic
Missing ID remains non-destructive
Stale generation
Highlight cleanup
Happy DOM cannot prove geometry or actual scrolling; unit tests should assert node selection and lifecycle only.
Playwright regression test
Add real-browser coverage, preferably by extending the existing KPI movement fixture or adding a focused sibling fixture/spec.
The fixture must:
flowlayout;.dash-pageviewport so scrolling is required;.dash-pageas the scroll container.Required browser assertions:
.dash-page.scrollTopto0and invoke tile navigation;.dash-page.scrollTopbecomes greater than0, or otherwise changes enough to bring the target into view;document.activeElementis the requested.dash-kpi-memberhost;Run this in Chromium at minimum because the bug depends on real layout geometry. It may run in the repository's full browser matrix where stable.
Regression boundaries
The change must not alter:
grafana-gridtile focus;Acceptance criteria
.dash-kpi-memberreceives programmatic focus and retains its accessible group name.Verification
The implementation PR must report results for:
Run the complete Playwright matrix when the local/CI environment supports it.
Related work
display: contentsKPI hosts.