@esri/calcite-components
Version:
Web Components for Esri's Calcite Design System.
436 lines (435 loc) • 20.9 kB
JavaScript
/* COPYRIGHT Esri - https://js.arcgis.com/5.1/LICENSE.txt */
import { c as customElement } from "../../chunks/runtime.js";
import { html, isServer, css, nothing } from "lit";
import { LitElement, createEvent, safeClassMap } from "@arcgis/lumina";
import { createRef, ref } from "lit/directives/ref.js";
import { useDirection } from "@arcgis/lumina/controllers";
import { g as getRoundRobinIndex } from "../../chunks/array.js";
import { q as queryElementRoots, c as closestElementCrossShadowBoundary } from "../../chunks/dom.js";
import { c as connectLabel, d as disconnectLabel, g as getLabelText } from "../../chunks/label.js";
import { I as InternalLabel } from "../../chunks/InternalLabel.js";
import { u as useSetFocus } from "../../chunks/useSetFocus.js";
import { u as useInteractive } from "../../chunks/useInteractive.js";
const hiddenFormInputSlotName = "hidden-form-input";
function isCheckable(component) {
return "checked" in component;
}
const onFormResetMap = /* @__PURE__ */ new WeakMap();
const formComponentSet = /* @__PURE__ */ new WeakSet();
function hasRegisteredFormComponentParent(form, formComponentEl) {
const hasParentComponentWithFormIdSet = closestElementCrossShadowBoundary(formComponentEl.parentElement, "[form]");
if (hasParentComponentWithFormIdSet) {
return true;
}
const formComponentRegisterEventName = "calciteInternalFormComponentRegister";
let hasRegisteredFormComponentParent2 = false;
form.addEventListener(formComponentRegisterEventName, (event) => {
hasRegisteredFormComponentParent2 = event.composedPath().some((element) => formComponentSet.has(element));
event.stopPropagation();
}, { once: true });
formComponentEl.dispatchEvent(new CustomEvent(formComponentRegisterEventName, {
bubbles: true,
composed: true
}));
return hasRegisteredFormComponentParent2;
}
function getValidationComponent(el) {
if (el.nodeName === "CALCITE-RADIO-BUTTON") {
return closestElementCrossShadowBoundary(el, "calcite-radio-button-group");
}
return el;
}
function connectForm(component) {
const { el, value } = component;
const associatedForm = findAssociatedForm(component);
if (!associatedForm || hasRegisteredFormComponentParent(associatedForm, el)) {
return;
}
component.formEl = associatedForm;
component.defaultValue = value;
if (isCheckable(component)) {
component.defaultChecked = component.checked;
}
const boundOnFormReset = onFormReset.bind(component);
associatedForm.addEventListener("reset", boundOnFormReset);
onFormResetMap.set(component.el, boundOnFormReset);
formComponentSet.add(el);
}
function findAssociatedForm(component) {
const { el, form } = component;
return form ? queryElementRoots(el, { id: form }) : closestElementCrossShadowBoundary(el, "form");
}
function onFormReset() {
if ("status" in this) {
this.status = "idle";
}
if ("validationIcon" in this) {
this.validationIcon = false;
}
if ("validationMessage" in this) {
this.validationMessage = "";
}
if (isCheckable(this)) {
this.checked = this.defaultChecked;
return;
}
this.value = this.defaultValue;
this.onFormReset?.();
}
function disconnectForm(component) {
const { el, formEl } = component;
if (!formEl) {
return;
}
const boundOnFormReset = onFormResetMap.get(el);
formEl.removeEventListener("reset", boundOnFormReset);
onFormResetMap.delete(el);
component.formEl = null;
formComponentSet.delete(el);
}
const internalHiddenInputInputEvent = "calciteInternalHiddenInputInput";
const hiddenInputInputHandler = (event) => {
event.target.dispatchEvent(new CustomEvent(internalHiddenInputInputEvent, { bubbles: true }));
};
const removeHiddenInputChangeEventListener = (input) => input.removeEventListener("input", hiddenInputInputHandler);
function syncHiddenFormInput(component) {
const { el, formEl, name, value } = component;
const { ownerDocument } = el;
if (isServer) {
return;
}
const inputs = el.querySelectorAll(`input[slot="${hiddenFormInputSlotName}"]`);
if (!formEl || !name) {
inputs.forEach((input) => {
removeHiddenInputChangeEventListener(input);
input.remove();
});
return;
}
const values = Array.isArray(value) ? value : [value];
const extra = [];
const seen = /* @__PURE__ */ new Set();
inputs.forEach((input) => {
const valueMatch = values.find((val) => (
/* intentional non-strict equality check */
val == input.value
));
if (valueMatch != null) {
seen.add(valueMatch);
defaultSyncHiddenFormInput(component, input, valueMatch);
} else {
extra.push(input);
}
});
let docFrag;
values.forEach((value2) => {
if (seen.has(value2)) {
return;
}
let input = extra.pop();
if (!input) {
input = ownerDocument.createElement("input");
input.ariaHidden = "true";
input.slot = hiddenFormInputSlotName;
}
if (!docFrag) {
docFrag = ownerDocument.createDocumentFragment();
}
docFrag.append(input);
input.addEventListener("input", hiddenInputInputHandler);
defaultSyncHiddenFormInput(component, input, value2);
});
if (docFrag) {
el.append(docFrag);
}
extra.forEach((input) => {
removeHiddenInputChangeEventListener(input);
input.remove();
});
}
function defaultSyncHiddenFormInput(component, input, value) {
const { defaultValue, disabled, form, name, required } = component;
input.defaultValue = defaultValue;
input.disabled = disabled;
input.name = name;
input.required = required;
input.tabIndex = -1;
if (form) {
input.setAttribute("form", form);
} else {
input.removeAttribute("form");
}
if (isCheckable(component)) {
input.checked = component.checked;
input.defaultChecked = component.defaultChecked;
input.value = component.checked ? value || "on" : "";
} else {
input.value = value || "";
}
component.syncHiddenFormInput?.(input);
const validationComponent = getValidationComponent(component.el);
if (validationComponent && "validity" in validationComponent) {
for (const key in { ...input?.validity }) {
validationComponent.validity[key] = input.validity[key];
}
}
}
const HiddenFormInputSlot = ({ component }) => {
syncHiddenFormInput(component);
return html`<slot name=${hiddenFormInputSlotName}></slot>`;
};
const CSS = {
container: "container",
radio: "radio"
};
const styles = css`:host([disabled]){cursor:default;-webkit-user-select:none;user-select:none;opacity:var(--calcite-opacity-disabled)}:host([disabled]) *,:host([disabled]) ::slotted(*){pointer-events:none}:host{display:block;cursor:pointer}:host .container{position:relative;outline:2px solid transparent;outline-offset:2px;display:flex}:host .radio{cursor:pointer;outline-color:transparent;transition-property:background-color,block-size,border-color,box-shadow,color,inset-block-end,inset-block-start,inset-inline-end,inset-inline-start,inset-size,opacity,outline-color,transform;transition-duration:var(--calcite-animation-timing);transition-timing-function:ease-in-out;border-radius:var(--calcite-radio-button-corner-radius, var(--calcite-corner-radius-pill));background-color:var(--calcite-radio-button-background-color, var(--calcite-color-foreground-1));box-shadow:inset 0 0 0 var(--calcite-border-width-sm) var(--calcite-radio-button-border-color, var(--calcite-color-border-input))}:host([hovered]) .radio,:host(:not([checked])[focused]:not([disabled])) .radio{box-shadow:inset 0 0 0 var(--calcite-border-width-md) var(--calcite-radio-button-border-color, var(--calcite-color-brand-hover))}:host([hovered]) .radio:active,:host(:not([checked])[focused]:not([disabled])) .radio:active{box-shadow:inset 0 0 0 var(--calcite-border-width-md) var(--calcite-color-brand-press)}:host([focused]) .radio{outline:var(--calcite-border-width-md) solid var(--calcite-color-focus, var(--calcite-ui-focus-color, var(--calcite-color-brand)));outline-offset:calc(var(--calcite-spacing-base) * calc(1 - (2*clamp(0,var(--calcite-offset-invert-focus),1))))}:host([disabled]) .radio{cursor:default;opacity:var(--calcite-opacity-disabled)}:host([disabled]) ::slotted([calcite-hydrated][disabled]),:host([disabled]) [calcite-hydrated][disabled]{opacity:1}.interaction-container{display:contents}:host([hovered][disabled]) .radio{box-shadow:inset 0 0 0 var(--calcite-border-width-sm) var(--calcite-radio-button-border-color, var(--calcite-color-border-input))}:host([scale=s]){--calcite-internal-radio-size: var(--calcite-radio-button-size, var(--calcite-size-fixed-md))}:host([scale=m]){--calcite-internal-radio-size: var(--calcite-radio-button-size, var(--calcite-size-fixed-md-plus))}:host([scale=l]){--calcite-internal-radio-size: var(--calcite-radio-button-size, var(--calcite-size-fixed-lg))}.radio{block-size:var(--calcite-internal-radio-size);inline-size:var(--calcite-internal-radio-size);size:var(--calcite-internal-radio-size)}:host([scale=s][checked]) .radio,:host([hovered][scale=s][checked][disabled]) .radio{box-shadow:inset 0 0 0 var(--calcite-border-width-lg) var(--calcite-radio-button-border-color, var(--calcite-color-brand))}:host([scale=s][focused][checked]:not([disabled])) .radio{box-shadow:inset 0 0 0 var(--calcite-border-width-lg) var(--calcite-radio-button-border-color, var(--calcite-color-brand)),0 0 0 2px var(--calcite-radio-button-background-color, var(--calcite-color-foreground-1))}:host([scale=m][checked]) .radio,:host([hovered][scale=m][checked][disabled]) .radio{box-shadow:inset 0 0 0 5px var(--calcite-radio-button-border-color, var(--calcite-color-brand))}:host([scale=m][focused][checked]:not([disabled])) .radio{box-shadow:inset 0 0 0 5px var(--calcite-radio-button-border-color, var(--calcite-color-brand)),0 0 0 2px var(--calcite-radio-button-background-color, var(--calcite-color-foreground-1))}:host([scale=l][checked]) .radio,:host([hovered][scale=l][checked][disabled]) .radio{box-shadow:inset 0 0 0 6px var(--calcite-radio-button-border-color, var(--calcite-color-brand))}:host([scale=l][focused][checked]:not([disabled])) .radio{box-shadow:inset 0 0 0 6px var(--calcite-radio-button-border-color, var(--calcite-color-brand)),0 0 0 2px var(--calcite-radio-button-background-color, var(--calcite-color-foreground-1))}@media(forced-colors:active){:host([checked]) .radio:after,:host([checked][disabled]) .radio:after{content:"";inline-size:var(--calcite-internal-radio-size);block-size:var(--calcite-internal-radio-size);background-color:CanvasText;display:block}}.internal-label-alignment--center{align-items:center}.internal-label-alignment--end{align-items:end}.internal-label--container{display:flex;justify-content:space-between;color:var(--calcite-color-text-1)}.internal-label-required--indicator{font-weight:var(--calcite-font-weight-medium);color:var(--calcite-color-status-danger);padding-inline:var(--calcite-spacing-base)}.internal-label-required--indicator:hover{cursor:help}.internal-label--text{line-height:1}:host([scale=s]) .internal-label-spacing--bottom{margin-block-end:var(--calcite-spacing-xxs)}:host([scale=s]) .internal-label-spacing-inline--end{margin-inline-end:var(--calcite-spacing-sm)}:host([scale=s]) .internal-label-spacing-inline--start{margin-inline-start:var(--calcite-spacing-sm)}:host([scale=s]) .internal-label--text{font-size:var(--calcite-font-size--2)}:host([scale=m]) .internal-label-spacing--bottom{margin-block-end:var(--calcite-spacing-sm)}:host([scale=m]) .internal-label-spacing-inline--end{margin-inline-end:var(--calcite-spacing-sm)}:host([scale=m]) .internal-label-spacing-inline--start{margin-inline-start:var(--calcite-spacing-sm)}:host([scale=m]) .internal-label--text{font-size:var(--calcite-font-size--1)}:host([scale=l]) .internal-label-spacing--bottom{margin-block-end:var(--calcite-spacing-sm)}:host([scale=l]) .internal-label-spacing-inline--end{margin-inline-end:var(--calcite-spacing-md)}:host([scale=l]) .internal-label-spacing-inline--start{margin-inline-start:var(--calcite-spacing-md)}:host([scale=l]) .internal-label--text{font-size:var(--calcite-font-size-0)}::slotted(input[slot=hidden-form-input]){margin:0;opacity:0;outline:none;padding:0;position:absolute;inset:0;transform:none;-webkit-appearance:none;z-index:-1}:host([hidden]){display:none}[hidden]{display:none}`;
class RadioButton extends LitElement {
constructor() {
super();
this.containerRef = createRef();
this.direction = useDirection();
this.focusSetter = useSetFocus()(this);
this.interactiveContainer = useInteractive(this);
this.checked = false;
this.disabled = false;
this.focused = false;
this.hovered = false;
this.required = false;
this.scale = "m";
this.calciteInternalRadioButtonBlur = createEvent({ cancelable: false });
this.calciteInternalRadioButtonCheckedChange = createEvent({ cancelable: false });
this.calciteInternalRadioButtonFocus = createEvent({ cancelable: false });
this.calciteRadioButtonChange = createEvent({ cancelable: false });
this.listen("pointerenter", this.pointerEnterHandler);
this.listen("pointerleave", this.pointerLeaveHandler);
this.listen("click", this.clickHandler);
this.listen("keydown", this.handleKeyDown);
}
static {
this.properties = { checked: [7, {}, { reflect: true, type: Boolean }], disabled: [7, {}, { reflect: true, type: Boolean }], focused: [7, {}, { reflect: true, type: Boolean }], form: [3, {}, { reflect: true }], hovered: [7, {}, { reflect: true, type: Boolean }], label: 1, labelText: 1, name: [3, {}, { reflect: true }], required: [7, {}, { reflect: true, type: Boolean }], scale: [3, {}, { reflect: true }], value: 1 };
}
static {
this.styles = styles;
}
async emitCheckedChange() {
this.calciteInternalRadioButtonCheckedChange.emit();
}
async setFocus(options) {
return this.focusSetter(() => this.containerRef.value, options);
}
connectedCallback() {
this.rootNode = this.el.getRootNode();
if (this.name) {
this.checkLastRadioButton();
}
connectLabel(this);
connectForm(this);
this.updateTabIndexOfOtherRadioButtonsInGroup();
super.connectedCallback();
}
willUpdate(changes) {
if (this.hasUpdated && changes.has("checked")) {
this.checkedChanged(this.checked);
}
if (changes.has("disabled") && (this.hasUpdated || this.disabled !== false)) {
this.updateTabIndexOfOtherRadioButtonsInGroup();
}
if (changes.has("name")) {
this.checkLastRadioButton();
}
}
loaded() {
if (this.focused && !this.disabled) {
this.setFocus();
}
}
disconnectedCallback() {
super.disconnectedCallback();
disconnectLabel(this);
disconnectForm(this);
this.updateTabIndexOfOtherRadioButtonsInGroup();
}
checkedChanged(newChecked) {
if (newChecked) {
this.uncheckOtherRadioButtonsInGroup();
}
this.calciteInternalRadioButtonCheckedChange.emit();
}
syncHiddenFormInput(input) {
input.type = "radio";
}
selectItem(items, selectedIndex) {
items[selectedIndex].click();
}
queryButtons() {
return Array.from(this.rootNode.querySelectorAll("calcite-radio-button:not([hidden])")).filter((radioButton) => radioButton.name === this.name);
}
isFocusable() {
const radioButtons = this.queryButtons();
const firstFocusable = radioButtons.find((radioButton) => !radioButton.disabled);
const checked = radioButtons.find((radioButton) => radioButton.checked);
return firstFocusable === this.el && !checked;
}
check() {
if (this.disabled) {
return;
}
this.focused = true;
this.setFocus();
if (this.checked) {
return;
}
this.uncheckAllRadioButtonsInGroup();
this.checked = true;
this.calciteRadioButtonChange.emit();
}
clickHandler() {
if (this.disabled) {
return;
}
this.check();
}
onLabelClick(event) {
if (this.disabled || this.el.hidden) {
return;
}
const label = event.currentTarget;
const radioButton = label.for ? this.rootNode.querySelector(`calcite-radio-button[id="${label.for}"]`) : label.querySelector(`calcite-radio-button[name="${this.name}"]`);
if (!radioButton) {
return;
}
radioButton.focused = true;
this.setFocus();
if (radioButton.checked) {
return;
}
this.uncheckOtherRadioButtonsInGroup();
radioButton.checked = true;
this.calciteRadioButtonChange.emit();
}
checkLastRadioButton() {
const radioButtons = this.queryButtons();
const checkedRadioButtons = radioButtons.filter((radioButton) => radioButton.checked);
if (checkedRadioButtons?.length > 1) {
const lastCheckedRadioButton = checkedRadioButtons[checkedRadioButtons.length - 1];
checkedRadioButtons.filter((checkedRadioButton) => checkedRadioButton !== lastCheckedRadioButton).forEach((checkedRadioButton) => {
checkedRadioButton.checked = false;
checkedRadioButton.emitCheckedChange();
});
}
}
uncheckAllRadioButtonsInGroup() {
const radioButtons = this.queryButtons();
radioButtons.forEach((radioButton) => {
if (radioButton.checked) {
radioButton.checked = false;
radioButton.focused = false;
}
});
}
uncheckOtherRadioButtonsInGroup() {
const radioButtons = this.queryButtons();
const otherRadioButtons = radioButtons.filter((radioButton) => radioButton !== this.el);
otherRadioButtons.forEach((otherRadioButton) => {
if (otherRadioButton.checked) {
otherRadioButton.checked = false;
otherRadioButton.focused = false;
}
});
}
updateTabIndexOfOtherRadioButtonsInGroup() {
const radioButtons = this.queryButtons();
const otherFocusableRadioButtons = radioButtons.filter((radioButton) => radioButton !== this.el && !radioButton.disabled);
otherFocusableRadioButtons.forEach((radioButton) => {
radioButton.manager?.component.requestUpdate();
});
}
getTabIndex() {
if (this.disabled) {
return void 0;
}
return this.checked || this.isFocusable() ? 0 : -1;
}
pointerEnterHandler() {
if (this.disabled) {
return;
}
this.hovered = true;
}
pointerLeaveHandler() {
if (this.disabled) {
return;
}
this.hovered = false;
}
handleKeyDown(event) {
const keys = ["ArrowLeft", "ArrowUp", "ArrowRight", "ArrowDown", " "];
const { key } = event;
if (keys.indexOf(key) === -1) {
return;
}
if (key === " ") {
this.check();
event.preventDefault();
return;
}
let adjustedKey = key;
if (this.direction === "rtl") {
if (key === "ArrowRight") {
adjustedKey = "ArrowLeft";
}
if (key === "ArrowLeft") {
adjustedKey = "ArrowRight";
}
}
const radioButtons = Array.from(this.rootNode.querySelectorAll("calcite-radio-button:not([hidden])")).filter((radioButton) => radioButton.name === this.name);
let currentIndex = 0;
radioButtons.some((item, index) => {
if (item.checked) {
currentIndex = index;
return true;
}
});
switch (adjustedKey) {
case "ArrowLeft":
case "ArrowUp":
event.preventDefault();
this.selectItem(radioButtons, this.getNextNonDisabledIndex(radioButtons, currentIndex, "left"));
return;
case "ArrowRight":
case "ArrowDown":
event.preventDefault();
this.selectItem(radioButtons, this.getNextNonDisabledIndex(radioButtons, currentIndex, "right"));
return;
default:
return;
}
}
getNextNonDisabledIndex(radioButtons, startIndex, dir) {
const totalButtons = radioButtons.length;
const offset = dir === "left" ? -1 : 1;
let selectIndex = getRoundRobinIndex(startIndex + offset, totalButtons);
while (radioButtons[selectIndex].disabled) {
selectIndex = getRoundRobinIndex(selectIndex + offset, totalButtons);
}
return selectIndex;
}
onContainerBlur() {
this.focused = false;
this.calciteInternalRadioButtonBlur.emit();
}
onContainerFocus() {
if (!this.disabled) {
this.focused = true;
this.calciteInternalRadioButtonFocus.emit();
}
}
render() {
const tabIndex = this.getTabIndex();
return this.interactiveContainer({ disabled: this.disabled, children: html`<div .ariaChecked=${this.checked} .ariaLabel=${getLabelText(this)} class=${safeClassMap(CSS.container)} @blur=${this.onContainerBlur} @focus=${this.onContainerFocus} role=radio tabindex=${tabIndex ?? nothing} ${ref(this.containerRef)}><div class=${safeClassMap(CSS.radio)}></div>${this.labelText && InternalLabel({ labelText: this.labelText, spacingInlineStart: true }) || ""}</div>${HiddenFormInputSlot({ component: this })}` });
}
}
customElement("calcite-radio-button", RadioButton);
export {
RadioButton
};