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
|
#### 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:
|
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 },
|
message: { type: String, fluent: true },
|
||||||
tooltipText: { type: String, fluent: true },
|
tooltipText: { type: String, fluent: true },
|
||||||
renamedAttribute: { type: String, fluent: true, attribute: "rename" },
|
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() {
|
render() {
|
||||||
@@ -80,6 +83,20 @@ example-label =
|
|||||||
"Text rendered automatically on upgrade"
|
"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>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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`
|
* 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
|
* 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() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
let { queries } = this.constructor;
|
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() {
|
get #l10n() {
|
||||||
if (!this.#l10nObj) {
|
if (!this.#l10nObj) {
|
||||||
this.#l10nObj =
|
this.#l10nObj =
|
||||||
|
|||||||
@@ -47,19 +47,12 @@ export default class MozButton extends MozLitElement {
|
|||||||
type: { type: String, reflect: true },
|
type: { type: String, reflect: true },
|
||||||
size: { type: String, reflect: true },
|
size: { type: String, reflect: true },
|
||||||
disabled: { type: Boolean, reflect: true },
|
disabled: { type: Boolean, reflect: true },
|
||||||
title: { type: String, state: true },
|
title: { type: String, mapped: true },
|
||||||
titleAttribute: { type: String, attribute: "title", reflect: true },
|
|
||||||
tooltipText: { type: String, fluent: true },
|
tooltipText: { type: String, fluent: true },
|
||||||
ariaLabelAttribute: {
|
ariaLabel: { type: String, mapped: true },
|
||||||
type: String,
|
|
||||||
attribute: "aria-label",
|
|
||||||
reflect: true,
|
|
||||||
},
|
|
||||||
ariaLabel: { type: String, state: true },
|
|
||||||
iconSrc: { type: String },
|
iconSrc: { type: String },
|
||||||
hasVisibleLabel: { type: Boolean, state: true },
|
hasVisibleLabel: { type: Boolean, state: true },
|
||||||
accessKeyAttribute: { type: String, attribute: "accesskey", reflect: true },
|
accessKey: { type: String, mapped: true },
|
||||||
accessKey: { type: String, state: true },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static queries = {
|
static queries = {
|
||||||
@@ -76,21 +69,6 @@ export default class MozButton extends MozLitElement {
|
|||||||
this.hasVisibleLabel = !!this.label;
|
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.
|
// Delegate clicks on host to the button element.
|
||||||
click() {
|
click() {
|
||||||
this.buttonEl.click();
|
this.buttonEl.click();
|
||||||
|
|||||||
@@ -34,12 +34,7 @@ export default class MozCheckbox extends MozLitElement {
|
|||||||
checked: { type: Boolean, reflect: true },
|
checked: { type: Boolean, reflect: true },
|
||||||
disabled: { type: Boolean, reflect: true },
|
disabled: { type: Boolean, reflect: true },
|
||||||
description: { type: String, fluent: true },
|
description: { type: String, fluent: true },
|
||||||
accessKeyAttribute: {
|
accessKey: { type: String, mapped: true },
|
||||||
type: String,
|
|
||||||
attribute: "accesskey",
|
|
||||||
reflect: true,
|
|
||||||
},
|
|
||||||
accessKey: { type: String, state: true },
|
|
||||||
supportPage: { type: String, attribute: "support-page" },
|
supportPage: { type: String, attribute: "support-page" },
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -88,13 +83,6 @@ export default class MozCheckbox extends MozLitElement {
|
|||||||
this.dispatchEvent(newEvent);
|
this.dispatchEvent(newEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
willUpdate(changes) {
|
|
||||||
if (changes.has("accessKeyAttribute")) {
|
|
||||||
this.accessKey = this.accessKeyAttribute;
|
|
||||||
this.accessKeyAttribute = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
iconTemplate() {
|
iconTemplate() {
|
||||||
if (this.iconSrc) {
|
if (this.iconSrc) {
|
||||||
return html`<img src=${this.iconSrc} role="presentation" class="icon" />`;
|
return html`<img src=${this.iconSrc} role="presentation" class="icon" />`;
|
||||||
|
|||||||
@@ -235,12 +235,7 @@ export class MozRadio extends MozLitElement {
|
|||||||
#controller;
|
#controller;
|
||||||
|
|
||||||
static properties = {
|
static properties = {
|
||||||
accessKey: { type: String, state: true },
|
accessKey: { type: String, mapped: true },
|
||||||
accessKeyAttribute: {
|
|
||||||
type: String,
|
|
||||||
attribute: "accesskey",
|
|
||||||
reflect: true,
|
|
||||||
},
|
|
||||||
checked: { type: Boolean, reflect: true },
|
checked: { type: Boolean, reflect: true },
|
||||||
description: { type: String, fluent: true },
|
description: { type: String, fluent: true },
|
||||||
disabled: { type: Boolean, reflect: true },
|
disabled: { type: Boolean, reflect: true },
|
||||||
@@ -277,6 +272,8 @@ export class MozRadio extends MozLitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
willUpdate(changedProperties) {
|
willUpdate(changedProperties) {
|
||||||
|
super.willUpdate(changedProperties);
|
||||||
|
|
||||||
// Handle setting checked directly via JS.
|
// Handle setting checked directly via JS.
|
||||||
if (
|
if (
|
||||||
changedProperties.has("checked") &&
|
changedProperties.has("checked") &&
|
||||||
@@ -306,11 +303,6 @@ export class MozRadio extends MozLitElement {
|
|||||||
this.#controller.syncFocusState();
|
this.#controller.syncFocusState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changedProperties.has("accessKeyAttribute")) {
|
|
||||||
this.accessKey = this.accessKeyAttribute;
|
|
||||||
this.accessKeyAttribute = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClick() {
|
handleClick() {
|
||||||
|
|||||||
@@ -28,8 +28,7 @@ export default class MozToggle extends MozLitElement {
|
|||||||
label: { type: String },
|
label: { type: String },
|
||||||
description: { type: String },
|
description: { type: String },
|
||||||
ariaLabel: { type: String, attribute: "aria-label" },
|
ariaLabel: { type: String, attribute: "aria-label" },
|
||||||
accessKeyAttribute: { type: String, attribute: "accesskey", reflect: true },
|
accessKey: { type: String, mapped: true },
|
||||||
accessKey: { type: String, state: true },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static get queries() {
|
static get queries() {
|
||||||
@@ -66,13 +65,6 @@ export default class MozToggle extends MozLitElement {
|
|||||||
this.buttonEl.focus();
|
this.buttonEl.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
willUpdate(changes) {
|
|
||||||
if (changes.has("accessKeyAttribute")) {
|
|
||||||
this.accessKey = this.accessKeyAttribute;
|
|
||||||
this.accessKeyAttribute = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
descriptionTemplate() {
|
descriptionTemplate() {
|
||||||
if (this.description) {
|
if (this.description) {
|
||||||
return html`
|
return html`
|
||||||
|
|||||||
Reference in New Issue
Block a user