Skip to content
Next Next commit
fix(native-windows): switch to list tab on embedded editor back navig…
…ation
  • Loading branch information
CookieDarb committed Oct 5, 2026
commit 2a1d7bb8f19680ed6c8dc58a4435f8ce5e24801f
72 changes: 72 additions & 0 deletions src/native-windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ import { injectInlineScript, loadVendorScript } from './wallpapers/vendor-loader
import { registerSyntheticIframe } from './connection';
import { isShellDocumentUrl } from './shell-url';
import {
OS_TAB_PARAM,
registerNativeUrlRemap,
resolveNativeUrlRemap,
tryNativeUrlRemap,
unregisterNativeUrlRemap,
} from './native-url-remap';
import { setPanelTabs } from './window/tab-strip';
Expand Down Expand Up @@ -538,6 +541,75 @@ function buildIframeContentRender(
( data as { payload?: unknown } ).payload,
);
}
// In-iframe admin link navigation from the synthetic iframe.
// Synthetic iframes in native window tabs lack the outer
// `Window.iframe` binding that routes through `handleWindowMessage`,
// so in-frame navigation events are handled here.
if (
data &&
typeof data === 'object' &&
( data as { type?: string } ).type === 'os-iframe-admin-link' &&
typeof ( data as { url?: string } ).url === 'string' &&
( data as { url: string } ).url !== ''
) {
const linkUrl = ( data as { url: string } ).url;

// Switch tab in place when the link targets this same native window
// (e.g. Gutenberg's "View Posts" / "View Pages" button).
const targetNativeId = resolveNativeUrlRemap( linkUrl );
if ( targetNativeId === windowId ) {
let targetTab = 'posts';
try {
const parsed = new URL( linkUrl, window.location.origin );
const tagged = parsed.searchParams.get( OS_TAB_PARAM );
if ( tagged && /^[a-z0-9_-]+$/.test( tagged ) ) {
targetTab = tagged;
}
} catch {
/* Use fallback default 'posts' */
}

const root =
body.closest( '.os-app-list' ) ??
body.closest( '[data-window-id]' ) ??
body.parentElement;
const tabs = root?.querySelector< HTMLElement & { value: string } >( 'os-tabs' );
if ( tabs ) {
tabs.value = targetTab;
tabs.dispatchEvent(
new CustomEvent( 'os-tab-change', {
detail: { value: targetTab },
bubbles: true,
composed: true,
} ),
);
return;
}
}

// Delegate to another native window if registered.
if ( tryNativeUrlRemap( linkUrl ) ) {
return;
}

// Fall back to opening a standard iframe window.
const osApi = window.wp?.os;
if ( osApi?.deriveWindowId && osApi?.windowManager ) {
const id = osApi.deriveWindowId( linkUrl );
const linkLabel =
typeof ( data as { label?: string } ).label === 'string'
? ( data as { label: string } ).label
: '';
void osApi.windowManager.open( {
id,
baseId: id,
url: linkUrl,
title: linkLabel || linkUrl,
icon: 'dashicons-admin-generic',
} );
}
}

