From 2361b62e821b07408d4f7ee58c9b8ba4b332f060 Mon Sep 17 00:00:00 2001 From: ansBAkula Date: Mon, 14 Sep 2026 04:45:40 +0200 Subject: [PATCH 1/9] up --- .../src/lib/components/Visordash.react.js | 2 +- .../ui-panels/Panel_BottomMiddle.tsx | 5 ++- .../visor/visor-client/src/styles/index.css | 37 +++++++++++++------ .../src/styles/themes/visor-theme-rules.css | 7 +++- .../visor/visor-client/src/utils/portal.js | 21 +++++++++++ .../visor/visor-client/src/utils/portal.ts | 26 +++++++++++++ 6 files changed, 82 insertions(+), 16 deletions(-) create mode 100644 src/ansys/visor/visor-client/src/utils/portal.js create mode 100644 src/ansys/visor/visor-client/src/utils/portal.ts diff --git a/src/ansys/visor/dash/src/lib/components/Visordash.react.js b/src/ansys/visor/dash/src/lib/components/Visordash.react.js index c9b0e073..a6f68b69 100644 --- a/src/ansys/visor/dash/src/lib/components/Visordash.react.js +++ b/src/ansys/visor/dash/src/lib/components/Visordash.react.js @@ -96,7 +96,7 @@ const Visordash = (props) => { }, [fireSnapshot]); return ( -
+
); diff --git a/src/ansys/visor/visor-client/src/components/ui-panels/Panel_BottomMiddle.tsx b/src/ansys/visor/visor-client/src/components/ui-panels/Panel_BottomMiddle.tsx index 30e873fb..d59d5131 100644 --- a/src/ansys/visor/visor-client/src/components/ui-panels/Panel_BottomMiddle.tsx +++ b/src/ansys/visor/visor-client/src/components/ui-panels/Panel_BottomMiddle.tsx @@ -2,6 +2,7 @@ import { FC, RefObject, useEffect, useRef } from 'react'; import { AwcIcons } from '../AwcIcons'; import { VisorFrontend } from '../../VisorFrontend.tsx'; import { makeTooltip } from '../../utils/Tooltip'; +import { appendToPortal, removeFromPortal } from '../../utils/portal'; export type Panel_BottomMiddle_Util = {}; @@ -112,7 +113,7 @@ export const Panel_BottomMiddle: FC<{ z-index: 1000; white-space: nowrap; `; - document.body.appendChild(popup); + appendToPortal(popup); const subModes: SelectionMode[] = ['edge', 'face', 'vertex']; const popupCleanupFuncs: Array<() => void> = []; @@ -180,7 +181,7 @@ export const Panel_BottomMiddle: FC<{ cleanupFuncs.push(...popupCleanupFuncs); cleanupFuncs.push(() => document.removeEventListener('click', closePopupOnOutsideClick)); - cleanupFuncs.push(() => popup.remove()); + cleanupFuncs.push(() => removeFromPortal(popup)); ////////////////////////// onLoad({}); ////////////////////////// diff --git a/src/ansys/visor/visor-client/src/styles/index.css b/src/ansys/visor/visor-client/src/styles/index.css index aad782cd..5e96db95 100644 --- a/src/ansys/visor/visor-client/src/styles/index.css +++ b/src/ansys/visor/visor-client/src/styles/index.css @@ -1,6 +1,14 @@ -*, -*::before, -*::after { +/* Scope the global reset to `.visor-embed-style` / `.visor-root` so it does not + affect the host application when Visor is embedded. Wrap Visor content + in a container with class `visor-embed-style` (preferred) or `visor-root`. */ +.visor-embed-style, +.visor-root, +.visor-embed-style *, +.visor-root *, +.visor-embed-style *::before, +.visor-root *::before, +.visor-embed-style *::after, +.visor-root *::after { margin: 0; padding: 0; line-height: 1; @@ -19,20 +27,17 @@ html { @font-face { /* The "Source Sans 3" font is used by the Ansys AWC React library, - and was imported manually from there for use in Visor. + and is served by the Visor backend for embedded usage. */ font-family: 'Source Sans 3'; src: url('/fonts/source-sans-3.woff2') format('woff2'); } -body { +/* Scoped styles: set height and the AWC font only within the Visor root + so these font and sizing rules do not leak into host applications. */ +.visor-embed-style, +.visor-root { height: 100%; - /* - This font list is used by the Ansys AWC React library, - and was imported manually from there for use in Visor. - This font list can be found in CSS file "@ansys\awc-react\styles\themes\css\default.css" - after installing the AWC React npm package. - */ font-family: 'Source Sans 3', -apple-system, @@ -50,6 +55,16 @@ body { 'Noto Color Emoji'; } +/* Ensure Visor typography matches standalone appearance while keeping + styles scoped to the root container so they don't leak into host apps. */ +.visor-embed-style, +.visor-root, +.visor-embed-style *, +.visor-root * { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + a, a:active, a:link, diff --git a/src/ansys/visor/visor-client/src/styles/themes/visor-theme-rules.css b/src/ansys/visor/visor-client/src/styles/themes/visor-theme-rules.css index 911acc5a..6067617c 100644 --- a/src/ansys/visor/visor-client/src/styles/themes/visor-theme-rules.css +++ b/src/ansys/visor/visor-client/src/styles/themes/visor-theme-rules.css @@ -4,8 +4,11 @@ border-color: rgba(var(--theme-color-borders), 1); } -html, -body { +/* Theme scoping: apply these base rules only inside `.visor-embed-style` to avoid + leaking layout and color changes into the host application when Visor is + embedded as a component. Wrap the Visor content in a container with class + `visor-embed-style`. */ +.visor-embed-style { font-size: 14px; color: rgba(var(--theme-color-text), 1); } diff --git a/src/ansys/visor/visor-client/src/utils/portal.js b/src/ansys/visor/visor-client/src/utils/portal.js new file mode 100644 index 00000000..238d4bc4 --- /dev/null +++ b/src/ansys/visor/visor-client/src/utils/portal.js @@ -0,0 +1,21 @@ +// Lightweight portal helper for runtime JS modules. +// This mirrors the TypeScript helper to support plain .js consumers +// (e.g. wasm/runtime files) without depending on TS compilation. +export function getPortalRoot() { + const el = + document.querySelector('.visor-embed-style') || + document.getElementById('TheiaContainer') || + document.getElementById('VisorContainer') || + document.body; + return el; +} + +export function appendToPortal(el) { + getPortalRoot().appendChild(el); +} + +export function removeFromPortal(el) { + if (el && el.parentElement) { + el.parentElement.removeChild(el); + } +} diff --git a/src/ansys/visor/visor-client/src/utils/portal.ts b/src/ansys/visor/visor-client/src/utils/portal.ts new file mode 100644 index 00000000..0476767f --- /dev/null +++ b/src/ansys/visor/visor-client/src/utils/portal.ts @@ -0,0 +1,26 @@ +/** + * Helpers to resolve a scoped portal root for UI elements that would + * otherwise be appended to `document.body` (tooltips, popups, invisible + * DOM containers used by wasm). Prefer an element with the + * `.visor-embed-style` class (added by host integrations), then fall back + * to the client container id `TheiaContainer` for compatibility, then to + * `VisorContainer`, and finally to `document.body`. + */ +export function getPortalRoot(): HTMLElement { + const el = + document.querySelector('.visor-embed-style') || + document.getElementById('TheiaContainer') || + document.getElementById('VisorContainer') || + document.body; + return el as HTMLElement; +} + +export function appendToPortal(el: HTMLElement): void { + getPortalRoot().appendChild(el); +} + +export function removeFromPortal(el: HTMLElement): void { + if (el && el.parentElement) { + el.parentElement.removeChild(el); + } +} From 131d8e95d138cce33de80b6ba77dd0adbfd35c9f Mon Sep 17 00:00:00 2001 From: ansBAkula Date: Mon, 14 Sep 2026 05:15:39 +0200 Subject: [PATCH 2/9] up --- .../visor/visor-client/src/styles/index.css | 1 + .../visor/visor-client/src/utils/portal.js | 23 ++++++++---- .../visor/visor-client/src/utils/portal.ts | 35 ++++++++++++++----- 3 files changed, 44 insertions(+), 15 deletions(-) diff --git a/src/ansys/visor/visor-client/src/styles/index.css b/src/ansys/visor/visor-client/src/styles/index.css index 5e96db95..26d4aace 100644 --- a/src/ansys/visor/visor-client/src/styles/index.css +++ b/src/ansys/visor/visor-client/src/styles/index.css @@ -31,6 +31,7 @@ html { */ font-family: 'Source Sans 3'; src: url('/fonts/source-sans-3.woff2') format('woff2'); + font-display: swap; } /* Scoped styles: set height and the AWC font only within the Visor root diff --git a/src/ansys/visor/visor-client/src/utils/portal.js b/src/ansys/visor/visor-client/src/utils/portal.js index 238d4bc4..4f4b8273 100644 --- a/src/ansys/visor/visor-client/src/utils/portal.js +++ b/src/ansys/visor/visor-client/src/utils/portal.js @@ -2,12 +2,23 @@ // This mirrors the TypeScript helper to support plain .js consumers // (e.g. wasm/runtime files) without depending on TS compilation. export function getPortalRoot() { - const el = - document.querySelector('.visor-embed-style') || - document.getElementById('TheiaContainer') || - document.getElementById('VisorContainer') || - document.body; - return el; + const doc = typeof document !== 'undefined' ? document : null; + if (!doc) { + return {}; + } + + const el = doc.querySelector('.visor-embed-style') || doc.getElementById('VisorContainer'); + if (el) return el; + + let portalRoot = doc.getElementById('visor-portal-root'); + if (!portalRoot) { + portalRoot = doc.createElement('div'); + portalRoot.id = 'visor-portal-root'; + portalRoot.setAttribute('data-visor-portal', '1'); + doc.body.appendChild(portalRoot); + } + + return portalRoot; } export function appendToPortal(el) { diff --git a/src/ansys/visor/visor-client/src/utils/portal.ts b/src/ansys/visor/visor-client/src/utils/portal.ts index 0476767f..0f998b6a 100644 --- a/src/ansys/visor/visor-client/src/utils/portal.ts +++ b/src/ansys/visor/visor-client/src/utils/portal.ts @@ -1,18 +1,35 @@ /** * Helpers to resolve a scoped portal root for UI elements that would * otherwise be appended to `document.body` (tooltips, popups, invisible - * DOM containers used by wasm). Prefer an element with the + * DOM containers used by wasm). Prefer an element with the * `.visor-embed-style` class (added by host integrations), then fall back - * to the client container id `TheiaContainer` for compatibility, then to - * `VisorContainer`, and finally to `document.body`. + * to the client container id `VisorContainer`, and finally to `document.body`. */ export function getPortalRoot(): HTMLElement { - const el = - document.querySelector('.visor-embed-style') || - document.getElementById('TheiaContainer') || - document.getElementById('VisorContainer') || - document.body; - return el as HTMLElement; + const doc = typeof document !== 'undefined' ? document : null; + if (!doc) { + // Non-browser environment (SSR). Return a harmless stub to avoid + // throwing in places that call this helper during static analysis. + // Callers should guard when used outside the browser. + return ({} as HTMLElement); + } + + // Prefer an explicit Visor embed root when present, then fall back to + // legacy container ids for compatibility. If no suitable container is + // present, create a dedicated portal root under document.body so callers + // don't have to append directly to body. + const el = doc.querySelector('.visor-embed-style') || doc.getElementById('VisorContainer'); + if (el) return el as HTMLElement; + + let portalRoot = doc.getElementById('visor-portal-root') as HTMLElement | null; + if (!portalRoot) { + portalRoot = doc.createElement('div'); + portalRoot.id = 'visor-portal-root'; + portalRoot.setAttribute('data-visor-portal', '1'); + doc.body.appendChild(portalRoot); + } + + return portalRoot; } export function appendToPortal(el: HTMLElement): void { From 4bfe8f38bc1ba1ee6a67dee9a5270aacaa78f1b4 Mon Sep 17 00:00:00 2001 From: ansBAkula Date: Mon, 14 Sep 2026 13:13:39 +0200 Subject: [PATCH 3/9] clean-up --- src/ansys/visor/visor-client/index.html | 1 + .../visor/visor-client/src/styles/index.css | 20 +++++++------------ 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/ansys/visor/visor-client/index.html b/src/ansys/visor/visor-client/index.html index c4655b80..e18d191b 100644 --- a/src/ansys/visor/visor-client/index.html +++ b/src/ansys/visor/visor-client/index.html @@ -9,6 +9,7 @@
diff --git a/src/ansys/visor/visor-client/src/styles/index.css b/src/ansys/visor/visor-client/src/styles/index.css index 26d4aace..5fb9fa06 100644 --- a/src/ansys/visor/visor-client/src/styles/index.css +++ b/src/ansys/visor/visor-client/src/styles/index.css @@ -1,14 +1,10 @@ -/* Scope the global reset to `.visor-embed-style` / `.visor-root` so it does not +/* Scope the global reset to `.visor-embed-style` so it does not affect the host application when Visor is embedded. Wrap Visor content - in a container with class `visor-embed-style` (preferred) or `visor-root`. */ + in a container with class `visor-embed-style`. */ .visor-embed-style, -.visor-root, .visor-embed-style *, -.visor-root *, .visor-embed-style *::before, -.visor-root *::before, -.visor-embed-style *::after, -.visor-root *::after { +.visor-embed-style *::after { margin: 0; padding: 0; line-height: 1; @@ -34,10 +30,9 @@ html { font-display: swap; } -/* Scoped styles: set height and the AWC font only within the Visor root +/* Scoped styles: set height and the AWC font only within the Visor embed root so these font and sizing rules do not leak into host applications. */ -.visor-embed-style, -.visor-root { +.visor-embed-style { height: 100%; font-family: 'Source Sans 3', @@ -57,11 +52,10 @@ html { } /* Ensure Visor typography matches standalone appearance while keeping - styles scoped to the root container so they don't leak into host apps. */ + styles scoped to the embed root container so they don't leak into host apps. */ .visor-embed-style, -.visor-root, .visor-embed-style *, -.visor-root * { +.visor-embed-style * { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } From 67a1c719f99ffb4f6a629fc135d2800c8fd4398a Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Mon, 14 Sep 2026 11:18:00 +0000 Subject: [PATCH 4/9] chore: adding changelog file 109.fixed.md [dependabot-skip] --- doc/changelog.d/109.fixed.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/109.fixed.md diff --git a/doc/changelog.d/109.fixed.md b/doc/changelog.d/109.fixed.md new file mode 100644 index 00000000..a8a06727 --- /dev/null +++ b/doc/changelog.d/109.fixed.md @@ -0,0 +1 @@ +Visor dash styling From 0839b5b670a2a2a398c74561b99858c07a026f06 Mon Sep 17 00:00:00 2001 From: ansBAkula Date: Tue, 15 Sep 2026 05:48:32 +0200 Subject: [PATCH 5/9] code fixes for tests --- src/ansys/visor/visor-client/index.html | 2 +- .../src/components/ui-panels/Panel_TopLeft.tsx | 6 +++++- .../src/components/ui-panels/Panel_TopRight.tsx | 6 +++++- src/ansys/visor/visor-client/src/styles/index.css | 9 ++++++++- src/ansys/visor/visor-client/src/treeview/TreeView.tsx | 6 +++++- src/ansys/visor/visor-client/src/utils/JsHelpers.js | 8 ++++++-- 6 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/ansys/visor/visor-client/index.html b/src/ansys/visor/visor-client/index.html index e18d191b..283475de 100644 --- a/src/ansys/visor/visor-client/index.html +++ b/src/ansys/visor/visor-client/index.html @@ -1,5 +1,5 @@ - + diff --git a/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopLeft.tsx b/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopLeft.tsx index 5f3bf8a8..4d4a1a94 100644 --- a/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopLeft.tsx +++ b/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopLeft.tsx @@ -45,7 +45,11 @@ export const Panel_TopLeft: FC<{ await visorState.render(); })(); }); - const fontSize: number = getRootFontSize(14); + const fontSize: number = getRootFontSize( + 14, + (reactComponentContainer.closest('.visor-embed-style') ?? + document.documentElement) as HTMLElement + ); const collapseButton = AwcIcons.getArrowHeadDownIcon(fontSize * 1.2, 3, true, [ 'theme-hover-background-3', ]) as HTMLButtonElement; diff --git a/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopRight.tsx b/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopRight.tsx index 631d4647..42f45b92 100644 --- a/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopRight.tsx +++ b/src/ansys/visor/visor-client/src/components/ui-panels/Panel_TopRight.tsx @@ -218,7 +218,11 @@ export const Panel_TopRight: FC<{ legendPanelApplyRangeButtonId ) as HTMLButtonElement; - const fontSize: number = getRootFontSize(14); + const fontSize: number = getRootFontSize( + 14, + (componentContainer.closest('.visor-embed-style') ?? + document.documentElement) as HTMLElement + ); let collapsePanel: () => void; let expandPanel: () => void; diff --git a/src/ansys/visor/visor-client/src/styles/index.css b/src/ansys/visor/visor-client/src/styles/index.css index 5fb9fa06..e2a58177 100644 --- a/src/ansys/visor/visor-client/src/styles/index.css +++ b/src/ansys/visor/visor-client/src/styles/index.css @@ -16,8 +16,15 @@ visibility: inherit; } -html { +html, +body { + /* Baseline reset kept global (not scoped): a non-zero default body + margin causes a viewport scrollbar and a resized/clipped full-page + canvas in standalone/Dash-hosted pages. This is standard boilerplate + (unlike font-family/line-height) and does not visibly affect a host + app's existing content when Visor is truly embedded as a widget. */ height: 100%; + margin: 0; } @font-face { diff --git a/src/ansys/visor/visor-client/src/treeview/TreeView.tsx b/src/ansys/visor/visor-client/src/treeview/TreeView.tsx index fa797b47..25b28ff3 100644 --- a/src/ansys/visor/visor-client/src/treeview/TreeView.tsx +++ b/src/ansys/visor/visor-client/src/treeview/TreeView.tsx @@ -160,10 +160,14 @@ export const TreeView = >(props: TreeViewProps) => if (sceneGraph == null) { return; } - const fontSize: number = getRootFontSize(14); const componentContainerElem = document.getElementById( componentContainerElemId ) as HTMLDivElement; + // Read the base font size from the nearest `.visor-embed-style` root + // rather than `document.documentElement`: when embedded, only the + // Visor root (not the host page's ) is scoped to Visor's font size. + const embedRoot = componentContainerElem.closest('.visor-embed-style'); + const fontSize: number = getRootFontSize(14, (embedRoot ?? document.documentElement) as HTMLElement); const headerContainer = document.createElement('div'); const treeContainer = document.createElement('div'); componentContainerElem.appendChild(headerContainer); diff --git a/src/ansys/visor/visor-client/src/utils/JsHelpers.js b/src/ansys/visor/visor-client/src/utils/JsHelpers.js index 9b0c3a44..1271c4cb 100644 --- a/src/ansys/visor/visor-client/src/utils/JsHelpers.js +++ b/src/ansys/visor/visor-client/src/utils/JsHelpers.js @@ -10,16 +10,20 @@ let rootFontSize = null; * * @param {number} fallback - Value to return when the root font size cannot be * determined in pixels. Must be zero or greater. + * @param {Element} [element] - Element whose computed font size is read. + * Defaults to `document.documentElement`. Pass the `.visor-embed-style` root + * when available, since that is where Visor's base font size is scoped to + * (the host page's `` element is not modified when embedded). * @returns {number} The root font size in pixels, or the fallback value. * @throws {Error} If `fallback` is not a non-negative number. */ -export const getRootFontSize = (fallback) => { +export const getRootFontSize = (fallback, element = document.documentElement) => { if (typeof fallback !== 'number' || fallback < 0) { throw new Error('fallback must be a number 0 or greater'); } if (rootFontSize == null) { - const str = getComputedStyle(document.documentElement, null).getPropertyValue('font-size'); + const str = getComputedStyle(element, null).getPropertyValue('font-size'); if (/^(?:-?\d+|-?\d*[.,]\d+)px$/i.test(str)) { return (rootFontSize = parseFloat(str.slice(0, -2))); From 9783e7bc88add44939f1c1493f2bdbad85442131 Mon Sep 17 00:00:00 2001 From: ansBAkula Date: Tue, 15 Sep 2026 05:56:36 +0200 Subject: [PATCH 6/9] prettier fix --- src/ansys/visor/visor-client/src/treeview/TreeView.tsx | 5 ++++- src/ansys/visor/visor-client/src/utils/portal.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ansys/visor/visor-client/src/treeview/TreeView.tsx b/src/ansys/visor/visor-client/src/treeview/TreeView.tsx index 25b28ff3..067649ca 100644 --- a/src/ansys/visor/visor-client/src/treeview/TreeView.tsx +++ b/src/ansys/visor/visor-client/src/treeview/TreeView.tsx @@ -167,7 +167,10 @@ export const TreeView = >(props: TreeViewProps) => // rather than `document.documentElement`: when embedded, only the // Visor root (not the host page's ) is scoped to Visor's font size. const embedRoot = componentContainerElem.closest('.visor-embed-style'); - const fontSize: number = getRootFontSize(14, (embedRoot ?? document.documentElement) as HTMLElement); + const fontSize: number = getRootFontSize( + 14, + (embedRoot ?? document.documentElement) as HTMLElement + ); const headerContainer = document.createElement('div'); const treeContainer = document.createElement('div'); componentContainerElem.appendChild(headerContainer); diff --git a/src/ansys/visor/visor-client/src/utils/portal.ts b/src/ansys/visor/visor-client/src/utils/portal.ts index 0f998b6a..dc3e64f5 100644 --- a/src/ansys/visor/visor-client/src/utils/portal.ts +++ b/src/ansys/visor/visor-client/src/utils/portal.ts @@ -11,7 +11,7 @@ export function getPortalRoot(): HTMLElement { // Non-browser environment (SSR). Return a harmless stub to avoid // throwing in places that call this helper during static analysis. // Callers should guard when used outside the browser. - return ({} as HTMLElement); + return {} as HTMLElement; } // Prefer an explicit Visor embed root when present, then fall back to From 683c599d1015140a35f55570fca84598f771cc43 Mon Sep 17 00:00:00 2001 From: ansBAkula Date: Thu, 17 Sep 2026 08:12:08 +0200 Subject: [PATCH 7/9] address Lauras comment --- .../src/jest-tests/JsHelpers.test.js | 23 +++++++++++++++++-- .../visor/visor-client/src/utils/JsHelpers.js | 21 +++++++++++++---- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/ansys/visor/visor-client/src/jest-tests/JsHelpers.test.js b/src/ansys/visor/visor-client/src/jest-tests/JsHelpers.test.js index 305e41a0..3800c057 100644 --- a/src/ansys/visor/visor-client/src/jest-tests/JsHelpers.test.js +++ b/src/ansys/visor/visor-client/src/jest-tests/JsHelpers.test.js @@ -17,8 +17,9 @@ import { } from '../utils/JsHelpers.js'; /** - * getRootFontSize caches module-level state, so each test needs a fresh - * instance of the module. + * getRootFontSize caches per-element state at module scope, so each test + * needs a fresh instance of the module to avoid leaking cached values + * between tests. * * @returns {Promise<(fallback: number) => number>} */ @@ -126,6 +127,24 @@ describe('getRootFontSize', () => { expect(getRootFontSize(20)).toBe(20); expect(getComputedStyleSpy).toHaveBeenCalledTimes(1); }); + + test('caches independently per element, regardless of call order', async () => { + const getRootFontSize = await loadFreshGetRootFontSize(); + const htmlElem = document.documentElement; + const embedRootElem = document.createElement('div'); + + jest.spyOn(globalThis, 'getComputedStyle').mockImplementation((element) => ({ + getPropertyValue: () => (element === embedRootElem ? '14px' : '16px'), + })); + + // The host resolves first (e.g. a panel without an embed root yet)... + expect(getRootFontSize(16, htmlElem)).toBe(16); + // ...but a later call with the embed root must not reuse that cached value. + expect(getRootFontSize(14, embedRootElem)).toBe(14); + // Subsequent calls for each element still return their own cached value. + expect(getRootFontSize(16, htmlElem)).toBe(16); + expect(getRootFontSize(14, embedRootElem)).toBe(14); + }); }); describe('hashString', () => { diff --git a/src/ansys/visor/visor-client/src/utils/JsHelpers.js b/src/ansys/visor/visor-client/src/utils/JsHelpers.js index 1271c4cb..a0faefe7 100644 --- a/src/ansys/visor/visor-client/src/utils/JsHelpers.js +++ b/src/ansys/visor/visor-client/src/utils/JsHelpers.js @@ -1,12 +1,18 @@ -/** @type {number|null} */ -let rootFontSize = null; +/** + * Per-element cache for computed root font sizes. Keyed by element (rather + * than a single module-level value) so callers passing different elements + * (e.g. `document.documentElement` vs. a `.visor-embed-style` root) don't + * clobber each other's cached result regardless of call order. + * @type {WeakMap} + */ +const rootFontSizeCache = new WeakMap(); /** - * Returns the root element's computed font size in pixels. + * Returns the given element's computed font size in pixels. * * If the computed font size is not expressed in pixels, the supplied fallback * value is returned. The computed value, or the failure to obtain one, is - * cached for subsequent calls. + * cached per-element for subsequent calls. * * @param {number} fallback - Value to return when the root font size cannot be * determined in pixels. Must be zero or greater. @@ -22,17 +28,22 @@ export const getRootFontSize = (fallback, element = document.documentElement) => throw new Error('fallback must be a number 0 or greater'); } + let rootFontSize = rootFontSizeCache.get(element); + if (rootFontSize == null) { const str = getComputedStyle(element, null).getPropertyValue('font-size'); if (/^(?:-?\d+|-?\d*[.,]\d+)px$/i.test(str)) { - return (rootFontSize = parseFloat(str.slice(0, -2))); + rootFontSize = parseFloat(str.slice(0, -2)); + rootFontSizeCache.set(element, rootFontSize); + return rootFontSize; } const msg = `TreeView warning: computed root element font size '${str}'`; console.warn(`${msg} is not a pixel value. Using fallback.`); rootFontSize = -1; + rootFontSizeCache.set(element, rootFontSize); } if (rootFontSize < 0) { From f48ab18a02a3583bb3fa9fc9d80bd68d22792386 Mon Sep 17 00:00:00 2001 From: ansBAkula Date: Thu, 17 Sep 2026 08:20:42 +0200 Subject: [PATCH 8/9] address Lauras comment --- .../visor/visor-client/src/utils/portal.js | 31 ++++++++++--- .../visor/visor-client/src/utils/portal.ts | 43 ------------------- 2 files changed, 26 insertions(+), 48 deletions(-) delete mode 100644 src/ansys/visor/visor-client/src/utils/portal.ts diff --git a/src/ansys/visor/visor-client/src/utils/portal.js b/src/ansys/visor/visor-client/src/utils/portal.js index 4f4b8273..f8e2b7e1 100644 --- a/src/ansys/visor/visor-client/src/utils/portal.js +++ b/src/ansys/visor/visor-client/src/utils/portal.js @@ -1,14 +1,27 @@ -// Lightweight portal helper for runtime JS modules. -// This mirrors the TypeScript helper to support plain .js consumers -// (e.g. wasm/runtime files) without depending on TS compilation. +/** + * Helpers to resolve a scoped portal root for UI elements that would + * otherwise be appended to `document.body` (tooltips, popups, invisible + * DOM containers used by wasm). Prefer an element with the + * `.visor-embed-style` class (added by host integrations), then fall back + * to the client container id `VisorContainer`, and finally to a dedicated + * `visor-portal-root` element created under `document.body`. + * + * A single JS implementation is kept (no separate .ts version) so there is + * one source of truth; JSDoc types below give TS/TSX consumers proper typing. + * + * @returns {HTMLElement} + */ export function getPortalRoot() { const doc = typeof document !== 'undefined' ? document : null; if (!doc) { - return {}; + // Non-browser environment (SSR). Return a harmless stub to avoid + // throwing in places that call this helper during static analysis. + // Callers should guard when used outside the browser. + return /** @type {HTMLElement} */ ({}); } const el = doc.querySelector('.visor-embed-style') || doc.getElementById('VisorContainer'); - if (el) return el; + if (el) return /** @type {HTMLElement} */ (el); let portalRoot = doc.getElementById('visor-portal-root'); if (!portalRoot) { @@ -21,10 +34,18 @@ export function getPortalRoot() { return portalRoot; } +/** + * @param {HTMLElement} el + * @returns {void} + */ export function appendToPortal(el) { getPortalRoot().appendChild(el); } +/** + * @param {HTMLElement} el + * @returns {void} + */ export function removeFromPortal(el) { if (el && el.parentElement) { el.parentElement.removeChild(el); diff --git a/src/ansys/visor/visor-client/src/utils/portal.ts b/src/ansys/visor/visor-client/src/utils/portal.ts deleted file mode 100644 index dc3e64f5..00000000 --- a/src/ansys/visor/visor-client/src/utils/portal.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Helpers to resolve a scoped portal root for UI elements that would - * otherwise be appended to `document.body` (tooltips, popups, invisible - * DOM containers used by wasm). Prefer an element with the - * `.visor-embed-style` class (added by host integrations), then fall back - * to the client container id `VisorContainer`, and finally to `document.body`. - */ -export function getPortalRoot(): HTMLElement { - const doc = typeof document !== 'undefined' ? document : null; - if (!doc) { - // Non-browser environment (SSR). Return a harmless stub to avoid - // throwing in places that call this helper during static analysis. - // Callers should guard when used outside the browser. - return {} as HTMLElement; - } - - // Prefer an explicit Visor embed root when present, then fall back to - // legacy container ids for compatibility. If no suitable container is - // present, create a dedicated portal root under document.body so callers - // don't have to append directly to body. - const el = doc.querySelector('.visor-embed-style') || doc.getElementById('VisorContainer'); - if (el) return el as HTMLElement; - - let portalRoot = doc.getElementById('visor-portal-root') as HTMLElement | null; - if (!portalRoot) { - portalRoot = doc.createElement('div'); - portalRoot.id = 'visor-portal-root'; - portalRoot.setAttribute('data-visor-portal', '1'); - doc.body.appendChild(portalRoot); - } - - return portalRoot; -} - -export function appendToPortal(el: HTMLElement): void { - getPortalRoot().appendChild(el); -} - -export function removeFromPortal(el: HTMLElement): void { - if (el && el.parentElement) { - el.parentElement.removeChild(el); - } -} From 07fbfa4eca9b9bee34c8e49304eb5e0cc0622c81 Mon Sep 17 00:00:00 2001 From: ansBAkula Date: Thu, 17 Sep 2026 09:33:57 +0200 Subject: [PATCH 9/9] chore: trigger CI