diff --git a/packages/playground/remote/src/lib/boot-playground-remote.ts b/packages/playground/remote/src/lib/boot-playground-remote.ts index 073218133c1..aca29dbb8dc 100644 --- a/packages/playground/remote/src/lib/boot-playground-remote.ts +++ b/packages/playground/remote/src/lib/boot-playground-remote.ts @@ -130,6 +130,7 @@ export async function bootPlaygroundRemote() { ); const wpFrame = document.querySelector('#wp') as HTMLIFrameElement; + let editorNoticeBottomOffset: number | undefined; const phpRemoteApi: PHPRemoteApi = { async onDownloadProgress(fn) { return phpWorkerApi.onDownloadProgress(fn); @@ -366,6 +367,15 @@ export async function bootPlaygroundRemote() { sandbox: wpFrame.getAttribute('sandbox'), }); }, + async setEditorNoticeBottomOffset(bottom) { + editorNoticeBottomOffset = + typeof bottom === 'number' && + Number.isFinite(bottom) && + bottom > 0 + ? bottom + : undefined; + postEditorNoticeBottomOffset(); + }, async setIframeSandboxFlags(flags: string[]) { wpFrame.setAttribute('sandbox', flags.join(' ')); }, @@ -554,6 +564,17 @@ export async function bootPlaygroundRemote() { } }, }; + wpFrame.addEventListener('load', postEditorNoticeBottomOffset); + + function postEditorNoticeBottomOffset() { + wpFrame.contentWindow?.postMessage( + { + type: 'playground-editor-notice-bottom-offset', + bottom: editorNoticeBottomOffset, + }, + '*' + ); + } await phpWorkerApi.isConnected(); diff --git a/packages/playground/remote/src/lib/playground-client.ts b/packages/playground/remote/src/lib/playground-client.ts index 0ff6b6a115b..764b5de89d9 100644 --- a/packages/playground/remote/src/lib/playground-client.ts +++ b/packages/playground/remote/src/lib/playground-client.ts @@ -54,6 +54,14 @@ export interface WebClientMixin extends ProgressReceiver { */ captureSiteThumbnail(): Promise; + /** + * Sets the bottom offset for WordPress editor notices. + * + * @internal Used by the Playground website to keep notices clear of its + * floating Dock. + */ + setEditorNoticeBottomOffset(bottom: number | undefined): Promise; + /** * Sets the iframe sandbox flags. * @param flags The iframe sandbox flags. diff --git a/packages/playground/remote/src/lib/playground-mu-plugin/0-playground.php b/packages/playground/remote/src/lib/playground-mu-plugin/0-playground.php index 7969d5f9c16..b83da5389b1 100644 --- a/packages/playground/remote/src/lib/playground-mu-plugin/0-playground.php +++ b/packages/playground/remote/src/lib/playground-mu-plugin/0-playground.php @@ -45,6 +45,58 @@ function ( $message ) { '; }); +/** + * Keeps editor snackbars above the floating Playground Dock. + * + * The host measures the Dock and relays its bottom offset through the + * Playground client. postMessage crosses the iframe boundary even when the + * host, remote, and WordPress documents cannot inspect each other's DOM. + */ +add_action('admin_footer', function () { + ?> + + + observer.disconnect(); }, []); + useEffect(() => { + const dock = dockRef.current; + if (!activeClient || !dock) { + return; + } + + const updateEditorNoticePosition = () => { + const dockRect = dock.getBoundingClientRect(); + const viewportCenter = window.innerWidth / 2; + const overlap = window.innerHeight - dockRect.top; + const overlapsNotices = + !isMobile && + !isFullWidth && + dockRect.width > 0 && + dockRect.height > 0 && + overlap > 0 && + dockRect.left < viewportCenter && + dockRect.right > viewportCenter; + void activeClient.setEditorNoticeBottomOffset( + overlapsNotices ? overlap + DOCK_PANE_GAP : undefined + ); + }; + + let resizeObserver: ResizeObserver | undefined; + if (typeof ResizeObserver !== 'undefined') { + resizeObserver = new ResizeObserver(updateEditorNoticePosition); + resizeObserver.observe(dock); + } + const mutationObserver = new MutationObserver( + updateEditorNoticePosition + ); + mutationObserver.observe(dock, { + attributes: true, + attributeFilter: ['class', 'style'], + }); + window.addEventListener('resize', updateEditorNoticePosition); + dock.addEventListener('transitionend', updateEditorNoticePosition); + dock.addEventListener('animationend', updateEditorNoticePosition); + updateEditorNoticePosition(); + + return () => { + resizeObserver?.disconnect(); + mutationObserver.disconnect(); + window.removeEventListener('resize', updateEditorNoticePosition); + dock.removeEventListener( + 'transitionend', + updateEditorNoticePosition + ); + dock.removeEventListener( + 'animationend', + updateEditorNoticePosition + ); + }; + }, [activeClient, isFullWidth, isMobile]); + useEffect(() => { /** Keeps floating geometry inside the live viewport. */ const updateViewportSize = () => {