UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

270 lines (269 loc) • 17.8 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 { keyed } from "lit-html/directives/keyed.js"; import { html } from "lit"; import { createRef, ref } from "lit-html/directives/ref.js"; import { LitElement, safeClassMap, safeStyleMap } from "@arcgis/lumina"; import { c as connectForm, a as afterConnectDefaultValueSet, d as disconnectForm } from "../../chunks/form.js"; import { b as getSupportedLocale, n as numberStringFormatter } from "../../chunks/locale.js"; import { j as intersects } from "../../chunks/dom.js"; import { c as createObserver } from "../../chunks/observers.js"; import { u as useT9n } from "../../chunks/useT9n.js"; import { css } from "@lit/reactive-element/css-tag.js"; const CSS = { container: "container", fill: "fill", stepLine: "step-line", label: "label", labelHidden: "label-hidden", labelRange: "label-range", labelValue: "label-value", unitLabel: "unit-label", stepsVisible: "steps-visible", valueVisible: "value-visible", success: "fill-success", warning: "fill-warning", danger: "fill-danger" }; 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([hidden]){display:none}[hidden]{display:none}:host([disabled]) ::slotted([calcite-hydrated][disabled]),:host([disabled]) [calcite-hydrated][disabled]{opacity:1}.interaction-container{display:contents}:host{display:flex;--calcite-internal-meter-space: var(--calcite-spacing-base);--calcite-internal-meter-height: var(--calcite-spacing-lg);--calcite-internal-meter-font-size: var(--calcite-font-size--1);--calcite-internal-meter-fill-color: var(--calcite-meter-fill-color, var(--calcite-color-brand));--calcite-internal-meter-background-color: var(--calcite-meter-background-color, var(--calcite-color-foreground-2));--calcite-internal-meter-border-color: var(--calcite-meter-border-color, var(--calcite-color-border-3));--calcite-internal-meter-shadow: var(--calcite-meter-shadow, var(--calcite-shadow-none));--calcite-internal-meter-corner-radius: var(--calcite-meter-corner-radius, 9999px);--calcite-internal-meter-value-text-color: var(--calcite-meter-value-text-color, var(--calcite-color-text-1));--calcite-internal-meter-range-text-color: var(--calcite-meter-range-text-color, var(--calcite-color-text-3))}:host([scale=s]){--calcite-internal-meter-height: var(--calcite-spacing-md);--calcite-internal-meter-font-size: var(--calcite-font-size--2)}:host([scale=l]){--calcite-internal-meter-height: var(--calcite-spacing-xxl);--calcite-internal-meter-font-size: var(--calcite-font-size-0)}:host([appearance=solid]){--calcite-internal-meter-border-color: var(--calcite-color-foreground-3);--calcite-internal-meter-background-color: var(--calcite-color-foreground-3)}:host([appearance=outline]){--calcite-internal-meter-background-color: transparent}.fill{--calcite-internal-meter-fill-color: var(--calcite-meter-fill-color, var(--calcite-color-brand))}.fill-danger{--calcite-internal-meter-fill-color: var(--calcite-meter-fill-color, var(--calcite-color-status-danger))}.fill-success{--calcite-internal-meter-fill-color: var(--calcite-meter-fill-color, var(--calcite-color-status-success))}.fill-warning{--calcite-internal-meter-fill-color: var(--calcite-meter-fill-color, var(--calcite-color-status-warning))}.container{position:relative;display:flex;align-items:center;margin:0;inline-size:var(--calcite-container-size-content-fluid);block-size:var(--calcite-internal-meter-height);background-color:var(--calcite-internal-meter-background-color);border:var(--calcite-border-width-sm) solid var(--calcite-internal-meter-border-color);border-radius:var(--calcite-internal-meter-corner-radius);box-shadow:var(--calcite-internal-meter-shadow)}.value-visible{margin-block-start:var(--calcite-spacing-xxl)}.steps-visible{margin-block-end:var(--calcite-spacing-xxl)}.step-line{position:absolute;inset-block:0px;display:block;inline-size:var(--calcite-internal-meter-space);background-color:var(--calcite-internal-meter-border-color)}.label{position:absolute;font-size:var(--calcite-internal-meter-font-size)}.label-hidden{visibility:hidden;opacity:0}.label-value{inset-block-end:calc(100% + .5em);font-weight:var(--calcite-font-weight-bold);color:var(--calcite-internal-meter-value-text-color)}.label-range{color:var(--calcite-internal-meter-range-text-color);inset-block-start:calc(100% + .5em)}.label-range .unit-label{font-weight:var(--calcite-font-weight-medium)}.fill{position:absolute;z-index:var(--calcite-z-index);display:block;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);inset-inline-start:var(--calcite-internal-meter-space);inset-block:var(--calcite-internal-meter-space);border-radius:var(--calcite-internal-meter-corner-radius);max-inline-size:calc(100% - var(--calcite-internal-meter-space) * 2);min-inline-size:calc(var(--calcite-internal-meter-height) - var(--calcite-internal-meter-space) * 2);background-color:var(--calcite-internal-meter-fill-color);transition-property:inline-size,background-color,box-shadow}.solid .fill{inset-block:0;inset-inline-start:0;max-inline-size:100%;min-inline-size:calc(var(--calcite-internal-meter-height));box-shadow:0 0 0 1px var(--calcite-internal-meter-fill-color)}`; class Meter extends LitElement { constructor() { super(...arguments); this.highLabelEl = createRef(); this.labelFlipMax = 0.8; this.labelFlipProximity = 0.15; this.lowLabelEl = createRef(); this.maxLabelEl = createRef(); this.maxPercent = 100; this.messages = useT9n({ name: null }); this.meterContainerEl = createRef(); this.minLabelEl = createRef(); this.minPercent = 0; this.resizeObserver = createObserver("resize", () => this.resizeHandler()); this.valueLabelEl = createRef(); this.appearance = "outline-fill"; this.disabled = false; this.fillType = "range"; this.groupSeparator = false; this.max = 100; this.min = 0; this.rangeLabelType = "percent"; this.rangeLabels = false; this.scale = "m"; this.unitLabel = ""; this.valueLabel = false; this.valueLabelType = "percent"; } static { this.properties = { currentPercent: [16, {}, { state: true }], highActive: [16, {}, { state: true }], highPercent: [16, {}, { state: true }], lowActive: [16, {}, { state: true }], lowPercent: [16, {}, { state: true }], appearance: [3, {}, { reflect: true }], disabled: [7, {}, { reflect: true, type: Boolean }], fillType: [3, {}, { reflect: true }], form: [3, {}, { reflect: true }], groupSeparator: [7, {}, { reflect: true, type: Boolean }], high: [11, {}, { reflect: true, type: Number }], label: 1, low: [11, {}, { reflect: true, type: Number }], max: [11, {}, { reflect: true, type: Number }], min: [11, {}, { reflect: true, type: Number }], name: [3, {}, { reflect: true }], numberingSystem: 1, rangeLabelType: [3, {}, { reflect: true }], rangeLabels: [7, {}, { reflect: true, type: Boolean }], scale: [3, {}, { reflect: true }], unitLabel: 1, value: [9, {}, { type: Number }], valueLabel: [7, {}, { reflect: true, type: Boolean }], valueLabelType: [3, {}, { reflect: true }] }; } static { this.styles = styles; } connectedCallback() { super.connectedCallback(); connectForm(this); this.resizeObserver?.observe(this.el); } load() { this.calculateValues(); afterConnectDefaultValueSet(this, this.value); } willUpdate(changes) { if (changes.has("min") && (this.hasUpdated || this.min !== 0) || changes.has("max") && (this.hasUpdated || this.max !== 100) || changes.has("low") || changes.has("high") || changes.has("value")) { this.handleRangeChange(); } if (changes.has("rangeLabels") && (this.hasUpdated || this.rangeLabels !== false) || changes.has("rangeLabelType") && (this.hasUpdated || this.rangeLabelType !== "percent") || changes.has("unitLabel") && (this.hasUpdated || this.unitLabel !== "") || changes.has("valueLabel") && (this.hasUpdated || this.valueLabel !== false) || changes.has("valueLabelType") && (this.hasUpdated || this.valueLabelType !== "percent")) { this.updateLabels(); } } loaded() { this.updateLabels(); } disconnectedCallback() { super.disconnectedCallback(); disconnectForm(this); this.resizeObserver?.disconnect(); } handleRangeChange() { this.calculateValues(); this.updateLabels(); } resizeHandler() { this.updateLabels(); } updateLabels() { if (this.valueLabelEl.value) { this.determineValueLabelPosition(); } if (this.rangeLabels) { this.determineVisibleLabels(); } } calculateValues() { const { min, max, low, high, value } = this; const lowPercent = 100 * (low - min) / (max - min); const highPercent = 100 * (high - min) / (max - min); const currentPercent = 100 * (value - min) / (max - min); if (!low || low < min || low > high || low > max) { this.low = min; } if (!high || high > max || high < low || high < min) { this.high = max; } if (!value) { this.value = min; } this.lowPercent = lowPercent; this.highPercent = highPercent; this.currentPercent = value ? currentPercent : 0; this.lowActive = !!low && low > min && (!value || low > value) && (!high || low < high); this.highActive = !!high && min <= high && high < max && (!value || high > value) && (!low || high > low); } formatLabel(value, labelType) { if (labelType === "percent") { if (!this.percentFormatting) { const locale = getSupportedLocale(this.messages._lang); const formatter = new Intl.NumberFormat(locale, { useGrouping: this.groupSeparator, style: "percent" }); this.percentFormatting = { formatter, locale }; } return this.percentFormatting.formatter.format(value); } else { numberStringFormatter.numberFormatOptions = { locale: this.messages._lang, numberingSystem: this.numberingSystem, useGrouping: this.groupSeparator }; return numberStringFormatter.localize(value.toString()); } } getMeterKindCssClass() { const { low, high, min, max, value } = this; const lowest = low ? low : min; const highest = high ? high : max; const aboveLowest = value >= lowest; const belowLowest = value < lowest; const aboveHighest = value >= highest; const belowHighest = value < highest; if (!value || !low && belowHighest || belowLowest) { return CSS.success; } else if (aboveLowest && belowHighest) { return CSS.warning; } else if (aboveHighest) { return CSS.danger; } else { return CSS.success; } } intersects(el1, el2) { return el1 && el2 && intersects(el1.getBoundingClientRect(), el2.getBoundingClientRect()); } determineVisibleLabels() { const { minLabelEl: { value: minLabelEl }, lowLabelEl: { value: lowLabelEl }, highLabelEl: { value: highLabelEl }, maxLabelEl: { value: maxLabelEl } } = this; const highMaxOverlap = this.intersects(highLabelEl, maxLabelEl); const lowHighOverlap = this.intersects(lowLabelEl, highLabelEl); const lowMaxOverlap = this.intersects(lowLabelEl, maxLabelEl); const minHighOverlap = this.intersects(minLabelEl, highLabelEl); const minLowOverlap = this.intersects(minLabelEl, lowLabelEl); const minMaxOverlap = this.intersects(minLabelEl, maxLabelEl); const hiddenClass = CSS.labelHidden; if (lowLabelEl) { if (minLowOverlap || lowMaxOverlap || lowHighOverlap) { lowLabelEl.classList.add(hiddenClass); } else { lowLabelEl.classList.remove(hiddenClass); } } if (highLabelEl) { if (minHighOverlap || lowMaxOverlap || highMaxOverlap) { highLabelEl.classList.add(hiddenClass); } else { highLabelEl.classList.remove(hiddenClass); } } if (minLabelEl && maxLabelEl) { if (minMaxOverlap) { maxLabelEl.classList.add(hiddenClass); } else { maxLabelEl.classList.remove(hiddenClass); } } } determineValueLabelPosition() { const { valueLabelEl: { value: valueLabelEl }, meterContainerEl: { value: meterContainerEl }, currentPercent } = this; const valuePosition = currentPercent > 100 ? 100 : currentPercent > 0 ? currentPercent : 0; const valueLabelWidth = valueLabelEl.getBoundingClientRect().width; const containerWidth = meterContainerEl.getBoundingClientRect().width; const labelWidthPercent = 100 * (valueLabelWidth - 0) / (containerWidth - 0); if (valuePosition + labelWidthPercent >= 100) { valueLabelEl.style.insetInlineEnd = "0%"; valueLabelEl.style.removeProperty("inset-inline-start"); } else { valueLabelEl.style.insetInlineStart = `${valuePosition}% `; valueLabelEl.style.removeProperty("inset-inline-end"); } } renderMeterFill() { const { currentPercent, fillType } = this; const kindClass = this.getMeterKindCssClass(); return html`<div class=${safeClassMap({ [CSS.fill]: true, [kindClass]: fillType !== "single" })} style=${safeStyleMap({ width: `${currentPercent}%` })}></div>`; } renderRangeLine(position) { const style = { insetInlineStart: `${position}%` }; return html`<div class=${safeClassMap(CSS.stepLine)} style=${safeStyleMap(style)}></div>`; } renderValueLabel() { const { currentPercent, valueLabelType, unitLabel, value } = this; const label = this.formatLabel(valueLabelType === "percent" ? currentPercent / 100 : value || 0, valueLabelType); return keyed("low-label-line", html`<div class=${safeClassMap({ [CSS.label]: true, [CSS.labelValue]: true })} ${ref(this.valueLabelEl)}>${label}${unitLabel && valueLabelType !== "percent" && html`<span class=${safeClassMap(CSS.unitLabel)}>&nbsp;${unitLabel}</span>` || ""}</div>`); } renderMinLabel() { const { rangeLabelType, min, minPercent, unitLabel } = this; const style = { insetInlineStart: `${minPercent}%` }; const labelMin = this.formatLabel(rangeLabelType === "percent" ? minPercent : min, rangeLabelType); return keyed("min-label-line", html`<div class=${safeClassMap({ [CSS.label]: true, [CSS.labelRange]: true })} style=${safeStyleMap(style)} ${ref(this.minLabelEl)}>${labelMin}${unitLabel && rangeLabelType !== "percent" && html`<span class=${safeClassMap(CSS.unitLabel)}>&nbsp;${unitLabel}</span>` || ""}</div>`); } renderLowLabel() { const { rangeLabelType, low, lowPercent, highPercent, labelFlipProximity } = this; const label = low ? this.formatLabel(rangeLabelType === "percent" ? lowPercent / 100 : low, rangeLabelType) : ""; const styleDefault = { insetInlineStart: `${lowPercent}%` }; const styleFlipped = { insetInlineEnd: `${100 - lowPercent}%` }; const style = (highPercent - lowPercent) / 100 < labelFlipProximity ? styleFlipped : styleDefault; return keyed("low-label-line", html`<div class=${safeClassMap({ [CSS.label]: true, [CSS.labelRange]: true })} style=${safeStyleMap(style)} ${ref(this.lowLabelEl)}>${label}</div>`); } renderHighLabel() { const { rangeLabelType, high, highPercent, labelFlipMax } = this; const label = high ? this.formatLabel(rangeLabelType === "percent" ? highPercent / 100 : high, rangeLabelType) : ""; const styleDefault = { insetInlineStart: `${highPercent}%` }; const styleFlipped = { insetInlineEnd: `${100 - highPercent}%` }; const style = highPercent / 100 >= labelFlipMax ? styleFlipped : styleDefault; return keyed("high-label-line", html`<div class=${safeClassMap({ [CSS.label]: true, [CSS.labelRange]: true })} style=${safeStyleMap(style)} ${ref(this.highLabelEl)}>${label}</div>`); } renderMaxLabel() { const { rangeLabelType, max, maxPercent } = this; const style = { insetInlineEnd: `${100 - maxPercent}%` }; const labelMax = this.formatLabel(rangeLabelType === "percent" ? maxPercent / 100 : max, rangeLabelType); return keyed("max-label-line", html`<div class=${safeClassMap({ [CSS.label]: true, [CSS.labelRange]: true })} style=${safeStyleMap(style)} ${ref(this.maxLabelEl)}>${labelMax}</div>`); } render() { const { appearance, currentPercent, highActive, highPercent, label, lowActive, lowPercent, max, maxPercent, min, minPercent, rangeLabels, rangeLabelType, unitLabel, value, valueLabel, valueLabelType } = this; const textPercentLabelWithPercent = this.formatLabel(currentPercent / 100, "percent"); const textUnitLabel = `${value} ${unitLabel}`; const valueText = valueLabelType === "percent" ? textPercentLabelWithPercent : unitLabel ? textUnitLabel : void 0; return html`<div .ariaLabel=${label} .ariaValueMax=${rangeLabelType === "percent" ? maxPercent : max} .ariaValueMin=${rangeLabelType === "percent" ? minPercent : min} .ariaValueNow=${valueLabelType === "percent" ? currentPercent : value} .ariaValueText=${valueText} class=${safeClassMap({ [CSS.container]: true, [CSS.stepsVisible]: rangeLabels, [CSS.valueVisible]: valueLabel, [appearance]: appearance !== "outline-fill" })} role=meter ${ref(this.meterContainerEl)}>${this.renderMeterFill()}${valueLabel && this.renderValueLabel() || ""}${lowActive && this.renderRangeLine(lowPercent) || ""}${highActive && this.renderRangeLine(highPercent) || ""}${rangeLabels && this.renderMinLabel() || ""}${rangeLabels && lowActive && this.renderLowLabel() || ""}${rangeLabels && highActive && this.renderHighLabel() || ""}${rangeLabels && this.renderMaxLabel() || ""}</div>`; } } customElement("calcite-meter", Meter); export { Meter };