Shadow tree navigation doesn’t go through Ember router

Any navigation triggered within a shadow tree will cause a regular navigation (i.e. a full reload) instead of going through Ember’s router if the source (e.g. a HTML a element) is located in a shadow tree.

Discourse almost has this covered in these files:

  • app/assets/javascripts/discourse/initializers/click-interceptor.js.es6
  • app/assets/javascripts/discourse/lib/intercept-click.js.es6

In order to fix this in Discourse, we would need to replace jQuery’s event listener as it only listens for clicks on anchor elements. The events from inside the shadow DOM; however, will have the shadow host as a target. This can’t even be an anchor element; hence, this event listener never catches these clicks. The following code would need to listen on anchor elements and any shadow host elements. In general, this would mean listening on all elements, but only proceed with a) anchor elements or b) elements that are a shadow host. For the latter, one can walk their composed path as shown in my workaround below.

$("#main").on("click.discourse", "a", interceptClick);

I understand if that’s something you don’t even want to fix right now as Discourse doesn’t use shadow DOM. I’m putting this here for documentation and users who want to use shadow DOM inside Discourse.

Plugin workaround

I have small workaround active in my plugin. I listen for all click events on the shadow host …

import DiscourseURL from 'discourse/lib/url';

// …

shadowHost.addEventListener('click', interceptClick);

… and check if in their composed path, there is an anchor element.

function interceptClick(event) {
  for (const target of event.composedPath()) {
    if (target.tagName === 'A' && target.href !== '') {

This is pretty similar to what Discourse does in the files mentioned above.

(If you don’t consider this a bug, I change the category to dev.)


Any thoughts on this @eviltrout or @sam?

I have not yet used a shadow DOM so this is all a bit foreign to me. Therefore I am not sure if this is the best approach but it certainly seems to work and thank you for doing the leg work.

I think we should leave this here for now and see how many others benefit and/or offer feedback, and eventually we can add a stable API to discourse core to handle this.