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
2
Answers
The issue here likely has to do with how Quill handles selection and formatting within a Shadow DOM context. the problem could be due to event handling being disrupted by the Shadow DOM boundary, CSS specificity and inheritance issues, or selection range calculations being affected by the Shadow DOM context. You should try the changes I made below:
To use this:
A few additional tips:
enhancedModules.toolbar.handlers
objectcustomStyles
section if you need to match your application’s stylingpointer-events: auto
on toolbar buttons is crucial for proper click handlingHope this helps
If you have control over the content of the buttons, this should be rather straightforward to workaround.
Your code might look like this:
The idea here is to prevent the button from receiving focus when the mouse click begins while still using the
onClick
handler to apply the bold effect.If I’m off base, please ping me if/when you update your CodeSandbox link and I can take a look.