dom
dom copied to clipboard
Proposal: Enable Custom Form Controls to (Optionally) Conceal Internal/Irrelevant Events
What problem are you trying to solve?
Currently, there is no way for a Custom Form Control composed of primitive form controls to prevent "the outside world" from receiving undesirable/noisy events from said primitives. Consider the following example:
Use Case: Comboboxes (where only valid values are accepted)
Many combobox components in the wild effectively act as stylable <select> elements whose restricted values can be conveniently searched via a textbox. In such cases, the combobox's value is only permitted to be empty or a valid option value -- just as in the <select>'s case.
From an event dispatching standpoint, the <select> element only dispatches input/change events when a user interaction updates the value. Similarly, many of these comboboxes desire to only dispatch an input/change event when a new valid value is selected. And this is where things get tricky...
Typically, searchable comboboxes are implemented with a native <input> (or some other contenteditable element). But these emit input events on every keystroke -- creating undesirable noise for delegated event listeners which are trying to watch for updates to the Combobox Value only. Particularly, the delegated event listeners will see input events whenever the textbox is typed into, and whenever a valid option is selected (in which case the Combobox will dispatch its own custom input event). But ideally, these events should only be detected/observed in the latter case to avoid noise/confusion.
There is currently no way to prevent this undesirable behavior. Even if someone tried to use a Shadow DOM, both open and closed Shadow DOMs will propagate input events to the Light DOM because input is composed. But closed Shadow DOMs will yield a slightly worse UX because they make it impossible to know whether the input came from the Combobox itself or the inner <input>. (I guess technically, someone could check event.isTrusted, but that requires knowledge of implementation details.)
(Sidenote for Clarity: the Combobox value should not be updated until the user selects a valid option. If the user bails out while searching, the textfield should revert its text content back to the last valid value of the Combobox, and the Combobox should not emit an input/change event. This prevents bad data from being sent to the server.)
Depending on how a <checkbox-group> component with minimum-selected items would be implemented, it might run into similar issues.
What solutions exist today?
There technically aren't any real/genuine solutions to this problem today. Probably the best thing that developers can do is tell users how to filter out the events that they don't want in their delegated event listeners. However, this will almost certainly entail exposing implementation details, which is not optimal. For example, a developer could say, "Check for event.isTrusted === false or event instanceof CustomEvent to distinguish between the correct input/change events."
Perhaps another option is for the developer to create very clever listeners for keydown (composed textboxes), click (composed checkboxes), and the like which could call event.preventDefault() as needed. But it's possible that this approach could get out of hand quickly. And it might not cover all use cases.
How would you solve it?
I've noticed that a number of new ShadowDOM-related attributes/options have appeared in the spec to resolve problems like these. Perhaps a new one could be created for this scenario? Something like:
this.attachShadow({ mode: "closed", concealsEvents: true });
or
<template shadowrootmode="closed" shadowrootconcealsevents>
Another alternative is perhaps to enable ElementInternals to specify that it conceals its own events through a togglable property.
const internals = this.attachInternals();
internals.concealsEvents = true;
Could also be a static property, perhaps, like formAssociated and observedAttributes.
class ComboboxField extends HTMLElement {
static get concealedEvents() {
return ["input", "change"];
}
// ...
}
The main idea is that in some way, the Web Component would be able to dictate that it conceals its events (or a specific set of events) within the class declaration.
Anything else?
After writing up this issue, I'm starting to conclude that the issue may surround Custom Form Controls and the events which they want to emit more than it does the Shadow DOM. In simple terms, the issue is that a Custom Form Control may wish to take full responsibility over a specific set of events, because the events are irrelevant/noisy if they bubble up from any children (e.g., input/change in the combobox scenario). To that end, I'd probably prefer the static concealedEvents approach, with an ability to specify some kind of "all" option as needed.
Whether it's related to this problem or not, I feel like I can still make this comparison: Developers can't listen to click events on <option> elements in a <select> element -- nor do they need to. All that they really need to listen for is input/change from the <select> element itself. A sophisticated Custom Form Control leveraging "subcomponents"/"primitive controls" may require similar behavior. On that note... Perhaps the greater issue is needing the ability to conceal all events from specific sub-elements (as with the <select>/<option> combination)?
I imagine if we did anything here we wouldn't want it limited to custom form controls, but be some kind of more general filter for composed events at the shadow boundary.
cc @jakearchibald @smaug---- @domenic
I think having input be composed was a mistake. command looks set to be the same as discussion in https://github.com/whatwg/html/issues/11148 has stalled.
Having pointer & key events compose makes sense, and changing that would create similar issues as iframes, which create 'black holes' for pointer events. I think focus and blur makes sense composed too, but I'm less sure there.
The attachShadow and <template> additions make sense to me. I don't think the static property is the right approach, as it's a feature of the shadow root rather than the custom element.
Ideally the API should make it easy to 'conceal' input, and possibly command too if we don't get to make that change in time. So perhaps that would be the behaviour if true is the value, but allowing an array of event names seems ok too, eg if you want more control over focus and blur.
The change event isn't composed.
Ideally the API should make it easy to 'conceal' input, and possibly command too if we don't get to make that change in time. So perhaps that would be the behaviour if true is the value, but allowing an array of event names seems ok too, eg if you want more control over focus and blur.
Yeah thinking about it a little more, I think an array of values might be ideal for people who only want to conceal specific events. And perhaps true/"all" could still be allowed to represent concealing everything? Providing a more general solution like this out of the box might help prevent issues similar to this one from being brought up in the future.
If it's possible to group events into categories, then another option might be to have pre-defined strings that capture certain groups of events. For example concealEvents: ["form"] could conceal things like input/paste, etc. I'm not sure how maintainable that concept would be, though. And I imagine most people would know the specific events they'd want to block.