commit 6cc653bf5158678fae38491d5f73307c7773c2e4
parent 752a05fc1c2a22992bf9d11a1ee30d1224287096
Author: Tim Giles <tgiles@mozilla.com>
Date: Tue, 4 Nov 2025 22:04:16 +0000
Bug 1991743 - Allow moz-input elements to be used in forms. r=mkennedy
By utilizing the static `formAssociated` property, we allow elements
that extend the MozBaseInputElement to work as expected when used in
form elements. Since we are changing the base input element to be a
form-associated element, we have to implement some additional functions
that are common to form elements. These are the `form()` getter, the
public `setFormValue` method, and the public `formResetCallback` method.
The `setFormValue` is a public method because we need subclasses of the
base input element to be able to modify the private internals of the
base input class.
In order to correctly implement `formResetCallback`, we also added logic
to the MozBaseInputElement to track the default value of the element.
This has the benefit of making elements that extend the base input
behave more like native HTML elements.
Differential Revision: https://phabricator.services.mozilla.com/D268049
Diffstat:
3 files changed, 310 insertions(+), 0 deletions(-)
diff --git a/toolkit/content/tests/widgets/chrome.toml b/toolkit/content/tests/widgets/chrome.toml
@@ -50,6 +50,8 @@ skip-if = ["os == 'mac' && os_version == '14.70' && processor == 'x86_64'"] # Bu
["test_moz_input_color.html"]
+["test_moz_input_elems_in_form.html"]
+
["test_moz_input_folder.html"]
["test_moz_input_password.html"]
diff --git a/toolkit/content/tests/widgets/test_moz_input_elems_in_form.html b/toolkit/content/tests/widgets/test_moz_input_elems_in_form.html
@@ -0,0 +1,285 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <title>test_moz_input_elems_in_form</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <link
+ rel="stylesheet"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ />
+ <script src="lit-test-helpers.js"></script>
+ <script class="testbody" type="application/javascript">
+ /* Bug 1996125: There's a lot of duplicated logic that should be refactored
+ For example:
+ * The template functions
+ * The `assert*` event handlers
+ * The `*with_form_attribute` tasks, since they're copies of the
+ submission and reset tests respectively
+ Most of the `is` asserts can probably be pulled into a function
+ so that we can reduce the number of lines.
+ Additionally, we should have some dynamic way of determining valid
+ moz-* input elements as this test will be out of date as soon as
+ a new moz input element is created.
+ **/
+ let testHelpers = new InputTestHelpers();
+ let formAttributeTemplateFn;
+
+ add_setup(async function setup() {
+ let { html } = await testHelpers.setupLit();
+ let templateFn = () =>
+ html` <form id="main-form">
+ <moz-fieldset>
+ <moz-toggle
+ name="moz-toggle"
+ value="toggle_val"
+ pressed="true"
+ ></moz-toggle>
+ <moz-input-search
+ name="moz-input-search"
+ value="input_search_val"
+ ></moz-input-search>
+ <moz-input-text
+ name="moz-input-text"
+ value="input_text_val"
+ ></moz-input-text>
+ </moz-fieldset>
+ <button id="submit" type="submit"></button>
+ </form>`;
+ testHelpers.setupTests({ templateFn });
+ formAttributeTemplateFn = html`<form id="form-attribute-id"></form>
+ <moz-fieldset>
+ <moz-toggle
+ name="moz-toggle"
+ value="toggle_val"
+ pressed="true"
+ form="form-attribute-id"
+ ></moz-toggle>
+ <moz-input-search
+ name="moz-input-search"
+ value="input_search_val"
+ form="form-attribute-id"
+ ></moz-input-search>
+ <moz-input-text
+ name="moz-input-text"
+ value="input_text_val"
+ form="form-attribute-id"
+ ></moz-input-text>
+ </moz-fieldset>
+ <button id="submit" type="submit" form="form-attribute-id"></button>`;
+ });
+ add_task(async function test_form_submission() {
+ let renderTarget = await testHelpers.renderTemplate();
+ let form = renderTarget.firstElementChild;
+
+ let assertDefaultValue = (event, form) => {
+ event.preventDefault();
+ const formData = new FormData(form);
+ is(
+ formData.get("moz-toggle"),
+ "toggle_val",
+ "moz-toggle should have a submitted value"
+ );
+ is(
+ formData.get("moz-input-search"),
+ "input_search_val",
+ "moz-input-search should have a submitted value"
+ );
+ is(
+ formData.get("moz-input-text"),
+ "input_text_val",
+ "moz-input-text should have a submitted value"
+ );
+ };
+ form.addEventListener(
+ "submit",
+ event => assertDefaultValue(event, form),
+ { once: true }
+ );
+
+ let submit = document.getElementById("submit");
+ await synthesizeMouseAtCenter(submit, {});
+
+ let assertChangedValue = (event, form) => {
+ event.preventDefault();
+ const formData = new FormData(form);
+ for (let [key, value] of formData) {
+ is(
+ value,
+ "non-default value",
+ `${key} should have a non-default submitted value`
+ );
+ }
+ };
+ form.addEventListener(
+ "submit",
+ event => assertChangedValue(event, form),
+ { once: true }
+ );
+ let fieldset = form.querySelector("moz-fieldset");
+ for (let c of fieldset.children) {
+ c.value = "non-default value";
+ }
+
+ // Wait for the input elements to update before clicking the
+ // submit button. Otherwise, the value property may not have
+ // updated which will cause a test failure when submitting the
+ // form.
+ let childArray = Array.from(fieldset.children);
+ await Promise.all(childArray.map(item => item.updateComplete));
+ await synthesizeMouseAtCenter(submit, {});
+ });
+
+ add_task(async function test_form_reset() {
+ let renderTarget = await testHelpers.renderTemplate();
+ let form = renderTarget.firstElementChild;
+ let fieldset = document.querySelector("moz-fieldset");
+ for (let c of fieldset.children) {
+ c.value = "non-default value";
+ }
+ // Assert that the value property of the input elements has
+ // changed before we reset the form
+ for (let c of fieldset.children) {
+ is(
+ c.value,
+ "non-default value",
+ `${c.name} should have a non-default value`
+ );
+ }
+ form.reset();
+ let formData = new FormData(form);
+ // Each tested element's value property should reset to its
+ // value attribute.
+ is(
+ formData.get("moz-toggle"),
+ document.querySelector("moz-toggle").getAttribute("value"),
+ "moz-toggle should reset to default value"
+ );
+ is(
+ formData.get("moz-input-search"),
+ document.querySelector("moz-input-search").getAttribute("value"),
+ "moz-input-search should reset to default value"
+ );
+ is(
+ formData.get("moz-input-text"),
+ document.querySelector("moz-input-text").getAttribute("value"),
+ "moz-input-text should reset to default value"
+ );
+ });
+
+ // Assert that the input elements submit their values as expected
+ // when using the `form` attribute
+ add_task(async function test_form_submission_with_form_attribute() {
+ let renderTarget = await testHelpers.renderTemplate(
+ formAttributeTemplateFn
+ );
+ let form = renderTarget.firstElementChild;
+
+ let assertDefaultValue = (event, form) => {
+ event.preventDefault();
+ const formData = new FormData(form);
+ is(
+ formData.get("moz-toggle"),
+ "toggle_val",
+ "moz-toggle should have a submitted value"
+ );
+ is(
+ formData.get("moz-input-search"),
+ "input_search_val",
+ "moz-input-search should have a submitted value"
+ );
+ is(
+ formData.get("moz-input-text"),
+ "input_text_val",
+ "moz-input-text should have a submitted value"
+ );
+ };
+ form.addEventListener(
+ "submit",
+ event => assertDefaultValue(event, form),
+ { once: true }
+ );
+
+ let submit = document.getElementById("submit");
+ await synthesizeMouseAtCenter(submit, {});
+
+ let assertChangedValue = (event, form) => {
+ event.preventDefault();
+ const formData = new FormData(form);
+ for (let [key, value] of formData) {
+ is(
+ value,
+ "non-default value",
+ `${key} should have a non-default submitted value`
+ );
+ }
+ };
+ form.addEventListener(
+ "submit",
+ event => assertChangedValue(event, form),
+ { once: true }
+ );
+ let fieldset = document.querySelector("div moz-fieldset");
+ for (let c of fieldset.children) {
+ c.value = "non-default value";
+ }
+
+ // Wait for the input elements to update before clicking the
+ // submit button. Otherwise, the value property may not have
+ // updated which will cause a test failure when submitting the
+ // form.
+ let childArray = Array.from(fieldset.children);
+ await Promise.all(childArray.map(item => item.updateComplete));
+ await synthesizeMouseAtCenter(submit, {});
+ });
+
+ // Assert that the input elements reset their values as expected
+ // when using the `form` attribute
+ add_task(async function test_form_reset_with_form_attribute() {
+ let renderTarget = await testHelpers.renderTemplate(
+ formAttributeTemplateFn
+ );
+ let form = renderTarget.firstElementChild;
+ let fieldset = document.querySelector("moz-fieldset");
+ for (let c of fieldset.children) {
+ c.value = "non-default value";
+ }
+
+ // Assert that the value property of the input elements has
+ // changed before we reset the form
+ for (let c of fieldset.children) {
+ is(
+ c.value,
+ "non-default value",
+ `${c.name} should have a non-default value`
+ );
+ }
+ form.reset();
+
+ let formData = new FormData(form);
+ // Each tested element's value property should reset to its
+ // value attribute.
+ is(
+ formData.get("moz-toggle"),
+ document.querySelector("moz-toggle").getAttribute("value"),
+ "moz-toggle should reset to default value"
+ );
+ is(
+ formData.get("moz-input-search"),
+ document.querySelector("moz-input-search").getAttribute("value"),
+ "moz-input-search should reset to default value"
+ );
+ is(
+ formData.get("moz-input-text"),
+ document.querySelector("moz-input-text").getAttribute("value"),
+ "moz-input-text should reset to default value"
+ );
+ });
+ </script>
+ </head>
+ <body>
+ <div id="render"></div>
+ <pre id="test"></pre>
+ </body>
+</html>
diff --git a/toolkit/content/widgets/lit-utils.mjs b/toolkit/content/widgets/lit-utils.mjs
@@ -246,6 +246,7 @@ export class MozLitElement extends LitElement {
* @property {string} ariaDescription - The aria-description text when there is no visible description.
*/
export class MozBaseInputElement extends MozLitElement {
+ static formAssociated = true;
#internals;
#hasSlottedContent = new Map();
@@ -270,9 +271,28 @@ export class MozBaseInputElement extends MozLitElement {
this.#internals = this.attachInternals();
}
+ get form() {
+ return this.#internals.form;
+ }
+
+ /**
+ * @param {string} value The current value of the element.
+ */
+ setFormValue(value) {
+ this.#internals.setFormValue(value);
+ }
+
+ formResetCallback() {
+ this.value = this.defaultValue;
+ }
+
connectedCallback() {
super.connectedCallback();
this.setAttribute("inputlayout", this.constructor.inputLayout);
+ let val = this.getAttribute("value");
+ this.defaultValue = val;
+ this.value = val;
+ this.#internals.setFormValue(this.value || null);
}
willUpdate(changedProperties) {
@@ -281,6 +301,9 @@ export class MozBaseInputElement extends MozLitElement {
this.#updateInternalState(this.supportPage, "support-link");
this.#updateInternalState(this.label, "label");
+ if (changedProperties.has("value")) {
+ this.setFormValue(this.value);
+ }
let activatedProperty = this.constructor.activatedProperty;
if (
(activatedProperty && changedProperties.has(activatedProperty)) ||