Keyboard Navigation Implementation in Vanilla-Grid

This document explains how keyboard navigation is implemented in Vanilla-Grid, including event gating, key-to-scroll translation, repeat behavior, momentum guards, and interaction with wheel/custom scrollbar input.


1. Feature Scope

Keyboard navigation supports scrolling by:

Behavior is integrated with virtualization, custom scrollbar state, and momentum rejection safeguards.


2. Event Wiring and Lifetime

Shared listener registry (default). Rather than every grid instance registering its own keydown/keyup (document) and resize/blur (window) listeners, vanilla-grid.js maintains a module-level singleton registry (_vgKeyboardRegistry) that installs exactly one listener of each type, regardless of how many grids are on the page. _initEventListeners() calls _vgKeyboardRegistry.add(this), which:

destroy() calls _vgKeyboardRegistry.remove(this); the shared listeners are removed once the last registered grid is destroyed.

Escape hatch: set window.VanillaGridKeyboardSingleton = false before constructing grids to restore the previous behaviour — each instance binds and registers its own four listeners (boundKeydown, boundKeyup, boundResize, boundWindowBlur), removed individually in that instance's destroy(). Singleton and legacy-mode instances can coexist on the same page (each instance remembers which path it used via _useKeyboardSingleton, decided once in _initEventListeners()).

Cleanup in destroy() removes listeners (shared or per-instance) and resets key-scrolling state.

On window blur, handleWindowBlur() calls stopKeyScroll() and resets gesture state to avoid stuck interactions.


3. Keyboard State Model

Internal fields:

These fields allow keyboard input to coexist with wheel/drag/programmatic scroll paths.


4. Input Gating (shouldHandleKeyboardEvent)

Keyboard events are ignored when:

Events are handled only when viewport is effectively active:

This avoids hijacking typing interactions elsewhere in the page. The button exemption exists so a focused in-cell <button> — today, only row grouping's caption toggle — keeps its own native Space/Enter activation instead of the grid claiming Space as a page-down scroll. This is a shared-path rule, not grouping-specific: any future in-cell button gets the same native-activation behavior for free. Space still pages the grid normally whenever focus is on the viewport itself, not on a button (regression-tested in tests/playwright/keyboard-navigation.spec.js).


5. Keydown Flow (handleKeydown)

handleKeydown(e) sequence:

  1. guard checks (viewport, loading state, ctrl key, keyboard eligibility)
  2. map supported key
  3. preventDefault() to fully own scroll behavior
  4. compute deltaPixels
  5. dispatch by key type:
    • Home / End: immediate jump and short momentum guard
    • repeatable keys (Arrow*, Page*, Space): startKeyScroll(...)
    • fallback immediate delta apply

Key mapping:


6. Keyup and Idle Stop

handleKeyup(e) stops key scrolling when released key matches active key (including space alias handling).

startKeyScroll(key, shiftKey, isRepeat):

  1. stores active key state
  2. clears previous idle timer
  3. computes normalized key step via getKeyScrollDelta(...)
  4. applies delta through applyScrollDelta(..., 'key')
  5. schedules idle stop (scheduleKeyScrollStop())

scheduleKeyScrollStop() clears key-active state after 300ms of inactivity and arms momentum guard.

stopKeyScroll() performs explicit key-state reset and guard arming.


7. Step Sizing and Repeat Behavior

getKeyScrollDelta(key, shiftKey, isRepeat) scales movement:

This produces responsive repeat scrolling while avoiding extreme jumps.


8. Scroll Application Path

All keyboard movement uses applyScrollDelta(deltaPixels, 'key').

applyScrollDelta(...):

  1. clamps target scroll top to viewport bounds
  2. records programmatic write metadata
  3. sets viewport.scrollTop

Because the same path is used by wheel/scrollbar inputs (with different source labels), downstream guard logic can reason about input origin consistently.


9. Momentum Guard and Scroll Rejection

9.1 Guard arming

armMomentumGuard(duration) sets a future window where uncontrolled follow-up scroll is scrutinized.

Keyboard code arms this after:

9.2 Rejection (shouldRejectUncontrolledScroll)

During active guard window, scroll events are rejected when they appear uncontrolled relative to the last programmatic write.

Rejection is bypassed while user is actively interacting via:

Direct follow-up events close in time/position to programmatic writes are allowed.

This reduces momentum run-on and resize/gesture feedback loops.


10. Interaction with Wheel and Scrollbar Input

Keyboard navigation is part of a unified input model:

The result is predictable arbitration across mixed input modalities.


11. Virtualization Coupling

Keyboard updates scrollTop; virtualization then reacts in handleScroll() and renderVisibleRows(...).

Because keyboard uses deterministic pixel deltas and clamp logic:


12. Practical Notes


13. Example Usage

No extra API call is required; keyboard support is active by default.

To ensure good UX: