From 7d61fc179040a6a865fbd0bf7d36e9f5032fcc3a Mon Sep 17 00:00:00 2001 From: Dorian Date: Mon, 30 Mar 2026 09:17:39 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20gamepad=20input=20field=20navigation=20?= =?UTF-8?q?=E2=80=94=20exit=20at=20cursor=20edges?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Up/Down from input: try containers as fallback when spatial nav fails - Left/Right from input: exit field when cursor is at start/end (e.g. Left from search bar at position 0 → category buttons) Co-Authored-By: Claude Opus 4.6 (1M context) --- neode-ui/src/composables/useControllerNav.ts | 34 +++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/neode-ui/src/composables/useControllerNav.ts b/neode-ui/src/composables/useControllerNav.ts index b1297b6f..03b799e7 100644 --- a/neode-ui/src/composables/useControllerNav.ts +++ b/neode-ui/src/composables/useControllerNav.ts @@ -246,14 +246,38 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) { if (nearest) { focusEl(nearest) } else { - // Fallback: tab order when spatial navigation fails - const idx = all.indexOf(target) - const fallback = dir === 'down' ? all[idx + 1] : all[idx - 1] - if (fallback) focusEl(fallback) + // Spatial nav failed — try containers directly (e.g. search bar → first container) + const containers = getContainers() + const containerNearest = containers.length + ? findNearestInDirection(target, containers, dir) + : null + if (containerNearest) { + focusEl(containerNearest) + } else { + // Last fallback: tab order + const idx = all.indexOf(target) + const fallback = dir === 'down' ? all[idx + 1] : all[idx - 1] + if (fallback) focusEl(fallback) + } } return } - // Left/Right: stay in field (cursor movement). Escape: handled below. + // Left/Right: cursor movement in field, but exit at edges + if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') { + const input = target as HTMLInputElement + const atStart = input.selectionStart === 0 && input.selectionEnd === 0 + const atEnd = input.selectionStart === (input.value?.length ?? 0) + if ((e.key === 'ArrowLeft' && atStart) || (e.key === 'ArrowRight' && atEnd)) { + e.preventDefault() + const dir = e.key === 'ArrowLeft' ? 'left' as const : 'right' as const + const all = getFocusableElements(containerRef?.value ?? document) + const candidates = all.filter(el => el !== target) + const nearest = findNearestInDirection(target, candidates, dir) + if (nearest) focusEl(nearest) + } + return + } + // Other keys (Escape): handled below. if (e.key !== 'Escape') return }