BetterTyped/react-zoom-pan-pinch

Support Use Under Shadow DOM

Opened this issue · 2 comments

Is your feature request related to a problem?

I find myself embedding react apps into other pages frequently. The rest of the page is developed in some other technology (like Jekyll, Shopify, or Drupal) and I want my react app to really just be a component within the existing page.

This is best done using the ShadowDOM. I embed the root of my react app into a shadow DOM root. Then, the styling that exists on the rest of the page won't contaminate my react app. However, all the different libraries and components I'm using (like react-zoom-pan-pinch) have to be made with this possibility in mind. If they assume they will be connected to the DOM with 'Document' as the eventual root and use JS events or CSS in this fashion, they can break. This is exactly what is happening for react-zoom-pan-pinch.

Two things break if you use it under a ShadowDOM:

  • Styles are not applied correctly: They are attached to the root document instead of the shadow root so the TransformWrapper and TransformComponent do not receive their proper styles.
  • Panning does not work: the events for these will always show only the host element for the shadowDOM as their target and this means isPanningStartAllowed will fail because isWrapperChild always comes back false.

Describe the solution you'd like

It would mostly just be nice to not have to hack around this limitation! :-) But I don't know how many other people would actually benefit from this. Here's the solution I thought of:

  • CSS should be injected as a child of the ShadowDOM root, not the document root. This may be tricky as near as I can tell, it is rollup that is putting it in the document root. I have no idea if rollup allows you to control this behavior.
  • Careful consideration of which events MUST be on the document root and which ones should be on the shadowDOM root. I think only the mousemove event truly needs to be global (and possibly mouseleave). The rest can be on the ShadowDOM root instead.

Here's my own quick workaround I found for the events:

 initializeWindowEvents = (): void => {
    const passive = makePassiveEventOption();
    const currentDocument = this.wrapperComponent?.getRootNode() as HTMLElement;
    const pageDocument = this.wrapperComponent?.ownerDocument;
    const currentWindow = pageDocument?.defaultView;

    // Panning on window to allow panning when mouse is out of component wrapper
    currentDocument?.addEventListener("mousedown", this.onPanningStart, passive);
    currentDocument?.addEventListener("mouseup", this.onPanningStop, passive);
    currentDocument?.addEventListener("mouseleave", this.clearPanning, passive);
    document?.addEventListener("mouseleave", this.clearPanning, passive);

    currentWindow?.addEventListener("mousemove", this.onPanning, passive);
    currentWindow?.addEventListener("keyup", this.setKeyUnPressed, passive);
    currentWindow?.addEventListener("keydown", this.setKeyPressed, passive);
  };

  cleanupWindowEvents = (): void => {
    const passive = makePassiveEventOption();
    const currentDocument = this.wrapperComponent?.getRootNode() as HTMLElement;
    const pageDocument = this.wrapperComponent?.ownerDocument;
    const currentWindow = pageDocument?.defaultView;

    currentDocument?.removeEventListener("mousedown", this.onPanningStart, passive);
    currentDocument?.removeEventListener("mouseup", this.onPanningStop, passive);
    currentDocument?.removeEventListener("mouseleave", this.clearPanning, passive);
    document?.removeEventListener("mouseleave", this.clearPanning, passive);

    currentWindow?.removeEventListener("mousemove", this.onPanning, passive);
    currentWindow?.removeEventListener("keyup", this.setKeyUnPressed, passive);
    currentWindow?.removeEventListener("keydown", this.setKeyPressed, passive);

    handleCancelAnimation(this);
    this.observer?.disconnect();
  };

Note, this could possibly affect other features (like pinching). I have not tested that as my target platform is a desktop/laptop browser, not a touchscreen device.

Describe alternatives you've considered

One alternative I've used in situations where I didn't have access to source code was to create a relay event listener installed on the ShadowDOM root. It will take the specific events I want to handle and re-direct them to the page document root while still keeping the proper event.target in place. I don't recommend this. It gets messy!

Currently I am also manually re-applying the missing styles to the wrapper and component by defining them inside the ShadowDOM and then utilizing 'wrapperClass' and 'contentClass' to have it grab mine instead (thank you for exposing those props!)

If it were somehow possible to also expose a prop to receive an alternative 'currentDocument' element that would also be helpful. It would handle this case but also maybe others.

@Olliebrown I was looking to implement something similar as well and using on different platforms, I would like if is possible if you can show some example of using this in a shadow DOM on different platforms.

Here's a fix for anybody else who's hitting this issue: #448

My use case is pretty similar to @Olliebrown's-- I'm running a React app in a browser extension, so I'm using shadow DOM to keep it encapsulated.