feat(ui): refine governed workbench and dual-screen flows
This commit is contained in:
@@ -112,16 +112,27 @@ function copyLoadedFonts(target: Window): void {
|
||||
})
|
||||
}
|
||||
|
||||
function initialiseSecondaryDocument(target: Window, onClose: () => void): HTMLElement {
|
||||
function syncSecondaryTheme(target: Window): void {
|
||||
const theme = document.body.dataset.theme
|
||||
if (theme === 'light' || theme === 'dark') target.document.body.dataset.theme = theme
|
||||
else delete target.document.body.dataset.theme
|
||||
}
|
||||
|
||||
export function initialiseSecondaryDocument(target: Window, onClose: () => void): HTMLElement {
|
||||
const targetDocument = target.document
|
||||
targetDocument.title = 'GeoIntel · Analyseconsole'
|
||||
targetDocument.documentElement.lang = document.documentElement.lang || 'nl'
|
||||
targetDocument.head.replaceChildren()
|
||||
|
||||
const charset = targetDocument.createElement('meta')
|
||||
charset.setAttribute('charset', 'utf-8')
|
||||
const title = targetDocument.createElement('title')
|
||||
title.textContent = 'GeoIntel · Analyseconsole'
|
||||
const base = targetDocument.createElement('base')
|
||||
base.href = document.baseURI
|
||||
const viewport = targetDocument.createElement('meta')
|
||||
viewport.name = 'viewport'
|
||||
viewport.content = 'width=device-width, initial-scale=1'
|
||||
targetDocument.head.append(viewport)
|
||||
targetDocument.head.append(charset, title, base, viewport)
|
||||
copyDocumentStyles(targetDocument)
|
||||
copyLoadedFonts(target)
|
||||
|
||||
@@ -133,7 +144,7 @@ function initialiseSecondaryDocument(target: Window, onClose: () => void): HTMLE
|
||||
header.innerHTML = `
|
||||
<div class="secondary-display-brand">
|
||||
<img src="/geointel-icon.svg" alt="" aria-hidden="true" />
|
||||
<span><small>LIVE GEKOPPELD</small><strong>Analyseconsole</strong></span>
|
||||
<span><small>LIVE GEKOPPELD</small><h1 tabindex="-1">Analyseconsole</h1></span>
|
||||
</div>
|
||||
<div class="secondary-display-status"><i aria-hidden="true"></i><span>Gesynchroniseerd met de kaart</span></div>
|
||||
`
|
||||
@@ -148,16 +159,23 @@ function initialiseSecondaryDocument(target: Window, onClose: () => void): HTMLE
|
||||
content.className = 'secondary-display-content'
|
||||
content.id = 'secondary-display-content'
|
||||
content.setAttribute('aria-label', 'Live analyse en resultaten')
|
||||
const contentHeading = targetDocument.createElement('h2')
|
||||
contentHeading.className = 'sr-only'
|
||||
contentHeading.textContent = 'Live analyse en resultaten'
|
||||
content.append(contentHeading)
|
||||
|
||||
shell.append(header, content)
|
||||
targetDocument.body.replaceChildren(shell)
|
||||
targetDocument.body.className = 'secondary-display-body'
|
||||
syncSecondaryTheme(target)
|
||||
header.querySelector<HTMLElement>('h1')?.focus({ preventScroll: true })
|
||||
return content
|
||||
}
|
||||
|
||||
export function useSecondaryDisplay() {
|
||||
const popupRef = useRef<Window | null>(null)
|
||||
const monitorTimerRef = useRef<number | null>(null)
|
||||
const returnFocusRef = useRef<HTMLElement | null>(null)
|
||||
const [container, setContainer] = useState<HTMLElement | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
@@ -183,21 +201,54 @@ export function useSecondaryDisplay() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const restoreFocus = useCallback(() => {
|
||||
const target = returnFocusRef.current
|
||||
returnFocusRef.current = null
|
||||
if (target?.isConnected) target.focus({ preventScroll: true })
|
||||
}, [])
|
||||
|
||||
const close = useCallback(() => {
|
||||
const popup = popupRef.current
|
||||
if (popup && !popup.closed) {
|
||||
rememberGeometry(popup)
|
||||
popup.close()
|
||||
try {
|
||||
rememberGeometry(popup)
|
||||
} catch {
|
||||
// A navigated popup can refuse geometry access; closing still wins.
|
||||
}
|
||||
try {
|
||||
popup.close()
|
||||
} catch {
|
||||
// State is cleared below even when the browser has already detached it.
|
||||
}
|
||||
}
|
||||
popupRef.current = null
|
||||
setContainer(null)
|
||||
stopMonitoring()
|
||||
}, [rememberGeometry, stopMonitoring])
|
||||
restoreFocus()
|
||||
}, [rememberGeometry, restoreFocus, stopMonitoring])
|
||||
|
||||
const open = useCallback(() => {
|
||||
const existing = popupRef.current
|
||||
if (existing && !existing.closed) {
|
||||
existing.focus()
|
||||
try {
|
||||
const existingContent = existing.document.getElementById('secondary-display-content')
|
||||
const content = existingContent?.nodeType === Node.ELEMENT_NODE
|
||||
? existingContent as HTMLElement
|
||||
: initialiseSecondaryDocument(existing, close)
|
||||
syncSecondaryTheme(existing)
|
||||
setContainer(content)
|
||||
setError(null)
|
||||
existing.focus()
|
||||
} catch {
|
||||
try {
|
||||
existing.close()
|
||||
} finally {
|
||||
popupRef.current = null
|
||||
setContainer(null)
|
||||
stopMonitoring()
|
||||
}
|
||||
setError('Het tweede venster kon niet opnieuw worden gekoppeld. Sluit het venster en probeer opnieuw.')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -207,6 +258,7 @@ export function useSecondaryDisplay() {
|
||||
} catch {
|
||||
saved = null
|
||||
}
|
||||
returnFocusRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null
|
||||
const popup = window.open('', SECONDARY_DISPLAY_NAME, secondaryDisplayFeatures(saved ?? defaultGeometry()))
|
||||
if (!popup) {
|
||||
setError('De browser blokkeerde het tweede venster. Sta pop-ups voor GeoIntel toe en probeer opnieuw.')
|
||||
@@ -226,11 +278,24 @@ export function useSecondaryDisplay() {
|
||||
popupRef.current = null
|
||||
setContainer(null)
|
||||
stopMonitoring()
|
||||
restoreFocus()
|
||||
return
|
||||
}
|
||||
rememberGeometry(current)
|
||||
}, 1_000)
|
||||
}, [close, rememberGeometry, stopMonitoring])
|
||||
}, [close, rememberGeometry, restoreFocus, stopMonitoring])
|
||||
|
||||
useEffect(() => {
|
||||
if (!container) return
|
||||
const sync = () => {
|
||||
const popup = popupRef.current
|
||||
if (popup && !popup.closed) syncSecondaryTheme(popup)
|
||||
}
|
||||
sync()
|
||||
const observer = new MutationObserver(sync)
|
||||
observer.observe(document.body, { attributes: true, attributeFilter: ['data-theme'] })
|
||||
return () => observer.disconnect()
|
||||
}, [container])
|
||||
|
||||
useEffect(() => close, [close])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user