From 0a73efef22cd935d9c6f98935fcec1b808e84899 Mon Sep 17 00:00:00 2001 From: Dmitri Date: Thu, 9 Jul 2026 19:27:39 +0200 Subject: [PATCH] fixed fullscreen mode --- documentation/global-ui-controls.md | 80 ++++++++++++------- frontend/docs/runtime-presentation.md | 32 +++++--- frontend/src/lib/fullscreen.test.ts | 106 ++++++++++++++++++++++++++ frontend/src/lib/fullscreen.ts | 72 +++++++++++------ 4 files changed, 230 insertions(+), 60 deletions(-) create mode 100644 frontend/src/lib/fullscreen.test.ts diff --git a/documentation/global-ui-controls.md b/documentation/global-ui-controls.md index 1d94f56..046ed06 100644 --- a/documentation/global-ui-controls.md +++ b/documentation/global-ui-controls.md @@ -7,6 +7,7 @@ sound mute, and offline mode. They are not regular canvas elements and are not stored in `tour_pages.ui_schema_json.elements`. They behave like runtime chrome: + - button dimensions use canvas-relative percentages - button positions are relative to the visible canvas using `xPercent/yPercent` - constructor edit mode renders controls even when hidden @@ -16,6 +17,7 @@ They behave like runtime chrome: ## Implementation Files Backend: + - `backend/src/db/models/global_ui_control_defaults.js` - `backend/src/db/models/project_ui_control_settings.js` - `backend/src/db/api/global_ui_control_defaults.ts` @@ -26,6 +28,7 @@ Backend: - `backend/src/services/project_ui_control_settings.ts` Frontend: + - `frontend/src/types/uiControls.ts` - `frontend/src/components/UiControls/UiControlsSettingsForm.tsx` - `frontend/src/components/Runtime/RuntimeControls.tsx` @@ -58,27 +61,39 @@ hardcoded defaults are only a safety fallback. Each control (`fullscreen`, `sound`, `offline`) supports: -| Field | Purpose | -|-------|---------| -| `enabled` | Disable action while keeping the control selectable in edit mode | -| `hidden` | Hide in runtime/preview; still render as ghost in constructor edit mode | -| `xPercent`, `yPercent` | Canvas-relative position | -| `anchor` | Which button point is placed at the coordinate | -| `buttonSizePercent`, `iconSizePercent`, `borderRadiusPercent` | Canvas-width-relative dimensions | -| `defaultIconUrl`, `activeIconUrl` | Optional custom asset URL/storage key per state | -| `defaultBackgroundColor`, `activeBackgroundColor` | Button background color per state | -| `defaultBorderColor`, `activeBorderColor` | Button border color per state | -| `hoverBackgroundColor`, `color` | Shared hover background and icon color | -| `opacity`, `boxShadow`, `zIndex`, `order` | Presentation and initial ordering | +| Field | Purpose | +| ------------------------------------------------------------- | ----------------------------------------------------------------------- | +| `enabled` | Disable action while keeping the control selectable in edit mode | +| `hidden` | Hide in runtime/preview; still render as ghost in constructor edit mode | +| `xPercent`, `yPercent` | Canvas-relative position | +| `anchor` | Which button point is placed at the coordinate | +| `buttonSizePercent`, `iconSizePercent`, `borderRadiusPercent` | Canvas-width-relative dimensions | +| `defaultIconUrl`, `activeIconUrl` | Optional custom asset URL/storage key per state | +| `defaultBackgroundColor`, `activeBackgroundColor` | Button background color per state | +| `defaultBorderColor`, `activeBorderColor` | Button border color per state | +| `hoverBackgroundColor`, `color` | Shared hover background and icon color | +| `opacity`, `boxShadow`, `zIndex`, `order` | Presentation and initial ordering | Active state means downloaded/offline for the offline button, muted for the sound button, and fullscreen for the fullscreen button. -When runtime is embedded in an iframe, the fullscreen control first tries the -browser Fullscreen API for the presentation document, then tries to fullscreen -the embedding iframe when same-origin access is available. For cross-origin -wrappers it posts `tour-builder:request-fullscreen` to `window.parent`; exit -attempts post `tour-builder:exit-fullscreen`. +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`. + +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. + The runtime keeps a local fullscreen-active state after an embedded parent fullscreen request because the child document may not expose a local `document.fullscreenElement` while the parent iframe is fullscreen. The hook @@ -86,16 +101,24 @@ listens to both `fullscreenchange` and `webkitfullscreenchange` for native fullscreen exits. ```html - +