Skip to content

Commit 8d97c41

Browse files
authored
Merge pull request #2933 from pie-framework/fix/PD-5638-version2
fix(multiple-choice, placement-ordering): use observer to render math…
2 parents 51746c7 + 4c2a662 commit 8d97c41

3 files changed

Lines changed: 83 additions & 12 deletions

File tree

packages/multiple-choice/src/index.js

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ export default class MultipleChoice extends HTMLElement {
5555
this._keyboardEventsEnabled = false;
5656
this._audioInitialized = false;
5757
this._root = null;
58+
this._mathObserver = null;
59+
this._mathRenderPending = false;
5860

5961
this._rerender = debounce(
6062
() => {
@@ -75,15 +77,12 @@ export default class MultipleChoice extends HTMLElement {
7577
this.setAttribute('role', 'region');
7678
this.setLangAttribute();
7779

80+
this._initMathObserver();
81+
7882
if (!this._root) {
7983
this._root = createRoot(this);
8084
}
8185
this._root.render(element);
82-
// Use requestAnimationFrame to ensure DOM is fully painted before rendering math
83-
requestAnimationFrame(() => {
84-
log('render complete - render math');
85-
renderMath(this);
86-
});
8786

8887
if (this._model.keyboardEventsEnabled === true && !this._keyboardEventsEnabled) {
8988
this.enableKeyboardEvents();
@@ -120,6 +119,38 @@ export default class MultipleChoice extends HTMLElement {
120119
);
121120
}
122121

122+
_scheduleMathRender = () => {
123+
if (this._mathRenderPending) return;
124+
this._mathRenderPending = true;
125+
126+
requestAnimationFrame(() => {
127+
if (this._mathObserver) {
128+
this._mathObserver.disconnect();
129+
}
130+
log('render complete - render math');
131+
renderMath(this);
132+
this._mathRenderPending = false;
133+
setTimeout(() => {
134+
if (this._mathObserver) {
135+
this._mathObserver.observe(this, { childList: true, subtree: true });
136+
}
137+
}, 50);
138+
});
139+
};
140+
141+
_initMathObserver() {
142+
if (this._mathObserver) return;
143+
this._mathObserver = new MutationObserver(this._scheduleMathRender);
144+
this._mathObserver.observe(this, { childList: true, subtree: true });
145+
}
146+
147+
_disconnectMathObserver() {
148+
if (this._mathObserver) {
149+
this._mathObserver.disconnect();
150+
this._mathObserver = null;
151+
}
152+
}
153+
123154
onShowCorrectToggle() {
124155
renderMath(this);
125156
}
@@ -192,6 +223,7 @@ export default class MultipleChoice extends HTMLElement {
192223
}
193224

194225
connectedCallback() {
226+
this._initMathObserver();
195227
this._rerender();
196228

197229
// Observation: audio in Chrome will have the autoplay attribute,
@@ -281,6 +313,7 @@ export default class MultipleChoice extends HTMLElement {
281313
}
282314

283315
disconnectedCallback() {
316+
this._disconnectMathObserver();
284317
if (this._keyboardEventsEnabled) {
285318
window.removeEventListener('keydown', this._boundHandleKeyDown);
286319
this._keyboardEventsEnabled = false;

packages/multiple-choice/src/print.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,12 @@ export default class MultipleChoicePrint extends HTMLElement {
6868
this._root = createRoot(this);
6969
}
7070
this._root.render(element);
71-
// Use requestAnimationFrame to ensure DOM is fully painted before rendering math
72-
// This is especially important for nested components like PreviewPrompt (rationale, choice labels)
71+
// Use double requestAnimationFrame so React has committed to the DOM before we render math
7372
requestAnimationFrame(() => {
74-
log('render complete - render math');
75-
renderMath(this);
73+
requestAnimationFrame(() => {
74+
log('render complete - render math');
75+
renderMath(this);
76+
});
7677
});
7778
} else {
7879
log('skip');

packages/placement-ordering/src/index.js

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,39 @@ export default class Ordering extends HTMLElement {
2323
constructor() {
2424
super();
2525
this._root = null;
26+
this._mathObserver = null;
27+
this._mathRenderPending = false;
28+
}
29+
30+
_scheduleMathRender = () => {
31+
if (this._mathRenderPending) return;
32+
this._mathRenderPending = true;
33+
34+
requestAnimationFrame(() => {
35+
if (this._mathObserver) {
36+
this._mathObserver.disconnect();
37+
}
38+
renderMath(this);
39+
this._mathRenderPending = false;
40+
setTimeout(() => {
41+
if (this._mathObserver) {
42+
this._mathObserver.observe(this, { childList: true, subtree: true });
43+
}
44+
}, 50);
45+
});
46+
};
47+
48+
_initMathObserver() {
49+
if (this._mathObserver) return;
50+
this._mathObserver = new MutationObserver(this._scheduleMathRender);
51+
this._mathObserver.observe(this, { childList: true, subtree: true });
52+
}
53+
54+
_disconnectMathObserver() {
55+
if (this._mathObserver) {
56+
this._mathObserver.disconnect();
57+
this._mathObserver = null;
58+
}
2659
}
2760

2861
isComplete = (value) => value && compact(value).length === this._model.completeLength;
@@ -63,6 +96,8 @@ export default class Ordering extends HTMLElement {
6396
log('[render] session: ', this._session.value);
6497
log('[render] model: ', this._model);
6598

99+
this._initMathObserver();
100+
66101
const element = React.createElement(Main, {
67102
model: this._model,
68103
session: this._session,
@@ -73,13 +108,15 @@ export default class Ordering extends HTMLElement {
73108
this._root = createRoot(this);
74109
}
75110
this._root.render(element);
76-
queueMicrotask(() => {
77-
renderMath(this);
78-
});
79111
}
80112
}
81113

114+
connectedCallback() {
115+
this._initMathObserver();
116+
}
117+
82118
disconnectedCallback() {
119+
this._disconnectMathObserver();
83120
if (this._root) {
84121
this._root.unmount();
85122
}

0 commit comments

Comments
 (0)