@esri/calcite-components
Version:
Web Components for Esri's Calcite Design System.
270 lines (269 loc) • 17.8 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 { 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)}> ${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)}> ${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
};