@esri/calcite-components
Version:
Web Components for Esri's Calcite Design System.
237 lines (236 loc) • 13 kB
JavaScript
/*! All material copyright ESRI, All Rights Reserved, unless otherwise specified.
See https://github.com/Esri/calcite-design-system/blob/dev/LICENSE.md for details.
v3.2.1 */
import { c as customElement } from "../../chunks/runtime.js";
import { live } from "lit-html/directives/live.js";
import { html } from "lit-html";
import { safeClassMap, LitElement, createEvent, stringOrBoolean, nothing, safeStyleMap } from "@arcgis/lumina";
import { c as connectForm, d as disconnectForm, H as HiddenFormInputSlot } from "../../chunks/form.js";
import { g as guid } from "../../chunks/guid.js";
import { u as updateHostInteraction, I as InteractiveContainer } from "../../chunks/interactive.js";
import { c as connectLabel, d as disconnectLabel } from "../../chunks/label.js";
import { c as componentFocusable } from "../../chunks/component.js";
import { h as focusFirstTabbable } from "../../chunks/dom.js";
import { V as Validation } from "../../chunks/Validation.js";
import { u as useT9n } from "../../chunks/useT9n.js";
import { css } from "@lit/reactive-element/css-tag.js";
const StarIcon = ({ full, scale, partial }) => html`<calcite-icon class=${safeClassMap(partial ? void 0 : "icon")} .icon=${full ? "star-f" : "star"} .scale=${scale}></calcite-icon>`;
const IDS = {
validationMessage: "validationMessage"
};
const styles = css`:host([disabled]){cursor:default;-webkit-user-select:none;user-select:none;opacity:var(--calcite-opacity-disabled)}:host([read-only]) *,:host([disabled]) *,:host([read-only]) ::slotted(*),:host([disabled]) ::slotted(*){pointer-events:none}:host{position:relative;display:flex;align-items:center;inline-size:fit-content}:host([disabled]) ::slotted([calcite-hydrated][disabled]),:host([disabled]) [calcite-hydrated][disabled]{opacity:1}.interaction-container{display:contents}:host([scale=s]){block-size:1.5rem;--calcite-internal-rating-spacing: .25rem}:host([scale=m]){block-size:2rem;--calcite-internal-rating-spacing: .5rem}:host([scale=l]){block-size:2.75rem;--calcite-internal-rating-spacing: .75rem}.fieldset{margin:0;display:flex;border-width:0;padding:0;align-items:center;gap:var(--calcite-rating-spacing, var(--calcite-internal-rating-spacing))}.wrapper{display:inline-block}.star{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;position:relative;display:flex;flex-direction:column;cursor:pointer;color:var(--calcite-rating-color, var(--calcite-color-border-input))}.star:hover{color:var(--calcite-rating-color-hover, var(--calcite-color-brand-hover))}.star:active{color:var(--calcite-rating-color-press, var(--calcite-color-brand-press))}.star:focus{outline:2px solid var(--calcite-color-focus, var(--calcite-ui-focus-color, var(--calcite-color-brand)));outline-offset:calc(2px*(1 - (2*clamp(0,var(--calcite-offset-invert-focus),1))))}.average,.fraction{color:var(--calcite-rating-average-color, var(--calcite-color-status-warning))}.hovered,.selected{color:var(--calcite-rating-color, var(--calcite-color-brand))}.fraction{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;position:absolute;pointer-events:none;inset-block-start:0;overflow:hidden;inset-inline-start:0}calcite-chip{pointer-events:none;cursor:default}.number--average{font-weight:700;color:var(--calcite-rating-average-text-color)}.number--count{color:var(--calcite-rating-count-text-color, var(--calcite-color-text-2));font-style:italic}.number--count:not(:first-child){margin-inline-start:var(--calcite-rating-spacing, var(--calcite-internal-rating-spacing))}.visually-hidden{position:absolute;inline-size:1px;block-size:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.validation-container{display:flex;flex-direction:column;align-items:flex-start;align-self:stretch}:host([scale=m]) .validation-container,:host([scale=l]) .validation-container{padding-block-start:.5rem}:host([scale=s]) .validation-container{padding-block-start:.25rem}::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 Rating extends LitElement {
constructor() {
super();
this.emit = false;
this.guid = `calcite-ratings-${guid()}`;
this.isKeyboardInteraction = true;
this.labelElements = [];
this.max = 5;
this._value = 0;
this.messages = useT9n({ blocking: true });
this.disabled = false;
this.readOnly = false;
this.required = false;
this.scale = "m";
this.showChip = false;
this.status = "idle";
this.validity = {
valid: false,
badInput: false,
customError: false,
patternMismatch: false,
rangeOverflow: false,
rangeUnderflow: false,
stepMismatch: false,
tooLong: false,
tooShort: false,
typeMismatch: false,
valueMissing: false
};
this.calciteRatingChange = createEvent({ cancelable: false });
this.listen("keydown", this.handleHostKeyDown);
this.listen("pointerout", this.handleRatingPointerOut);
this.listen("pointerover", this.handleRatingPointerOver);
}
static {
this.properties = { hoverValue: [16, {}, { state: true }], average: [11, {}, { reflect: true, type: Number }], count: [11, {}, { reflect: true, type: Number }], disabled: [7, {}, { reflect: true, type: Boolean }], form: [3, {}, { reflect: true }], messageOverrides: [0, {}, { attribute: false }], name: [3, {}, { reflect: true }], readOnly: [7, {}, { reflect: true, type: Boolean }], required: [7, {}, { reflect: true, type: Boolean }], scale: [3, {}, { reflect: true }], showChip: [7, {}, { reflect: true, type: Boolean }], status: [3, {}, { reflect: true }], validationIcon: [3, { converter: stringOrBoolean }, { reflect: true }], validationMessage: 1, validity: [0, {}, { attribute: false }], value: [11, {}, { reflect: true, type: Number }] };
}
static {
this.styles = styles;
}
get value() {
return this._value;
}
set value(value) {
const oldValue = this._value;
if (value !== oldValue) {
this._value = value;
if (this.hasUpdated) {
this.handleValueUpdate(value);
}
}
}
async setFocus() {
await componentFocusable(this);
focusFirstTabbable(this.el);
}
connectedCallback() {
super.connectedCallback();
connectLabel(this);
connectForm(this);
}
async load() {
this.requestUpdate("value");
}
willUpdate() {
this.starsMap = Array.from({ length: this.max }, (_, i) => {
const value = i + 1;
const average = !this.hoverValue && this.average && !this.value && value <= this.average;
const checked = value === this.value;
const fraction = this.average && this.average + 1 - value;
const hovered = value <= this.hoverValue;
const id = `${this.guid}-${value}`;
const partial = !this.hoverValue && !this.value && !hovered && fraction > 0 && fraction < 1;
const selected = this.value >= value;
const tabIndex = this.getTabIndex(value);
return {
average,
checked,
fraction,
hovered,
id,
partial,
selected,
value,
tabIndex
};
});
}
updated() {
updateHostInteraction(this);
}
loaded() {
this.labelElements = Array.from(this.renderRoot.querySelectorAll("label"));
}
disconnectedCallback() {
super.disconnectedCallback();
disconnectLabel(this);
disconnectForm(this);
}
handleValueUpdate(newValue) {
this.hoverValue = newValue;
if (this.emit) {
this.calciteRatingChange.emit();
}
this.emit = false;
}
onLabelClick() {
this.setFocus();
}
handleRatingPointerOver() {
this.isKeyboardInteraction = false;
}
handleRatingPointerOut() {
this.isKeyboardInteraction = true;
this.hoverValue = null;
}
handleHostKeyDown() {
this.isKeyboardInteraction = true;
}
handleLabelKeyDown(event) {
const inputValue = this.getValueFromLabelEvent(event);
const key = event.key;
const numberKey = key == " " ? void 0 : Number(key);
this.emit = true;
if (isNaN(numberKey)) {
switch (key) {
case "Enter":
case " ":
this.value = !this.required && this.value === inputValue ? 0 : inputValue;
break;
case "ArrowLeft":
this.value = this.getPreviousRatingValue(inputValue);
this.updateFocus();
event.preventDefault();
break;
case "ArrowRight":
this.value = this.getNextRatingValue(inputValue);
this.updateFocus();
event.preventDefault();
break;
case "Tab":
this.hoverValue = null;
break;
}
} else {
if (!this.required && numberKey >= 0 && numberKey <= this.max) {
this.value = numberKey;
} else if (this.required && numberKey > 0 && numberKey <= this.max) {
this.value = numberKey;
}
this.updateFocus();
}
}
handleInputChange(event) {
if (this.isKeyboardInteraction === true) {
const inputVal = Number(event.target["value"]);
this.hoverValue = inputVal;
this.value = inputVal;
}
}
handleLabelPointerOver(event) {
this.hoverValue = this.getValueFromLabelEvent(event);
}
handleLabelPointerDown(event) {
const target = event.currentTarget;
const inputValue = this.getValueFromLabelEvent(event);
this.hoverValue = inputValue;
this.emit = true;
this.value = !this.required && this.value === inputValue ? 0 : inputValue;
target.focus();
}
handleLabelClick(event) {
event.preventDefault();
}
handleLabelFocus(event) {
const inputValue = this.getValueFromLabelEvent(event);
this.hoverValue = inputValue;
}
updateFocus() {
this.hoverValue = this.value;
this.labelElements[this.value - 1].focus();
}
getTabIndex(value) {
if (this.readOnly || this.value !== value && (this.value || value !== 1)) {
return -1;
}
return 0;
}
getValueFromLabelEvent(event) {
const target = event.currentTarget;
return Number(target.getAttribute("data-value"));
}
getNextRatingValue(currentValue) {
return currentValue === 5 ? 1 : currentValue + 1;
}
getPreviousRatingValue(currentValue) {
return currentValue === 1 ? 5 : currentValue - 1;
}
render() {
const countString = this.count?.toString();
return InteractiveContainer({ disabled: this.disabled, children: html`<span class="wrapper"><fieldset class="fieldset" .disabled=${this.disabled}><legend class="visually-hidden">${this.messages.rating}</legend>${this.starsMap.map(({ average, checked, fraction, hovered, id, partial, selected, value, tabIndex }) => {
return html`<label class=${safeClassMap({
star: true,
selected,
hovered,
average,
partial
})} data-value=${value ?? nothing} for=${id ?? nothing} @click=${this.handleLabelClick} @focus=${this.handleLabelFocus} @keydown=${this.handleLabelKeyDown} @pointerdown=${this.handleLabelPointerDown} @pointerover=${this.handleLabelPointerOver} tabindex=${tabIndex ?? nothing}><input aria-errormessage=${IDS.validationMessage} .ariaInvalid=${this.status === "invalid"} .checked=${checked} class="visually-hidden" .disabled=${this.disabled || this.readOnly} id=${id ?? nothing} name=${this.guid ?? nothing} @change=${this.handleInputChange} tabindex=-1 type=radio .value=${live(value ?? "")}>${StarIcon({ full: selected || average || hovered, scale: this.scale })}${partial && html`<div class="fraction" style=${safeStyleMap({ width: `${fraction * 100}%` })}>${StarIcon({ full: true, partial: true, scale: this.scale })}</div>` || ""}<span class="visually-hidden">${this.messages.stars.replace("{num}", `${value}`)}</span></label>`;
})}${(this.count || this.average) && this.showChip ? html`<calcite-chip .label=${countString} .scale=${this.scale} .value=${countString}>${!!this.average && html`<span class="number--average">${this.average.toString()}</span>` || ""}${!!this.count && html`<span class="number--count">(${countString})</span>` || ""}</calcite-chip>` : null}</fieldset>${HiddenFormInputSlot({ component: this })}${this.validationMessage && this.status === "invalid" ? Validation({ icon: this.validationIcon, id: IDS.validationMessage, message: this.validationMessage, scale: this.scale, status: this.status }) : null}</span>` });
}
}
customElement("calcite-rating", Rating);
export {
Rating
};