Revision control

Copy as Markdown

Other Tools

/* 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/. */
/**
* Forms a radio group with all other account-hub-radio-card-large in the same
* form. If all of them are contained in one element, that element should be a
* radiogroup. If no radio card starts out checked, the first card checks
* itself.
*
* Template ID: #accountHubRadioCardLargeTemplate
*
* @tagname account-hub-radio-card-large
* @slot title
* @slot tag
* @slot description
* @attribute {string} value - The value of this radio checkbox.
* @attribute {string} aria-checked - Set this to "true" to initialize in a
* checked state. After the element is attached, use the checked property to
* manipulate the state.
*/
class AccountHubRadioCardLarge extends HTMLElement {
static formAssociated = true;
/**
* @type {ElementInternals}
*/
#internals;
constructor() {
super();
this.#internals = this.attachInternals();
}
connectedCallback() {
if (this.shadowRoot) {
return;
}
this.role = "radio";
this.#internals.setFormValue(this.getAttribute("value"));
const checkedCardExists = Boolean(
this.#internals.form.querySelector(
`${this.localName}[aria-checked="true"]`
)
);
const isFirstCard = this.matches(":first-of-type");
if (this.ariaChecked) {
this.tabIndex = 0;
} else if (!checkedCardExists && isFirstCard) {
this.checked = true;
} else {
// Can't use checked property here, since it will see false as identical
// to null.
this.tabIndex = -1;
this.ariaChecked = "false";
}
const shadowRoot = this.attachShadow({ mode: "open" });
const template = document
.getElementById("accountHubRadioCardLargeTemplate")
.content.cloneNode(true);
const style = document.createElement("link");
style.rel = "stylesheet";
shadowRoot.append(style, template);
this.addEventListener("keydown", this);
this.addEventListener("click", this);
}
handleEvent(event) {
if (!this.checked && (event.type == "click" || event.key == " ")) {
this.checked = true;
this.#dispatchChange();
return;
}
if (event.type != "keydown") {
return;
}
// Focus control, since we manage tabindex.
const rightIsForward = document.dir == "ltr";
const forwardKey = rightIsForward ? "ArrowRight" : "ArrowLeft";
const backwardKey = rightIsForward ? "ArrowLeft" : "ArrowRight";
const allRadios = Array.from(
this.#internals.form.querySelectorAll(this.localName)
);
const thisIndex = allRadios.indexOf(this);
if (event.key == forwardKey || event.key == "ArrowDown") {
const next = allRadios.at((thisIndex + 1) % allRadios.length);
next.focus();
if (!next.checked) {
next.checked = true;
next.#dispatchChange();
}
} else if (event.key == backwardKey || event.key == "ArrowUp") {
const previous = allRadios.at(thisIndex - 1);
previous.focus();
if (!previous.checked) {
previous.checked = true;
previous.#dispatchChange();
}
}
}
/**
* Dispatches a change event when user interaction changes the selected card.
*/
#dispatchChange() {
this.dispatchEvent(new Event("change", { bubbles: true }));
}
/**
* @type {boolean}
*/
get checked() {
return this.ariaChecked == "true";
}
set checked(value) {
const boolValue = Boolean(value);
if (boolValue == this.checked) {
return;
}
if (boolValue) {
// Ensure all other radios are unchecked.
const checkedElements = this.#internals.form.querySelectorAll(
`${this.localName}[aria-checked="true"]`
);
for (const radio of checkedElements) {
radio.checked = false;
}
}
this.ariaChecked = boolValue.toString();
this.tabIndex = boolValue ? 0 : -1;
}
/**
* @type {string}
* @readonly
*/
get value() {
return this.getAttribute("value");
}
}
customElements.define("account-hub-radio-card-large", AccountHubRadioCardLarge);