Events and disabled form fields

I've been working on the web since I was a small child all the way through to the haggard old man I am to day. However, the web still continues to surprise me.

Turns out, mouse events don't fire when the pointer is over disabled form elements, except in Firefox.

Serious?

Serious. Give it a go. Move the mouse from the blue area below into the disabled button:

This calls setPointerCapture
Misc Mouse Touch Pointer

It's not like the disabled button element is stopping the event propagating either, it prevents capturing listeners on parent elements too.

The spec says:

A form control that is disabled must prevent any click events that are queued on the user interaction task source from being dispatched on the element.

The HTML Spec

…but it seems like most browsers are applying this behaviour to all mouse events. I imagine this is an ancient oddity, but given that Firefox doesn't do it, I hope other browsers can drop this weird behaviour. If not, it should be added to the spec (issue).

This kind of thing is especially painful when implementing drag & drop, as you suddenly lose the ability to track the pointer.

Touch events vs pointer events

The weird disabled-element behaviour doesn't happen with touch events. I guess this is because they're a new set of events, so they were able to break away from the legacy of mouse events.

Unfortunately, the weird behaviour is duplicated in pointer events. It's a little sad that this new set of events is taking on legacy behaviour from day one. However, it isn't explicitly part of the spec, so maybe it can change. I've filed an issue to see what can be done about it.

Capturing pointer events

With touch events, all of the touchmove and touchend events fire on the same element that received the corresponding touchstart event. Whereas with mouse/pointer events, events fire on the element under the pointer.

However, pointer events allow you to switch to the touch events model:

element.addEventListener('pointerdown', event => {
  element.setPointerCapture(event.pointerId);
});

element.addEventListener('pointerup', event => {
  element.releasePointerCapture(event.pointerId);
});

Once you do this, events will continue to fire on the captured element, even if you move across a disabled form element.

This works in Edge & Chrome, although Chrome stops firing events when you move across an iframe (issue).

Firefox supports a similar method, setCapture, which is an old non-standard IE API, but achieves roughly the same thing.

Unfortunately these methods don't fix the problem entirely. You still lose pointerdown/pointerup events that start on a disabled form element.

Workaround

input[disabled],
button[disabled] {
  pointer-events: none;
}

This means disabled form elements cannot be interacted with, but it also solves the event issue.

Additionally, for drag & drop interactions, you may want to set pointer-events: none on all iframes during the drag interaction. This means you'll continue to get move events across the whole page.

comments powered by Disqus