try {
cfg.onMessage?.( e.data );
} catch ( err ) {
Expand Down
165 changes: 165 additions & 0 deletions tests/vitest/native-window-iframe-admin-link.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/**
* Tests for in-iframe admin link navigation inside native windows (`os-iframe-admin-link`).
*
* Synthetic iframes in native windows lack the shell-managed `Window.iframe`
* binding, so `native-windows.ts` handles in-frame navigation events directly:
* same-window remaps switch `<os-tabs>` in place, cross-window remaps delegate
* to `tryNativeUrlRemap()`, and other links fall back to `windowManager.open()`.
*/
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { embedAdminPage } from '../../src/native-windows';
import {
bindNativeUrlRemap,
registerNativeUrlRemap,
_resetNativeUrlRemap,
} from '../../src/native-url-remap';

describe( 'native windows — in-iframe admin link navigation', () => {
let container: HTMLElement;
let tabs: HTMLElement & { value: string };
let host: HTMLElement;
let iframe: HTMLIFrameElement;
let teardown: () => void;

const postFromIframe = ( payload: unknown ) => {
const ev = new MessageEvent( 'message', {
data: payload,
origin: window.location.origin,
} );
Object.defineProperty( ev, 'source', {
value: iframe.contentWindow,
configurable: true,
} );
window.dispatchEvent( ev );
};

beforeEach( () => {
_resetNativeUrlRemap();
bindNativeUrlRemap( {
adminUrl: `${ window.location.origin }/wp-admin/`,
getSnapshot: () => ( {} ) as any,
openById: vi.fn( () => true ),
} );

container = document.createElement( 'div' );
container.className = 'os-app-list desktop-mode-posts';
container.setAttribute( 'data-window-id', 'desktop-mode-posts' );

tabs = document.createElement( 'os-tabs' ) as HTMLElement & { value: string };
tabs.value = 'new';
container.appendChild( tabs );

host = document.createElement( 'div' );
container.appendChild( host );
document.body.appendChild( container );

teardown = embedAdminPage(
host,
`${ window.location.origin }/wp-admin/post-new.php`,
{ windowId: 'desktop-mode-posts' },
);
iframe = host.querySelector( 'iframe' )!;
} );

afterEach( () => {
teardown?.();
container.remove();
_resetNativeUrlRemap();
delete ( window as unknown as { wp?: unknown } ).wp;
} );

test( 'switches tab in place and dispatches os-tab-change (respecting os_tab)', () => {
registerNativeUrlRemap( {
id: 'desktop-mode-posts',
nativeWindowId: 'desktop-mode-posts',
matches: ( _url, parsed ) => parsed.pathname.endsWith( '/edit.php' ),
} );

let tabChangeValue = '';
tabs.addEventListener( 'os-tab-change', ( ( e: CustomEvent ) => {
tabChangeValue = e.detail?.value;
} ) as EventListener );

// Default fallback tab 'posts' when no os_tab parameter is given
postFromIframe( {
type: 'os-iframe-admin-link',
url: `${ window.location.origin }/wp-admin/edit.php`,
} );
expect( tabs.value ).toBe( 'posts' );
expect( tabChangeValue ).toBe( 'posts' );

// Custom os_tab parameter
postFromIframe( {
type: 'os-iframe-admin-link',
url: `${ window.location.origin }/wp-admin/edit.php?os_tab=categories`,
} );
expect( tabs.value ).toBe( 'categories' );
expect( tabChangeValue ).toBe( 'categories' );
} );

test( 'delegates to tryNativeUrlRemap when link targets another native window', () => {
const openById = vi.fn( () => true );
bindNativeUrlRemap( {
adminUrl: `${ window.location.origin }/wp-admin/`,
getSnapshot: () => ( {} ) as any,
openById,
} );
registerNativeUrlRemap( {
id: 'desktop-mode-pages',
nativeWindowId: 'desktop-mode-pages',
matches: ( _url, parsed ) =>
parsed.searchParams.get( 'post_type' ) === 'page',
} );

postFromIframe( {
type: 'os-iframe-admin-link',
url: `${ window.location.origin }/wp-admin/edit.php?post_type=page`,
} );

expect( tabs.value ).toBe( 'new' );
expect( openById ).toHaveBeenCalledWith( 'desktop-mode-pages' );
} );

test( 'falls back to windowManager.open when link is unmapped', () => {
const openMock = vi.fn();
const deriveMock = vi.fn( () => 'options-general-php' );
( window as unknown as { wp?: unknown } ).wp = {
os: {
windowManager: { open: openMock },
deriveWindowId: deriveMock,
},
};

const linkUrl = `${ window.location.origin }/wp-admin/options-general.php`;
postFromIframe( {
type: 'os-iframe-admin-link',
url: linkUrl,
label: 'Settings',
} );

expect( deriveMock ).toHaveBeenCalledWith( linkUrl );
expect( openMock ).toHaveBeenCalledWith( {
id: 'options-general-php',
baseId: 'options-general-php',
url: linkUrl,
title: 'Settings',
icon: 'dashicons-admin-generic',
} );
} );

test( 'teardown removes message listener so subsequent messages are ignored', () => {
registerNativeUrlRemap( {
id: 'desktop-mode-posts',
nativeWindowId: 'desktop-mode-posts',
matches: ( _url, parsed ) => parsed.pathname.endsWith( '/edit.php' ),
} );

teardown();

postFromIframe( {
type: 'os-iframe-admin-link',
url: `${ window.location.origin }/wp-admin/edit.php`,
} );
expect( tabs.value ).toBe( 'new' );
} );
} );
Loading