Event delegation order

I have a setup where there is a custom behavior attached to a container, say, list. I add a mousedown event listener to the container to capture an interaction which does a “reorder in place”. This is done using addEventListener on the container. The container in turn contains elements inside of which multiple buttons / widgets controlled by Stimulus are present. So the structure is roughly this:

<div class="list"> 
  <div class="list-item"><!-- this item become draggable=true after the mousedown behavior on "list" completes -->
      <a href="..." data-controller="magic_widget" data-action=""mousedown->magic_widget#activate">
         ...
      </a>
  </div>
</div>

The mousedown handler on “list” is configured in such a way that it will look for closest with the class of “list” and will engage the reorderable behavior only if one is found. The odd thing is that the “mousedown” action on the anchor element in this case is not detected. So it seems as if the Stimulus event listener is installed on, say, document and the event is not bubbling from the element, but instead is handled after it reaches the listener on “list”.

Is there anything peculiar I should be aware of regarding where event listeners are added in Stimulus for the actions?

Hey @julik!

This doesn’t sound like anything Stimulus specific, it handles listeners just as you’d expect vanilla JS to.

Can you share some more of your code, such as the event listener on List? It’s possible it’s swallowing the event somehow.

A few questions:

  1. Is the JS on the list container not handled via a Stimulus controller? (not that it should matter, just curious)
  2. If you remove the event listener from list, does the mouse down event on anchor trigger as expected?
  3. Are you setting any options on your event listener or just the event name and function?
  4. Is the Stimulus controller actually participating in the reordering process or is that all handled on the list? (just curious, not likely related)

Can you share an example somewhere?

Hi @thijs :slight_smile: I’ll have a second look on which event handlers I have attached in more detail and post the structure here.

@welearnednothing

  1. Is the JS on the list container not handled via a Stimulus controller? (not that it should matter, just curious)

No, it isn’t - it is separately attached imperatively.

  1. If you remove the event listener from list , does the mouse down event on anchor trigger as expected?

That I need to investigate. It seems there is a tricky drag-and-drop dropzone DOM element which is overlaying the element on which I attach the Stimulus action - I suspect that is more likely the issue (so it is a hit test problem, not an event delegation issue)

  1. Are you setting any options on your event listener or just the event name and function?

No options, just the event name and function - so no bubble reconfiguration and no capture.

  1. Is the Stimulus controller actually participating in the reordering process or is that all handled on the list? (just curious, not likely related)

The Stimulus controller not receiving the mousedown is not participating in the reorder

I haven’t tried to duplicate the issue but I noticed som oddities on your action element:

<a data-action=""mousedown->magic_widget#activate">

Stimulus controller identifiers uses kebab case where you are using an underscore notation. Also, you have an extra double quote at the beginning of your data-action value. Maybe fixing those issues will resolve it:

<a data-action="mousedown->magic-widget#activate">

Coming back to this after some time but it looks like I did manage to find what was going on. In the layout the elements which were being nested were actually in a “card”-like UI block. The card itself was an “a” element, but also the menu activation widget inside the card was an “a” element. It seems that something very weird would happen to it when the browser would convert the HTML into a DOM - Chrome would actually restructure my DOM and place the element inside of which the “inner” anchor element was next to the card element - so it would become a sibling. That, in turn, would lead to all sorts of things breaking.

To circumvent the problem I had to replace the “a” element on the card itself with a “fake-link” activation using JavaScript. For now this will work as we are not subject to hard accessibility requirements, but in the future we will need to revisit this.

So event delegation was not the issue - the restructured DOM was.

Thanks everyone for your suggestions!

We solved the same problem in Basecamp by wrapping inner <a>s in <object>s (to create a nested browsing context). Example: https://codepen.io/javan/pen/oNxZrrj :grimacing:

2 Likes

Yup, that makes perfect sense. You cannot nest an A inside another A. The browser does its best to turn tag-soup into a meaningful DOM, but it has to guess when you go that far off the rails. Each different browser may guess in their own special way, since there’s no legal way to interpret that broken structure. It’s like your phone keyboard auto-correct!

Walter

And actually… that’s not all. What also turned out to be the case was that we would have a :before element on the entire card. We would then have this element constantly present but be invisible (!). When a certain (also mouse-driven) interaction would happen in the card this :before element would become semi-transparent instead, it would become visibility: visible and it would also get pointer-events: none. This was done to provide a highlight when the card is receiving mouse interactions :man_facepalming: as well as to deactivate text selection.

So, in summary:

  • Pseudo elements which would jeopardize hit-tests, with pointer-events altering CSS on them
  • HTML structure that led to restructuring of the DOM which made elements (and controllers) not nest correctly

@javan thanks for the tip with object!

1 Like