diff --git a/toolkit/content/customElements.js b/toolkit/content/customElements.js
index 3735ffe9b465..d6b6ac791a62 100644
--- a/toolkit/content/customElements.js
+++ b/toolkit/content/customElements.js
@@ -874,6 +874,14 @@
"chrome://global/content/elements/moz-support-link.mjs",
],
["moz-toggle", "chrome://global/content/elements/moz-toggle.mjs"],
+ [
+ "moz-visual-picker",
+ "chrome://global/content/elements/moz-visual-picker.mjs",
+ ],
+ [
+ "moz-visual-picker-item",
+ "chrome://global/content/elements/moz-visual-picker.mjs",
+ ],
]) {
if (!customElements.get(tag)) {
customElements.setElementCreationCallback(
diff --git a/toolkit/content/jar.mn b/toolkit/content/jar.mn
index 94caf8642b88..d2a70e7d3cf7 100644
--- a/toolkit/content/jar.mn
+++ b/toolkit/content/jar.mn
@@ -132,6 +132,8 @@ toolkit.jar:
content/global/elements/moz-support-link.mjs (widgets/moz-support-link/moz-support-link.mjs)
content/global/elements/moz-toggle.css (widgets/moz-toggle/moz-toggle.css)
content/global/elements/moz-toggle.mjs (widgets/moz-toggle/moz-toggle.mjs)
+ content/global/elements/moz-visual-picker-item.css (widgets/moz-visual-picker/moz-visual-picker-item.css)
+ content/global/elements/moz-visual-picker.mjs (widgets/moz-visual-picker/moz-visual-picker.mjs)
content/global/elements/named-deck.js (widgets/named-deck.js)
content/global/elements/infobar.css (widgets/infobar.css)
content/global/elements/notificationbox.js (widgets/notificationbox.js)
diff --git a/toolkit/content/tests/widgets/chrome.toml b/toolkit/content/tests/widgets/chrome.toml
index 2d6038b69e10..01accb5d9a21 100644
--- a/toolkit/content/tests/widgets/chrome.toml
+++ b/toolkit/content/tests/widgets/chrome.toml
@@ -72,6 +72,8 @@ skip-if = [
["test_moz_toggle.html"]
+["test_moz_visual_picker.html"]
+
["test_panel_item_accesskey.html"]
["test_panel_item_checkbox.html"]
diff --git a/toolkit/content/tests/widgets/lit-test-helpers.js b/toolkit/content/tests/widgets/lit-test-helpers.js
index 395032f09ce3..ef425076a5cd 100644
--- a/toolkit/content/tests/widgets/lit-test-helpers.js
+++ b/toolkit/content/tests/widgets/lit-test-helpers.js
@@ -115,12 +115,12 @@ class InputTestHelpers extends LitTestHelpers {
let { activatedProperty } = this;
function trackEvent(event) {
- let reactiveProps = event.target.constructor.properties;
+ let reactiveProps = event.target.constructor?.properties;
seenEvents.push({
type: event.type,
- value: event.target.value,
+ value: event.currentTarget.value,
localName: event.currentTarget.localName,
- ...(reactiveProps.hasOwnProperty(activatedProperty) && {
+ ...(reactiveProps?.hasOwnProperty(activatedProperty) && {
[activatedProperty]: event.target[activatedProperty],
}),
});
diff --git a/toolkit/content/tests/widgets/test_moz_visual_picker.html b/toolkit/content/tests/widgets/test_moz_visual_picker.html
new file mode 100644
index 000000000000..c74d411441c9
--- /dev/null
+++ b/toolkit/content/tests/widgets/test_moz_visual_picker.html
@@ -0,0 +1,491 @@
+
+
+
+
+ MozVisualPicker Tests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 2b9be2e0a454..642371008c5a 100644
--- a/toolkit/content/widgets/moz-radio-group/moz-radio-group.mjs
+++ b/toolkit/content/widgets/moz-radio-group/moz-radio-group.mjs
@@ -54,6 +54,8 @@ export class MozRadioGroup extends MozLitElement {
#radioButtons;
#value;
+ static childElementName = "moz-radio";
+
static properties = {
disabled: { type: Boolean, reflect: true },
description: { type: String, fluent: true },
@@ -100,7 +102,9 @@ export class MozRadioGroup extends MozLitElement {
this.shadowRoot
?.querySelector("slot:not([name])")
?.assignedElements() || [...this.children]
- )?.filter(el => el.localName === "moz-radio" && !el.slot);
+ )?.filter(
+ el => el.localName === this.constructor.childElementName && !el.slot
+ );
this.#radioButtons.forEach(button => customElements.upgrade(button));
}
return this.#radioButtons;
@@ -277,6 +281,15 @@ export class MozRadio extends MozBaseInputElement {
return this.#controller;
}
+ get isDisabled() {
+ return (
+ this.disabled ||
+ this.#controller.disabled ||
+ this.parentDisabled ||
+ this.#controller.parentDisabled
+ );
+ }
+
constructor() {
super();
this.checked = false;
@@ -330,8 +343,12 @@ export class MozRadio extends MozBaseInputElement {
}
handleClick() {
+ if (this.isDisabled || this.checked) {
+ return;
+ }
+
this.#controller.value = this.value;
- if (this.getRootNode().activeElement?.localName == "moz-radio") {
+ if (this.getRootNode().activeElement?.localName == this.localName) {
this.focus();
}
}
@@ -342,11 +359,6 @@ export class MozRadio extends MozBaseInputElement {
}
inputTemplate() {
- let isDisabled =
- this.disabled ||
- this.#controller.disabled ||
- this.parentDisabled ||
- this.#controller.parentDisabled;
return html`
+
+
+
+ `;
+ }
+}
+customElements.define("moz-visual-picker-item", MozVisualPickerItem);
diff --git a/toolkit/content/widgets/moz-visual-picker/moz-visual-picker.stories.mjs b/toolkit/content/widgets/moz-visual-picker/moz-visual-picker.stories.mjs
new file mode 100644
index 000000000000..05c7b8e76277
--- /dev/null
+++ b/toolkit/content/widgets/moz-visual-picker/moz-visual-picker.stories.mjs
@@ -0,0 +1,142 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { html, ifDefined, classMap } from "../vendor/lit.all.mjs";
+import "./moz-visual-picker.mjs";
+
+export default {
+ title: "UI Widgets/Visual Picker",
+ component: "moz-visual-picker",
+ argTypes: {
+ value: {
+ options: ["1", "2", "3"],
+ control: { type: "select" },
+ },
+ slottedItem: {
+ options: ["card", "avatar"],
+ control: { type: "select" },
+ },
+ pickerL10nId: {
+ options: ["moz-visual-picker", "moz-visual-picker-description"],
+ control: { type: "select" },
+ },
+ },
+ parameters: {
+ actions: {
+ handles: ["click", "input", "change"],
+ },
+ status: "in-development",
+ fluent: `
+moz-visual-picker =
+ .label = Pick something
+moz-visual-picker-description =
+ .label = Pick something
+ .description = Pick one of these cool things please
+`,
+ },
+};
+
+const AVATAR_ICONS = [
+ "chrome://global/skin/icons/defaultFavicon.svg",
+ "chrome://global/skin/icons/experiments.svg",
+ "chrome://global/skin/icons/heart.svg",
+];
+
+function getSlottedContent(type, index) {
+ if (type == "card") {
+ return html`
+

+
I'm card number ${index + 1}
+
`;
+ }
+ return html`
+

+
`;
+}
+
+const Template = ({ value, slottedItem, pickerL10nId, supportPage }) => {
+ return html`
+
+
+ ${[...Array.from({ length: 3 })].map(
+ (_, i) =>
+ html`
+ ${getSlottedContent(slottedItem, i)}
+ `
+ )}
+
+ `;
+};
+
+export const Default = Template.bind({});
+Default.args = {
+ pickerL10nId: "moz-visual-picker",
+ slottedItem: "card",
+ value: "1",
+ supportPage: "",
+};
+
+export const WithPickerDescription = Template.bind({});
+WithPickerDescription.args = {
+ ...Default.args,
+ pickerL10nId: "moz-visual-picker-description",
+};
+
+export const WithPickerSupportLink = Template.bind({});
+WithPickerSupportLink.args = {
+ ...WithPickerDescription.args,
+ supportPage: "foo",
+};
+
+export const AllUnselected = Template.bind({});
+AllUnselected.args = {
+ ...Default.args,
+ value: "",
+};