Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
113 changes: 73 additions & 40 deletions src/renderer/src/common/vueCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLElement | undefined>(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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1754,8 +1754,8 @@ const onTrackRun = (): void => {
}
}

tooltipRows.push(`<tr>
<td style="vertical-align: top; font-weight: bold;">
tooltipRows.push(`<tr style="vertical-align: top;">
<td style="font-weight: bold;">
${input.name}:
</td>
<td style="padding-left: 8px;">
Expand Down
19 changes: 10 additions & 9 deletions src/renderer/src/components/widgets/TooltipWidget.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
}
Expand Down
Loading