diff --git a/browser/components/storybook/docs/README.lit-guide.stories.md b/browser/components/storybook/docs/README.lit-guide.stories.md
index 25358e49b8b8..1c6060aef442 100644
--- a/browser/components/storybook/docs/README.lit-guide.stories.md
+++ b/browser/components/storybook/docs/README.lit-guide.stories.md
@@ -84,6 +84,26 @@ class MyCustomElement extends MozLitElement {
}
```
+#### It provides the mapped attribute helpers for standard web attributes
+
+When you want to accept a standard attribute such as accesskey, title or
+aria-label at the component level but it should really be set on a child
+element then you can set the `mapped: true` option in your property
+definition and the attribute will be removed from the host when it is set.
+Note that the attribute can not be unset once it is set.
+
+```js
+class MyElement extends MozLitElement {
+ static properties = {
+ accessKey: { type: String, mapped: true },
+ };
+
+ render() {
+ return html``;
+ }
+}
+```
+
#### It implements support for Lit's `@query` and `@queryAll` decorators
The Lit library includes `@query` and `@queryAll` [decorators](https://lit.dev/docs/components/shadow-dom/#@query-@queryall-and-@queryasync-decorators) that provide an easy way of finding elements within the internal component DOM. These do not work in `mozilla-central` as we do not have support for JavaScript decorators. Instead, `MozLitElement` provides equivalent [DOM querying functionality](https://searchfox.org/mozilla-central/source/toolkit/content/widgets/lit-utils.mjs#87-99) via defining a static `queries` property on the subclass. For example the following Lit code that queries the component's DOM for certain selectors and assigns the results to different class properties:
diff --git a/toolkit/content/tests/widgets/test_moz_lit_element.html b/toolkit/content/tests/widgets/test_moz_lit_element.html
index 55b080bda223..d2d38b383945 100644
--- a/toolkit/content/tests/widgets/test_moz_lit_element.html
+++ b/toolkit/content/tests/widgets/test_moz_lit_element.html
@@ -52,6 +52,9 @@ example-label =
message: { type: String, fluent: true },
tooltipText: { type: String, fluent: true },
renamedAttribute: { type: String, fluent: true, attribute: "rename" },
+ mappedAttribute: { type: String, mapped: true, attribute: "mapped" },
+ accessKey: { type: String, mapped: true },
+ ariaLabel: { type: String, mapped: true },
};
render() {
@@ -80,6 +83,20 @@ example-label =
"Text rendered automatically on upgrade"
);
});
+
+ add_task(async function testMappedAttributes() {
+ let el = await renderTemplate(
+ html``
+ );
+ is(el.accessKey, "f", "accessKey property is correct");
+ ok(!el.hasAttribute("accesskey"), "accesskey attribute was removed");
+
+ is(el.mappedAttribute, "mapped-val", "mappedAttribute is mapped with custom attribute");
+ ok(!el.hasAttribute("mapped"), "mapped attribute was removed");
+
+ is(el.ariaLabel, "Label!", "ariaLabel property is set");
+ ok(!el.hasAttribute("aria-label"), "aria-label was removed");
+ });
diff --git a/toolkit/content/widgets/lit-utils.mjs b/toolkit/content/widgets/lit-utils.mjs
index 841bbda360b1..21859d25aae2 100644
--- a/toolkit/content/widgets/lit-utils.mjs
+++ b/toolkit/content/widgets/lit-utils.mjs
@@ -63,6 +63,16 @@ function queryAll(el, selector) {
*
*******
*
+ * Mapped properties support (moving a standard attribute to rendered content)
+ *
+ * When you want to accept a standard attribute such as accesskey, title or
+ * aria-label at the component level but it should really be set on a child
+ * element then you can set the `mapped: true` option in your property
+ * definition and the attribute will be removed from the host when it is set.
+ * Note that the attribute can not be unset once it is set.
+ *
+ *******
+ *
* Test helper for sending events after a change: `dispatchOnUpdateComplete`
*
* When some async stuff is going on and you want to wait for it in a test, you
@@ -106,6 +116,25 @@ export class MozLitElement extends LitElement {
}
}
+ static createProperty(attrName, options) {
+ if (options.mapped) {
+ let domAttrPropertyName = `${attrName}Attribute`;
+ let domAttrName = options.attribute ?? attrName.toLowerCase();
+ if (attrName.startsWith("aria")) {
+ domAttrName = domAttrName.replace("aria", "aria-");
+ }
+ this.mappedAttributes ??= [];
+ this.mappedAttributes.push([attrName, domAttrPropertyName]);
+ options.state = true;
+ super.createProperty(domAttrPropertyName, {
+ type: String,
+ attribute: domAttrName,
+ reflect: true,
+ });
+ }
+ return super.createProperty(attrName, options);
+ }
+
constructor() {
super();
let { queries } = this.constructor;
@@ -156,6 +185,22 @@ export class MozLitElement extends LitElement {
}
}
+ willUpdate(changes) {
+ this.#handleMappedAttributeChange(changes);
+ }
+
+ #handleMappedAttributeChange(changes) {
+ if (!this.constructor.mappedAttributes) {
+ return;
+ }
+ for (let [attrName, domAttrName] of this.constructor.mappedAttributes) {
+ if (changes.has(domAttrName)) {
+ this[attrName] = this[domAttrName];
+ this[domAttrName] = null;
+ }
+ }
+ }
+
get #l10n() {
if (!this.#l10nObj) {
this.#l10nObj =
diff --git a/toolkit/content/widgets/moz-button/moz-button.mjs b/toolkit/content/widgets/moz-button/moz-button.mjs
index a47f422a4828..cc8799d291eb 100644
--- a/toolkit/content/widgets/moz-button/moz-button.mjs
+++ b/toolkit/content/widgets/moz-button/moz-button.mjs
@@ -47,19 +47,12 @@ export default class MozButton extends MozLitElement {
type: { type: String, reflect: true },
size: { type: String, reflect: true },
disabled: { type: Boolean, reflect: true },
- title: { type: String, state: true },
- titleAttribute: { type: String, attribute: "title", reflect: true },
+ title: { type: String, mapped: true },
tooltipText: { type: String, fluent: true },
- ariaLabelAttribute: {
- type: String,
- attribute: "aria-label",
- reflect: true,
- },
- ariaLabel: { type: String, state: true },
+ ariaLabel: { type: String, mapped: true },
iconSrc: { type: String },
hasVisibleLabel: { type: Boolean, state: true },
- accessKeyAttribute: { type: String, attribute: "accesskey", reflect: true },
- accessKey: { type: String, state: true },
+ accessKey: { type: String, mapped: true },
};
static queries = {
@@ -76,21 +69,6 @@ export default class MozButton extends MozLitElement {
this.hasVisibleLabel = !!this.label;
}
- willUpdate(changes) {
- if (changes.has("titleAttribute")) {
- this.title = this.titleAttribute;
- this.titleAttribute = null;
- }
- if (changes.has("ariaLabelAttribute")) {
- this.ariaLabel = this.ariaLabelAttribute;
- this.ariaLabelAttribute = null;
- }
- if (changes.has("accessKeyAttribute")) {
- this.accessKey = this.accessKeyAttribute;
- this.accessKeyAttribute = null;
- }
- }
-
// Delegate clicks on host to the button element.
click() {
this.buttonEl.click();
diff --git a/toolkit/content/widgets/moz-checkbox/moz-checkbox.mjs b/toolkit/content/widgets/moz-checkbox/moz-checkbox.mjs
index 7fa6beda1e1d..5caa4d215dc2 100644
--- a/toolkit/content/widgets/moz-checkbox/moz-checkbox.mjs
+++ b/toolkit/content/widgets/moz-checkbox/moz-checkbox.mjs
@@ -34,12 +34,7 @@ export default class MozCheckbox extends MozLitElement {
checked: { type: Boolean, reflect: true },
disabled: { type: Boolean, reflect: true },
description: { type: String, fluent: true },
- accessKeyAttribute: {
- type: String,
- attribute: "accesskey",
- reflect: true,
- },
- accessKey: { type: String, state: true },
+ accessKey: { type: String, mapped: true },
supportPage: { type: String, attribute: "support-page" },
};
@@ -88,13 +83,6 @@ export default class MozCheckbox extends MozLitElement {
this.dispatchEvent(newEvent);
}
- willUpdate(changes) {
- if (changes.has("accessKeyAttribute")) {
- this.accessKey = this.accessKeyAttribute;
- this.accessKeyAttribute = null;
- }
- }
-
iconTemplate() {
if (this.iconSrc) {
return html``;
diff --git a/toolkit/content/widgets/moz-radio-group/moz-radio-group.mjs b/toolkit/content/widgets/moz-radio-group/moz-radio-group.mjs
index d021fac30631..5f91ad2c37fa 100644
--- a/toolkit/content/widgets/moz-radio-group/moz-radio-group.mjs
+++ b/toolkit/content/widgets/moz-radio-group/moz-radio-group.mjs
@@ -235,12 +235,7 @@ export class MozRadio extends MozLitElement {
#controller;
static properties = {
- accessKey: { type: String, state: true },
- accessKeyAttribute: {
- type: String,
- attribute: "accesskey",
- reflect: true,
- },
+ accessKey: { type: String, mapped: true },
checked: { type: Boolean, reflect: true },
description: { type: String, fluent: true },
disabled: { type: Boolean, reflect: true },
@@ -277,6 +272,8 @@ export class MozRadio extends MozLitElement {
}
willUpdate(changedProperties) {
+ super.willUpdate(changedProperties);
+
// Handle setting checked directly via JS.
if (
changedProperties.has("checked") &&
@@ -306,11 +303,6 @@ export class MozRadio extends MozLitElement {
this.#controller.syncFocusState();
}
}
-
- if (changedProperties.has("accessKeyAttribute")) {
- this.accessKey = this.accessKeyAttribute;
- this.accessKeyAttribute = null;
- }
}
handleClick() {
diff --git a/toolkit/content/widgets/moz-toggle/moz-toggle.mjs b/toolkit/content/widgets/moz-toggle/moz-toggle.mjs
index 1c6fb5a5fc3a..fbb2f18f8303 100644
--- a/toolkit/content/widgets/moz-toggle/moz-toggle.mjs
+++ b/toolkit/content/widgets/moz-toggle/moz-toggle.mjs
@@ -28,8 +28,7 @@ export default class MozToggle extends MozLitElement {
label: { type: String },
description: { type: String },
ariaLabel: { type: String, attribute: "aria-label" },
- accessKeyAttribute: { type: String, attribute: "accesskey", reflect: true },
- accessKey: { type: String, state: true },
+ accessKey: { type: String, mapped: true },
};
static get queries() {
@@ -66,13 +65,6 @@ export default class MozToggle extends MozLitElement {
this.buttonEl.focus();
}
- willUpdate(changes) {
- if (changes.has("accessKeyAttribute")) {
- this.accessKey = this.accessKeyAttribute;
- this.accessKeyAttribute = null;
- }
- }
-
descriptionTemplate() {
if (this.description) {
return html`