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

Bug: Binding to webcomponent shadow root fails #2119

Closed
yuzuquats opened this issue May 9, 2022 · 20 comments
Closed

Bug: Binding to webcomponent shadow root fails #2119

yuzuquats opened this issue May 9, 2022 · 20 comments
Projects

Comments

@yuzuquats
Copy link

yuzuquats commented May 9, 2022

I'm working on converting lexical into an es6 module + webcomponent. I've been able to successfully bundle an es6 module (using webpack). Using it as a top-level component in the HTML document root works, but including it as a webcomponent doesn't.

Lexical version: 0.25

Steps To Reproduce

  1. import a bundled lexical.bundle.min.js file
  2. bind Lexical to an editor in the root DOM (ie. document.getElementById("editor")), and observe that it works
  3. bind Lexical to an editor in the shadow DOM (this.shadowRoot.querySelector("#editor"), and observe that it doesn't work.
  • The shadow dom's editor does receive attributes for data-lexical-editor=true among others
  • The shadow dom does not receive key inputs

Link to code example:

Screen.Recording.2022-05-09.at.12.53.07.PM.mov

https://github.com/yuzuquats/lexical-web-component

Any help would be appreciated! I'm fairly new to npm so if there's anything obvious I'm missing I'd love to learn!

@trueadm
Copy link
Collaborator

trueadm commented May 9, 2022

That's strange. Lexical attaches the event listeners directly to the content editable (apart from selection, which is document level). Do you maybe have a codesandbox I could try?

@yuzuquats
Copy link
Author

let me see if i can put together a code sandbox - the github should have a couple of quick steps to repro in the meantime.

In any case, I've done some digging and it seems like the root cause might be because we're using window.event. Specifically, I can confirm that key events (onBeforeInput) is called but the selection is null - thus early returning. In fact, it seems like selection is not actually being instantiated within the beginUpdate method. And this seems to be because the way we're grabbing the current active event is through window.event which is null/inaccurate for shadow trees.

I'm not sure if this is the only issue with webcomponent but it's the immediate reason why insertTextInput events are being swallowed.

@trueadm
Copy link
Collaborator

trueadm commented May 9, 2022

I had no idea that window.event is swallowed in shadow DOM. That's definitely problematic, and also concerning as we lean on it so much. Let me think about this.

@trueadm
Copy link
Collaborator

trueadm commented May 10, 2022

I attempted to fix this, can you maybe let me know if it works for you? #2126

@yuzuquats
Copy link
Author

yup! i'll take a look

@yuzuquats
Copy link
Author

const useDOMSelection =
    !getIsProcesssingMutations() &&
    (isSelectionChange ||
      eventType === "beforeinput" ||
      eventType === "compositionstart" ||
      eventType === "compositionend" ||
      (eventType === "click" && window.event.detail === 3) ||
      eventType == null);

useDOMSelection is still false here since getIsProcesssingMutations returns true

@yuzuquats
Copy link
Author

I dove a little bit deeper:

  • Even when useDOMSelection is true, lexical isn't able to resolve selection points for us (internalResolveSelectionPoints).
  • This appears to be a consequence of the window.getSelection() returning seemingly inaccurate values for anchorDOM or focusDOM:
    • anchorDOM and focusDOM are being bound to the parent of the webcomponent and not the webcomponent or the webcomponent's internal contenteditable.
  • As a result, the following lines fail:
    • rootElement.contains(anchorDOM)
    • rootElement.contains(focusDOM)
    • getNearestEditorFromDOMNode(anchorDOM) == editor

See 8ceb55a528cd7cba21987b68a92f9f80ba54cbb9

Next, I removed Lexical completely from the webcomponent and I can confirm that "onfocus" does indeed set all window.selection nodes to the container: 6933c895a04ea0b28aa9382b3f8c2cc25c585986

@yuzuquats
Copy link
Author

FYI: seems like this might be a common problem and selection APIs across shadow DOM boundaries are being worked on

In the meantime, I've opted to work around webcomponents with a simple class shim. Feel free to close this out unless you think there are better ways to work around this!

@trueadm
Copy link
Collaborator

trueadm commented May 11, 2022

Thanks for the comprehensive report and digging into the logic on this!

@zurfyx zurfyx added this to Backlog in Lexical Jun 14, 2022
@zurfyx zurfyx moved this from Backlog to Future in Lexical Jun 14, 2022
@thegreatercurve
Copy link
Contributor

I'm going to close this task for the moment as the core ShadowDOM selection APIs are still being actively worked on. We the progress there. Plus, it'll be a while until we can add these new APIs into Lexical, once they are stabilised:

@johnstonmatt
Copy link

Support for Shadow DOM would be so awesome, building an editor as web component has a lot of potential!

@noobG
Copy link

noobG commented Oct 21, 2023

Super need this as well

@klauss194
Copy link

+1 Guys can I help ? Did anyone succeed into this ?

@xantinium
Copy link

+1

@xantinium
Copy link

xantinium commented Feb 13, 2024

I guess, as a workaround, we can use slots from shadow dom api.

I mean, make some simple component:

function Editable() {
    const [editor] = useLexicalComposerContext();

    useEffect(() => {
        const node: HTMLElement | null = document.querySelector('[slot="editable-div"]');

        editor.setRootElement(node);
    }, [editor]);

    return <slot name="editable-div" />;
}

and then create a <div> with contenteditable and slot attributes outside of shadow root:

<div contenteditable slot="editable-div"><div>

@johnstonmatt
Copy link

@xantinium thanks for sharing!

My use case would benefit hugely from the isolated nature of a shadow root, but I'll take what I can get in the meantime 🙏

@shaileshiyer
Copy link

shaileshiyer commented Feb 19, 2024

Hopefully this might help others too and is just a patch for now.
After, a lot of trial and error trying to make it work. Since my research project involved using text Editors. And the people on whose work I am basing my thesis on. They used lit-html and they too had issues trying to get their text editor working with the shadow DOM.
https://github.com/PAIR-code/wordcraft

https://github.com/PAIR-code/wordcraft/blob/main/app/core/services/text_editor_service.ts

They ended up patching the current node in focus and the window.getSelection() which is what Lexical also seems to be using. So this solution works as a patch for now just call the getPatchSelection() before registering the different plugins and it should work even for components within the shadow DOM.

/**
 * We need to hack the window.getSelection method to use the shadow DOM,
 * since the mobiledoc editor internals need to get the selection to detect
 * cursor changes. First, we walk down into the shadow DOM to find the
 * actual focused element. Then, we get the root node of the active element
 * (either the shadow root or the document itself) and call that root's
 * getSelection method.
 */
export function patchGetSelection() {
    const oldGetSelection = window.getSelection.bind(window);
    window.getSelection = (useOld: boolean = false) => {
      const activeElement = findActiveElementWithinShadow();
      const shadowRootOrDocument: ShadowRoot | Document = activeElement
        ? (activeElement.getRootNode() as ShadowRoot | Document)
        : document;
      const selection = (shadowRootOrDocument as any).getSelection();
  
      if (!selection || useOld) return oldGetSelection();
      return selection;
    };
  }
  
  /**
   * Recursively walks down the DOM tree to find the active element within any
   * shadow DOM that it might be contained in.
   */
function findActiveElementWithinShadow(
    element: Element | null = document.activeElement
  ): Element | null {
    if (element?.shadowRoot) {
      return findActiveElementWithinShadow(element.shadowRoot.activeElement);
    }
    return element;
  }

All credits go to the wordcraft team contributers Andy Coen and G. Hussain Chinoy

@anasteele206
Copy link

anasteele206 commented May 30, 2024

FYI, the above fix only works in chrome. https://stackoverflow.com/questions/62054839/shadowroot-getselection

@adrian-bobev
Copy link

@thegreatercurve are there any updates on this issue?
I see it is closed but by reading all the comments I am still not sure what is the recommended way to use lexical in shadow dom.

For sure we can use the workaround provided by @shaileshiyer or put the contenteditable div in the light dom, but it will be good if we have support in shadow dom.

@au5ton
Copy link

au5ton commented Jun 4, 2024

@adrian-bobev See: #2119 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
No open projects
Lexical
Future
Development

Successfully merging a pull request may close this issue.