Skip to content

feat(charts): keyboard navigation for time-series charts#106

Open
JeremyFunk wants to merge 1 commit into
feat/chart-time-range-uxfrom
feat/chart-keyboard-nav
Open

feat(charts): keyboard navigation for time-series charts#106
JeremyFunk wants to merge 1 commit into
feat/chart-time-range-uxfrom
feat/chart-keyboard-nav

Conversation

@JeremyFunk

Copy link
Copy Markdown
Collaborator

Keyboard control over the selected time window. Stacked on #105 (the expand modal that F opens, plus the shared isOverlayOpen guard, live there) — review/merge #105 first; this PR's diff is keyboard-only.

Keys (dashboard, service-detail, home)

Key Action
← / → Pan the window into the past / future (→ clamps at now)
↑ / ↓ Zoom in / out around the window center (min-width + ~2y lookback clamps)
Shift Much larger step
Ctrl / Meta Much finer step
F Open the hovered chart in the fullscreen expand modal

Implementation

  • useTimeRangeKeyboardControls — a window capture-phase keydown listener that operates on the resolved absolute window and writes an absolute range back. It preventDefaults only the keys it handles, and bails when an editable element is focused or an overlay (dialog / menu / listbox via isOverlayOpen) owns the keyboard — so it never hijacks menu/select/popover navigation.
  • F is handled per widget-shell (only the hovered chart responds).

Notes

  • Verified end-to-end: pan, zoom, Shift/Ctrl magnitudes, right-clamp-to-now, and the overlay guard (arrows + F don't fire while the picker is open).
  • bun typecheck at baseline (web 23 pre-existing, ui 0); lint/format clean.

🤖 Generated with Claude Code

Adds keyboard control over the selected time window on the dashboard,
service-detail, and home charts (a global capture-phase listener that
defers to focused inputs and any open dialog/menu/listbox):

- Left / Right  — pan the window into the past / future (Right clamps at now)
- Up / Down     — zoom in / out around the window center (clamped to a
                  min width and a ~2y lookback)
- Shift         — much larger step; Ctrl/Meta — much finer step
- F             — open the hovered chart in the fullscreen expand modal

Stacked on the chart time-range UX PR (the expand modal F targets, plus the
shared `isOverlayOpen` guard, live there).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@pullfrog pullfrog 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.

ℹ️ No critical issues — two informational observations, nothing blocking.

Reviewed changes — keyboard pan/zoom of the selected time window across the dashboard, service-detail, and home pages, plus an F shortcut to expand the hovered chart. This is the keyboard-only slice; it stacks on #105 (base feat/chart-time-range-ux), which carries the expand modal and the isOverlayOpen plumbing.

  • Add useTimeRangeKeyboardControls — a window capture-phase keydown listener that reads the resolved absolute window, applies pan (←/→) or center-zoom (↑/↓) with Shift/Ctrl-Meta step modifiers, and writes an absolute range back via onChange. Clamps into [now − 2y, now] with a 1-minute floor via a single-pass clampToBand, and no-ops when already clamped.
  • Add isOverlayOpen() to keyboard.ts — broader than isDialogOpen(), also matching role="menu"/role="listbox" [data-open] so page shortcuts defer to dropdown/select navigation.
  • Per-widget F listener in widget-shell.tsx — attached only while the shell is hovered and canExpand, opening the expand modal for the chart under the mouse.
  • Route wiringdashboards/$dashboardId.tsx (enabled when resolvedTimeRange != null), services/$serviceName.tsx (enabled only on the overview tab), and index.tsx (no enabled guard, since the home page always renders charts).

The pan/zoom math, the warehouse-string round-trip (no timezone drift through parseMs/formatForTinybird), and the disjoint-key handling between the two capture-phase listeners all check out. The two notes below are deliberate-design questions, not bugs.

ℹ️ Arrow keys stop scrolling the page on these three routes

The capture-phase listener preventDefault()s every arrow keypress it owns whenever no input is focused and no overlay is open, on all three routes. That removes arrow-key vertical scrolling for keyboard users on pages that can be long and scrollable (dashboards in particular). It is an intentional tradeoff for the pan/zoom feature, but worth a conscious call since it affects the whole page, not just the charts.

Technical details
# Arrow keys no longer scroll the page on keyboard-nav routes

## Affected sites
- `apps/web/src/hooks/use-time-range-keyboard.ts:155-156``preventDefault()`/`stopPropagation()` fire for any handled arrow key once the editable/overlay guards pass, regardless of whether the user intended to scroll.

## Required outcome
- Confirm losing arrow-key page scroll on the dashboard / service-detail / home routes is the intended UX. Page Up/Down and the mouse wheel still scroll, so this may be fine — but it is a global behavior change, not chart-scoped.

## Open questions for the human
- Should scroll be preserved when the pointer/focus is not over a chart region (e.g. only capture arrows while a chart is hovered, mirroring the `F` hover-scoping in `widget-shell.tsx`)? That would localize the keyboard takeover to the charts the feature is about.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

end: resolvedTimeRange?.endTime ?? "",
enabled: resolvedTimeRange != null,
onChange: ({ startTime, endTime }) => setTimeRange({ type: "absolute", startTime, endTime }),
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Keyboard pan/zoom stays active in dashboard edit mode here — it's gated only on resolvedTimeRange != null, with no mode === "view" check (unlike the service-detail wiring, which gates on the overview tab). While editing widget layout, arrow keys will pan the time window and the capture-phase preventDefault will swallow them before anything else. Benign today since react-grid-layout's keyboard a11y doesn't appear to be enabled, but worth a conscious decision on whether arrows should drive the time window while the user is positioning widgets.

Technical details
# Keyboard nav active during dashboard edit mode

## Affected sites
- `apps/web/src/routes/dashboards/$dashboardId.tsx:50-55``useTimeRangeKeyboardControls` enabled whenever `resolvedTimeRange != null`; `mode` (`"edit"`/`"view"`, computed at line 89) is not consulted.

## Required outcome
- Decide whether arrow-key pan/zoom should be suppressed in edit mode. If so, thread `mode` into `DashboardRefreshBridge` (or read it where the bridge is rendered) and pass `enabled: mode === "view" && resolvedTimeRange != null`.

## Open questions for the human
- Is panning the window while editing widgets desirable (the charts still render the new range), or should editing fully own the keyboard? The service-detail route already takes the conservative path (`enabled` scoped to the overview tab), so scoping here would be consistent.


useEffect(() => {
if (!hovered || !canExpand) return
const handler = (e: KeyboardEvent) => {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Please use https://tanstack.com/hotkeys/latest/docs/overview for hotkey managment ratehr than this manual one

<Card className="group/card h-full flex flex-col">
<Card
className="group/card h-full flex flex-col"
onMouseEnter={() => setHovered(true)}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

should use real focus states rather than this hovered state i think this is a bit hacky since this wont work with focus rn

useEffect(() => {
if (!enabled) return

const handler = (e: KeyboardEvent) => {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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