You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Sync developer-defined behaviors with type-attribute proposal and category model
Aligns the doc with the static behaviors API and the ActivationBehavior/EmbeddedContentBehavior category model, and folds in the lifecycle, disabled-state, cooperation, and references improvements from PR MicrosoftEdge#1343 (which this supersedes).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The [Platform-Provided Behaviors](explainer.md) proposal introduces a set of browser-supplied behaviors (e.g., `HTMLSubmitButtonBehavior`) that custom elements can opt into via `attachInternals()`. A natural extension of this model is to allow developers to define their own reusable behaviors by subclassing an `ElementBehavior`base class. This would enable patterns such as:
11
+
The [Platform-Provided Behaviors](explainer.md) proposal introduces a set of browser-supplied behaviors (e.g., `HTMLButtonBehavior`) that custom elements declare via a `static behaviors` class property. A natural extension of this model is to allow developers to define their own reusable behaviors by subclassing one of the platform's [category base classes](explainer.md#behavior-categories-and-composition). This would enable patterns such as:
12
12
13
-
-Encapsulating common interaction patterns (tooltips, drag-and-drop, keyboard shortcuts) as composable units.
13
+
-Defining new activation or embedded-content identities that the platform does not provide.
14
14
- Polyfilling upcoming platform behaviors before they ship natively.
15
-
- Composing developer-defined behaviors with platform-provided ones on the same element.
15
+
- Composing developer-defined behaviors with platform-provided ones across categories on the same element.
// Encode this.#value and draw the QR matrix onto the canvas.
48
+
}
49
+
50
+
getvalue() {
51
+
returnthis.#value;
48
52
}
49
-
setcontent(val) {
50
-
this.#content = val;
53
+
setvalue(val) {
54
+
this.#value = val;
55
+
this.#render();
51
56
}
52
57
}
53
58
```
54
59
55
-
Behaviors are classes with a `behaviorAttachedCallback` method. The behavior is instantiated and passed to `behaviors`:
60
+
Behaviors are classes with a `behaviorAttachedCallback` method. A behavior is declared as a class reference in `static behaviors`; the platform instantiates it per host, and the author reaches the instance through `internals.behaviors`:
`TooltipBehavior` could be combined with platform-provided behaviors. Here, `CustomButton` gains both tooltip functionality (show on hover/focus) and submit button semantics (click/Enter submits forms, implicit submission, `role="button"`).
86
+
`QRCodeBehavior` extends `EmbeddedContentBehavior`, and `HTMLButtonBehavior` is in the activation category. Because the two behaviors are in different categories, they compose: `QRCodeButton` renders a QR code (from `QRCodeBehavior`) and activates like a button (from `HTMLButtonBehavior`), so a click can, for example, copy the encoded link.
87
+
88
+
## Choosing a category
89
+
90
+
A developer-defined behavior extends one of the platform's [category base classes](explainer.md#behavior-categories-and-composition) (`ElementBehavior` is the abstract root):
91
+
92
+
- A behavior that activates (runs an action on click or keyboard) extends `ActivationBehavior`.
93
+
- A behavior that renders replaced content extends `EmbeddedContentBehavior`.
94
+
95
+
The platform enforces composition with the same membership check it uses for platform behaviors, so developer-defined behaviors slot into the model without a separate compatibility mechanism.
96
+
97
+
A capability that is not an activation or embedded-content identity does not map to a current category.
98
+
99
+
## Goals
100
+
101
+
- Let developers define reusable behaviors as subclasses of a platform category base (`ActivationBehavior` or `EmbeddedContentBehavior`) that attach through the same `static behaviors` declaration as platform-provided behaviors.
102
+
- Give a behavior a complete, well-defined lifecycle (attach, connect, disconnect) and a clear story for cleaning up resources it creates outside the host element.
103
+
- Encourage behaviors to be self-contained units with isolated effects.
104
+
- Reuse the platform-provided-behaviors [category composition model](explainer.md#behavior-categories-and-composition) for developer-defined behaviors.
105
+
106
+
## Non-goals
77
107
78
-
#### ElementBehavior API
108
+
- Cooperating between sibling behaviors through a shared `super` chain.
109
+
- Granting a behavior capabilities it cannot already reach from script.
79
110
80
-
For developer-defined behaviors to work, `ElementBehavior` would need to expose an API that lets web developers set accessibility defaults, receive lifecycle notifications, and reference the host element:
111
+
## ElementBehavior API
112
+
113
+
`ElementBehavior` exposes an API that lets web developers reference the host element, set accessibility and form defaults through `ElementInternals`, receive lifecycle notifications, and clean up resources. The members below mirror the subset of the custom-element lifecycle a behavior needs, without re-exposing callbacks that belong to the element itself:
81
114
82
115
| Member | Kind | Description |
83
116
|--------|------|-------------|
84
-
| `element` | Property (read-only) | Reference to the host element. |
85
-
| `behaviorAttachedCallback(internals)` | Lifecycle | Called when the behavior is attached to an element via `attachInternals()`. Receives the `ElementInternals` object. |
117
+
| `element` | Property (read-only) | The custom element the behavior is attached to. Set by the platform before `behaviorAttachedCallback` runs. |
118
+
| `behaviorAttachedCallback(internals)` | Lifecycle | Called once when the behavior is attached. Receives the host's `ElementInternals`. The place to set defaults (e.g. `internals.role`) and, for a category behavior, to override the category's hooks. |
119
+
| `behaviorConnectedCallback()` | Lifecycle | Called when the host is inserted into the document, after the element's own `connectedCallback`. Use for work that only makes sense while connected (positioning, document-scoped listeners, observers). May run multiple times if the host moves in and out of the document. |
120
+
| `behaviorDisconnectedCallback()` | Lifecycle | Called when the host is removed from the document. The place to tear down anything the behavior created outside the host (elements appended to `document.body`, listeners on `document`/`window`, observers, timers). |
121
+
122
+
Because behaviors cannot be detached once attached (per the [platform-provided behaviors model](explainer.md#proposed-approach)), there is no `behaviorDetachedCallback`. Listeners registered directly on `element` are released together with the host when it is garbage-collected, so they do not need explicit removal; resources a behavior creates outside the host do.
86
123
87
-
The following example shows how `HTMLButtonBehavior` (`type="button"`) would be implemented in userland:
124
+
The following example shows how a userland behavior would implement `HTMLButtonBehavior` (`type="button"`) as an activation identity. Because it reimplements a platform behavior, the same class doubles as a polyfill. As an `ActivationBehavior` subclass, it receives the activation dispatch path from its base (click, keyboard activation via Space and Enter, `element.click()`, and `preventDefault()`/`stopPropagation()` handling) and overrides the activation algorithm to define what the button does when activated.
- The subclass overrides `behaviorAttachedCallback(internals)` to receive the `ElementInternals` object; sets defaults such as `internals.role`; and register event listeners.
203
-
- The platform would set `this.element` before calling `behaviorAttachedCallback`, so it is already available inside the callback. The example uses `this.element` to register event listeners and to trigger clicks during keyboard activation.
204
-
- `ElementBehavior` needs to provide a way to affect the `:disabled` pseudo-class. The `setDisabled()` method (called in the `disabled` setter) would need to integrate with `ElementInternals` states.
205
-
206
-
#### Polyfilling behaviors
207
-
208
-
This design also would enable **polyfilling** new platform behaviors before they ship natively. Consider `HTMLDialogBehavior` (from `<dialog>`):
- The subclass overrides `behaviorAttachedCallback(internals)` to receive the `ElementInternals` object and set its defaults, such as `internals.role` and focusability.
217
+
- The subclass overrides the activation algorithm `activationBehavior(event)`. The `ActivationBehavior` base invokes it when the host is activated by click, keyboard (Space/Enter), or `element.click()`, so the subclass does not register its own activation listeners.
218
+
- The platform sets `this.element` before calling `behaviorAttachedCallback`, so the host is available inside the callback and the activation algorithm.
219
+
- A developer-defined behavior cannot make the real `:disabled` UA pseudo-class match (that is reserved to platform-provided behaviors), so the example stores disabled state in the host's [`CustomStateSet`](https://developer.mozilla.org/en-US/docs/Web/API/CustomStateSet) (`internals.states`) as the single source of truth for its getter, setter, and activation guard. Authors style it with `:state(disabled)`; a native `HTMLButtonBehavior` would drive `:disabled` directly.
// Implementation of focus trapping, backdrop click handling, etc.
223
+
Developer-defined behaviors compose the same way platform-provided behaviors do: each is listed as a class in `static behaviors`, the platform instantiates one per host, and each acts on the shared host element and its `ElementInternals`. Sibling behaviors share no prototype chain, so when two behaviors need to coordinate, the host mediates rather than one behavior reaching into another. The `QRCodeButton` example above already shows this shape: the host adds a `click` listener that reacts to the activation behavior and reads the embedded-content behavior's `value`.
Although the polyfill above can't fully replicate a native `<dialog>` element (no true top layer, no `::backdrop`, no `:modal`), it provides a reasonable approximation.
227
+
- Should the platform offer a channel for one behavior to observe or extend another, or is host-mediated coordination enough?
228
+
- Do developer-defined behaviors need any registration or naming convention, or are they purely local to the author's code?
281
229
282
-
#### Considerations for developer-defined behaviors
230
+
## References
283
231
284
-
- They can compose with platform-provided behaviors.
285
-
- The same conflict resolution strategies that apply to platform behaviors would need to work with developer-defined behaviors.
232
+
- [Elix functional mixins](https://elix.org/elix/mixins). A component library that composes reusable behavior as class-level functional mixins (cooperating along the prototype chain via `super`, designed for order-independence and isolated effects). Useful prior art for how a behavior-composition system can be designed.
0 commit comments