Click event not firing when React Component in a Shadow DOM

前端 未结 4 1433
耶瑟儿~
耶瑟儿~ 2020-12-08 10:23

I have a special case where I need to encapsulate a React Component with a Web Component. The setup seems very straight forward. Here is the React Code:

//         


        
相关标签:
4条回答
  • 2020-12-08 11:04

    Replacing this.el = this.createShadowRoot(); with this.el = document.getElementById("mountReact"); just worked. Maybe because react has a global event handler and shadow dom implies event retargeting.

    0 讨论(0)
  • 2020-12-08 11:12

    I've discovered another solution by accident. Use preact-compat instead of react. Seems to work fine in a ShadowDOM; Preact must bind to events differently?

    0 讨论(0)
  • 2020-12-08 11:16

    I fixed a bug cleaned up the code of @josephvnu's accepted answer. I published it as an npm package here: https://www.npmjs.com/package/react-shadow-dom-retarget-events

    Usage goes as follows

    Install

    yarn add react-shadow-dom-retarget-events or

    npm install react-shadow-dom-retarget-events --save

    Use

    import retargetEvents and call it on the shadowDom

    import retargetEvents from 'react-shadow-dom-retarget-events';
    
    class App extends React.Component {
      render() {
        return (
            <div onClick={() => alert('I have been clicked')}>Click me</div>
        );
      }
    }
    
    const proto = Object.create(HTMLElement.prototype, {
      attachedCallback: {
        value: function() {
          const mountPoint = document.createElement('span');
          const shadowRoot = this.createShadowRoot();
          shadowRoot.appendChild(mountPoint);
          ReactDOM.render(<App/>, mountPoint);
          retargetEvents(shadowRoot);
        }
      }
    });
    document.registerElement('my-custom-element', {prototype: proto});
    

    For reference, this is the full sourcecode of the fix https://github.com/LukasBombach/react-shadow-dom-retarget-events/blob/master/index.js

    0 讨论(0)
  • 2020-12-08 11:26

    As it turns out the Shadow DOM retargets click events and encapsulates the events in the shadow. React does not like this because they do not support Shadow DOM natively, so the event delegation is off and events are not being fired.

    What I decided to do was to rebind the event to the actual shadow container which is technically "in the light". I track the event's bubbling up using event.path and fire all the React event handlers within context up to the shadow container.

    I added a 'retargetEvents' method which binds all the possible event types to the container. It then will dispatch the correct React event by finding the "__reactInternalInstances" and seek out the respective event handler within the event scope/path.

    retargetEvents() {
        let events = ["onClick", "onContextMenu", "onDoubleClick", "onDrag", "onDragEnd", 
          "onDragEnter", "onDragExit", "onDragLeave", "onDragOver", "onDragStart", "onDrop", 
          "onMouseDown", "onMouseEnter", "onMouseLeave","onMouseMove", "onMouseOut", 
          "onMouseOver", "onMouseUp"];
    
        function dispatchEvent(event, eventType, itemProps) {
          if (itemProps[eventType]) {
            itemProps[eventType](event);
          } else if (itemProps.children && itemProps.children.forEach) {
            itemProps.children.forEach(child => {
              child.props && dispatchEvent(event, eventType, child.props);
            })
          }
        }
    
        // Compatible with v0.14 & 15
        function findReactInternal(item) {
          let instance;
          for (let key in item) {
            if (item.hasOwnProperty(key) && ~key.indexOf('_reactInternal')) {
              instance = item[key];
              break;
            } 
          }
          return instance;
        }
    
        events.forEach(eventType => {
          let transformedEventType = eventType.replace(/^on/, '').toLowerCase();
    
          this.el.addEventListener(transformedEventType, event => {
            for (let i in event.path) {
              let item = event.path[i];
    
              let internalComponent = findReactInternal(item);
              if (internalComponent
                  && internalComponent._currentElement 
                  && internalComponent._currentElement.props
              ) {
                dispatchEvent(event, eventType, internalComponent._currentElement.props);
              }
    
              if (item == this.el) break;
            }
    
          });
        });
      }
    

    I would execute the "retargetEvents" when I render the React component into the shadow DOM

    createdCallback() {
        this.el      = this.createShadowRoot();
        this.mountEl = document.createElement('div');
        this.el.appendChild(this.mountEl);
    
        document.onreadystatechange = () => {
          if (document.readyState === "complete") {
    
            ReactDOM.render(
              <Box label="Web Comp" />,
              this.mountEl
            );
    
            this.retargetEvents();
          }
        };
      }
    

    I hope this works for future versions of React. Here is the codePen of it working:

    http://codepen.io/homeslicesolutions/pen/ZOpbWb

    Thanks to @mrlew for the link which gave me the clue to how to fix this and also thanks to @Wildhoney for thinking on the same wavelengths as me =).

    0 讨论(0)
提交回复
热议问题