diff --git a/package.json b/package.json index 7f4fcc6e..6f7e7fcc 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "url": "git+https://github.com/opencor/webapp.git" }, "type": "module", - "version": "0.20260612.3", + "version": "0.20260612.4", "engines": { "bun": ">=1.2.0" }, diff --git a/src/renderer/package.json b/src/renderer/package.json index 8b858fe8..6dacaff5 100644 --- a/src/renderer/package.json +++ b/src/renderer/package.json @@ -42,7 +42,7 @@ }, "./style.css": "./dist/opencor.css" }, - "version": "0.20260612.3", + "version": "0.20260612.4", "libopencorVersion": "0.20260604.0", "scripts": { "build": "vite build && bun scripts/generate.version.js", diff --git a/src/renderer/src/common/vueCommon.ts b/src/renderer/src/common/vueCommon.ts index deeb4a53..00429870 100644 --- a/src/renderer/src/common/vueCommon.ts +++ b/src/renderer/src/common/vueCommon.ts @@ -100,66 +100,99 @@ export const trackElementHeight = ( return stopTrackingElementHeight; }; -// Create a `position: fixed` overlay container inside `.opencor` to serve as PrimeVue's append target. This keeps -// overlays visible in full-screen mode and counters PrimeVue's `absolutePosition()` which adds `windowScrollTop/Left` -// to viewport-relative coordinates. The `getBoundingClientRect()`-based correction handles both plain scroll offsets -// and cases where CSS ancestors (transform/will-change/filter/perspective) create new containing blocks for -// `position: fixed`. +// Hybrid append target strategy for PrimeVue overlays: +// - Default: append to `document.body`. This avoids containing-block issues from CSS ancestors (e.g., +// `position: relative` or `overflow: hidden`) that would clip the overlay. Even if an ancestor has +// `overflow: hidden`, the fixed container gets clipped entirely. +// - Full-screen fallback: when a non-`body` element enters full-screen mode, `body`-level overlays become hidden +// behind the full-screen container. In that case, we fall back to a `position: fixed` container inside `.opencor` +// (which is a descendant of the full-screen element). The `getBoundingClientRect()`-based correction handles both +// plain scroll offsets and any containing-block shifts from CSS ancestors. export const useAppendTarget = () => { const appendTarget = vue.shallowRef(undefined); const containerClass = 'opencor-overlay-container'; - vue.onMounted(() => { - // Find the `.opencor` element to append the target to. First try to find it from the current component's root - // element to support multiple instances of OpenCOR in the same page, and if that fails we fall back to searching - // the whole document. + // Lazily create the fixed container inside `.opencor` (only needed for full-screen fallback). - const instance = vue.getCurrentInstance(); + const containerInsideOpencor = (instance: vue.ComponentInternalInstance | null): HTMLElement | undefined => { const rootEl = instance?.vnode?.el; const opencor = rootEl instanceof Element ? rootEl.closest('.opencor') : document.querySelector('.opencor'); - if (opencor) { - let overlayContainer = opencor.querySelector(`.${containerClass}`) as HTMLElement | null; + if (!opencor) { + return undefined; + } + + let overlayContainer = opencor.querySelector(`.${containerClass}`) as HTMLElement | null; + + if (!overlayContainer) { + overlayContainer = document.createElement('div'); - if (!overlayContainer) { - overlayContainer = document.createElement('div'); + overlayContainer.className = containerClass; + overlayContainer.style.cssText = + 'position: fixed; top: 0; left: 0; width: 0; height: 0; overflow: visible; pointer-events: none; z-index: 99999;'; - overlayContainer.className = containerClass; - overlayContainer.style.cssText = - 'position: fixed; top: 0; left: 0; width: 0; height: 0; overflow: visible; pointer-events: none; z-index: 99999;'; + // Restore pointer events for overlay content teleported into the container. - // Restore pointer events for overlay content teleported into the container. + overlayContainer.appendChild( + Object.assign(document.createElement('style'), { + textContent: `.${containerClass} > * { pointer-events: auto; }` + }) + ); - overlayContainer.appendChild( - Object.assign(document.createElement('style'), { - textContent: `.${containerClass} > * { pointer-events: auto; }` - }) - ); + opencor.appendChild(overlayContainer); - opencor.appendChild(overlayContainer); + const container = overlayContainer; + const updateScrollOffset = () => { + const rect = container.getBoundingClientRect(); + const oldTop = parseFloat(container.style.top) || 0; + const oldLeft = parseFloat(container.style.left) || 0; + const newTop = oldTop - rect.top - window.scrollY; + const newLeft = oldLeft - rect.left - window.scrollX; - const container = overlayContainer; - const updateScrollOffset = () => { - const rect = container.getBoundingClientRect(); - const oldTop = parseFloat(container.style.top) || 0; - const oldLeft = parseFloat(container.style.left) || 0; - const newTop = oldTop - rect.top - window.scrollY; - const newLeft = oldLeft - rect.left - window.scrollX; + if (Math.abs(newTop - oldTop) >= 0.5 || Math.abs(newLeft - oldLeft) >= 0.5) { + container.style.top = `${newTop}px`; + container.style.left = `${newLeft}px`; + } + }; + + updateScrollOffset(); + + window.addEventListener('scroll', updateScrollOffset, { passive: true }); + } + + return overlayContainer; + }; - if (Math.abs(newTop - oldTop) >= 0.5 || Math.abs(newLeft - oldLeft) >= 0.5) { - container.style.top = `${newTop}px`; - container.style.left = `${newLeft}px`; - } - }; + // Resolve the correct append target based on the full-screen state. - updateScrollOffset(); + const resolveAppendTarget = (instance: vue.ComponentInternalInstance | null): HTMLElement => { + const fullscreenEl = document.fullscreenElement; - window.addEventListener('scroll', updateScrollOffset, { passive: true }); - } + // If we're in full-screen mode and the full-screen element doesn't contain `body`, then `body` is hidden behind the + // full-screen container, in which case we use the fixed container inside `.opencor`. - appendTarget.value = overlayContainer; + if (fullscreenEl && !fullscreenEl.contains(document.body)) { + return containerInsideOpencor(instance) ?? document.body; } + + // In normal mode, we use `document.body` to avoid containing-block/clipping issues. + + return document.body; + }; + + vue.onMounted(() => { + const instance = vue.getCurrentInstance(); + + // Initial resolution. + + appendTarget.value = resolveAppendTarget(instance); + + // Re-resolve when full-screen state changes. + + document.addEventListener('fullscreenchange', () => { + appendTarget.value = resolveAppendTarget(instance); + }); }); return appendTarget; diff --git a/src/renderer/src/components/views/SimulationExperimentView.vue b/src/renderer/src/components/views/SimulationExperimentView.vue index 3ae79ac6..fa570707 100644 --- a/src/renderer/src/components/views/SimulationExperimentView.vue +++ b/src/renderer/src/components/views/SimulationExperimentView.vue @@ -1754,8 +1754,8 @@ const onTrackRun = (): void => { } } - tooltipRows.push(` - + tooltipRows.push(` + ${input.name}: diff --git a/src/renderer/src/components/widgets/TooltipWidget.vue b/src/renderer/src/components/widgets/TooltipWidget.vue index 841eb33e..48eca583 100644 --- a/src/renderer/src/components/widgets/TooltipWidget.vue +++ b/src/renderer/src/components/widgets/TooltipWidget.vue @@ -47,17 +47,18 @@ const onMouseLeave = () => { } /* Styles for teleported v-html content. - * Note: they must be global since scoped styles do not survive PrimeVue's Popover teleportation. Scoped under - * `.opencor` to prevent leaking into host apps that may have their own .tooltip-content class. + * Note: they must be global since scoped styles do not survive PrimeVue's Popover teleportation. Using + * `.tooltip .tooltip-content` (the Popover's class) works regardless of append target (`document.body` or + * `.opencor` fixed container) and avoids leaking into host apps. */ -.opencor .tooltip-content { +.tooltip .tooltip-content { width: max-content; max-width: 20rem; font-size: 0.8rem; } -.opencor .tooltip-content code { +.tooltip .tooltip-content code { font-size: 0.85em; padding: 0.1em 0.25em; background-color: var(--p-form-field-background); @@ -66,17 +67,17 @@ const onMouseLeave = () => { border-radius: 0.2em; } -.opencor .tooltip-content b, -.opencor .tooltip-content strong { +.tooltip .tooltip-content b, +.tooltip .tooltip-content strong { font-weight: 600; } -.opencor .tooltip-content em, -.opencor .tooltip-content i { +.tooltip .tooltip-content em, +.tooltip .tooltip-content i { font-style: italic; } -.opencor .tooltip-content sub { +.tooltip .tooltip-content sub { vertical-align: sub; font-size: smaller; }