added window.top calling for fullscreen mode

This commit is contained in:
Dmitri 2026-07-09 19:33:35 +02:00
parent 0a73efef22
commit 593f1c99c0
4 changed files with 81 additions and 35 deletions

View File

@ -80,19 +80,20 @@ sound button, and fullscreen for the fullscreen button.
When runtime is embedded in an iframe, the fullscreen control requests
fullscreen on the embedding iframe first when same-origin access is available.
For cross-origin wrappers it posts `tour-builder:request-fullscreen` to
`window.parent` before attempting child-document fullscreen, because a
permissions-policy rejection inside the iframe can consume the user activation
needed by the parent fallback. If the iframe itself grants fullscreen but the
parent does not listen for the message, the child-document fullscreen fallback
can still enter fullscreen. Standalone runtime pages use the browser Fullscreen
API for the presentation document. Exit attempts post
`tour-builder:exit-fullscreen`.
`window.parent` and, for nested iframe wrappers, `window.top` before attempting
child-document fullscreen, because a permissions-policy rejection inside the
iframe can consume the user activation needed by the parent fallback. If the
iframe itself grants fullscreen but the parent does not listen for the message,
the child-document fullscreen fallback can still enter fullscreen. Standalone
runtime pages use the browser Fullscreen API for the presentation document.
Exit attempts post `tour-builder:exit-fullscreen` to the same parent/top
targets.
Browser fullscreen is not equivalent for top-level pages and iframes. A
top-level runtime page can request document fullscreen from a user click, while
a cross-origin iframe normally needs `allow="fullscreen"` / `allowfullscreen`
or a parent page that handles `tour-builder:request-fullscreen` and fullscreens
the iframe element.
or a parent/top page that handles `tour-builder:request-fullscreen` and
fullscreens the iframe element.
The runtime keeps a local fullscreen-active state after an embedded parent
fullscreen request because the child document may not expose a local

View File

@ -167,16 +167,17 @@ Custom icons use each control's `defaultIconUrl` and `activeIconUrl`; empty
values fall back to the built-in MDI icons.
Embedded runtime fullscreen requests the embedding iframe first when same-origin
access is available. For cross-origin wrappers it posts
`tour-builder:request-fullscreen` to the parent before attempting
child-document fullscreen, so a permissions-policy rejection inside the iframe
does not consume the user activation needed by the parent fallback. If the
iframe itself grants fullscreen but the parent does not listen for the message,
the child-document fullscreen fallback can still enter fullscreen. Standalone
runtime pages use native document fullscreen. Exit from cross-origin wrapper
fullscreen posts `tour-builder:exit-fullscreen`.
`tour-builder:request-fullscreen` to the parent and, for nested iframe wrappers,
the top window before attempting child-document fullscreen, so a
permissions-policy rejection inside the iframe does not consume the user
activation needed by the parent fallback. If the iframe itself grants fullscreen
but the parent does not listen for the message, the child-document fullscreen
fallback can still enter fullscreen. Standalone runtime pages use native
document fullscreen. Exit from cross-origin wrapper fullscreen posts
`tour-builder:exit-fullscreen` to the same parent/top targets.
Top-level runtime pages can request document fullscreen from a user click, but
cross-origin iframe embeds normally require `allow="fullscreen"` /
`allowfullscreen` or a parent listener that fullscreens the iframe element.
`allowfullscreen` or a parent/top listener that fullscreens the iframe element.
After a parent iframe fullscreen request, `useRuntimeFullscreen` keeps the
fullscreen button in its active state even when the child document does not
expose `document.fullscreenElement`; native enter/exit events still clear or set
@ -1231,11 +1232,11 @@ actual browser operations to `runtimeFullscreen.actions.ts` and
`lib/fullscreen.ts`:
- enter standalone: request document fullscreen
- enter embedded: request same-origin embedding iframe fullscreen or post parent
`tour-builder:request-fullscreen`, then fall back to child-document fullscreen
when iframe permissions allow it
- enter embedded: request same-origin embedding iframe fullscreen or post
`tour-builder:request-fullscreen` to parent/top wrappers, then fall back to
child-document fullscreen when iframe permissions allow it
- exit: exit native fullscreen when the child document owns it; otherwise post
`tour-builder:exit-fullscreen` to the parent wrapper
`tour-builder:exit-fullscreen` to parent/top wrappers
- state: preserve active fullscreen state for parent iframe fullscreen requests
that do not create a child `document.fullscreenElement`
- events: listen to both `fullscreenchange` and `webkitfullscreenchange`

View File

@ -73,6 +73,44 @@ test('requestDocumentFullscreen asks the parent before child fullscreen when emb
]);
});
test('requestDocumentFullscreen also asks the top window when embedded in nested frames', async () => {
const calls: string[] = [];
const parent = {
postMessage: (message: unknown, targetOrigin: string) => {
calls.push(`parent:${targetOrigin}:${JSON.stringify(message)}`);
},
};
const top = {
postMessage: (message: unknown, targetOrigin: string) => {
calls.push(`top:${targetOrigin}:${JSON.stringify(message)}`);
},
};
setBrowserGlobals({
windowValue: {
parent,
top,
frameElement: null,
},
documentValue: {
documentElement: {
requestFullscreen: async () => {
calls.push('document-fullscreen');
},
},
},
});
const entered = await requestDocumentFullscreen('runtime-global-control');
assert.equal(entered, true);
assert.deepEqual(calls, [
'parent:*:{"type":"tour-builder:request-fullscreen","source":"runtime-global-control"}',
'top:*:{"type":"tour-builder:request-fullscreen","source":"runtime-global-control"}',
'document-fullscreen',
]);
});
test('requestDocumentFullscreen returns parent request when embedded child fullscreen is blocked', async () => {
const calls: string[] = [];
const parent = {

View File

@ -74,13 +74,16 @@ const requestSameOriginEmbeddingFrameFullscreen =
const requestParentFullscreen = (source: string): boolean => {
if (!isEmbeddedWindow()) return false;
window.parent.postMessage(
{
type: 'tour-builder:request-fullscreen',
source,
},
'*',
);
const message = {
type: 'tour-builder:request-fullscreen',
source,
};
window.parent.postMessage(message, '*');
if (window.top && window.top !== window.parent) {
window.top.postMessage(message, '*');
}
return true;
};
@ -127,13 +130,16 @@ export const requestDocumentFullscreen = async (
export const requestParentFullscreenExit = (source = 'runtime'): boolean => {
if (!isEmbeddedWindow()) return false;
window.parent.postMessage(
{
type: 'tour-builder:exit-fullscreen',
source,
},
'*',
);
const message = {
type: 'tour-builder:exit-fullscreen',
source,
};
window.parent.postMessage(message, '*');
if (window.top && window.top !== window.parent) {
window.top.postMessage(message, '*');
}
return true;
};