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:
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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" />`;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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`
|
||||
|
||||
Reference in New Issue
Block a user