fix: gamepad nav dead ends on Apps page, orange glass active sidebar style
All checks were successful
Build Archipelago ISO (dev) / build-iso (push) Successful in 57m48s
All checks were successful
Build Archipelago ISO (dev) / build-iso (push) Successful in 57m48s
- Nav-tab-active now uses orange glass (bg, border, glow, gradient) - Sidebar focus-visible uses matching orange tint - Enter on containers skips uninstall button, finds primary action - Down/Right from grid edges falls back to all focusable elements - Global fallback for standalone buttons in empty/error states - Full gamepad nav map for all onboarding screens + login modes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -301,28 +301,38 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
|
||||
e.preventDefault()
|
||||
|
||||
if (isContainer(activeEl)) {
|
||||
// Container has a primary action link (the > chevron)?
|
||||
const primaryLink = activeEl.querySelector<HTMLElement>('a[href]')
|
||||
// Prioritised action: install button
|
||||
if (activeEl.hasAttribute('data-controller-install')) {
|
||||
const btn = activeEl.querySelector<HTMLButtonElement>('[data-controller-install-btn]:not([disabled])')
|
||||
if (btn) { playNavSound('action'); btn.click(); return }
|
||||
}
|
||||
// Prioritised action: launch button
|
||||
if (activeEl.hasAttribute('data-controller-launch')) {
|
||||
const btn = activeEl.querySelector<HTMLButtonElement>('[data-controller-launch-btn]:not([disabled])')
|
||||
if (btn) { playNavSound('action'); btn.click(); return }
|
||||
}
|
||||
// Default: click the primary link to navigate to that section
|
||||
// Primary link (e.g. dashboard cards with a[href])
|
||||
const primaryLink = activeEl.querySelector<HTMLElement>('a[href]')
|
||||
if (primaryLink) {
|
||||
playNavSound('action')
|
||||
primaryLink.click()
|
||||
return
|
||||
}
|
||||
// No primary link — drill into inner controls
|
||||
// Fallback: first non-disabled action button (skip uninstall/delete buttons)
|
||||
const inner = getInnerFocusables(activeEl)
|
||||
if (inner[0]) {
|
||||
focusEl(inner[0], 'action')
|
||||
const actionBtn = inner.find(el =>
|
||||
(el.tagName === 'BUTTON' || el.getAttribute('role') === 'button') &&
|
||||
!el.getAttribute('aria-label')?.toLowerCase().includes('uninstall') &&
|
||||
!el.closest('[class*="absolute top"]')
|
||||
) ?? inner[0]
|
||||
if (actionBtn) {
|
||||
focusEl(actionBtn, 'action')
|
||||
return
|
||||
}
|
||||
// Last resort: click the container itself (triggers goToApp on AppCard)
|
||||
playNavSound('action')
|
||||
activeEl.click()
|
||||
return
|
||||
}
|
||||
|
||||
// Regular element: click it
|
||||
@@ -462,9 +472,36 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
|
||||
return
|
||||
}
|
||||
|
||||
// At grid edges (down/right with no target): do nothing
|
||||
// At grid edges: try all focusable elements in main zone as fallback
|
||||
// (prevents dead ends when spatial nav between containers fails)
|
||||
if (dir === 'down' || dir === 'right') {
|
||||
const zone = document.querySelector('[data-controller-zone="main"]') as HTMLElement | null
|
||||
if (zone) {
|
||||
const allFocusable = getFocusableElements(zone)
|
||||
const fallback = findNearestInDirection(activeEl, allFocusable, dir)
|
||||
if (fallback) {
|
||||
rememberFocus('main', fallback)
|
||||
focusEl(fallback)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ── FALLBACK: unhandled focusable element ───────────────
|
||||
// Covers standalone buttons/links in empty/error states, modals, etc.
|
||||
// that aren't inside a recognized zone or container.
|
||||
if (dir === 'left') {
|
||||
const sidebar = getSidebarElements()
|
||||
const sidebarZone = document.querySelector('[data-controller-zone="sidebar"]')
|
||||
const activeTab = sidebarZone?.querySelector<HTMLElement>('.nav-tab-active')
|
||||
const target = activeTab ?? sidebar[0]
|
||||
if (target) { rememberFocus('main', activeEl); focusEl(target) }
|
||||
} else {
|
||||
const all = getFocusableElements()
|
||||
const next = findNearestInDirection(activeEl, all, dir)
|
||||
if (next) focusEl(next)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Gamepad Detection ──────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user