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:
@@ -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>
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user