From 4c2a662a56b20550168929faeaf9c1eb5b9a8ca1 Mon Sep 17 00:00:00 2001 From: arimieandreea Date: Mon, 2 Mar 2026 17:46:42 +0200 Subject: [PATCH] fix(multiple-choice, placement-ordering): use observer to render math content PD-5638 --- packages/multiple-choice/src/index.js | 43 +++++++++++++++++++++--- packages/multiple-choice/src/print.js | 9 ++--- packages/placement-ordering/src/index.js | 43 ++++++++++++++++++++++-- 3 files changed, 83 insertions(+), 12 deletions(-) diff --git a/packages/multiple-choice/src/index.js b/packages/multiple-choice/src/index.js index d209551bc6..defbec4baf 100644 --- a/packages/multiple-choice/src/index.js +++ b/packages/multiple-choice/src/index.js @@ -55,6 +55,8 @@ export default class MultipleChoice extends HTMLElement { this._keyboardEventsEnabled = false; this._audioInitialized = false; this._root = null; + this._mathObserver = null; + this._mathRenderPending = false; this._rerender = debounce( () => { @@ -75,15 +77,12 @@ export default class MultipleChoice extends HTMLElement { this.setAttribute('role', 'region'); this.setLangAttribute(); + this._initMathObserver(); + if (!this._root) { this._root = createRoot(this); } this._root.render(element); - // Use requestAnimationFrame to ensure DOM is fully painted before rendering math - requestAnimationFrame(() => { - log('render complete - render math'); - renderMath(this); - }); if (this._model.keyboardEventsEnabled === true && !this._keyboardEventsEnabled) { this.enableKeyboardEvents(); @@ -120,6 +119,38 @@ export default class MultipleChoice extends HTMLElement { ); } + _scheduleMathRender = () => { + if (this._mathRenderPending) return; + this._mathRenderPending = true; + + requestAnimationFrame(() => { + if (this._mathObserver) { + this._mathObserver.disconnect(); + } + log('render complete - render math'); + 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; + } + } + onShowCorrectToggle() { renderMath(this); } @@ -192,6 +223,7 @@ export default class MultipleChoice extends HTMLElement { } connectedCallback() { + this._initMathObserver(); this._rerender(); // Observation: audio in Chrome will have the autoplay attribute, @@ -281,6 +313,7 @@ export default class MultipleChoice extends HTMLElement { } disconnectedCallback() { + this._disconnectMathObserver(); if (this._keyboardEventsEnabled) { window.removeEventListener('keydown', this._boundHandleKeyDown); this._keyboardEventsEnabled = false; diff --git a/packages/multiple-choice/src/print.js b/packages/multiple-choice/src/print.js index 0e7a6ddc1d..09c921a362 100644 --- a/packages/multiple-choice/src/print.js +++ b/packages/multiple-choice/src/print.js @@ -68,11 +68,12 @@ export default class MultipleChoicePrint extends HTMLElement { this._root = createRoot(this); } this._root.render(element); - // Use requestAnimationFrame to ensure DOM is fully painted before rendering math - // This is especially important for nested components like PreviewPrompt (rationale, choice labels) + // Use double requestAnimationFrame so React has committed to the DOM before we render math requestAnimationFrame(() => { - log('render complete - render math'); - renderMath(this); + requestAnimationFrame(() => { + log('render complete - render math'); + renderMath(this); + }); }); } else { log('skip'); diff --git a/packages/placement-ordering/src/index.js b/packages/placement-ordering/src/index.js index 454f044747..2496fef3d5 100644 --- a/packages/placement-ordering/src/index.js +++ b/packages/placement-ordering/src/index.js @@ -23,6 +23,39 @@ export default class Ordering extends HTMLElement { constructor() { super(); this._root = null; + this._mathObserver = null; + this._mathRenderPending = false; + } + + _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; + } } isComplete = (value) => value && compact(value).length === this._model.completeLength; @@ -63,6 +96,8 @@ export default class Ordering extends HTMLElement { log('[render] session: ', this._session.value); log('[render] model: ', this._model); + this._initMathObserver(); + const element = React.createElement(Main, { model: this._model, session: this._session, @@ -73,13 +108,15 @@ export default class Ordering extends HTMLElement { this._root = createRoot(this); } this._root.render(element); - queueMicrotask(() => { - renderMath(this); - }); } } + connectedCallback() { + this._initMathObserver(); + } + disconnectedCallback() { + this._disconnectMathObserver(); if (this._root) { this._root.unmount(); }