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

Shadow DOM support for widgets #434

Open
krassowski opened this issue Oct 9, 2022 · 1 comment · May be fixed by #517
Open

Shadow DOM support for widgets #434

krassowski opened this issue Oct 9, 2022 · 1 comment · May be fixed by #517
Labels
enhancement New feature or request performance Addresses performance

Comments

@krassowski
Copy link
Member

krassowski commented Oct 9, 2022

Problem

Lumino can be used in situations where there are hundreds of thousands of nodes, and where stylesheets from different providers are attached at the same time. Limiting the stylesheet applicability to a specific subset of nodes is possible with two approaches:

Lumino currently does not support shadow DOM in the sense that individual widgets cannot be moved to shadow DOM and there are no methods exposed allowing to attach stylesheets to specific widgets/shadow DOM roots.

Proposed Solution

Note: If you have not worked with shadow DOM before please see the details below to understand why the solution (2) wraps the shadow root into another <div>.

DOM nodes can be moved to the shadow DOM by attaching them to a transient shadow DOM root which is attached to another DOM node. There can ever be only one shadow DOM root in each DOM element:

This is allowed:

<div>
    # shadow root
        <div class="element-in-shadow-dom"></div>
        <div class="another-element-in-shadow-dom"></div>
</div>
<div>
    # shadow root
        <div class="element-in-another-shadow-dom"></div>
</div>

This is forbidden (multiple shadow roots were removed from the specification in 2015):

<div>
    # shadow root
        <div class="element-in-shadow-dom"></div>
    # shadow root
        <div class="another-in-another-shadow-dom"></div>
</div>

The solutions I considered are:

  1. At widget attachment, check if node of the node of the parent widget hosts a shadow root and if it does, attaching to the shadow root instead of the parent node itself:
    -    this.parent!.node.insertBefore(widget.node, ref);
    +    (this.parent!.node.shadowRoot || this.parent!.node).insertBefore(widget.node, ref);
    in this scenario, the parent widget is in the shadow DOM:
    <div class="lm-Widget">    <!-- this.parent.node -->
        # shadow root    <!-- this.parent.node.shadowRoot -->
            <div class="lm-Widget"></div>    <!-- widget.node -->
            <div class="lm-Widget"></div>    <!-- anotherWidget.node -->
    </div>
  2. Separate Widget.node and Widget.attachmentNode; by default Widget.attachmentNode would be just an alias for Widget.node but when a widget is instructed to wrap itself into shadow DOM, it would point to the ShadowRoot (but wrapped into a translucent div to avoid the issue of multiple-roots)
    -    this.parent!.node.insertBefore(widget.node, ref);
    +    this.parent!.node.insertBefore(widget.attachmentNode, ref);
    in this scenario, the child widgets are in the shadow DOM:
    <div class="lm-Widget">    <!-- this.parent.node -->
        <div class="lm-translucentWrapper">    <!-- widget.attachmentNode -->
            # shadow root    <!-- widget.attachmentNode.shadowRoot -->
                <div class="lm-Widget"></div>    <!-- widget.node -->
        </div>
        <div class="lm-translucentWrapper">    <!-- anotherWidget.attachmentNode -->
            # shadow root    <!-- anotherWidget.attachmentNode.shadowRoot -->
                <div class="lm-Widget"></div>    <!-- anotherWidget.node -->
        </div>
    </div>
    the problem with this approach is that it requires rewritting CSS styles since the widget.node is no longer a direct descendant of its parent (lm-translucentWrapper is).
  3. A variation of (2):
     <div class="lm-Widget">    <!-- this.parent.node -->
         <div class="lm-Widget">    <!-- widget.node -->
             # shadow root    <!-- widget.node.shadowRoot -->
                 <div class="lm-contentWrapper"></div>
         </div>
         <div class="lm-Widget">    <!-- anotherWidget.node -->
             # shadow root    <!-- anotherWidget.node.shadowRoot -->
                 <div class="lm-contentWrapper"></div>
          </div>
     </div>
  4. Using Proxy to create Frankenstein-kind hybrid of ShadowRoot and HTMLElement and keeping node without changes: this does not work because native code in insertBefore and friends does not accept non-native nodes (strict class checks are performed at runtime so even if the proxy quacks like a perfect node, it will be rejected).

Then we could either:

  • a) allow each widget to be moved into shadow DOM via a new option in constructor, or
  • b) provide a subclass of Widget say ShadowWidget which would put its node into shadow DOM.

Further we would need to be consider how to handle CSS stylesheets. Widgets could have a method like:

class Widget {
  /**
  * Adopt a constructed CSS stylesheet for the use in shadow DOM.
  * Is a no-op if the widget is not a shadow DOM root.
  * @returns whether the stylesheet was successfully adopted.
  */
  adoptStyleSheet(stylesheet: CSSStyleSheet): boolean {
    if (!this.options.shadowDOM) { return false; }   // not needed in (b)
    this.node.shadowRoot.adoptedStyleSheets.push(stylesheet);
    return true;
  }
}

I am slightly leaning towards 2a as that would allow us to enable shadow DOM downstream by changing the options without a need to duplicate class definitions. The separation of node and attachmentNode could be introduced in Lumino 2.0 whether we decide to proceed with the shadow DOM implementation or not.

Additional context

I was able to set it up in JupyterLab and created a code snipped to copy all stylesheets for adoption in widgets, thus limiting the performance benefits to containing style changes to nodes within the shadow DOM but still using all styles; this is super preliminary and I will update if I get a chance to perform a proper benchmark:

  • I saw very little performance benefits when moving MainAreaWidget to shadow DOM
  • I saw more performance benefits when moving the entire DockLayout to shadow DOM

My confidence in the results above is low. I expect that gains will differ depending on the browser.

@krassowski krassowski added enhancement New feature or request status:Needs Triage performance Addresses performance labels Oct 9, 2022
@krassowski
Copy link
Member Author

One place where shadow DOM wrappers could be very useful (performance aside) are CellOutput widgets in JupyterLab; this would allow outputs to add any CSS stylesheet they want without breaking the JupyterLab styling (on the other hand it would prevent them from modifying the theme which is probably a good thing security-wise).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request performance Addresses performance
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant