I have a problem that only exists when using a Shadow DOM. It’s difficult to explain, but when the Shadow DOM is present (i.e. using the ShadowWrapper component), it behaves so that, when typing content in the Quill editor, selecting it and clicking a button like bold, it doesn’t apply the bold to the selected text - instead, it seems to deselect the selected text and then turns the button on, so if you were to add new text, that text would then be emboldened. The link button also does not work whatsoever for example.
I think it might not be a CSS/styling issue since I tried including the same CSS as with the working version (without the Shadow DOM) and it still doesn’t work.
Here is the complete code:
import "./styles.css";
import React, { useRef, useEffect, useState } from "react";
import ReactDOM from "react-dom";
import ReactQuill from "react-quill";
import "react-quill/dist/quill.snow.css";
const modules = {
toolbar: [
[{ font: [] }],
[{ header: [1, 2, 3, 4, 5, 6, false] }],
["bold", "italic", "underline", "strike"],
[{ color: [] }, { background: [] }],
[{ script: "sub" }, { script: "super" }],
["blockquote", "code-block"],
[{ list: "ordered" }, { list: "bullet" }],
[{ indent: "-1" }, { indent: "+1" }, { align: [] }],
["link", "image", "video"],
["clean"],
],
};
const selectionChange = (e: any) => {
if (e) console.log(e.index, e.length);
};
interface ShadowWrapperProps {
children: React.ReactNode;
}
const ShadowWrapper: React.FC<ShadowWrapperProps> = ({ children }) => {
const shadowRootRef = useRef<HTMLDivElement | null>(null); // Reference to the div that will contain the shadow DOM
const [shadowRoot, setShadowRoot] = useState<ShadowRoot | null>(null); // State to store the shadow root
useEffect(() => {
// Check if the shadowRootRef is set and shadowRoot is not already created
if (shadowRootRef.current && !shadowRoot) {
// Check if the shadow DOM is not already attached to the div
if (!shadowRootRef.current.shadowRoot) {
// Attach shadow DOM to the div
const shadow = shadowRootRef.current.attachShadow({ mode: "open" });
setShadowRoot(shadow);
// Inject Quill styles into the shadow DOM
const style = document.createElement("style");
style.textContent = `
@import url('https://cdn.quilljs.com/1.3.6/quill.snow.css');
`;
shadow.appendChild(style);
} else {
// If shadow root already exists, set it to state
setShadowRoot(shadowRootRef.current.shadowRoot);
}
}
}, [shadowRoot]);
return (
// Render children inside the shadow DOM
<div ref={shadowRootRef}>
{shadowRoot && ReactDOM.createPortal(children, shadowRoot)}
</div>
);
};
const App = () => {
const initialValue = `highlight this text`;
const [value, setValue] = useState(initialValue);
return (
<>
<ShadowWrapper>
<ReactQuill
modules={modules}
value={value}
theme="snow"
onChange={setValue}
onChangeSelection={selectionChange}
placeholder="Content goes here..."
/>
</ShadowWrapper>
<div style={{ width: "100%" }}>
<pre>{value}</pre>
</div>
</>
);
};
export default App;
For convenience, I have also been able to make a clone of the problem here (you may need to use Google Chrome for it to work):
Load the URL and then click inside the box, select the 'highlight this text' value in the editor and click bold - you'll notice that the text isn't emboldened, but if you type again it will be emboldened.
To fix the problem, comment out the ShadowWrapper component.
So the main question is how can it be made so that it works with the ShadowWrapper component? (Because in my real app I need a Shadow DOM to prevent external styles affecting it). Thanks for any help
Here it seems like the ReactQuill editor is being rendered inside a ShadowWrapper, but it doesn't correctly handle Quill's initialization inside the shadow DOM. The problem is that Quill expects the root element to be attached directly to the DOM. Since the ReactQuill component is wrapped inside the ShadowWrapper, the editor's internal root is not getting properly set or initialized within the shadow DOM.
It seems to get fixed by manually attaching the Quill editor's root to the element inside the shadow DOM (quillRef.current.getEditor().root = el;)
, this ensures that the Quill editor operates correctly within the shadow DOM.
import "./styles.css";
import React, { useRef, useEffect, useState } from "react";
import ReactDOM from "react-dom";
import ReactQuill from "react-quill";
import "react-quill/dist/quill.snow.css";
const modules = {
toolbar: [
[{ font: [] }],
[{ header: [1, 2, 3, 4, 5, 6, false] }],
["bold", "italic", "underline", "strike"],
[{ color: [] }, { background: [] }],
[{ script: "sub" }, { script: "super" }],
["blockquote", "code-block"],
[{ list: "ordered" }, { list: "bullet" }],
[{ indent: "-1" }, { indent: "+1" }, { align: [] }],
["link", "image", "video"],
["clean"],
],
};
const selectionChange = (e: any) => {
if (e) console.log(e.index, e.length);
};
interface ShadowWrapperProps {
children: React.ReactNode;
}
const ShadowWrapper: React.FC<ShadowWrapperProps> = ({ children }) => {
const shadowRootRef = useRef<HTMLDivElement | null>(null); // Reference to the div that will contain the shadow DOM
const [shadowRoot, setShadowRoot] = useState<ShadowRoot | null>(null); // State to store the shadow root
useEffect(() => {
// Check if the shadowRootRef is set and shadowRoot is not already created
if (shadowRootRef.current && !shadowRoot) {
// Attach shadow DOM to the div
const shadow = shadowRootRef.current.attachShadow({ mode: "open" });
setShadowRoot(shadow);
// Inject Quill styles into the shadow DOM
const style = document.createElement("style");
style.textContent = `
@import url('https://cdn.quilljs.com/1.3.6/quill.snow.css');
`;
shadow.appendChild(style);
}
}, [shadowRoot]);
return (
// Render children inside the shadow DOM
<div ref={shadowRootRef}>
{shadowRoot && ReactDOM.createPortal(children, shadowRoot)}
</div>
);
};
const App = () => {
const initialValue = `<p>highlight this text</p>`;
const [value, setValue] = useState(initialValue);
const quillRef = useRef<ReactQuill | null>(null);
return (
<>
{/* Toolbar outside the Shadow DOM */}
<div>
<ReactQuill
ref={quillRef}
modules={modules}
value={value}
theme="snow"
onChange={setValue}
onChangeSelection={selectionChange}
placeholder="Content goes here..."
/>
</div>
{/* Shadow wrapper for the Quill editor content */}
<ShadowWrapper>
<div>
<div
ref={(el) => {
// Attach the Quill editor instance to the shadow DOM
if (el && quillRef.current) {
quillRef.current.getEditor().root = el;
}
}}
className="editor-container"
/>
</div>
</ShadowWrapper>
{/* Preview of the editor content */}
<div style={{ width: "100%" }}>
<pre>{value}</pre>
</div>
</>
);
};
export default App;