Why does event.stopPropagation() not prevent a <label> from checking its checkbox on clicks from children elements?

Viewed 369

There are similar questions to this that have been asked on SO, but they're either only concerned with resolving their specific implementation, or they don't clearly ask this question. Additionally, none of the answers actually address the issue.

Given the following example:

document.querySelector('span').addEventListener('click', function(event) {
  event.stopPropagation();
});
<label>
    <input type='checkbox'/>
    <span>Click Me!</span>
</label>

Why is this insufficient for preventing the checkbox from being checked when clicking the span?

Instead, it is necessary to call event.preventDefault() to prevent the label from checking (or unchecking) the checkbox. This seems counterintuitive. In this example, this implies that it's the clicking of the span itself that causes the checkbox toggling, not the click that then bubbles up to the label.

1 Answers

This is not very well specified and is actually platform specific (even though all major platforms/vendors combinations seem to act similarly here). specs - temp link

what [the label element's] activation behavior might be, if anything, should match the platform's label behavior

The de-facto default behavior of the label is to dispatch the click to its linked <input> element ("labeled control"), probably from the legacy-pre-activation behavior, which is triggered before the event is actually dispatched through its path (step 12 here, temp-link).

So even if the event's propagation is stopped at the document's layer in capture phase (first target in the DOM), the default behavior of triggering a new click event on the <input> will happen:

let block_at_capture = false;
const span = document.querySelector("span");
const label = document.querySelector("label");
const input = document.querySelector("input");

span.onclick = (evt) => {
  console.log("clicked the span");
};

input.onclick = (evt) => {
  console.log("clicked the input");
};

label.addEventListener( "click", (evt) => {
  console.log("clicked the label in capturing phase");
}, true );

label.addEventListener( "click", (evt) => {
  console.log("clicked the label in bubbling phase");
}, false );

document.addEventListener( "click", (evt) => {
  if( evt.target !== span ) { return; } 
  if( block_at_capture ) {
    console.log("blocking at document capture");
    evt.stopPropagation();
  }
  block_at_capture = !block_at_capture;
}, true );
<input id="inp" type="checkbox">
<label for="inp"><span>click me</span></label>

That is, unless the default behavior is prevented or the link is broken while dispatching the event...

Note that the same happens for the checkbox directly, even without a <label>:

// The click event won't get called
document.querySelector("input").onclick = (evt) => 
  console.log("clicked on input");
// even though the default behavior ("change") will still happen
document.querySelector("input").onchange = (evt) =>
  console.log("changed input"); // will get called

document.addEventListener("click",
  (evt) => evt.stopPropagation(),
  true
);
<input type="checkbox">

Related