skip to Main Content

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):

https://codesandbox.io/p/sandbox/react-quill-editor-playground-forked-mcz33c?file=%2Fsrc%2FApp.tsx%3A6%2C1&workspaceId=ws_XF7JbL8XCDBxF1yKmujfTX

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


  1. 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:

    import React, { useRef, useEffect, useState } from "react";
    import ReactDOM from "react-dom";
    import ReactQuill from "react-quill";
    import "react-quill/dist/quill.snow.css";
    
    // Define toolbar modules
    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"]
      ]
    };
    
    interface ShadowWrapperProps {
      children: React.ReactNode;
    }
    
    const ShadowWrapper: React.FC<ShadowWrapperProps> = ({ children }) => {
      const shadowRootRef = useRef<HTMLDivElement | null>(null);
      const [shadowRoot, setShadowRoot] = useState<ShadowRoot | null>(null);
      
      useEffect(() => {
        if (shadowRootRef.current && !shadowRoot) {
          if (!shadowRootRef.current.shadowRoot) {
            const shadow = shadowRootRef.current.attachShadow({ mode: "open" });
            setShadowRoot(shadow);
    
            // Create a container for styles
            const styleContainer = document.createElement("div");
            
            // Add Quill styles
            const quillStyles = document.createElement("link");
            quillStyles.rel = "stylesheet";
            quillStyles.href = "https://cdn.quilljs.com/1.3.6/quill.snow.css";
            styleContainer.appendChild(quillStyles);
    
            // Add custom styles to fix selection and toolbar interactions
            const customStyles = document.createElement("style");
            customStyles.textContent = `
              .ql-container {
                position: relative;
                z-index: 1;
              }
              
              .ql-toolbar {
                position: relative;
                z-index: 2;
              }
              
              .ql-editor {
                min-height: 200px;
              }
              
              /* Fix selection handling */
              ::selection {
                background: #b4d5fe;
              }
              
              /* Ensure toolbar buttons are clickable */
              .ql-toolbar button {
                pointer-events: auto;
              }
              
              /* Fix tooltip positioning */
              .ql-tooltip {
                position: absolute;
                z-index: 3;
              }
            `;
            styleContainer.appendChild(customStyles);
            
            shadow.appendChild(styleContainer);
          } else {
            setShadowRoot(shadowRootRef.current.shadowRoot);
          }
        }
      }, [shadowRoot]);
    
      // Add a container div to help with positioning and events
      return (
        <div ref={shadowRootRef} className="quill-shadow-wrapper">
          {shadowRoot && ReactDOM.createPortal(
            <div className="quill-container">
              {children}
            </div>,
            shadowRoot
          )}
        </div>
      );
    };
    
    const App = () => {
      const initialValue = `highlight this text`;
      const [value, setValue] = useState(initialValue);
      const quillRef = useRef<any>(null);
    
      // Enhanced modules configuration with custom handlers
      const enhancedModules = {
        ...modules,
        toolbar: {
          ...modules.toolbar,
          handlers: {
            // Add custom handlers for toolbar actions if needed
          }
        }
      };
    
      // Handle selection changes
      const handleSelectionChange = (range: any, source: string, editor: any) => {
        if (range) {
          console.log('Selection:', range.index, range.length);
        }
      };
    
      return (
        <>
          <ShadowWrapper>
            <ReactQuill
              ref={quillRef}
              modules={enhancedModules}
              value={value}
              theme="snow"
              onChange={setValue}
              onChangeSelection={handleSelectionChange}
              placeholder="Content goes here..."
            />
          </ShadowWrapper>
          <div style={{ width: "100%" }}>
            <pre>{value}</pre>
          </div>
        </>
      );
    };
    
    export default App;
    

    To use this:

    • Replace your current code with the updated version
    • Make sure all required dependencies are installed
    • You may need to adjust the z-index values based on your specific needs

    A few additional tips:

    • If you still experience issues with specific toolbar actions, you can add custom handlers in the enhancedModules.toolbar.handlers object
    • You might want to add additional CSS rules in the customStyles section if you need to match your application’s styling
    • The pointer-events: auto on toolbar buttons is crucial for proper click handling

    Hope this helps

    Login or Signup to reply.
  2. If you have control over the content of the buttons, this should be rather straightforward to workaround.

    Your code might look like this:

    <Button 
      onMouseDown={evt => evt.preventDefault()} 
      onClick={makeBold}
    >
      Bold
    </Button>
    

    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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search