Enhance AppLauncherOverlay and navigation logic for improved user experience

- Added functionality to close the overlay and return focus to the launcher when the Escape key is pressed inside an iframe.
- Implemented message handling to close the app launcher from the parent window.
- Updated navigation logic in useControllerNav to improve focus management when navigating between sidebar and main content.
- Enhanced Dashboard and Settings views with data attributes for better controller navigation support.
This commit is contained in:
Dorian 2026-02-18 11:29:05 +00:00
parent 473c58894c
commit d0312c6721
5 changed files with 51 additions and 7 deletions

View File

@ -101,6 +101,13 @@ function injectScrollbarHideIfSameOrigin() {
*::-webkit-scrollbar { display: none; }
`
doc.head.appendChild(style)
// Escape from inside iframe close overlay and return focus to launcher
doc.addEventListener('keydown', (e) => {
if ((e as KeyboardEvent).key === 'Escape') {
e.preventDefault()
window.parent.postMessage({ type: 'app-launcher-escape' }, '*')
}
})
} catch {
/* Cross-origin: cannot access iframe document */
}
@ -120,6 +127,12 @@ function onKeyDown(e: KeyboardEvent) {
}
}
function onMessage(e: MessageEvent) {
if (e.data?.type === 'app-launcher-escape' && store.isOpen) {
store.close()
}
}
watch(
() => store.isOpen,
(open) => {
@ -133,10 +146,12 @@ watch(
onMounted(() => {
window.addEventListener('keydown', onKeyDown, true)
window.addEventListener('message', onMessage)
})
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKeyDown, true)
window.removeEventListener('message', onMessage)
})
</script>

View File

@ -266,10 +266,25 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
const mainEls = getElementsInZone('main')
const hasZones = sidebarEls.length > 0 && mainEls.length > 0
// Right: from sidebar → main (on App Store/My Apps, go straight to first app container)
// Right: from sidebar → main
// - On Apps/Marketplace: go to first app container
// - On Cloud: go to first folder (Pictures)
// - On Network (server): go to Services container
// - On Web5: go to Networking Profits container
// - On Settings: go to Change Password container
// - Otherwise: go to top right (App Switcher)
const mainZone = document.querySelector('[data-controller-zone="main"]')
const isAppsOrMarketplace = /^\/dashboard\/(apps|marketplace)(\/|$)/.test(route.path)
const isCloud = /^\/dashboard\/cloud(\/|$)/.test(route.path)
const isNetwork = /^\/dashboard\/server(\/|$)/.test(route.path)
const isWeb5 = /^\/dashboard\/web5(\/|$)/.test(route.path)
const isSettings = /^\/dashboard\/settings(\/|$)/.test(route.path)
const firstAppContainer = mainZone?.querySelector<HTMLElement>('[data-controller-container]')
const firstMain = firstAppContainer ?? mainEls[0]
const topRightEntry = mainZone?.querySelector<HTMLElement>('[data-controller-main-entry]')
const firstFocusableInTopRight = topRightEntry ? getFocusableElements(topRightEntry)[0] : null
const firstMain = ((isAppsOrMarketplace || isCloud || isNetwork || isWeb5 || isSettings) && firstAppContainer)
? firstAppContainer
: (firstFocusableInTopRight ?? mainEls[0])
if (e.key === 'ArrowRight' && hasZones && isInZone(activeEl, 'sidebar') && firstMain) {
playNavSound('move')
firstMain.focus()
@ -323,12 +338,17 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
}
}
// Sidebar: linear up/down
// Sidebar: linear up/down with wrap (Home+Up→Logout, Logout+Down→Home)
if (isInZone(activeEl, 'sidebar')) {
const idx = sidebarEls.indexOf(activeEl)
if (idx >= 0) {
const isDown = e.key === 'ArrowDown'
const nextIdx = isDown ? Math.min(idx + 1, sidebarEls.length - 1) : Math.max(idx - 1, 0)
let nextIdx: number
if (isDown) {
nextIdx = idx >= sidebarEls.length - 1 ? 0 : idx + 1
} else {
nextIdx = idx <= 0 ? sidebarEls.length - 1 : idx - 1
}
const next = sidebarEls[nextIdx]
if (next && next !== activeEl) {
playNavSound('move')

View File

@ -5,17 +5,26 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
const isOpen = ref(false)
const url = ref('')
const title = ref('')
let previousActiveElement: HTMLElement | null = null
function open(payload: { url: string; title: string }) {
previousActiveElement = (document.activeElement as HTMLElement) || null
url.value = payload.url
title.value = payload.title
isOpen.value = true
}
function close() {
const toRestore = previousActiveElement
previousActiveElement = null
isOpen.value = false
url.value = ''
title.value = ''
if (toRestore && typeof toRestore.focus === 'function') {
requestAnimationFrame(() => {
toRestore.focus()
})
}
}
return {

View File

@ -130,8 +130,8 @@
class="flex-1 overflow-hidden relative pb-20 md:pb-0 glass-piece z-10"
:class="{ 'glass-throw-main': showZoomIn }"
>
<!-- App Switcher - top right, compact -->
<div class="absolute top-4 right-4 md:top-6 md:right-8 z-20">
<!-- App Switcher - top right, compact (Right arrow from sidebar goes here first) -->
<div data-controller-main-entry class="absolute top-4 right-4 md:top-6 md:right-8 z-20">
<AppSwitcher />
</div>

View File

@ -91,7 +91,7 @@
</div>
<!-- Change Password -->
<div class="mb-6">
<div data-controller-container tabindex="0" class="mb-6">
<button
@click="showChangePasswordModal = true"
class="w-full flex items-center justify-center gap-2 mb-4 px-4 py-2 rounded-lg border border-orange-500/50 text-orange-400 font-medium hover:bg-orange-500/10 transition-colors"