From aeb4e0c9e2c67e5915ef0c435ea4065447a7c1b1 Mon Sep 17 00:00:00 2001 From: Patricia Romaniuc Date: Wed, 17 Jun 2026 14:21:41 +0300 Subject: [PATCH] fix(rubric/math-observer): implement math rendering observer for improved sync PIE-671 --- packages/multi-trait-rubric/src/index.js | 45 ++++++++++++++++++++++-- packages/rubric/src/index.js | 45 ++++++++++++++++++++++-- 2 files changed, 84 insertions(+), 6 deletions(-) diff --git a/packages/multi-trait-rubric/src/index.js b/packages/multi-trait-rubric/src/index.js index 75f6e0362f..36d65933eb 100644 --- a/packages/multi-trait-rubric/src/index.js +++ b/packages/multi-trait-rubric/src/index.js @@ -10,6 +10,44 @@ export default class MultiTraitRubric extends HTMLElement { this._model = {}; this._session = null; this._root = null; + this._mathObserver = null; + this._mathRenderPending = false; + } + + // React commits asynchronously, so a queueMicrotask(renderMath) can run before + // the LaTeX spans are in the DOM and leave raw LaTeX on first render — this is + // especially visible when this element is mounted inside complex-rubric, which + // swaps its innerHTML and re-defines the inner element. Observing the DOM and + // typesetting after each commit keeps math in sync regardless of timing. + _scheduleMathRender = () => { + if (this._mathRenderPending) return; + this._mathRenderPending = true; + + requestAnimationFrame(() => { + if (this._mathObserver) { + this._mathObserver.disconnect(); + } + renderMath(this); + this._mathRenderPending = false; + setTimeout(() => { + if (this._mathObserver) { + this._mathObserver.observe(this, { childList: true, subtree: true }); + } + }, 50); + }); + }; + + _initMathObserver() { + if (this._mathObserver) return; + this._mathObserver = new MutationObserver(this._scheduleMathRender); + this._mathObserver.observe(this, { childList: true, subtree: true }); + } + + _disconnectMathObserver() { + if (this._mathObserver) { + this._mathObserver.disconnect(); + this._mathObserver = null; + } } set model(s) { @@ -28,22 +66,23 @@ export default class MultiTraitRubric extends HTMLElement { } connectedCallback() { + this._initMathObserver(); this._render(); } _render() { + this._initMathObserver(); + const el = React.createElement(Main, { model: this._model, session: this._session }); if (!this._root) { this._root = createRoot(this); } this._root.render(el); - queueMicrotask(() => { - renderMath(this); - }); } disconnectedCallback() { + this._disconnectMathObserver(); if (this._root) { this._root.unmount(); } diff --git a/packages/rubric/src/index.js b/packages/rubric/src/index.js index 1866e74719..5008118e78 100644 --- a/packages/rubric/src/index.js +++ b/packages/rubric/src/index.js @@ -10,6 +10,44 @@ export default class RubricRender extends HTMLElement { debug.log('constructor called'); this.onModelChanged = this.onModelChanged.bind(this); this._root = null; + this._mathObserver = null; + this._mathRenderPending = false; + } + + // React commits asynchronously, so a queueMicrotask(renderMath) can run before + // the LaTeX spans are in the DOM and leave raw LaTeX on first render — this is + // especially visible when rubric is mounted inside complex-rubric, which swaps + // its innerHTML and re-defines the inner element. Observing the DOM and + // typesetting after each commit keeps math in sync regardless of timing. + _scheduleMathRender = () => { + if (this._mathRenderPending) return; + this._mathRenderPending = true; + + requestAnimationFrame(() => { + if (this._mathObserver) { + this._mathObserver.disconnect(); + } + renderMath(this); + this._mathRenderPending = false; + setTimeout(() => { + if (this._mathObserver) { + this._mathObserver.observe(this, { childList: true, subtree: true }); + } + }, 50); + }); + }; + + _initMathObserver() { + if (this._mathObserver) return; + this._mathObserver = new MutationObserver(this._scheduleMathRender); + this._mathObserver.observe(this, { childList: true, subtree: true }); + } + + _disconnectMathObserver() { + if (this._mathObserver) { + this._mathObserver.disconnect(); + this._mathObserver = null; + } } set model(s) { @@ -23,24 +61,25 @@ export default class RubricRender extends HTMLElement { } connectedCallback() { + this._initMathObserver(); this._render(); } _render() { if (this._model) { + this._initMathObserver(); + const el = React.createElement(Rubric, { value: this._model }); if (!this._root) { this._root = createRoot(this); } this._root.render(el); - queueMicrotask(() => { - renderMath(this); - }); } } disconnectedCallback() { + this._disconnectMathObserver(); if (this._root) { this._root.unmount(); }