dom
dom copied to clipboard
Custom elements need a way to provide their own preventable default event handlers
I'm adding a key handler to a behaviour that a consumer adds to an element to virtualise scrolling. The handler intercepts the keys that normally scroll the element and adjusts the scroll value in certain situations. However, a consumer is implementing this as a list and wants to handle the arrow keys to move through the list. So, the browser's action is to scroll by a certain amount (browser default), the custom element wants to scroll by a different amount (component default), and the consumer wants to decide not to scroll if scrolling isn't needed (prevent default).
This is tricky to implement with the current tools available, because events can be caught anywhere in the tree, and stopPropagation()
or stopImmediatePropagation()
make things harder. With addEventListener()
, the consumer would be forced to handle at the capturing phase and call stopPropagation()
instead of handling the event naturally.
I don't have a concrete proposal, but I was thinking of some way to override the default action when your custom element is in the event path. Let's say, for example, this functionality comes from ElementInternals
in the form of addDefaultHandler(event, fn)
. These handlers could be executed, in order, after the bubbling phase, wiith the browser's default handler executed last. The code might look something like this:
class MyElement extends HTMLElement {
#internals = this.attachInternals();
constructor() {
super();
this.#internals.addDefaultHandler("keydown", this.#handleKeydown);
}
#handleKeydown = (event) => {
// Perform our default action
doSomethingWith(event);
// Stop the chain of default actions (including the browser's)
event.preventDefault();
}
}
With this approach, you could guarantee that your handler will run after the consumer's event listener(s), unless they call event.preventDefault()
. It would even run if they stopped the event during the capturing phase. It would work just like a browser default handler.
What if the consumer adds the same handler? It seems either way you need to negotiate some kind of protocol.
Do you mean if they add the same callback function or if they bind to the "deferred phase" for the same event? I presume it's the latter, but just in case, I would expect the former to just ignore the call since the callback already exists within the set of callbacks attached to that event for the deferred phase. In the case of the consumer binding their own functions to an event in the deferred phase, for which a behaviour has already attached an event, I guess it would call the callbacks in the order that they were added, as is the case for the capturing and bubbling phases.
I did almost call out this potential issue in my original post, but I'm not sure if it's really an issue, and that we couldn't just make it clear in documentation that the deferred phase isn't intended for the majority of consumer event handlers. Maybe that wouldn't stop it from being misused, though.
Sorry, I meant if they also add a deferred handler. And I mostly meant to point out that you always need to cooperate with each other to some extent and figure out how that works as there are no real guarantees the platform can give at this level.
Yeah, I understand that, I probably should have made it clearer from the start that the proposal is not looking for such guarantees. This is really just a proposal for making it easier for the component to do its thing (probably) after everything else, and in particular make that cooperation more natural by allowing the consumer to use the familiar preventDefault()
to tell a component not to do whatever it normally does.
After running into the problem I described, I came back here but realised that my original proposal wasn't particularly eloquent. So, I've tried to make this more of a discussion where a more concrete proposal might form naturally. It makes the comments preceding this one make less sense, but I thought it would be bad etiquette to start a new discussion for the same thing. I'm happy to do that, if it's preferred.