Bug 1900122 - Part 1: Standardise mapped attributes with mapped:true Lit properties r=reusable-components-reviewers,hjones

Differential Revision: https://phabricator.services.mozilla.com/D221891
This commit is contained in:
Mark Striemer
2024-10-17 02:32:41 +00:00
parent 6fbc732e2b
commit fab35fa7e3
7 changed files with 90 additions and 58 deletions

View File

@@ -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`<button accesskey=${this.accessKey}>Hello</button>`;
}
}
```
#### 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:

View File

@@ -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`<example-element accesskey="f" mapped="mapped-val" aria-label="Label!"></example-element>`
);
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");
});
</script>
</head>
<body>

View File

@@ -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 =

View File

@@ -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();

View File

@@ -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`<img src=${this.iconSrc} role="presentation" class="icon" />`;

View File

@@ -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() {

View File

@@ -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`