Skip to content

Commit 5f0d8c6

Browse files
fix: delay parsing htmlValue when RTE is attached but not rendered (#7890) (#7930)
Co-authored-by: Diego Cardoso <diego@vaadin.com>
1 parent 52f6373 commit 5f0d8c6

File tree

5 files changed

+124
-70
lines changed

5 files changed

+124
-70
lines changed

packages/rich-text-editor/src/vaadin-rich-text-editor-mixin.js

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -793,10 +793,26 @@ export const RichTextEditorMixin = (superClass) =>
793793
*/
794794
dangerouslySetHtmlValue(htmlValue) {
795795
if (!this._editor) {
796-
// The editor isn't ready yet, store the value for later
797-
this.__pendingHtmlValue = htmlValue;
798-
// Clear a possible value to prevent it from clearing the pending htmlValue once the editor property is set
799-
this.value = '';
796+
this.__savePendingHtmlValue(htmlValue);
797+
798+
return;
799+
}
800+
801+
// In Firefox, the styles are not properly computed when the element is placed
802+
// in a Lit component, as the element is first attached to the DOM and then
803+
// the shadowRoot is initialized. This causes the `hmlValue` to not be correctly
804+
// parsed into the delta format used by Quill. To work around this, we check
805+
// if the display property is set and if not, we wait for the element to intersect
806+
// with the viewport before trying to set the value again.
807+
if (!getComputedStyle(this).display) {
808+
this.__savePendingHtmlValue(htmlValue);
809+
const observer = new IntersectionObserver(() => {
810+
if (getComputedStyle(this).display) {
811+
this.__flushPendingHtmlValue();
812+
observer.disconnect();
813+
}
814+
});
815+
observer.observe(this);
800816
return;
801817
}
802818

@@ -823,6 +839,14 @@ export const RichTextEditorMixin = (superClass) =>
823839
this._editor.setContents(deltaFromHtml, SOURCE.API);
824840
}
825841

842+
/** @private */
843+
__savePendingHtmlValue(htmlValue) {
844+
// The editor isn't ready yet, store the value for later
845+
this.__pendingHtmlValue = htmlValue;
846+
// Clear a possible value to prevent it from clearing the pending htmlValue once the editor property is set
847+
this.value = '';
848+
}
849+
826850
/** @private */
827851
__flushPendingHtmlValue() {
828852
if (this.__pendingHtmlValue) {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import '../theme/lumo/vaadin-rich-text-editor-styles.js';
2+
import '../src/vaadin-lit-rich-text-editor.js';
3+
import './attach.common.js';
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import '../theme/lumo/vaadin-rich-text-editor-styles.js';
2+
import '../src/vaadin-rich-text-editor.js';
3+
import './attach.common.js';
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { expect } from '@vaadin/chai-plugins';
2+
import { fixtureSync, nextRender, nextUpdate } from '@vaadin/testing-helpers';
3+
import sinon from 'sinon';
4+
5+
describe('attach/detach', () => {
6+
let rte, editor;
7+
8+
const flushValueDebouncer = () => rte.__debounceSetValue && rte.__debounceSetValue.flush();
9+
10+
async function attach(shadow = false) {
11+
const parent = fixtureSync('<div></div>');
12+
if (shadow) {
13+
parent.attachShadow({ mode: 'open' });
14+
}
15+
parent.appendChild(rte);
16+
await nextRender();
17+
flushValueDebouncer();
18+
}
19+
20+
beforeEach(async () => {
21+
rte = fixtureSync('<vaadin-rich-text-editor></vaaddin-rich-text-editor>');
22+
await nextRender();
23+
flushValueDebouncer();
24+
editor = rte._editor;
25+
});
26+
27+
describe('detach and re-attach', () => {
28+
it('should disconnect the emitter when detached', () => {
29+
const spy = sinon.spy(editor.emitter, 'disconnect');
30+
31+
rte.parentNode.removeChild(rte);
32+
33+
expect(spy).to.be.calledOnce;
34+
});
35+
36+
it('should re-connect the emitter when detached and re-attached', async () => {
37+
const parent = rte.parentNode;
38+
parent.removeChild(rte);
39+
40+
const spy = sinon.spy(editor.emitter, 'connect');
41+
42+
parent.appendChild(rte);
43+
await nextUpdate(rte);
44+
45+
expect(spy).to.be.calledOnce;
46+
});
47+
48+
it('should parse htmlValue correctly when element is attached but not rendered', async () => {
49+
await attach(true);
50+
rte.dangerouslySetHtmlValue('<p>Foo</p><ul><li>Bar</li><li>Baz</li></ul>');
51+
rte.parentNode.shadowRoot.innerHTML = '<slot></slot>';
52+
await nextRender();
53+
flushValueDebouncer();
54+
expect(rte.htmlValue).to.equal('<p>Foo</p><ul><li>Bar</li><li>Baz</li></ul>');
55+
});
56+
});
57+
58+
describe('unattached rich text editor', () => {
59+
beforeEach(() => {
60+
rte = document.createElement('vaadin-rich-text-editor');
61+
});
62+
63+
it('should not throw when setting html value', () => {
64+
expect(() => rte.dangerouslySetHtmlValue('<h1>Foo</h1>')).to.not.throw(Error);
65+
});
66+
67+
it('should have the html value once attached', async () => {
68+
rte.dangerouslySetHtmlValue('<h1>Foo</h1>');
69+
await attach();
70+
71+
expect(rte.htmlValue).to.equal('<h1>Foo</h1>');
72+
});
73+
74+
it('should override the htmlValue', async () => {
75+
rte.dangerouslySetHtmlValue('<h1>Foo</h1>');
76+
rte.value = JSON.stringify([{ insert: 'Vaadin' }]);
77+
await attach();
78+
79+
expect(rte.htmlValue).to.equal('<p>Vaadin</p>');
80+
});
81+
82+
it('should override the value', async () => {
83+
rte.value = JSON.stringify([{ insert: 'Vaadin' }]);
84+
rte.dangerouslySetHtmlValue('<h1>Foo</h1>');
85+
await attach();
86+
87+
expect(rte.htmlValue).to.equal('<h1>Foo</h1>');
88+
});
89+
});
90+
});

packages/rich-text-editor/test/basic.common.js

Lines changed: 0 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -573,70 +573,4 @@ describe('rich text editor', () => {
573573
);
574574
});
575575
});
576-
577-
describe('detach and re-attach', () => {
578-
it('should disconnect the emitter when detached', () => {
579-
const spy = sinon.spy(editor.emitter, 'disconnect');
580-
581-
rte.parentNode.removeChild(rte);
582-
583-
expect(spy).to.be.calledOnce;
584-
});
585-
586-
it('should re-connect the emitter when detached and re-attached', async () => {
587-
const parent = rte.parentNode;
588-
parent.removeChild(rte);
589-
590-
const spy = sinon.spy(editor.emitter, 'connect');
591-
592-
parent.appendChild(rte);
593-
await nextUpdate(rte);
594-
595-
expect(spy).to.be.calledOnce;
596-
});
597-
});
598-
});
599-
600-
describe('unattached rich text editor', () => {
601-
let rte;
602-
603-
beforeEach(() => {
604-
rte = document.createElement('vaadin-rich-text-editor');
605-
});
606-
607-
const flushValueDebouncer = () => rte.__debounceSetValue && rte.__debounceSetValue.flush();
608-
609-
async function attach() {
610-
const parent = fixtureSync('<div></div>');
611-
parent.appendChild(rte);
612-
await nextRender();
613-
flushValueDebouncer();
614-
}
615-
616-
it('should not throw when setting html value', () => {
617-
expect(() => rte.dangerouslySetHtmlValue('<h1>Foo</h1>')).to.not.throw(Error);
618-
});
619-
620-
it('should have the html value once attached', async () => {
621-
rte.dangerouslySetHtmlValue('<h1>Foo</h1>');
622-
await attach();
623-
624-
expect(rte.htmlValue).to.equal('<h1>Foo</h1>');
625-
});
626-
627-
it('should override the htmlValue', async () => {
628-
rte.dangerouslySetHtmlValue('<h1>Foo</h1>');
629-
rte.value = JSON.stringify([{ insert: 'Vaadin' }]);
630-
await attach();
631-
632-
expect(rte.htmlValue).to.equal('<p>Vaadin</p>');
633-
});
634-
635-
it('should override the value', async () => {
636-
rte.value = JSON.stringify([{ insert: 'Vaadin' }]);
637-
rte.dangerouslySetHtmlValue('<h1>Foo</h1>');
638-
await attach();
639-
640-
expect(rte.htmlValue).to.equal('<h1>Foo</h1>');
641-
});
642576
});

0 commit comments

Comments
 (0)