Bug 1942390 - Part 2: refactor common logic out of moz-radio-* elements r=reusable-components-reviewers,mstriemer

Differential Revision: https://phabricator.services.mozilla.com/D245620
This commit is contained in:
Hanna Jones
2025-05-16 16:08:03 +00:00
committed by hjones@mozilla.com
parent 3b0be84c10
commit d68a89bd79
7 changed files with 385 additions and 326 deletions

View File

@@ -159,6 +159,7 @@ toolkit.jar:
content/global/elements/wizard.js (widgets/wizard.js) content/global/elements/wizard.js (widgets/wizard.js)
content/global/vendor/lit.all.mjs (widgets/vendor/lit.all.mjs) content/global/vendor/lit.all.mjs (widgets/vendor/lit.all.mjs)
content/global/lit-utils.mjs (widgets/lit-utils.mjs) content/global/lit-utils.mjs (widgets/lit-utils.mjs)
content/global/lit-select-control.mjs (widgets/lit-select-control.mjs)
content/global/neterror/aboutNetErrorCodes.js (neterror/aboutNetErrorCodes.js) content/global/neterror/aboutNetErrorCodes.js (neterror/aboutNetErrorCodes.js)
content/global/neterror/supportpages/connection-not-secure.html (neterror/supportpages/connection-not-secure.html) content/global/neterror/supportpages/connection-not-secure.html (neterror/supportpages/connection-not-secure.html)
content/global/neterror/supportpages/time-errors.html (neterror/supportpages/time-errors.html) content/global/neterror/supportpages/time-errors.html (neterror/supportpages/time-errors.html)

View File

