UNPKG

@tanstack/devtools

Version:

TanStack Devtools is a set of tools for building advanced devtools for your application.

99 lines (93 loc) • 3.43 kB
import { createEffect, onCleanup } from 'solid-js' import type { Accessor } from 'solid-js' /** * A modal dialog of the app (`dialog.showModal()`) makes the rest of the page * inert, the devtools included, and no z-index or popover gets past that: only * the content of the topmost modal dialog takes input. * * While the panel is open and the app has a modal dialog open, this moves the * devtools root into a modal dialog of its own, shown on top of the app's. It * moves the root back when the panel or the app dialog closes. The app dialog * is inert in the meantime. */ export function createModalHost( enabled: Accessor<boolean>, root: Accessor<HTMLElement | undefined>, isOpen: Accessor<boolean>, ) { createEffect(() => { const element = root() if (!enabled() || !element) return const doc = element.ownerDocument const host = doc.createElement('dialog') // A zero-size box: the devtools are `position: fixed`, so they still lay // out against the viewport. host.style.cssText = 'position:fixed;inset:0;width:0;height:0;max-width:none;max-height:none;margin:0;padding:0;border:0;overflow:visible;background:transparent' // Escape closes the panel through the devtools' own keydown handler, which // also closes this dialog. Without preventDefault the browser then sends // the same Escape to the app dialog and closes it too. The capture phase // on the window sees the key wherever the focus is. const onKeyDown = (event: KeyboardEvent) => { if (host.open && event.key === 'Escape') event.preventDefault() } doc.defaultView?.addEventListener('keydown', onKeyDown, true) // The dialog must never close by itself, or the root stays hidden in it. host.addEventListener('cancel', (event) => event.preventDefault()) let home: Node | null = null const sync = () => { const wanted = isOpen() && Array.from(doc.querySelectorAll('dialog')).some( (dialog) => dialog !== host && dialog.matches(':modal'), ) if (wanted) { if (host.open) return home = element.parentNode host.append(element) doc.body.append(host) host.showModal() } else if (host.open) { host.close() home?.appendChild(element) host.remove() } } // `showModal()` and `close()` toggle the `open` attribute. Removing an open // dialog from the page is a child list change instead. const observer = new MutationObserver((records) => { // An app dialog shown after the host goes on top of it. Showing the host // again puts the host back on top. const appModalShown = records.some( (record) => record.type === 'attributes' && record.target !== host && (record.target as Element).matches('dialog:modal'), ) if (host.open && appModalShown) { host.close() host.showModal() return } sync() }) observer.observe(doc.documentElement, { subtree: true, childList: true, attributeFilter: ['open'], }) createEffect(() => { isOpen() sync() }) onCleanup(() => { doc.defaultView?.removeEventListener('keydown', onKeyDown, true) observer.disconnect() if (host.open) { host.close() home?.appendChild(element) } host.remove() }) }) }