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
1 change: 1 addition & 0 deletions doc/changelog.d/109.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Visor dash styling
2 changes: 1 addition & 1 deletion src/ansys/visor/dash/src/lib/components/Visordash.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ const Visordash = (props) => {
}, [fireSnapshot]);

return (
<div id={id} style={{ width: '100%', height: '100%' }}>
<div id={id} className="visor-embed-style" style={{ width: '100%', height: '100%' }}>
<div id="VisorContainer" style={{ width: '100%', height: '100%' }} />
</div>
);
Expand Down
3 changes: 2 additions & 1 deletion src/ansys/visor/visor-client/index.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en" class="light">
<html lang="en" class="light visor-embed-style">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
Expand All @@ -9,6 +9,7 @@
<body>
<div
id="VisorContainer"
class="visor-embed-style"
style="position: absolute; inset: 0; overflow: hidden; z-index: 2"
></div>
<script type="module" src="/src/main.tsx"></script>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};

Expand Down Expand Up @@ -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> = [];
Expand Down Expand Up @@ -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({});
//////////////////////////
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
23 changes: 21 additions & 2 deletions src/ansys/visor/visor-client/src/jest-tests/JsHelpers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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>}
*/
Expand Down Expand Up @@ -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 <html> 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', () => {
Expand Down
41 changes: 29 additions & 12 deletions src/ansys/visor/visor-client/src/styles/index.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
*,
*::before,
*::after {
/* 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`. */
.visor-embed-style,
.visor-embed-style *,
.visor-embed-style *::before,
.visor-embed-style *::after {
margin: 0;
padding: 0;
line-height: 1;
Expand All @@ -12,27 +16,31 @@
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 {
/*
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');
font-display: swap;
}

body {
/* 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 {
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,
Expand All @@ -50,6 +58,15 @@ body {
'Noto Color Emoji';
}

/* Ensure Visor typography matches standalone appearance while keeping
styles scoped to the embed root container so they don't leak into host apps. */
.visor-embed-style,
.visor-embed-style *,
.visor-embed-style * {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

a,
a:active,
a:link,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
9 changes: 8 additions & 1 deletion src/ansys/visor/visor-client/src/treeview/TreeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,17 @@ export const TreeView = <T extends ITreeViewNode<T>>(props: TreeViewProps<T>) =>
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 <html>) is scoped to Visor's font size.
const embedRoot = componentContainerElem.closest('.visor-embed-style');
const fontSize: number = getRootFontSize(
14,
(embedRoot ?? document.documentElement) as HTMLElement
Comment thread
ansBAkula marked this conversation as resolved.
);
const headerContainer = document.createElement('div');
const treeContainer = document.createElement('div');
componentContainerElem.appendChild(headerContainer);
Expand Down
29 changes: 22 additions & 7 deletions src/ansys/visor/visor-client/src/utils/JsHelpers.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,49 @@
/** @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<Element, number>}
*/
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.
* @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 `<html>` 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');
}

let rootFontSize = rootFontSizeCache.get(element);

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)));
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) {
Expand Down
53 changes: 53 additions & 0 deletions src/ansys/visor/visor-client/src/utils/portal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* 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) {
// 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 /** @type {HTMLElement} */ (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;
}

/**
* @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);
}
}
Loading