Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: delay parsing htmlValue when RTE is attached but not rendered (#7890) (CP: 24.4) #7929

Merged
merged 2 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>');
});
});