Connect Page D-pad Navigation Overhaul
What started as a quick visual tweak to the server/user selection screen turned into a deep dive into Android TV WebView D-pad focus management. Every change below exists so that D-pad navigation on this screen feels completely intuitive — no user guide needed.
Visual Improvements
- Focus box wraps username + server URL — both lines are now inside a single focusable container, so the D-pad highlight covers the full entry
- Username matches server URL styling — same font size and color (
text-base text-fg) instead of the previoustext-lg text-white font-bold - Focus box breathing room — 6px symmetric padding on left/right edges prevents text cutoff at the focus ring boundary
- GitHub links scrollable on TV — changed from
position: fixedto normal document flow so D-pad can actually reach and scroll to them
D-pad Navigation
- Full left/right sub-icon chain: Entry row →right→ ellipsis →right→ delete, then delete →left→ ellipsis →left→ back to entry row
- Up/down always lands on the entry row, never on an adjacent row's icon — cross-row icon focus is intercepted and redirected
- First entry scroll-to-top — focusing the first server entry always scrolls the page to the top, keeping the back arrow visible and reachable
Bugs Fixed
- Double-jump on up/down from icons — native D-pad engine and programmatic focus were both moving focus, causing rows to be skipped
- Bounce on right from ellipsis to delete — keydown events bubbling from icons to the parent entry row caused competing deferred focus calls
- Left from ellipsis stuck — native D-pad cannot navigate from a child element to its parent; explicit handler added
- Page oscillation on row 1 — native focus-scroll behavior caused the viewport to jitter when cycling between icons; all focus calls now save/restore scroll position
- Left from entry row entering children — native D-pad was entering the row's children from the right side, landing on the delete icon
Technical Approach
The Android TV WebView's native D-pad spatial navigation has fundamental limitations inside nested focusable containers. The solution layers five mechanisms:
dpadFocus()— defers.focus({ preventScroll: true })viasetTimeout(0)to execute after the native engine finishes; saves/restores scroll positionhandleIconFocus()(@focus) — intercepts cross-row focus on icons and redirects to the entry row; respects_dpadFocusActiveflag to avoid interfering with in-flight deferred operations.prevent.stopon icon keydown — prevents both default behavior and event bubbling to the parent entry rowlockScroll()— prevents scroll oscillation on dead-end key presses (e.g., right from delete icon)onEntryRowFocus()— deferred scroll-to-top when first entry receives focus
Documentation updated in TV_FOCUS_SYSTEM.md with full D-pad navigation map and mechanism table.