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:
parent
473c58894c
commit
d0312c6721
@ -101,6 +101,13 @@ function injectScrollbarHideIfSameOrigin() {
|
|||||||
*::-webkit-scrollbar { display: none; }
|
*::-webkit-scrollbar { display: none; }
|
||||||
`
|
`
|
||||||
doc.head.appendChild(style)
|
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 {
|
} catch {
|
||||||
/* Cross-origin: cannot access iframe document */
|
/* 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(
|
watch(
|
||||||
() => store.isOpen,
|
() => store.isOpen,
|
||||||
(open) => {
|
(open) => {
|
||||||
@ -133,10 +146,12 @@ watch(
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('keydown', onKeyDown, true)
|
window.addEventListener('keydown', onKeyDown, true)
|
||||||
|
window.addEventListener('message', onMessage)
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.removeEventListener('keydown', onKeyDown, true)
|
window.removeEventListener('keydown', onKeyDown, true)
|
||||||
|
window.removeEventListener('message', onMessage)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -266,10 +266,25 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
|
|||||||
const mainEls = getElementsInZone('main')
|
const mainEls = getElementsInZone('main')
|
||||||
const hasZones = sidebarEls.length > 0 && mainEls.length > 0
|
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 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 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) {
|
if (e.key === 'ArrowRight' && hasZones && isInZone(activeEl, 'sidebar') && firstMain) {
|
||||||
playNavSound('move')
|
playNavSound('move')
|
||||||
firstMain.focus()
|
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')) {
|
if (isInZone(activeEl, 'sidebar')) {
|
||||||
const idx = sidebarEls.indexOf(activeEl)
|
const idx = sidebarEls.indexOf(activeEl)
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
const isDown = e.key === 'ArrowDown'
|
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]
|
const next = sidebarEls[nextIdx]
|
||||||
if (next && next !== activeEl) {
|
if (next && next !== activeEl) {
|
||||||
playNavSound('move')
|
playNavSound('move')
|
||||||
|
|||||||
@ -5,17 +5,26 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
|
|||||||
const isOpen = ref(false)
|
const isOpen = ref(false)
|
||||||
const url = ref('')
|
const url = ref('')
|
||||||
const title = ref('')
|
const title = ref('')
|
||||||
|
let previousActiveElement: HTMLElement | null = null
|
||||||
|
|
||||||
function open(payload: { url: string; title: string }) {
|
function open(payload: { url: string; title: string }) {
|
||||||
|
previousActiveElement = (document.activeElement as HTMLElement) || null
|
||||||
url.value = payload.url
|
url.value = payload.url
|
||||||
title.value = payload.title
|
title.value = payload.title
|
||||||
isOpen.value = true
|
isOpen.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
|
const toRestore = previousActiveElement
|
||||||
|
previousActiveElement = null
|
||||||
isOpen.value = false
|
isOpen.value = false
|
||||||
url.value = ''
|
url.value = ''
|
||||||
title.value = ''
|
title.value = ''
|
||||||
|
if (toRestore && typeof toRestore.focus === 'function') {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
toRestore.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -130,8 +130,8 @@
|
|||||||
class="flex-1 overflow-hidden relative pb-20 md:pb-0 glass-piece z-10"
|
class="flex-1 overflow-hidden relative pb-20 md:pb-0 glass-piece z-10"
|
||||||
:class="{ 'glass-throw-main': showZoomIn }"
|
:class="{ 'glass-throw-main': showZoomIn }"
|
||||||
>
|
>
|
||||||
<!-- App Switcher - top right, compact -->
|
<!-- App Switcher - top right, compact (Right arrow from sidebar goes here first) -->
|
||||||
<div class="absolute top-4 right-4 md:top-6 md:right-8 z-20">
|
<div data-controller-main-entry class="absolute top-4 right-4 md:top-6 md:right-8 z-20">
|
||||||
<AppSwitcher />
|
<AppSwitcher />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -91,7 +91,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Change Password -->
|
<!-- Change Password -->
|
||||||
<div class="mb-6">
|
<div data-controller-container tabindex="0" class="mb-6">
|
||||||
<button
|
<button
|
||||||
@click="showChangePasswordModal = true"
|
@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"
|
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"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user