UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

237 lines (236 loc) • 13 kB
/*! 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!important;opacity:0!important;outline:none!important;padding:0!important;position:absolute!important;inset:0!important;transform:none!important;-webkit-appearance:none!important;z-index:-1!important}: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 };