Skip to content

Commit

Permalink
fix: delay parsing htmlValue when RTE is attached but not rendered (#…
Browse files Browse the repository at this point in the history
…7890) (#7929)

Co-authored-by: Diego Cardoso <[email protected]>
  • Loading branch information
vaadin-bot and DiegoCardoso authored Oct 3, 2024
1 parent 69c94a9 commit 362884b
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 70 deletions.
32 changes: 28 additions & 4 deletions packages/rich-text-editor/src/vaadin-rich-text-editor-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -703,10 +703,26 @@ export const RichTextEditorMixin = (superClass) =>
*/
dangerouslySetHtmlValue(htmlValue) {
if (!this._editor) {
// The editor isn't ready yet, store the value for later
this.__pendingHtmlValue = htmlValue;
// Clear a possible value to prevent it from clearing the pending htmlValue once the editor property is set
this.value = '';
this.__savePendingHtmlValue(htmlValue);

return;
}

// In Firefox, the styles are not properly computed when the element is placed
// in a Lit component, as the element is first attached to the DOM and then
// the shadowRoot is initialized. This causes the `hmlValue` to not be correctly
// parsed into the delta format used by Quill. To work around this, we check
// if the display property is set and if not, we wait for the element to intersect
// with the viewport before trying to set the value again.
if (!getComputedStyle(this).display) {
this.__savePendingHtmlValue(htmlValue);
const observer = new IntersectionObserver(() => {
if (getComputedStyle(this).display) {
this.__flushPendingHtmlValue();
observer.disconnect();
}
});
observer.observe(this);
return;
}

Expand All @@ -733,6 +749,14 @@ export const RichTextEditorMixin = (superClass) =>
this._editor.setContents(deltaFromHtml, SOURCE.API);
}

/** @private */
__savePendingHtmlValue(htmlValue) {
// The editor isn't ready yet, store the value for later
this.__pendingHtmlValue = htmlValue;
// Clear a possible value to prevent it from clearing the pending htmlValue once the editor property is set
this.value = '';
}

/** @private */
__flushPendingHtmlValue() {
if (this.__pendingHtmlValue) {
Expand Down
3 changes: 3 additions & 0 deletions packages/rich-text-editor/test/attach-lit.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import '../theme/lumo/vaadin-rich-text-editor-styles.js';
import '../src/vaadin-lit-rich-text-editor.js';
import './attach.common.js';
3 changes: 3 additions & 0 deletions packages/rich-text-editor/test/attach-polymer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import '../theme/lumo/vaadin-rich-text-editor-styles.js';
import '../src/vaadin-rich-text-editor.js';
import './attach.common.js';
90 changes: 90 additions & 0 deletions packages/rich-text-editor/test/attach.common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { expect } from '@esm-bundle/chai';
import { fixtureSync, nextRender, nextUpdate } from '@vaadin/testing-helpers';
import sinon from 'sinon';

describe('attach/detach', () => {
let rte, editor;

const flushValueDebouncer = () => rte.__debounceSetValue && rte.__debounceSetValue.flush();

async function attach(shadow = false) {
const parent = fixtureSync('<div></div>');
if (shadow) {
parent.attachShadow({ mode: 'open' });
}
parent.appendChild(rte);
await nextRender();
flushValueDebouncer();
}

beforeEach(async () => {
rte = fixtureSync('<vaadin-rich-text-editor></vaaddin-rich-text-editor>');
await nextRender();
flushValueDebouncer();
editor = rte._editor;
});

describe('detach and re-attach', () => {
it('should disconnect the emitter when detached', () => {
const spy = sinon.spy(editor.emitter, 'disconnect');

rte.parentNode.removeChild(rte);

expect(spy).to.be.calledOnce;
});

it('should re-connect the emitter when detached and re-attached', async () => {
const parent = rte.parentNode;
parent.removeChild(rte);

const spy = sinon.spy(editor.emitter, 'connect');

parent.appendChild(rte);
await nextUpdate(rte);

expect(spy).to.be.calledOnce;
});

it('should parse htmlValue correctly when element is attached but not rendered', async () => {
await attach(true);
rte.dangerouslySetHtmlValue('<p>Foo</p><ul><li>Bar</li><li>Baz</li></ul>');
rte.parentNode.shadowRoot.innerHTML = '<slot></slot>';
await nextRender();
flushValueDebouncer();
expect(rte.htmlValue).to.equal('<p>Foo</p><ul><li>Bar</li><li>Baz</li></ul>');
});
});

describe('unattached rich text editor', () => {
beforeEach(() => {
rte = document.createElement('vaadin-rich-text-editor');
});

it('should not throw when setting html value', () => {
expect(() => rte.dangerouslySetHtmlValue('<h1>Foo</h1>')).to.not.throw(Error);
});

it('should have the html value once attached', async () => {
rte.dangerouslySetHtmlValue('<h1>Foo</h1>');
await attach();

expect(rte.htmlValue).to.equal('<h1>Foo</h1>');
});

it('should override the htmlValue', async () => {
rte.dangerouslySetHtmlValue('<h1>Foo</h1>');
rte.value = JSON.stringify([{ insert: 'Vaadin' }]);
await attach();

expect(rte.htmlValue).to.equal('<p>Vaadin</p>');
});

it('should override the value', async () => {
rte.value = JSON.stringify([{ insert: 'Vaadin' }]);
rte.dangerouslySetHtmlValue('<h1>Foo</h1>');
await attach();

expect(rte.htmlValue).to.equal('<h1>Foo</h1>');
});
});
});
66 changes: 0 additions & 66 deletions packages/rich-text-editor/test/basic.common.js
Original file line number Diff line number Diff line change
Expand Up @@ -1046,70 +1046,4 @@ describe('rich text editor', () => {
);
});
});

describe('detach and re-attach', () => {
it('should disconnect the emitter when detached', () => {
const spy = sinon.spy(editor.emitter, 'disconnect');

rte.parentNode.removeChild(rte);

expect(spy).to.be.calledOnce;
});

it('should re-connect the emitter when detached and re-attached', async () => {
const parent = rte.parentNode;
parent.removeChild(rte);

const spy = sinon.spy(editor.emitter, 'connect');

parent.appendChild(rte);
await nextUpdate(rte);

expect(spy).to.be.calledOnce;
});
});
});

describe('unattached rich text editor', () => {
let rte;

beforeEach(() => {
rte = document.createElement('vaadin-rich-text-editor');
});

const flushValueDebouncer = () => rte.__debounceSetValue && rte.__debounceSetValue.flush();

async function attach() {
const parent = fixtureSync('<div></div>');
parent.appendChild(rte);
await nextRender();
flushValueDebouncer();
}

it('should not throw when setting html value', () => {
expect(() => rte.dangerouslySetHtmlValue('<h1>Foo</h1>')).to.not.throw(Error);
});

it('should have the html value once attached', async () => {
rte.dangerouslySetHtmlValue('<h1>Foo</h1>');
await attach();

expect(rte.htmlValue).to.equal('<h1>Foo</h1>');
});

it('should override the htmlValue', async () => {
rte.dangerouslySetHtmlValue('<h1>Foo</h1>');
rte.value = JSON.stringify([{ insert: 'Vaadin' }]);
await attach();

expect(rte.htmlValue).to.equal('<p>Vaadin</p>');
});

it('should override the value', async () => {
rte.value = JSON.stringify([{ insert: 'Vaadin' }]);
rte.dangerouslySetHtmlValue('<h1>Foo</h1>');
await attach();

expect(rte.htmlValue).to.equal('<h1>Foo</h1>');
});
});

0 comments on commit 362884b

Please sign in to comment.