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:
committed by
hjones@mozilla.com
parent
3b0be84c10
commit
d68a89bd79
@@ -159,6 +159,7 @@ toolkit.jar:
|
||||
content/global/elements/wizard.js (widgets/wizard.js)
|
||||
content/global/vendor/lit.all.mjs (widgets/vendor/lit.all.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/supportpages/connection-not-secure.html (neterror/supportpages/connection-not-secure.html)
|
||||
content/global/neterror/supportpages/time-errors.html (neterror/supportpages/time-errors.html)
|
||||
|
||||
@@ -94,12 +94,12 @@
|
||||
function verifySelectedPickerItem(selectedItem, setsFocus) {
|
||||
ok(selectedItem.checked, "The selected picker item is checked.");
|
||||
is(
|
||||
selectedItem.inputEl.getAttribute("aria-checked"),
|
||||
selectedItem.itemEl.getAttribute("aria-checked"),
|
||||
"true",
|
||||
"The checked item has the correct aria-checked value."
|
||||
);
|
||||
is(
|
||||
selectedItem.inputEl.tabIndex,
|
||||
selectedItem.itemEl.tabIndex,
|
||||
0,
|
||||
"The selected picker item is focusable."
|
||||
);
|
||||
@@ -115,12 +115,12 @@
|
||||
.forEach(item => {
|
||||
ok(!item.checked, "All other picker items are unchecked.");
|
||||
is(
|
||||
item.inputEl.getAttribute("aria-checked"),
|
||||
item.itemEl.getAttribute("aria-checked"),
|
||||
"false",
|
||||
"All other items have the correct aria-checked value."
|
||||
);
|
||||
is(
|
||||
item.inputEl.tabIndex,
|
||||
item.itemEl.tabIndex,
|
||||
-1,
|
||||
"All other items are not focusable."
|
||||
);
|
||||
@@ -316,7 +316,7 @@
|
||||
);
|
||||
[secondItem, thirdItem].forEach(item =>
|
||||
is(
|
||||
item.inputEl.getAttribute("tabindex"),
|
||||
item.itemEl.getAttribute("tabindex"),
|
||||
"-1",
|
||||
"All other items are not tab focusable."
|
||||
)
|
||||
@@ -399,7 +399,7 @@
|
||||
picker.addEventListener("input", trackEvent);
|
||||
|
||||
// Verify that clicking on a item emits the right events in the correct order.
|
||||
synthesizeMouseAtCenter(thirdItem.inputEl, {});
|
||||
synthesizeMouseAtCenter(thirdItem.itemEl, {});
|
||||
await TestUtils.waitForTick();
|
||||
|
||||
verifyEvents([
|
||||
|
||||
337
toolkit/content/widgets/lit-select-control.mjs
Normal file
337
toolkit/content/widgets/lit-select-control.mjs
Normal 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));
|
||||
}
|
||||
};
|
||||
@@ -36,6 +36,10 @@ legend {
|
||||
flex-direction: column;
|
||||
gap: var(--space-large);
|
||||
margin-top: var(--space-small);
|
||||
|
||||
fieldset[aria-orientation="horizontal"] & {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
a[is="moz-support-link"],
|
||||
|
||||
@@ -18,6 +18,7 @@ export default class MozFieldset extends MozLitElement {
|
||||
label: { type: String, fluent: true },
|
||||
description: { type: String, fluent: true },
|
||||
supportPage: { type: String, attribute: "support-page" },
|
||||
ariaOrientation: { type: String, mapped: true },
|
||||
};
|
||||
|
||||
descriptionTemplate() {
|
||||
@@ -29,6 +30,7 @@ export default class MozFieldset extends MozLitElement {
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
supportPageTemplate() {
|
||||
if (this.supportPage) {
|
||||
return html`<a
|
||||
@@ -47,7 +49,10 @@ export default class MozFieldset extends MozLitElement {
|
||||
href="chrome://global/content/elements/moz-fieldset.css"
|
||||
/>
|
||||
<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>
|
||||
${!this.description ? this.supportPageTemplate() : ""}
|
||||
|
||||
@@ -3,35 +3,11 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { html, ifDefined } from "../vendor/lit.all.mjs";
|
||||
import { MozLitElement, MozBaseInputElement } from "../lit-utils.mjs";
|
||||
// eslint-disable-next-line import/no-unassigned-import
|
||||
import "chrome://global/content/elements/moz-label.mjs";
|
||||
// eslint-disable-next-line import/no-unassigned-import
|
||||
import "chrome://global/content/elements/moz-fieldset.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,
|
||||
},
|
||||
};
|
||||
import {
|
||||
SelectControlBaseElement,
|
||||
SelectControlItemMixin,
|
||||
} from "../lit-select-control.mjs";
|
||||
import { MozBaseInputElement } from "../lit-utils.mjs";
|
||||
|
||||
/**
|
||||
* 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 support-link - The radio group's support link intended for moz-radio elements.
|
||||
*/
|
||||
export class MozRadioGroup extends MozLitElement {
|
||||
#radioButtons;
|
||||
#value;
|
||||
|
||||
export class MozRadioGroup extends SelectControlBaseElement {
|
||||
static childElementName = "moz-radio";
|
||||
static orientation = "vertical";
|
||||
|
||||
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 },
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
@@ -260,104 +44,22 @@ customElements.define("moz-radio-group", MozRadioGroup);
|
||||
* @property {string} description - Description for the input.
|
||||
* @property {boolean} disabled - Whether or not the input is disabled.
|
||||
* @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} name
|
||||
* 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 {number} value - Value of the radio input.
|
||||
* @property {string} value - Value of the radio input.
|
||||
*/
|
||||
export class MozRadio extends MozBaseInputElement {
|
||||
#controller;
|
||||
|
||||
static properties = {
|
||||
checked: { type: Boolean, reflect: true },
|
||||
inputTabIndex: { type: Number, state: true },
|
||||
};
|
||||
|
||||
export class MozRadio extends SelectControlItemMixin(MozBaseInputElement) {
|
||||
static activatedProperty = "checked";
|
||||
|
||||
get controller() {
|
||||
return this.#controller;
|
||||
}
|
||||
|
||||
get isDisabled() {
|
||||
return (
|
||||
this.disabled ||
|
||||
this.#controller.disabled ||
|
||||
this.parentDisabled ||
|
||||
this.#controller.parentDisabled
|
||||
super.isDisabled || 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() {
|
||||
return html`<input
|
||||
type="radio"
|
||||
@@ -367,7 +69,7 @@ export class MozRadio extends MozBaseInputElement {
|
||||
.checked=${this.checked}
|
||||
aria-checked=${this.checked}
|
||||
aria-describedby="description"
|
||||
tabindex=${this.inputTabIndex}
|
||||
tabindex=${this.itemTabIndex}
|
||||
?disabled=${this.isDisabled}
|
||||
accesskey=${ifDefined(this.accessKey)}
|
||||
@click=${this.handleClick}
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
|
||||
import { html } from "chrome://global/content/vendor/lit.all.mjs";
|
||||
import {
|
||||
MozRadioGroup,
|
||||
MozRadio,
|
||||
} from "chrome://global/content/elements/moz-radio-group.mjs";
|
||||
SelectControlItemMixin,
|
||||
SelectControlBaseElement,
|
||||
} from "../lit-select-control.mjs";
|
||||
import { MozLitElement } from "../lit-utils.mjs";
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @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 orientation = "horizontal";
|
||||
}
|
||||
customElements.define("moz-visual-picker", MozVisualPicker);
|
||||
|
||||
@@ -36,20 +38,28 @@ customElements.define("moz-visual-picker", MozVisualPicker);
|
||||
* @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
|
||||
* @property {number} itemTabIndex
|
||||
* 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 {
|
||||
export class MozVisualPickerItem extends SelectControlItemMixin(MozLitElement) {
|
||||
static queries = {
|
||||
itemEl: ".picker-item",
|
||||
};
|
||||
|
||||
get inputEl() {
|
||||
return this.itemEl;
|
||||
click() {
|
||||
this.itemEl.click();
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.itemEl.focus();
|
||||
}
|
||||
|
||||
blur() {
|
||||
this.itemEl.blur();
|
||||
}
|
||||
|
||||
handleKeydown(event) {
|
||||
@@ -96,7 +106,7 @@ export class MozVisualPickerItem extends MozRadio {
|
||||
role="radio"
|
||||
value=${this.value}
|
||||
aria-checked=${this.checked}
|
||||
tabindex=${this.inputTabIndex}
|
||||
tabindex=${this.itemTabIndex}
|
||||
?checked=${this.checked}
|
||||
?disabled=${this.isDisabled}
|
||||
@click=${this.handleClick}
|
||||
|
||||
Reference in New Issue
Block a user