Skip to content

Fix renderWebChat() ignoring clicks and typing inside a shadow root on React 16 - #5982

Draft
Eugene (OEvgeny) wants to merge 4 commits into
mainfrom
OEvgeny/fix-react-16-shadow-dom
Draft

Eugene (OEvgeny) wants to merge 4 commits into
mainfrom
OEvgeny/fix-react-16-shadow-dom

Conversation

@OEvgeny

@OEvgeny Eugene (OEvgeny) commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Related to #5315

Changelog Entry

Fixed

  • Fixed renderWebChat() ignoring clicks and typing inside a shadow root on React 16, related to #5315, in PR #5982, by @OEvgeny

Description

On React 16, the default in webchat.js, Web Chat in an element inside a shadow root ignored clicks and keystrokes: React 16 listens on the document, which sees the shadow host as the target. renderWebChat() now adds a hidden portal with React 16's event handlers into the shadow root, so React 16 listens there too.

Design

  • A portal instead of react-shadow-dom-retarget-events, which calls handlers with native events, or a shadow root of Web Chat's own, which the host's stylesRoot and CSS would not reach.
  • The bundled React DOM stays 16.8.6, the minimum supported version.
  • Not covered: <ReactWebChat> rendered by the host on React 16.

Specific Changes

  • renderWebChat.tsx: the portal, on React 16 with a container inside a shadow root

  • react16EventListenerProps.ts: every React 16 event but touch and wheel (scroll-blocking), unit-tested against React DOM 16.8.6

  • SuggestedActions.tsx: pass stylesRoot to the carousel, unstyled in a shadow root

  • customElement/shadowRoot.interact.html: React 16, type and send, click a suggested action

  • shadowRoot.interact.renderIntoShadowRoot.html, shadowRoot.interact.react18.html: same steps, no portal

  • I have added tests and executed them locally

  • I have added .changelog/<PR#>-<slug>.md

  • I have updated documentation

Review Checklist

This section is for contributors to review your work.

  • Accessibility reviewed (tab order, content readability, alt text, color contrast)
  • Browser and platform compatibilities reviewed
  • CSS styles reviewed (minimal rules, no z-index)
  • Documents reviewed (docs, samples, live demo)
  • Internationalization reviewed (strings, unit formatting)
  • package.json and package-lock.json reviewed
  • Security reviewed (no data URIs, check for nonce leak)
  • Tests reviewed (coverage, legitimacy)

Eugene Olonov added 4 commits October 29, 2026 11:02
React 16 listens to events on the document unless the container is a
document or a document fragment. Inside a shadow root, the document gets
events retargeted to the shadow host, so the bundled React DOM 16.8.6
ignored every click and keystroke. renderWebChat() now renders a hidden
portal into the shadow root with a no-op handler for every React 16
event, so React 16 also listens on the shadow root.
The carousel of suggested actions put its react-film styles into the
document head, so suggested actions inside a shadow root lost their
carousel layout and showed a list bullet.

// React 16 to 18, and Preact. Also the IIFE build, which bundles React DOM 16.8.6 unless `window.ReactDOM` is set.
ReactDOM.render(children, elementOrRoot);
ReactDOM.render(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if the passed node is nested inside the shadow root instead of shadow root being passed itself?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is the case the portal is for: getRootNode() returns the closest shadow root at any depth, so the portal goes into that shadow root and React 16 listens there; shadowRoot.interact.html renders into a <main> inside the shadow root. When the shadow root itself is passed, rootNode === elementOrRoot and there is no portal, because React 16 already listens on a shadow root container (shadowRoot.interact.renderIntoShadowRoot.html).

@@ -0,0 +1,102 @@
// Every event prop of React 16, from its event plugins, except the ones that block scrolling (see below).
const REACT_16_EVENT_PROP_NAMES = Object.freeze([

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any other way of handling this?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The alternatives were worse: reading the list from React DOM's private __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Events at runtime (the spec uses it only to check this list, which matches React DOM 16.8.6 to 16.14), listing only the handlers Web Chat uses (host middleware can use others), a re-dispatch shim like react-shadow-dom-retarget-events (handlers get native events, onChange fired twice for one keystroke, onFocus never fired), or rendering into the shadow root itself (React 16 removes the container's existing children on first render). Bundling React DOM 17+ in webchat.js would also fix it, but changes the 16.8.6 minimum.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking out loud.

What if we mount into the empty element, the empty root then portal the rest into Shadow DOM.

It's okay to not allow instancing inside Shadow DOM directly as some our tests do, for react 16

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried it on React 16.8.6 (3 runs each, React 18 as control): a portal into an element inside the shadow root still gets no events, because React 16 listens on the portal container's ownerDocument unless the container is a fragment (ensureListeningTo in react-dom 16.8.6), so only a portal into the ShadowRoot itself works. That one needs no event list, but Web Chat then becomes a direct child of the shadow root and the host's element and its CSS no longer wrap it (61 px tall instead of the element's 120 px in the probe). The list keeps Web Chat inside the host's element, so I would keep it, unless you prefer requiring that layout for React 16.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant