Bug 1942390 - Part 1: create basic moz-visual-picker elements r=tgiles,reusable-components-reviewers,mstriemer
Differential Revision: https://phabricator.services.mozilla.com/D245619
This commit is contained in:
committed by
hjones@mozilla.com
parent
d729f15bb2
commit
3b0be84c10
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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],
|
||||
}),
|
||||
});
|
||||
|
||||
491
toolkit/content/tests/widgets/test_moz_visual_picker.html
Normal file
491
toolkit/content/tests/widgets/test_moz_visual_picker.html
Normal file
@@ -0,0 +1,491 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>MozVisualPicker Tests</title>
|
||||
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
|
||||
<link rel="stylesheet" href="chrome://global/skin/in-content/common.css" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="chrome://mochikit/content/tests/SimpleTest/test.css"
|
||||
/>
|
||||
<script
|
||||
type="module"
|
||||
src="chrome://global/content/elements/moz-visual-picker.mjs"
|
||||
></script>
|
||||
<script src="lit-test-helpers.js"></script>
|
||||
<style>
|
||||
.slotted {
|
||||
height: 72px;
|
||||
width: 72px;
|
||||
border-radius: var(--border-radius-medium);
|
||||
background-color: var(--background-color-box);
|
||||
padding: var(--space-medium);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
let html;
|
||||
let defaultTemplate;
|
||||
let testHelpers = new InputTestHelpers();
|
||||
|
||||
add_setup(async function setup() {
|
||||
({ html } = await testHelpers.setupLit());
|
||||
let templateFn = attrs => html`
|
||||
<moz-visual-picker ${attrs}>
|
||||
<moz-visual-picker-item checked value="first">
|
||||
<div class="slotted">In the slot</div>
|
||||
</moz-visual-picker-item>
|
||||
<moz-visual-picker-item value="second">
|
||||
<div class="slotted">In the slot</div>
|
||||
</moz-visual-picker-item>
|
||||
<moz-visual-picker-item value="third">
|
||||
<div class="slotted">In the slot</div>
|
||||
</moz-visual-picker-item>
|
||||
</moz-visual-picker>
|
||||
`;
|
||||
defaultTemplate = templateFn(
|
||||
testHelpers.spread({ label: "Visual picker label" })
|
||||
);
|
||||
testHelpers.setupTests({ templateFn });
|
||||
});
|
||||
|
||||
add_task(async function testVisualPickerProperties() {
|
||||
const TEST_LABEL = "Testing...";
|
||||
const TEST_DESCRIPTION = "Testing description..";
|
||||
const TEST_SUPPORT_PAGE = "Testing support page..";
|
||||
let renderTarget = await testHelpers.renderTemplate(defaultTemplate);
|
||||
let picker = renderTarget.querySelector("moz-visual-picker");
|
||||
|
||||
is(
|
||||
picker.fieldset.label,
|
||||
"Visual picker label",
|
||||
"Visual picker supports a label."
|
||||
);
|
||||
picker.label = TEST_LABEL;
|
||||
picker.description = TEST_DESCRIPTION;
|
||||
picker.supportPage = TEST_SUPPORT_PAGE;
|
||||
await picker.updateComplete;
|
||||
is(
|
||||
picker.fieldset.label,
|
||||
TEST_LABEL,
|
||||
"Visual picker label is updated."
|
||||
);
|
||||
is(
|
||||
picker.fieldset.description,
|
||||
TEST_DESCRIPTION,
|
||||
"Visual picker description text is set."
|
||||
);
|
||||
is(
|
||||
picker.fieldset.supportPage,
|
||||
TEST_SUPPORT_PAGE,
|
||||
"Visual picker support page is set."
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function testChangingPickerValue() {
|
||||
let renderTarget = await testHelpers.renderTemplate(defaultTemplate);
|
||||
let picker = renderTarget.querySelector("moz-visual-picker");
|
||||
let items = [
|
||||
...renderTarget.querySelectorAll("moz-visual-picker-item"),
|
||||
];
|
||||
let [firstItem, secondItem, thirdItem] = items;
|
||||
|
||||
function verifySelectedPickerItem(selectedItem, setsFocus) {
|
||||
ok(selectedItem.checked, "The selected picker item is checked.");
|
||||
is(
|
||||
selectedItem.inputEl.getAttribute("aria-checked"),
|
||||
"true",
|
||||
"The checked item has the correct aria-checked value."
|
||||
);
|
||||
is(
|
||||
selectedItem.inputEl.tabIndex,
|
||||
0,
|
||||
"The selected picker item is focusable."
|
||||
);
|
||||
if (setsFocus) {
|
||||
is(
|
||||
document.activeElement,
|
||||
selectedItem,
|
||||
"The selected picker item is focused."
|
||||
);
|
||||
}
|
||||
items
|
||||
.filter(item => item !== selectedItem)
|
||||
.forEach(item => {
|
||||
ok(!item.checked, "All other picker items are unchecked.");
|
||||
is(
|
||||
item.inputEl.getAttribute("aria-checked"),
|
||||
"false",
|
||||
"All other items have the correct aria-checked value."
|
||||
);
|
||||
is(
|
||||
item.inputEl.tabIndex,
|
||||
-1,
|
||||
"All other items are not focusable."
|
||||
);
|
||||
});
|
||||
is(
|
||||
picker.value,
|
||||
selectedItem.value,
|
||||
"Picker value matches the value of the checked item."
|
||||
);
|
||||
}
|
||||
|
||||
// Verify the initial state.
|
||||
verifySelectedPickerItem(firstItem);
|
||||
|
||||
// Verify changing the checked property directly.
|
||||
secondItem.checked = true;
|
||||
await picker.updateComplete;
|
||||
verifySelectedPickerItem(secondItem);
|
||||
|
||||
// Verify clicking on a picker item to change checked state.
|
||||
synthesizeMouseAtCenter(thirdItem, {});
|
||||
await picker.updateComplete;
|
||||
verifySelectedPickerItem(thirdItem, true);
|
||||
|
||||
// Verify changing the picker value sets the selected state of child items.
|
||||
picker.value = "first";
|
||||
await picker.updateComplete;
|
||||
verifySelectedPickerItem(firstItem);
|
||||
});
|
||||
|
||||
// Verify that settings a value on the group works as expected creating
|
||||
// the elements programmatically via document.createElement. We ran into
|
||||
// issues with this in moz-visual-picker-item-group.
|
||||
add_task(async function testProgrammaticVisualPickerCreation() {
|
||||
let visualPicker = document.createElement("moz-visual-picker");
|
||||
visualPicker.label = "Created with document.createElement";
|
||||
visualPicker.value = "second";
|
||||
|
||||
let firstItem = document.createElement("moz-visual-picker-item");
|
||||
firstItem.value = "first";
|
||||
firstItem.label = "first";
|
||||
|
||||
let secondItem = document.createElement("moz-visual-picker-item");
|
||||
secondItem.value = "second";
|
||||
secondItem.label = "second";
|
||||
|
||||
visualPicker.append(firstItem, secondItem);
|
||||
testHelpers.render(html``, testHelpers.renderTarget);
|
||||
testHelpers.renderTarget.append(visualPicker);
|
||||
await visualPicker.updateComplete;
|
||||
|
||||
ok(!firstItem.checked, "The first item is not checked.");
|
||||
ok(secondItem.checked, "The second item is checked.");
|
||||
|
||||
visualPicker.value = "first";
|
||||
await visualPicker.updateComplete;
|
||||
|
||||
ok(firstItem.checked, "The first item is checked.");
|
||||
ok(!secondItem.checked, "The second item is no longer checked.");
|
||||
});
|
||||
|
||||
// Verify that keyboard navigation works as expected.
|
||||
add_task(async function testKeyboardNavigation() {
|
||||
let renderTarget = await testHelpers.renderTemplate(defaultTemplate);
|
||||
let picker = renderTarget.querySelector("moz-visual-picker");
|
||||
let [firstItem, secondItem, thirdItem] = renderTarget.querySelectorAll(
|
||||
"moz-visual-picker-item"
|
||||
);
|
||||
|
||||
async function keyboardNavigate(direction) {
|
||||
let keyCode = `KEY_Arrow${
|
||||
direction.charAt(0).toUpperCase() + direction.slice(1)
|
||||
}`;
|
||||
synthesizeKey(keyCode);
|
||||
await picker.updateComplete;
|
||||
}
|
||||
|
||||
function validateActiveElement(item) {
|
||||
is(
|
||||
document.activeElement,
|
||||
item,
|
||||
"Focus moves to the expected picker item."
|
||||
);
|
||||
is(picker.value, item.value, "Visual picker value is updated.");
|
||||
}
|
||||
|
||||
synthesizeMouseAtCenter(firstItem, {});
|
||||
await picker.updateComplete;
|
||||
|
||||
await keyboardNavigate("down");
|
||||
validateActiveElement(secondItem);
|
||||
|
||||
await keyboardNavigate("down");
|
||||
validateActiveElement(thirdItem);
|
||||
|
||||
await keyboardNavigate("down");
|
||||
validateActiveElement(firstItem);
|
||||
|
||||
await keyboardNavigate("up");
|
||||
validateActiveElement(thirdItem);
|
||||
|
||||
await keyboardNavigate("up");
|
||||
validateActiveElement(secondItem);
|
||||
|
||||
await keyboardNavigate("right");
|
||||
validateActiveElement(thirdItem);
|
||||
|
||||
await keyboardNavigate("right");
|
||||
validateActiveElement(firstItem);
|
||||
|
||||
await keyboardNavigate("left");
|
||||
validateActiveElement(thirdItem);
|
||||
|
||||
await keyboardNavigate("left");
|
||||
validateActiveElement(secondItem);
|
||||
|
||||
// Validate that disabled items get skipped over.
|
||||
thirdItem.disabled = true;
|
||||
await picker.updateComplete;
|
||||
|
||||
await keyboardNavigate("down");
|
||||
validateActiveElement(firstItem);
|
||||
|
||||
await keyboardNavigate("up");
|
||||
validateActiveElement(secondItem);
|
||||
|
||||
thirdItem.disabled = false;
|
||||
await picker.updateComplete;
|
||||
|
||||
// Validate left/right keys have opposite effect for RTL locales.
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [["intl.l10n.pseudo", "bidi"]],
|
||||
});
|
||||
|
||||
await keyboardNavigate("left");
|
||||
validateActiveElement(thirdItem);
|
||||
|
||||
await keyboardNavigate("left");
|
||||
validateActiveElement(firstItem);
|
||||
|
||||
await keyboardNavigate("right");
|
||||
validateActiveElement(thirdItem);
|
||||
|
||||
await keyboardNavigate("right");
|
||||
validateActiveElement(secondItem);
|
||||
|
||||
secondItem.click();
|
||||
await picker.updateComplete;
|
||||
validateActiveElement(secondItem);
|
||||
|
||||
await SpecialPowers.popPrefEnv();
|
||||
});
|
||||
|
||||
// Validate behavior when the picker has no value/no item is selected by default.
|
||||
add_task(async function testNoItemSelected() {
|
||||
let template = html`
|
||||
<button tabindex="0">Before picker</button>
|
||||
<moz-visual-picker name="test-name" label="No item selected">
|
||||
<moz-visual-picker-item value="first">
|
||||
<div class="slotted">In the slot</div>
|
||||
</moz-visual-picker-item>
|
||||
<moz-visual-picker-item value="second">
|
||||
<div class="slotted">In the slot</div>
|
||||
</moz-visual-picker-item>
|
||||
<moz-visual-picker-item value="third">
|
||||
<div class="slotted">In the slot</div>
|
||||
</moz-visual-picker-item>
|
||||
</moz-visual-picker>
|
||||
<button tabindex="0" id="after">After picker item></button>
|
||||
`;
|
||||
let renderTarget = await testHelpers.renderTemplate(template);
|
||||
let picker = renderTarget.querySelector("moz-visual-picker");
|
||||
let items = [
|
||||
...renderTarget.querySelectorAll("moz-visual-picker-item"),
|
||||
];
|
||||
let [firstItem, secondItem, thirdItem] = renderTarget.querySelectorAll(
|
||||
"moz-visual-picker-item"
|
||||
);
|
||||
let [beforeButton, afterButton] =
|
||||
renderTarget.querySelectorAll("button");
|
||||
|
||||
ok(!picker.value, "Visual picker does not have a value.");
|
||||
items.forEach(item =>
|
||||
ok(!item.checked, "All picker items are unselected.")
|
||||
);
|
||||
|
||||
beforeButton.focus();
|
||||
synthesizeKey("KEY_Tab", {});
|
||||
is(
|
||||
document.activeElement,
|
||||
firstItem,
|
||||
"First picker item is tab focusable when all items un-checked."
|
||||
);
|
||||
[secondItem, thirdItem].forEach(item =>
|
||||
is(
|
||||
item.inputEl.getAttribute("tabindex"),
|
||||
"-1",
|
||||
"All other items are not tab focusable."
|
||||
)
|
||||
);
|
||||
|
||||
synthesizeKey("KEY_Tab", {});
|
||||
is(
|
||||
document.activeElement,
|
||||
afterButton,
|
||||
"Tab moves focus out of the visual picker."
|
||||
);
|
||||
|
||||
synthesizeKey("KEY_Tab", { shiftKey: true });
|
||||
is(
|
||||
document.activeElement,
|
||||
firstItem,
|
||||
"Focus moves back to the first picker item."
|
||||
);
|
||||
|
||||
synthesizeKey("KEY_ArrowDown", {});
|
||||
is(
|
||||
document.activeElement,
|
||||
secondItem,
|
||||
"Focus moves to the second picker item with down arrow keypress."
|
||||
);
|
||||
is(
|
||||
picker.value,
|
||||
secondItem.value,
|
||||
"Picker value updates to second picker item value."
|
||||
);
|
||||
|
||||
secondItem.checked = false;
|
||||
await picker.updateComplete;
|
||||
|
||||
synthesizeKey("KEY_Tab", { shiftKey: true });
|
||||
ok(
|
||||
!picker.value,
|
||||
"Picker value is un-set when all picker items un-checked programmatically."
|
||||
);
|
||||
is(
|
||||
document.activeElement,
|
||||
firstItem,
|
||||
"First picker item becomes focusable again."
|
||||
);
|
||||
|
||||
synthesizeKey(" ");
|
||||
is(
|
||||
picker.value,
|
||||
firstItem.value,
|
||||
"Hitting space selects the focused item."
|
||||
);
|
||||
|
||||
synthesizeKey("KEY_Tab", { shiftKey: true });
|
||||
firstItem.disabled = true;
|
||||
secondItem.disabled = true;
|
||||
await picker.updateComplete;
|
||||
|
||||
synthesizeKey("KEY_Tab");
|
||||
is(
|
||||
document.activeElement,
|
||||
thirdItem,
|
||||
"First non-disabled picker item is focusable when all items are un-checked."
|
||||
);
|
||||
});
|
||||
|
||||
// Verify expected events emitted in the correct order.
|
||||
add_task(async function testPickerEvents() {
|
||||
let renderTarget = await testHelpers.renderTemplate(defaultTemplate);
|
||||
let picker = renderTarget.querySelector("moz-visual-picker");
|
||||
let items = renderTarget.querySelectorAll("moz-visual-picker-item");
|
||||
let [firstItem, secondItem, thirdItem] = items;
|
||||
let { trackEvent, verifyEvents } = testHelpers.getInputEventHelpers();
|
||||
|
||||
items.forEach(item => {
|
||||
item.addEventListener("click", trackEvent);
|
||||
item.addEventListener("input", trackEvent);
|
||||
item.addEventListener("change", trackEvent);
|
||||
});
|
||||
picker.addEventListener("change", trackEvent);
|
||||
picker.addEventListener("input", trackEvent);
|
||||
|
||||
// Verify that clicking on a item emits the right events in the correct order.
|
||||
synthesizeMouseAtCenter(thirdItem.inputEl, {});
|
||||
await TestUtils.waitForTick();
|
||||
|
||||
verifyEvents([
|
||||
{
|
||||
type: "click",
|
||||
value: "third",
|
||||
localName: "moz-visual-picker-item",
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
value: "third",
|
||||
localName: "moz-visual-picker-item",
|
||||
checked: true,
|
||||
},
|
||||
{ type: "input", value: "third", localName: "moz-visual-picker" },
|
||||
{
|
||||
type: "change",
|
||||
value: "third",
|
||||
localName: "moz-visual-picker-item",
|
||||
checked: true,
|
||||
},
|
||||
{ type: "change", value: "third", localName: "moz-visual-picker" },
|
||||
]);
|
||||
|
||||
// Verify that keyboard navigation emits the right events in the correct order.
|
||||
synthesizeKey("KEY_ArrowUp", {});
|
||||
await picker.updateComplete;
|
||||
is(picker.value, secondItem.value, "picker value is updated.");
|
||||
await TestUtils.waitForTick();
|
||||
|
||||
verifyEvents([
|
||||
{ type: "input", value: "second", localName: "moz-visual-picker" },
|
||||
{ type: "change", value: "second", localName: "moz-visual-picker" },
|
||||
]);
|
||||
|
||||
// Verify that changing the group's value directly doesn't emit any events.
|
||||
picker.value = firstItem.value;
|
||||
await picker.updateComplete;
|
||||
ok(firstItem.checked, "Expected item is checked.");
|
||||
await TestUtils.waitForTick();
|
||||
verifyEvents([]);
|
||||
|
||||
// Verify that changing a item's checked state directly doesn't emit any events.
|
||||
secondItem.checked = true;
|
||||
await picker.updateComplete;
|
||||
is(picker.value, secondItem.value, "Picker value is updated.");
|
||||
await TestUtils.waitForTick();
|
||||
verifyEvents([]);
|
||||
|
||||
// Verify activating item with space emits proper events.
|
||||
picker.value = "";
|
||||
await picker.updateComplete;
|
||||
|
||||
ok(!firstItem.checked, "The first item is not selected.");
|
||||
firstItem.focus();
|
||||
synthesizeKey(" ");
|
||||
await TestUtils.waitForTick();
|
||||
verifyEvents([
|
||||
{
|
||||
type: "click",
|
||||
value: "first",
|
||||
localName: "moz-visual-picker-item",
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
value: "first",
|
||||
localName: "moz-visual-picker-item",
|
||||
checked: true,
|
||||
},
|
||||
{ type: "input", value: "first", localName: "moz-visual-picker" },
|
||||
{
|
||||
type: "change",
|
||||
value: "first",
|
||||
localName: "moz-visual-picker-item",
|
||||
checked: true,
|
||||
},
|
||||
{ type: "change", value: "first", localName: "moz-visual-picker" },
|
||||
]);
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none"></div>
|
||||
<pre id="test"></pre>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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`<input
|
||||
type="radio"
|
||||
id="input"
|
||||
@@ -356,7 +368,7 @@ export class MozRadio extends MozBaseInputElement {
|
||||
aria-checked=${this.checked}
|
||||
aria-describedby="description"
|
||||
tabindex=${this.inputTabIndex}
|
||||
?disabled=${isDisabled}
|
||||
?disabled=${this.isDisabled}
|
||||
accesskey=${ifDefined(this.accessKey)}
|
||||
@click=${this.handleClick}
|
||||
@change=${this.handleChange}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/* 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/. */
|
||||
|
||||
:host {
|
||||
--visual-picker-item-border-radius: var(--border-radius-medium);
|
||||
--visual-picker-item-border-width: var(--border-width);
|
||||
}
|
||||
|
||||
.picker-item {
|
||||
border: var(--visual-picker-item-border-width) solid var(--border-color-interactive);
|
||||
border-radius: var(--visual-picker-item-border-radius);
|
||||
margin: 2px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: var(--focus-outline);
|
||||
outline-offset: var(--focus-outline-offset);
|
||||
}
|
||||
|
||||
&[checked] {
|
||||
--visual-picker-item-border-width: 3px;
|
||||
border-color: var(--color-accent-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
::slotted(:first-child) {
|
||||
--visual-picker-item-child-border-radius: calc(var(--visual-picker-item-border-radius) - var(--visual-picker-item-border-width));
|
||||
border-radius: var(--visual-picker-item-child-border-radius);
|
||||
}
|
||||
}
|
||||
110
toolkit/content/widgets/moz-visual-picker/moz-visual-picker.mjs
Normal file
110
toolkit/content/widgets/moz-visual-picker/moz-visual-picker.mjs
Normal file
@@ -0,0 +1,110 @@
|
||||
/* 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 } from "chrome://global/content/vendor/lit.all.mjs";
|
||||
import {
|
||||
MozRadioGroup,
|
||||
MozRadio,
|
||||
} from "chrome://global/content/elements/moz-radio-group.mjs";
|
||||
|
||||
/**
|
||||
* An element that groups related items and allows a user to navigate between
|
||||
* them to select an item. The appearance of the items of the group is
|
||||
* determined by the consumer.
|
||||
*
|
||||
* @tagname moz-visual-picker
|
||||
* @property {string} label - Label for the group of elements.
|
||||
* @property {string} description - Description for the group of elements.
|
||||
* @property {string} name
|
||||
* Name used to associate items in the group. Propagates to
|
||||
* moz-visual-picker's children.
|
||||
* @property {string} value
|
||||
* Selected value for the group. Changing the value updates the checked
|
||||
* state of moz-visual-picker-item children and vice versa.
|
||||
* @slot default - The picker's content, intended for moz-visual-picker-items.
|
||||
*/
|
||||
export class MozVisualPicker extends MozRadioGroup {
|
||||
static childElementName = "moz-visual-picker-item";
|
||||
}
|
||||
customElements.define("moz-visual-picker", MozVisualPicker);
|
||||
|
||||
/**
|
||||
* Element that allows a user to select one option from a group of options.
|
||||
* Visual appearance is determined by the slotted content.
|
||||
*
|
||||
* @tagname moz-visual-picker-item
|
||||
* @property {boolean} checked - Whether or not the item is selected.
|
||||
* @property {boolean} disabled - Whether or not the item is disabled.
|
||||
* @property {number} inputTabIndex
|
||||
* Tabindex of the input element. Only one item is focusable at a time.
|
||||
* @property {string} name
|
||||
* Name of the item, set by the associated moz-visual-picker parent element.
|
||||
* @property {string} value - Value of the item.
|
||||
* @slot default - The item's content, used for what gets displayed.
|
||||
*/
|
||||
export class MozVisualPickerItem extends MozRadio {
|
||||
static queries = {
|
||||
itemEl: ".picker-item",
|
||||
};
|
||||
|
||||
get inputEl() {
|
||||
return this.itemEl;
|
||||
}
|
||||
|
||||
handleKeydown(event) {
|
||||
if (event.keyCode == KeyEvent.DOM_VK_SPACE) {
|
||||
this.handleClick(event);
|
||||
}
|
||||
}
|
||||
|
||||
handleClick(event) {
|
||||
// re-target click events from the slot to the item and handle clicks from
|
||||
// space bar keydown.
|
||||
event.stopPropagation();
|
||||
this.dispatchEvent(
|
||||
new Event("click", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
|
||||
super.handleClick();
|
||||
|
||||
// Manually dispatch events since we're not using an input.
|
||||
this.dispatchEvent(
|
||||
new Event("input", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
this.dispatchEvent(
|
||||
new Event("change", {
|
||||
bubbles: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="chrome://global/content/elements/moz-visual-picker-item.css"
|
||||
/>
|
||||
<div
|
||||
class="picker-item"
|
||||
role="radio"
|
||||
value=${this.value}
|
||||
aria-checked=${this.checked}
|
||||
tabindex=${this.inputTabIndex}
|
||||
?checked=${this.checked}
|
||||
?disabled=${this.isDisabled}
|
||||
@click=${this.handleClick}
|
||||
@keydown=${this.handleKeydown}
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
customElements.define("moz-visual-picker-item", MozVisualPickerItem);
|
||||
@@ -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`<div class="slotted demo-card">
|
||||
<img
|
||||
src="chrome://browser/content/profiles/assets/system-theme-background.svg"
|
||||
/>
|
||||
<span>I'm card number ${index + 1}</span>
|
||||
</div>`;
|
||||
}
|
||||
return html`<div class="slotted avatar">
|
||||
<img src=${AVATAR_ICONS[index]} role="presentation" />
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const Template = ({ value, slottedItem, pickerL10nId, supportPage }) => {
|
||||
return html`
|
||||
<style>
|
||||
.slotted {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
flex-direction: column;
|
||||
width: 120px;
|
||||
|
||||
span {
|
||||
padding: var(--space-xsmall);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
img {
|
||||
border-top-left-radius: inherit;
|
||||
border-top-right-radius: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-item {
|
||||
--visual-picker-item-border-radius: var(--border-radius-circle);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
|
||||
img {
|
||||
height: var(--icon-size-default);
|
||||
width: var(--icon-size-default);
|
||||
-moz-context-properties: fill;
|
||||
fill: var(--icon-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<moz-visual-picker
|
||||
data-l10n-id=${pickerL10nId}
|
||||
value=${ifDefined(value)}
|
||||
support-page=${supportPage}
|
||||
>
|
||||
${[...Array.from({ length: 3 })].map(
|
||||
(_, i) =>
|
||||
html`<moz-visual-picker-item
|
||||
value=${i + 1}
|
||||
class=${classMap({ "avatar-item": slottedItem == "avatar" })}
|
||||
>
|
||||
${getSlottedContent(slottedItem, i)}
|
||||
</moz-visual-picker-item>`
|
||||
)}
|
||||
</moz-visual-picker>
|
||||
`;
|
||||
};
|
||||
|
||||
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: "",
|
||||
};
|
||||
Reference in New Issue
Block a user