@@ -94,12 +94,12 @@
function verifySelectedPickerItem(selectedItem, setsFocus) { function verifySelectedPickerItem(selectedItem, setsFocus) {
ok(selectedItem.checked, "The selected picker item is checked."); ok(selectedItem.checked, "The selected picker item is checked.");
is( is(
selectedItem.inputEl.getAttribute("aria-checked"), selectedItem.itemEl.getAttribute("aria-checked"),
"true", "true",
"The checked item has the correct aria-checked value." "The checked item has the correct aria-checked value."
); );
is( is(
selectedItem.inputEl.tabIndex, selectedItem.itemEl.tabIndex,
0, 0,
"The selected picker item is focusable." "The selected picker item is focusable."
); );
@@ -115,12 +115,12 @@
.forEach(item => { .forEach(item => {
ok(!item.checked, "All other picker items are unchecked."); ok(!item.checked, "All other picker items are unchecked.");
is( is(
item.inputEl.getAttribute("aria-checked"), item.itemEl.getAttribute("aria-checked"),
"false", "false",
"All other items have the correct aria-checked value." "All other items have the correct aria-checked value."
); );
is( is(
item.inputEl.tabIndex, item.itemEl.tabIndex,
-1, -1,
"All other items are not focusable." "All other items are not focusable."
); );
@@ -316,7 +316,7 @@
); );
[secondItem, thirdItem].forEach(item => [secondItem, thirdItem].forEach(item =>
is( is(
item.inputEl.getAttribute("tabindex"), item.itemEl.getAttribute("tabindex"),
"-1", "-1",
"All other items are not tab focusable." "All other items are not tab focusable."
) )
@@ -399,7 +399,7 @@
picker.addEventListener("input", trackEvent); picker.addEventListener("input", trackEvent);
// Verify that clicking on a item emits the right events in the correct order. // Verify that clicking on a item emits the right events in the correct order.
synthesizeMouseAtCenter(thirdItem.inputEl, {}); synthesizeMouseAtCenter(thirdItem.itemEl, {});
await TestUtils.waitForTick(); await TestUtils.waitForTick();
verifyEvents([ verifyEvents([

View File

@@ -0,0 +1,337 @@
/* 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 } from "./vendor/lit.all.mjs";
import { MozLitElement } from "./lit-utils.mjs";
const NAVIGATION_FORWARD = "forward";
const NAVIGATION_BACKWARD = "backward";
const NAVIGATION_VALUE = {
[NAVIGATION_FORWARD]: 1,
[NAVIGATION_BACKWARD]: -1,
};
const DIRECTION_RIGHT = "Right";
const DIRECTION_LEFT = "Left";
const NAVIGATION_DIRECTIONS = {
LTR: {
FORWARD: DIRECTION_RIGHT,
BACKWARD: DIRECTION_LEFT,
},
RTL: {
FORWARD: DIRECTION_LEFT,
BACKWARD: DIRECTION_RIGHT,
},
};
/**
* Class that can be extended to handle managing the selected and focus states
* of child elements using a roving tabindex. For more information on this focus
* management pattern, see:
* https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_roving_tabindex
*
* Child elements must use SelectControlItemMixin for behavior to work as
* expected.
*/
export class SelectControlBaseElement extends MozLitElement {
#childElements;
#value;
static properties = {
disabled: { type: Boolean, reflect: true },
description: { type: String, fluent: true },
supportPage: { type: String, attribute: "support-page" },
label: { type: String, fluent: true },
name: { type: String },
value: { type: String },
};
static queries = {
fieldset: "moz-fieldset",
};
set value(newValue) {
this.#value = newValue;
this.childElements.forEach(item => {
item.checked = this.value === item.value;
});
this.syncFocusState();
}
get value() {
return this.#value;
}
get focusableIndex() {
if (this.#value) {
let selectedIndex = this.childElements.findIndex(
item => item.value === this.#value && !item.disabled
);
if (selectedIndex !== -1) {
return selectedIndex;
}
}
return this.childElements.findIndex(item => !item.disabled);
}
// Query for child elements the first time they are needed + ensure they
// have been upgraded so we can access properties.
get childElements() {
if (!this.#childElements) {
this.#childElements = (
this.shadowRoot
?.querySelector("slot:not([name])")
?.assignedElements() || [...this.children]
)?.filter(
el => el.localName === this.constructor.childElementName && !el.slot
);
this.#childElements.forEach(item => customElements.upgrade(item));
}
return this.#childElements;
}
constructor() {
super();
this.disabled = false;
this.addEventListener("keydown", e => this.handleKeydown(e));
}
firstUpdated() {
this.syncStateToChildElements();
}
async getUpdateComplete() {
await super.getUpdateComplete();
await Promise.all(this.childElements.map(item => item.updateComplete));
}
syncStateToChildElements() {
this.childElements.forEach(item => {
if (item.checked && this.value == undefined) {
this.value = item.value;
}
item.name = this.name;
});
this.syncFocusState();
}
syncFocusState() {
let focusableIndex = this.focusableIndex;
this.childElements.forEach((item, index) => {
item.itemTabIndex = focusableIndex === index ? 0 : -1;
});
}
// NB: We may need to revise this to avoid bugs when we add more focusable
// elements to select control base/items.
handleKeydown(event) {
let directions = this.getNavigationDirections();
switch (event.key) {
case "Down":
case "ArrowDown":
case directions.FORWARD:
case `Arrow${directions.FORWARD}`: {
event.preventDefault();
this.navigate(NAVIGATION_FORWARD);
break;
}
case "Up":
case "ArrowUp":
case directions.BACKWARD:
case `Arrow${directions.BACKWARD}`: {
event.preventDefault();
this.navigate(NAVIGATION_BACKWARD);
break;
}
}
}
getNavigationDirections() {
if (this.isDocumentRTL) {
return NAVIGATION_DIRECTIONS.RTL;
}
return NAVIGATION_DIRECTIONS.LTR;
}
get isDocumentRTL() {
if (typeof Services !== "undefined") {
return Services.locale.isAppLocaleRTL;
}
return document.dir === "rtl";
}
navigate(direction) {
let currentIndex = this.focusableIndex;
let children = this.childElements;
let indexStep = children.length + NAVIGATION_VALUE[direction];
for (let i = 1; i < children.length; i++) {
let nextIndex = (currentIndex + indexStep * i) % children.length;
let nextItem = children[nextIndex];
if (!nextItem.disabled) {
this.value = nextItem.value;
nextItem.focus();
this.dispatchEvent(new Event("input"), {
bubbles: true,
composed: true,
});
this.dispatchEvent(new Event("change"), { bubbles: true });
return;
}
}
}
willUpdate(changedProperties) {
if (changedProperties.has("name")) {
this.handleSetName();
}
if (changedProperties.has("disabled")) {
this.childElements.forEach(item => {
item.requestUpdate();
});
}
}
handleSetName() {
this.childElements.forEach(item => {
item.name = this.name;
});
}
// Re-dispatch change event so it's re-targeted to the custom element.
handleChange(event) {
event.stopPropagation();
this.dispatchEvent(new Event(event.type));
}
handleSlotChange() {
this.#childElements = null;
this.syncStateToChildElements();
}
render() {
return html`
<moz-fieldset
part="fieldset"
description=${ifDefined(this.description)}
support-page=${ifDefined(this.supportPage)}
role="radiogroup"
?disabled=${this.disabled}
label=${this.label}
exportparts="inputs, support-link"
aria-orientation=${ifDefined(this.constructor.orientation)}
>
${!this.supportPage
? html`<slot slot="support-link" name="support-link"></slot>`
: ""}
<slot
@slotchange=${this.handleSlotChange}
@change=${this.handleChange}
></slot>
</moz-fieldset>
`;
}
}
/**
* Class that can be extended by items nested in a subclass of
* SelectControlBaseElement to handle selection, focus management, and keyboard
* navigation. Implemented as a mixin to enable use with elements that inherit
* from something other than MozLitElement.
*
* @param {LitElement} superClass
* @returns LitElement
*/
export const SelectControlItemMixin = superClass =>
class extends superClass {
#controller;
static properties = {
name: { type: String },
value: { type: String },
disabled: { type: Boolean, reflect: true },
checked: { type: Boolean, reflect: true },
itemTabIndex: { type: Number, state: true },
};
get controller() {
return this.#controller;
}
get isDisabled() {
return this.disabled || this.#controller.disabled;
}
constructor() {
super();
this.checked = false;
}
connectedCallback() {
super.connectedCallback();
let hostElement = this.parentElement || this.getRootNode().host;
if (!(hostElement instanceof SelectControlBaseElement)) {
console.error(
`${this.localName} should only be used in an element that extends SelectControlBaseElement.`
);
}
this.#controller = hostElement;
if (this.#controller.value) {
this.checked = this.value === this.#controller.value;
}
}
willUpdate(changedProperties) {
super.willUpdate(changedProperties);
// Handle setting checked directly via JS.
if (
changedProperties.has("checked") &&
this.checked &&
this.#controller.value &&
this.value !== this.#controller.value
) {
this.#controller.value = this.value;
}
// Handle un-checking directly via JS. If the checked item is un-checked,
// the value of the associated focus manager parent needs to be un-set.
if (
changedProperties.has("checked") &&
!this.checked &&
this.#controller.value &&
this.value === this.#controller.value
) {
this.#controller.value = "";
}
if (changedProperties.has("disabled")) {
// Prevent enabling a items if containing focus manager is disabled.
if (this.disabled === false && this.#controller.disabled) {
this.disabled = true;
} else if (this.checked || !this.#controller.value) {
// Update items via focus manager parent for proper keyboard nav behavior.
this.#controller.syncFocusState();
}
}
}
handleClick() {
if (this.isDisabled || this.checked) {
return;
}
this.#controller.value = this.value;
if (this.getRootNode().activeElement?.localName == this.localName) {
this.focus();
}
}
// Re-dispatch change event so it propagates out of the element.
handleChange(e) {
this.dispatchEvent(new Event(e.type, e));
}
};

View File

@@ -36,6 +36,10 @@ legend {
flex-direction: column; flex-direction: column;
gap: var(--space-large); gap: var(--space-large);
margin-top: var(--space-small); margin-top: var(--space-small);
fieldset[aria-orientation="horizontal"] & {
flex-direction: row;
}
} }
a[is="moz-support-link"], a[is="moz-support-link"],

View File

@@ -18,6 +18,7 @@ export default class MozFieldset extends MozLitElement {
label: { type: String, fluent: true }, label: { type: String, fluent: true },
description: { type: String, fluent: true }, description: { type: String, fluent: true },
supportPage: { type: String, attribute: "support-page" }, supportPage: { type: String, attribute: "support-page" },
ariaOrientation: { type: String, mapped: true },
}; };
descriptionTemplate() { descriptionTemplate() {
@@ -29,6 +30,7 @@ export default class MozFieldset extends MozLitElement {
} }
return ""; return "";
} }
supportPageTemplate() { supportPageTemplate() {
if (this.supportPage) { if (this.supportPage) {
return html`<a return html`<a
@@ -47,7 +49,10 @@ export default class MozFieldset extends MozLitElement {
href="chrome://global/content/elements/moz-fieldset.css" href="chrome://global/content/elements/moz-fieldset.css"
/> />
<fieldset <fieldset
aria-describedby=${ifDefined(this.description ? "description" : null)} aria-describedby=${ifDefined(
this.description ? "description" : undefined
)}
aria-orientation=${ifDefined(this.ariaOrientation)}
> >
<legend part="label">${this.label}</legend> <legend part="label">${this.label}</legend>
${!this.description ? this.supportPageTemplate() : ""} ${!this.description ? this.supportPageTemplate() : ""}

View File

@@ -3,35 +3,11 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { html, ifDefined } from "../vendor/lit.all.mjs"; import { html, ifDefined } from "../vendor/lit.all.mjs";
import { MozLitElement, MozBaseInputElement } from "../lit-utils.mjs"; import {
// eslint-disable-next-line import/no-unassigned-import SelectControlBaseElement,
import "chrome://global/content/elements/moz-label.mjs"; SelectControlItemMixin,
// eslint-disable-next-line import/no-unassigned-import } from "../lit-select-control.mjs";
import "chrome://global/content/elements/moz-fieldset.mjs"; import { MozBaseInputElement } from "../lit-utils.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "chrome://global/content/elements/moz-support-link.mjs";
const NAVIGATION_FORWARD = "forward";
const NAVIGATION_BACKWARD = "backward";
const NAVIGATION_VALUE = {
[NAVIGATION_FORWARD]: 1,
[NAVIGATION_BACKWARD]: -1,
};
const DIRECTION_RIGHT = "Right";
const DIRECTION_LEFT = "Left";
const NAVIGATION_DIRECTIONS = {
LTR: {
FORWARD: DIRECTION_RIGHT,
BACKWARD: DIRECTION_LEFT,
},
RTL: {
FORWARD: DIRECTION_LEFT,
BACKWARD: DIRECTION_RIGHT,
},
};
/** /**
* Element used to group and associate moz-radio buttons so that they function * Element used to group and associate moz-radio buttons so that they function
@@ -50,205 +26,13 @@ const NAVIGATION_DIRECTIONS = {
* @slot default - The radio group's content, intended for moz-radio elements. * @slot default - The radio group's content, intended for moz-radio elements.
* @slot support-link - The radio group's support link intended for moz-radio elements. * @slot support-link - The radio group's support link intended for moz-radio elements.
*/ */
export class MozRadioGroup extends MozLitElement { export class MozRadioGroup extends SelectControlBaseElement {
#radioButtons;
#value;
static childElementName = "moz-radio"; static childElementName = "moz-radio";
static orientation = "vertical";
static properties = { static properties = {
disabled: { type: Boolean, reflect: true },
description: { type: String, fluent: true },
supportPage: { type: String, attribute: "support-page" },
label: { type: String, fluent: true },
name: { type: String },
value: { type: String },
parentDisabled: { type: Boolean, state: true }, parentDisabled: { type: Boolean, state: true },
}; };
static queries = {
fieldset: "moz-fieldset",
};
set value(newValue) {
this.#value = newValue;
this.radioButtons.forEach(button => {
button.checked = this.value === button.value;
});
this.syncFocusState();
}
get value() {
return this.#value;
}
get focusableIndex() {
if (this.#value) {
let selectedIndex = this.radioButtons.findIndex(
button => button.value === this.#value && !button.disabled
);
if (selectedIndex !== -1) {
return selectedIndex;
}
}
return this.radioButtons.findIndex(button => !button.disabled);
}
// Query for moz-radio elements the first time they are needed + ensure they
// have been upgraded so we can access properties.
get radioButtons() {
if (!this.#radioButtons) {
this.#radioButtons = (
this.shadowRoot
?.querySelector("slot:not([name])")
?.assignedElements() || [...this.children]
)?.filter(
el => el.localName === this.constructor.childElementName && !el.slot
);
this.#radioButtons.forEach(button => customElements.upgrade(button));
}
return this.#radioButtons;
}
constructor() {
super();
this.disabled = false;
this.addEventListener("keydown", e => this.handleKeydown(e));
}
firstUpdated() {
this.syncStateToRadioButtons();
}
async getUpdateComplete() {
await super.getUpdateComplete();
await Promise.all(this.radioButtons.map(button => button.updateComplete));
}
syncStateToRadioButtons() {
this.radioButtons.forEach(button => {
if (button.checked && this.value == undefined) {
this.value = button.value;
}
button.name = this.name;
});
this.syncFocusState();
}
syncFocusState() {
let focusableIndex = this.focusableIndex;
this.radioButtons.forEach((button, index) => {
button.inputTabIndex = focusableIndex === index ? 0 : -1;
});
}
// NB: We may need to revise this to avoid bugs when we add more focusable
// elements to moz-radio-group / moz-radio.
handleKeydown(event) {
let directions = this.getNavigationDirections();
switch (event.key) {
case "Down":
case "ArrowDown":
case directions.FORWARD:
case `Arrow${directions.FORWARD}`: {
event.preventDefault();
this.navigate(NAVIGATION_FORWARD);
break;
}
case "Up":
case "ArrowUp":
case directions.BACKWARD:
case `Arrow${directions.BACKWARD}`: {
event.preventDefault();
this.navigate(NAVIGATION_BACKWARD);
break;
}
}
}
getNavigationDirections() {
if (this.isDocumentRTL) {
return NAVIGATION_DIRECTIONS.RTL;
}
return NAVIGATION_DIRECTIONS.LTR;
}
get isDocumentRTL() {
if (typeof Services !== "undefined") {
return Services.locale.isAppLocaleRTL;
}
return document.dir === "rtl";
}
navigate(direction) {
let currentIndex = this.focusableIndex;
let indexStep = this.radioButtons.length + NAVIGATION_VALUE[direction];
for (let i = 1; i < this.radioButtons.length; i++) {
let nextIndex = (currentIndex + indexStep * i) % this.radioButtons.length;
let nextButton = this.radioButtons[nextIndex];
if (!nextButton.disabled) {
this.value = nextButton.value;
nextButton.focus();
this.dispatchEvent(new Event("input"), {
bubbles: true,
composed: true,
});
this.dispatchEvent(new Event("change"), { bubbles: true });
return;
}
}
}
willUpdate(changedProperties) {
if (changedProperties.has("name")) {
this.handleSetName();
}
if (changedProperties.has("disabled")) {
this.radioButtons.forEach(button => {
button.requestUpdate();
});
}
}
handleSetName() {
this.radioButtons.forEach(button => {
button.name = this.name;
});
}
// Re-dispatch change event so it's re-targeted to moz-radio-group.
handleChange(event) {
event.stopPropagation();
this.dispatchEvent(new Event(event.type));
}
handleSlotChange() {
this.#radioButtons = null;
this.syncStateToRadioButtons();
}
render() {
return html`
<moz-fieldset
part="fieldset"
description=${ifDefined(this.description)}
support-page=${ifDefined(this.supportPage)}
role="radiogroup"
?disabled=${this.disabled}
label=${this.label}
exportparts="inputs, support-link"
>
${!this.supportPage
? html`<slot slot="support-link" name="support-link"></slot>`
: ""}
<slot
@slotchange=${this.handleSlotChange}
@change=${this.handleChange}
></slot>
</moz-fieldset>
`;
}
} }
customElements.define("moz-radio-group", MozRadioGroup); customElements.define("moz-radio-group", MozRadioGroup);
@@ -260,104 +44,22 @@ customElements.define("moz-radio-group", MozRadioGroup);
* @property {string} description - Description for the input. * @property {string} description - Description for the input.
* @property {boolean} disabled - Whether or not the input is disabled. * @property {boolean} disabled - Whether or not the input is disabled.
* @property {string} iconSrc - Path to an icon displayed next to the input. * @property {string} iconSrc - Path to an icon displayed next to the input.
* @property {number} inputTabIndex - Tabindex of the input element. * @property {number} itemTabIndex - Tabindex of the input element.
* @property {string} label - Label for the radio input. * @property {string} label - Label for the radio input.
* @property {string} name * @property {string} name
* Name of the input control, set by the associated moz-radio-group element. * Name of the input control, set by the associated moz-radio-group element.
* @property {string} supportPage - Name of the SUMO support page to link to. * @property {string} supportPage - Name of the SUMO support page to link to.
* @property {number} value - Value of the radio input. * @property {string} value - Value of the radio input.
*/ */
export class MozRadio extends MozBaseInputElement { export class MozRadio extends SelectControlItemMixin(MozBaseInputElement) {
#controller;
static properties = {
checked: { type: Boolean, reflect: true },
inputTabIndex: { type: Number, state: true },
};
static activatedProperty = "checked"; static activatedProperty = "checked";
get controller() {
return this.#controller;
}
get isDisabled() { get isDisabled() {
return ( return (
this.disabled || super.isDisabled || this.parentDisabled || this.controller.parentDisabled
this.#controller.disabled ||
this.parentDisabled ||
this.#controller.parentDisabled
); );
} }
constructor() {
super();
this.checked = false;
}
connectedCallback() {
super.connectedCallback();
let hostRadioGroup = this.parentElement || this.getRootNode().host;
if (!(hostRadioGroup instanceof MozRadioGroup)) {
console.error("moz-radio can only be used in moz-radio-group element.");
}
this.#controller = hostRadioGroup;
if (this.#controller.value) {
this.checked = this.value === this.#controller.value;
}
}
willUpdate(changedProperties) {
super.willUpdate(changedProperties);
// Handle setting checked directly via JS.
if (
changedProperties.has("checked") &&
this.checked &&
this.#controller.value &&
this.value !== this.#controller.value
) {
this.#controller.value = this.value;
}
// Handle un-checking directly via JS. If the checked input is un-checked,
// the value of the associated moz-radio-group needs to be un-set.
if (
changedProperties.has("checked") &&
!this.checked &&
this.#controller.value &&
this.value === this.#controller.value
) {
this.#controller.value = "";
}
if (changedProperties.has("disabled")) {
// Prevent enabling a radio button if containing radio-group is disabled.
if (this.disabled === false && this.#controller.disabled) {
this.disabled = true;
} else if (this.checked || !this.#controller.value) {
// Update buttons via moz-radio-group for proper keyboard nav behavior.
this.#controller.syncFocusState();
}
}
}
handleClick() {
if (this.isDisabled || this.checked) {
return;
}
this.#controller.value = this.value;
if (this.getRootNode().activeElement?.localName == this.localName) {
this.focus();
}
}
// Re-dispatch change event so it propagates out of moz-radio.
handleChange(e) {
this.dispatchEvent(new Event(e.type, e));
}
inputTemplate() { inputTemplate() {
return html`<input return html`<input
type="radio" type="radio"
@@ -367,7 +69,7 @@ export class MozRadio extends MozBaseInputElement {
.checked=${this.checked} .checked=${this.checked}
aria-checked=${this.checked} aria-checked=${this.checked}
aria-describedby="description" aria-describedby="description"
tabindex=${this.inputTabIndex} tabindex=${this.itemTabIndex}
?disabled=${this.isDisabled} ?disabled=${this.isDisabled}
accesskey=${ifDefined(this.accessKey)} accesskey=${ifDefined(this.accessKey)}
@click=${this.handleClick} @click=${this.handleClick}

View File

@@ -4,9 +4,10 @@
import { html } from "chrome://global/content/vendor/lit.all.mjs"; import { html } from "chrome://global/content/vendor/lit.all.mjs";
import { import {
MozRadioGroup, SelectControlItemMixin,
MozRadio, SelectControlBaseElement,
} from "chrome://global/content/elements/moz-radio-group.mjs"; } from "../lit-select-control.mjs";
import { MozLitElement } from "../lit-utils.mjs";
/** /**
* An element that groups related items and allows a user to navigate between * An element that groups related items and allows a user to navigate between
@@ -24,8 +25,9 @@ import {
* state of moz-visual-picker-item children and vice versa. * state of moz-visual-picker-item children and vice versa.
* @slot default - The picker's content, intended for moz-visual-picker-items. * @slot default - The picker's content, intended for moz-visual-picker-items.
*/ */
export class MozVisualPicker extends MozRadioGroup { export class MozVisualPicker extends SelectControlBaseElement {
static childElementName = "moz-visual-picker-item"; static childElementName = "moz-visual-picker-item";
static orientation = "horizontal";
} }
customElements.define("moz-visual-picker", MozVisualPicker); customElements.define("moz-visual-picker", MozVisualPicker);
@@ -36,20 +38,28 @@ customElements.define("moz-visual-picker", MozVisualPicker);
* @tagname moz-visual-picker-item * @tagname moz-visual-picker-item
* @property {boolean} checked - Whether or not the item is selected. * @property {boolean} checked - Whether or not the item is selected.
* @property {boolean} disabled - Whether or not the item is disabled. * @property {boolean} disabled - Whether or not the item is disabled.
* @property {number} inputTabIndex * @property {number} itemTabIndex
* Tabindex of the input element. Only one item is focusable at a time. * Tabindex of the input element. Only one item is focusable at a time.
* @property {string} name * @property {string} name
* Name of the item, set by the associated moz-visual-picker parent element. * Name of the item, set by the associated moz-visual-picker parent element.
* @property {string} value - Value of the item. * @property {string} value - Value of the item.
* @slot default - The item's content, used for what gets displayed. * @slot default - The item's content, used for what gets displayed.
*/ */
export class MozVisualPickerItem extends MozRadio { export class MozVisualPickerItem extends SelectControlItemMixin(MozLitElement) {
static queries = { static queries = {
itemEl: ".picker-item", itemEl: ".picker-item",
}; };
get inputEl() { click() {
return this.itemEl; this.itemEl.click();
}
focus() {
this.itemEl.focus();
}
blur() {
this.itemEl.blur();
} }
handleKeydown(event) { handleKeydown(event) {
@@ -96,7 +106,7 @@ export class MozVisualPickerItem extends MozRadio {
role="radio" role="radio"
value=${this.value} value=${this.value}
aria-checked=${this.checked} aria-checked=${this.checked}
tabindex=${this.inputTabIndex} tabindex=${this.itemTabIndex}
?checked=${this.checked} ?checked=${this.checked}
?disabled=${this.isDisabled} ?disabled=${this.isDisabled}
@click=${this.handleClick} @click=${this.handleClick}