From 809a55a027ae505a6f1edcc887961da5f25d3075 Mon Sep 17 00:00:00 2001 From: Ana Sollano Kim Date: Mon, 27 Apr 2026 14:38:55 -0700 Subject: [PATCH] Add platform-provided behaviors for custom elements --- source | 606 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 576 insertions(+), 30 deletions(-) diff --git a/source b/source index 9155017c896..caa5f24ba94 100644 --- a/source +++ b/source @@ -12828,7 +12828,7 @@ interface HTMLElement : Element { [CEReactions] attribute [LegacyNullToEmptyString] DOMString innerText; [CEReactions] attribute [LegacyNullToEmptyString] DOMString outerText; - ElementInternals attachInternals(); + ElementInternals attachInternals(optional AttachInternalsOptions options = {}); // The popover API undefined showPopover(optional ShowPopoverOptions options = {}); @@ -15864,10 +15864,17 @@ interface DOMStringMap {
  • If map["role"] exists, then return it.

  • +
  • If element has a behavior of type + HTMLSubmitButtonBehavior, then return "button".

  • +
  • Return no role.

  • +

    Because the internal content attribute map is checked first, setting + internals.role always takes precedence over the implicit "button" role provided by HTMLSubmitButtonBehavior.

    +

    Similarly, for a custom element element, the default ARIA state and property semantics, for a state or property named stateOrProperty, are determined as @@ -48178,6 +48185,10 @@ interface HTMLTableCellElement : HTMLElement { attributes, buttons. The prose below defines when an element is a button. Some buttons are specifically submit buttons.

    + +

    A form-associated custom element that has a + behavior of type HTMLSubmitButtonBehavior is a submit button.

    Resettable elements
    @@ -60435,11 +60446,16 @@ form.method === input; // => true content attributes, if specified, must have a value that is a valid non-empty URL potentially surrounded by spaces.

    -

    The action of an element is the value of the element's - formaction attribute, if the element is a submit button and has such an attribute, or the value of its - form owner's action attribute, if it has - one, or else the empty string.

    +

    The action of an element is: the value of the element's + HTMLSubmitButtonBehavior's formAction, if the element has a + behavior of type HTMLSubmitButtonBehavior and that value is not the empty + string; or the value of the element's formaction + attribute, if the element is a submit button that does + not have a behavior of type + HTMLSubmitButtonBehavior and has such an attribute; or the value of its form + owner's action attribute, if it has one, or + else the empty string.


    @@ -60483,10 +60499,17 @@ form.method === input; // => true

    The method of an element is one of those states. If the - element is a submit button and has a has a behavior of type + HTMLSubmitButtonBehavior and the behavior's formMethod is not the empty string, then the element's method is the state corresponding to that value, interpreted as + if it were the value of the formmethod attribute; + otherwise, if the element is a submit button that does + not have a behavior of type + HTMLSubmitButtonBehavior and has a formmethod attribute, then the element's method is that attribute's state; otherwise, it is the form - owner's method attribute's state.

    + data-x="concept-fs-method">method is that attribute's state; otherwise, it is the + form owner's method attribute's state.

    @@ -60574,8 +60597,15 @@ form.method === input; // => true

    The enctype of an element is one of those three states. - If the element is a submit button and has a formenctype attribute, then the element's has a behavior of type + HTMLSubmitButtonBehavior and the behavior's formEnctype is not the empty string, then the element's + enctype is the state corresponding to that value, + interpreted as if it were the value of the formenctype attribute; otherwise, if the element is a submit button that does not have a behavior of type HTMLSubmitButtonBehavior and has + a formenctype attribute, then the element's enctype is that attribute's state; otherwise, it is the form owner's enctype attribute's state.

    @@ -60597,10 +60627,14 @@ form.method === input; // => true

    The no-validate state of an element is true if the - element is a submit button and the element's formnovalidate attribute is present, or if the element's - form owner's novalidate attribute is present, - and false otherwise.

    + element has a behavior of type + HTMLSubmitButtonBehavior and the behavior's formNoValidate is true, or if the element is a submit button that does not have a behavior of type HTMLSubmitButtonBehavior and the + element's formnovalidate attribute is present, or if + the element's form owner's novalidate + attribute is present, and false otherwise.

    @@ -63675,9 +63709,10 @@ fur

    If the user agent supports letting the user submit a form implicitly (for example, on some platforms hitting the "enter" key while a text control is focused implicitly submits the form), then doing so for a form, whose default button has activation - behavior and is not disabled, must cause the user - agent to fire a click event at that default - button.

    + behavior and is not disabled or disabled due to a behavior, must + cause the user agent to fire a click event at that + default button.

    There are pages on the web that are only usable if there is a way to implicitly @@ -63847,8 +63882,11 @@ fur

  • Otherwise, if submitter is a submit - button, then set result to submitter's optional value.

  • + button: if submitter has a behavior of type + HTMLSubmitButtonBehavior, then set result to that behavior's value; otherwise, set result to + submitter's optional + value.

  • Close the dialog subject with result and null.

  • @@ -63885,10 +63923,17 @@ fur
  • Let formTarget be null.

  • -
  • If the submitter element is a submit - button and it has a formtarget attribute, then - set formTarget to the formtarget attribute - value.

  • +
  • +

    If submitter has a behavior of type + HTMLSubmitButtonBehavior and the behavior's formTarget is not the empty string, then set + formTarget to that value. Otherwise, if the submitter element is a submit button that does not have a behavior of type HTMLSubmitButtonBehavior and + it has a formtarget attribute, then set + formTarget to the formtarget attribute + value.

    +
  • Let target be the result of getting an element's target given submitter's form owner and @@ -64269,9 +64314,33 @@ fur

  • -
  • If the field is a form-associated custom element, then perform - the entry construction algorithm given - field and entry list, then continue.

  • +
  • +

    If the field is a form-associated custom element:

    + +
      +
    1. +

      If field is submitter and field has a behavior of type + HTMLSubmitButtonBehavior:

      + +
        +
      1. Let submitBehavior be the behavior of + type HTMLSubmitButtonBehavior for field.

      2. + +
      3. If submitBehavior's name is not the + empty string, create an entry with submitBehavior's name and submitBehavior's value, and append it to + entry list.

      4. +
      +
    2. + +
    3. Otherwise, perform the entry construction + algorithm given field and entry list.

    4. +
    + +

    Continue.

    +
  • If either the field element does not have a name attribute specified, or its @@ -78685,12 +78754,15 @@ customElements.define("x-foo", class extends HTMLElement { control over internal features which the user agent provides to all elements.

    -
    element.attachInternals()
    +
    element.attachInternals(options)

    Returns an ElementInternals object targeting the custom element element. Throws an exception if element is not a custom element, if the "internals" feature was disabled as part of the - element definition, or if it is called twice on the same element.

    + element definition, or if it is called twice on the same element. If options is + provided with a behaviors member, + the specified platform-provided behaviors are + associated with the element.

    @@ -78700,7 +78772,7 @@ customElements.define("x-foo", class extends HTMLElement {

    The attachInternals() method steps are:

    + data-x="dom-attachInternals">attachInternals(options) method steps are:

      @@ -78728,10 +78800,63 @@ customElements.define("x-foo", class extends HTMLElement { data-x="">precustomized" or "custom", then throw a "NotSupportedError" DOMException.

      +
    1. +

      If options is given and options["behaviors"] exists:

      + +
        +
      1. Let behaviorList be options["behaviors"].

      2. + +
      3. +

        For each behavior of + behaviorList:

        + +
          +
        1. If behavior's associated element is + not null, then throw a TypeError.

        2. + +
        3. If behaviorList contains another item that is the same object as + behavior, or another item that is an instance of the same interface as + behavior, then throw a TypeError.

        4. +
        +
      4. +
      +
    2. +
    3. Set this's attached internals to a new ElementInternals instance whose target element is this.

    4. +
    5. +

      If options is given and options["behaviors"] exists:

      + +
        +
      1. Let behaviorList be options["behaviors"].

      2. + +
      3. +

        For each behavior of + behaviorList:

        + +
          +
        1. Set behavior's associated element + to this.

        2. + +
        3. Set behavior's associated + internals to this's attached internals.

        4. +
        +
      4. + +
      5. Set this's attached internals's behaviors to the result of + creating a frozen array from behaviorList.

      6. +
      +
    6. +
    7. Return this's attached internals.

    @@ -78765,6 +78890,9 @@ interface ElementInternals { // Custom state pseudo-class [SameObject] readonly attribute CustomStateSet states; + + // Platform-provided behaviors + readonly attribute FrozenArray<ElementBehavior> behaviors; }; // Accessibility semantics @@ -78781,11 +78909,19 @@ dictionary ValidityStateFlags { boolean stepMismatch = false; boolean badInput = false; boolean customError = false; +}; + +dictionary AttachInternalsOptions { + sequence<ElementBehavior> behaviors; };

    Each ElementInternals has a target element, which is a custom element.

    +

    Each ElementInternals has behaviors, a FrozenArray of + ElementBehavior objects, initially an empty FrozenArray.

    +
    Shadow root access
    @@ -78952,6 +79088,10 @@ dictionary ValidityStateFlags {
  • If element is not a form-associated custom element, then throw a "NotSupportedError" DOMException.

  • +
  • If element has a behavior of type + HTMLSubmitButtonBehavior, then throw a + "NotSupportedError" DOMException.

  • +
  • Set element's submission value to value if value is not a FormData object, or to a clone of @@ -79183,6 +79323,401 @@ this._internals.states.delete("interactive"); this._internals.states.add("complete");

  • +
    Platform-provided behaviors
    + +

    A platform-provided behavior is an object that + gives a custom element built-in semantics and functionality, such as acting as a submit + button. Platform-provided behaviors are associated with a custom element through + attachInternals().

    + +
    [Exposed=Window]
    +interface ElementBehavior {
    +};
    +
    +[Exposed=Window]
    +interface HTMLSubmitButtonBehavior : ElementBehavior {
    +  constructor();
    +
    +  attribute boolean disabled;
    +  readonly attribute HTMLFormElement? form;
    +  attribute USVString formAction;
    +  attribute DOMString formEnctype;
    +  attribute DOMString formMethod;
    +  attribute boolean formNoValidate;
    +  attribute DOMString formTarget;
    +  readonly attribute NodeList? labels;
    +  attribute DOMString name;
    +  attribute DOMString value;
    +};
    + +

    Each ElementBehavior has an associated + element (null or an Element), initially null.

    + +

    Each ElementBehavior has associated + internals (null or an ElementInternals), initially null.

    + +
    +
    new HTMLSubmitButtonBehavior()
    +

    Creates a new HTMLSubmitButtonBehavior instance. The instance has to be passed + to attachInternals() via the behaviors option to take effect.

    + +
    submitBehavior.disabled
    +

    A boolean indicating whether the element is disabled. Defaults to false. When true, the + element is disabled due to a + behavior.

    + +
    submitBehavior.form
    +

    Returns the form element associated with the behavior's associated element, or null if the element is not a + form-associated custom element. This delegates to the element's form property.

    + +
    submitBehavior.formAction
    +

    The URL to use for form submission, overriding the form's own action attribute. Defaults to the empty string.

    + +
    submitBehavior.formEnctype
    +

    The encoding type to use for form submission, overriding the form's own enctype attribute. Defaults to the empty string.

    + +
    submitBehavior.formMethod
    +

    The HTTP method to use for form submission, overriding the form's own method attribute. Defaults to the empty string.

    + +
    submitBehavior.formNoValidate
    +

    A boolean that, when true, bypasses form validation during submission. Defaults to + false.

    + +
    submitBehavior.formTarget
    +

    The browsing context name or keyword for form submission, overriding the form's own target attribute. Defaults to the empty string.

    + +
    submitBehavior.labels
    +

    Returns the list of label elements associated with the behavior's associated element, or null. This delegates to the element's + labels property via the associated internals.

    + +
    submitBehavior.name
    +

    The name to use when the element is the submitter for form submission. Defaults to the + empty string.

    + +
    submitBehavior.value
    +

    The value to use when the element is the submitter for form submission. Defaults to the + empty string.

    +
    + +
    + +

    Each HTMLSubmitButtonBehavior has the following internal state:

    + +
      +
    • disabled, a boolean, initially false.

    • +
    • formAction, a scalar value string, + initially the empty string.

    • +
    • formEnctype, a string, initially the empty + string.

    • +
    • formMethod, a string, initially the empty + string.

    • +
    • formNoValidate, a boolean, initially + false.

    • +
    • formTarget, a string, initially the empty + string.

    • +
    • name, a string, initially the empty string.

    • +
    • value, a string, initially the empty string.

    • +
    + +
    +

    The HTMLSubmitButtonBehavior() constructor steps + are:

    + +
      +
    1. Return.

    2. +
    +
    + +
    +

    The disabled getter steps are to return + this's disabled.

    +
    + +
    +

    The disabled setter steps are to + set this's disabled to the given + value.

    +
    + +
    +

    The form getter steps are:

    + +
      +
    1. If this's associated element is null, + return null.

    2. + +
    3. If this's associated element is not a + form-associated custom element, return null.

    4. + +
    5. Return this's associated element's + form owner.

    6. +
    +
    + +
    +

    The formAction getter steps are to + return this's formAction.

    +
    + +
    +

    The formAction setter steps are + to set this's formAction to the given + value.

    +
    + +
    +

    The formEnctype getter steps are to + return this's formEnctype.

    +
    + +
    +

    The formEnctype setter steps are + to set this's formEnctype to the given + value.

    +
    + +
    +

    The formMethod getter steps are to + return this's formMethod.

    +
    + +
    +

    The formMethod setter steps are + to set this's formMethod to the given + value.

    +
    + +
    +

    The formNoValidate getter steps are + to return this's formNoValidate.

    +
    + +
    +

    The formNoValidate setter + steps are to set this's formNoValidate to the given value.

    +
    + +
    +

    The formTarget getter steps are to + return this's formTarget.

    +
    + +
    +

    The formTarget setter steps are + to set this's formTarget to the given + value.

    +
    + +
    +

    The labels getter steps are:

    + +
      +
    1. If this's associated internals is + null, return null.

    2. + +
    3. Return this's associated + internals's target element's labels.

    4. +
    +
    + +
    +

    The name getter steps are to return + this's name.

    +
    + +
    +

    The name setter steps are to set + this's name to the given value.

    +
    + +
    +

    The value getter steps are to return + this's value.

    +
    + +
    +

    The value setter steps are to set + this's value to the given value.

    +
    + +
    +

    The behaviors getter steps are to return + this's behaviors.

    +
    + +
    +

    A custom element element has a + behavior of a given type if element's attached internals is not null + and element's attached internals's behaviors contains an + instance of that type.

    +
    + +
    +

    To get the behavior of a given type for a custom + element element: if element's attached internals is + null, return null. Otherwise, return the item in element's attached + internals's behaviors that is an instance of + that type, or null if no such item exists.

    +
    + +

    Since duplicate behavior types are rejected during attachment, the above algorithm + returns at most one item. Other algorithms invoke it as, e.g., "the behavior of type HTMLSubmitButtonBehavior for + element".

    + +

    An element is disabled due to a + behavior if it is a custom element that has a + behavior of type HTMLSubmitButtonBehavior and any of the following conditions + are true:

    + +
      +
    • The element's HTMLSubmitButtonBehavior's disabled is true.

    • + +
    • The element is a form-associated custom element and is disabled.

    • + +
    • The element has an ancestor fieldset that is disabled, and the element is not a descendant of that + fieldset element's first legend element child, if any.

    • +
    + +
    +

    A custom element element that has a + behavior of type HTMLSubmitButtonBehavior has the following activation + behavior given event:

    + +
      +
    1. If element is disabled due to a behavior, then + return.

    2. + +
    3. If element's node document is not fully active, then + return.

    4. + +
    5. If element is not a form-associated custom element, then + return.

    6. + +
    7. If element does not have a form owner, then return.

    8. + +
    9. Submit element's form + owner from element with userInvolvement set to event's user navigation involvement.

    10. +
    +
    + +
    + +
    +

    The following example shows a basic custom submit button using + HTMLSubmitButtonBehavior:

    + +
    class MySubmitButton extends HTMLElement {
    +  static formAssociated = true;
    +
    +  constructor() {
    +    super();
    +    this._behavior = new HTMLSubmitButtonBehavior();
    +    this._internals = this.attachInternals({ behaviors: [this._behavior] });
    +  }
    +}
    +customElements.define('my-submit-button', MySubmitButton);
    + +

    Since formAssociated is true, the element participates in form + submission. The HTMLSubmitButtonBehavior makes it act as a submit button: it gets an + implicit button ARIA role, it becomes focusable, and clicking it submits + the form.

    + +
    <form action="/submit">
    + <label>Name: <input type=text name=username></label>
    + <my-submit-button>Submit</my-submit-button>
    +</form>
    + +

    To override the implicit role, set internals.role:

    + +
    // Override the implicit "button" role:
    +this._internals.role = 'menuitem';
    +
    + +
    +

    Authors can expose the behavior's properties through getters and setters, and forward relevant + attribute values using attributeChangedCallback:

    + +
    class FancySubmit extends HTMLElement {
    +  static formAssociated = true;
    +  static observedAttributes = ['disabled', 'formaction', 'name', 'value'];
    +
    +  constructor() {
    +    super();
    +    this._behavior = new HTMLSubmitButtonBehavior();
    +    this._internals = this.attachInternals({ behaviors: [this._behavior] });
    +  }
    +
    +  get disabled() { return this._behavior.disabled; }
    +  set disabled(v) { this._behavior.disabled = v; }
    +
    +  get name() { return this._behavior.name; }
    +  set name(v) { this._behavior.name = v; }
    +
    +  get value() { return this._behavior.value; }
    +  set value(v) { this._behavior.value = v; }
    +
    +  attributeChangedCallback(name, oldValue, newValue) {
    +    switch (name) {
    +      case 'disabled':
    +        this._behavior.disabled = newValue !== null;
    +        break;
    +      case 'formaction':
    +        this._behavior.formAction = newValue ?? '';
    +        break;
    +      case 'name':
    +        this._behavior.name = newValue ?? '';
    +        break;
    +      case 'value':
    +        this._behavior.value = newValue ?? '';
    +        break;
    +    }
    +  }
    +}
    + +

    The form-submission-attribute properties on the behavior (formAction, formEnctype, formMethod, formNoValidate, formTarget) are read from the behavior + instance, not from the element's content attributes. This means authors must explicitly forward + attribute values in attributeChangedCallback if they want + declarative attribute control.

    +
    +

    Common idioms without dedicated elements

    Breadcrumb navigation

    @@ -79572,6 +80107,9 @@ contradict people?
  • a form-associated custom element that is disabled
  • + +
  • a custom element that is disabled due to a behavior
  • @@ -84845,6 +85383,10 @@ partial interface Navigator {
  • If this element is a form control that is disabled, then return.

  • +
  • If this element is disabled due to a behavior, then + return.

  • +
  • If this element's click in progress flag is set, then return.

  • Set this element's click in progress flag.

  • @@ -85582,6 +86124,10 @@ dictionary CommandEventInit : EventInit {
  • Editing hosts
  • Navigable containers
  • + +
  • Custom elements that have + a behavior of type HTMLSubmitButtonBehavior and are not disabled due to a behavior