UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

720 lines (719 loc) • 41.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 { ref } from "lit-html/directives/ref.js"; import { keyed } from "lit-html/directives/keyed.js"; import { html, nothing } from "lit"; import { LitElement, createEvent, stringOrBoolean, setAttribute, safeClassMap, safeStyleMap } from "@arcgis/lumina"; import { g as guid } from "../../chunks/guid.js"; import { i as isPrimaryPointerButton, j as intersects } from "../../chunks/dom.js"; import { V as Validation } from "../../chunks/Validation.js"; import { c as connectForm, a as afterConnectDefaultValueSet, d as disconnectForm, H as HiddenFormInputSlot } from "../../chunks/form.js"; import { u as updateHostInteraction, I as InteractiveContainer } from "../../chunks/interactive.js"; import { i as isActivationKey } from "../../chunks/key.js"; import { c as connectLabel, d as disconnectLabel, g as getLabelText } from "../../chunks/label.js"; import { c as componentFocusable } from "../../chunks/component.js"; import { n as numberStringFormatter, B as BigDecimal } from "../../chunks/locale.js"; import { d as decimalPlaces, c as clamp } from "../../chunks/math.js"; import { u as useT9n } from "../../chunks/useT9n.js"; import { css } from "@lit/reactive-element/css-tag.js"; const CSS = { container: "container", containerRange: "container--range", graph: "graph", handle: "handle", handleExtension: "handle-extension", handleLabel: "handle__label", handleLabelMinValue: "handle__label--minValue", handleLabelValue: "handle__label--value", hyphen: "hyphen", hyphenWrap: "hyphen--wrap", static: "static", thumb: "thumb", thumbActive: "thumb--active", thumbContainer: "thumb-container", thumbMinValue: "thumb--minValue", thumbPrecise: "thumb--precise", thumbValue: "thumb--value", tick: "tick", tickActive: "tick--active", tickLabel: "tick__label", tickMax: "tick__label--max", tickMin: "tick__label--min", ticks: "ticks", track: "track", trackRange: "track__range", transformed: "transformed" }; const IDS = { validationMessage: "validationMessage" }; const maxTickElementThreshold = 250; const styles = css`@charset "UTF-8";:host([disabled]){cursor:default;-webkit-user-select:none;user-select:none;opacity:var(--calcite-opacity-disabled)}:host([disabled]) *,:host([disabled]) ::slotted(*){pointer-events:none}.scale--s{--calcite-slider-handle-size: .625rem;--calcite-slider-handle-extension-height: .4rem;--calcite-slider-container-font-size: var(--calcite-font-size--3)}.scale--s .handle__label,.scale--s .tick__label{line-height:.75rem}.scale--m{--calcite-slider-handle-size: .875rem;--calcite-slider-handle-extension-height: .5rem;--calcite-slider-container-font-size: var(--calcite-font-size--2)}.scale--m .handle__label,.scale--m .tick__label{line-height:1rem}.scale--l{--calcite-slider-handle-size: 1rem;--calcite-slider-handle-extension-height: .65rem;--calcite-slider-container-font-size: var(--calcite-font-size--1)}.scale--l .handle__label,.scale--l .tick__label{line-height:1rem}.handle__label,.tick__label{font-weight:var(--calcite-font-weight-medium);color:var(--calcite-slider-text-color, var(--calcite-color-text-2));font-size:var(--calcite-slider-container-font-size)}:host{display:block}:host .validation-container{padding-block-start:0!important}.container{position:relative;display:block;overflow-wrap:normal;word-break:normal;padding-inline:calc(var(--calcite-slider-handle-size) * .5);padding-block:calc(var(--calcite-slider-handle-size) * .5);margin-block:calc(var(--calcite-slider-handle-size) * .5);margin-inline:0;--calcite-slider-full-handle-height: calc( var(--calcite-slider-handle-size) + var(--calcite-slider-handle-extension-height) );touch-action:none;-webkit-user-select:none;user-select:none}:host([disabled]) .track__range,:host([disabled]) .tick--active{background-color:var(--calcite-color-text-3)}:host([disabled]) ::slotted([calcite-hydrated][disabled]),:host([disabled]) [calcite-hydrated][disabled]{opacity:1}.interaction-container{display:contents}.scale--s .thumb:not(.thumb--precise){--calcite-slider-thumb-y-offset: -.375rem}.scale--m .thumb:not(.thumb--precise){--calcite-slider-thumb-y-offset: -.5rem}.scale--l .thumb:not(.thumb--precise){--calcite-slider-thumb-y-offset: -.55rem}:host([precise]:not([has-histogram])) .container .thumb--value{--calcite-slider-thumb-y-offset: calc(var(--calcite-slider-full-handle-height) * -1)}.thumb-container{position:relative;max-inline-size:100%}.thumb{--calcite-slider-thumb-x-offset: calc(var(--calcite-slider-handle-size) * .5);position:absolute;margin:0;display:flex;cursor:pointer;flex-direction:column;align-items:center;border-style:none;background-color:transparent;padding:0;font-family:inherit;outline:2px solid transparent;outline-offset:2px;transform:translate(var(--calcite-slider-thumb-x-offset),var(--calcite-slider-thumb-y-offset))}.thumb .handle__label{white-space:nowrap}.thumb .handle__label.static,.thumb .handle__label.transformed{position:absolute;inset-block:0px;opacity:0}.thumb .handle__label.hyphen:after{content:"\u2014";display:inline-block;inline-size:1em}.thumb .handle__label.hyphen--wrap{display:flex}.thumb .handle{box-sizing:border-box;border-radius:9999px;outline-color:transparent;background-color:var(--calcite-slider-handle-fill-color, var(--calcite-color-foreground-1));block-size:var(--calcite-slider-handle-size);inline-size:var(--calcite-slider-handle-size);box-shadow:0 0 0 2px var(--calcite-color-text-3) inset;transition:border var(--calcite-internal-animation-timing-medium) ease,background-color var(--calcite-internal-animation-timing-medium) ease,box-shadow var(--calcite-animation-timing) ease}.thumb .handle-extension{inline-size:.125rem;block-size:var(--calcite-slider-handle-extension-height);background-color:var(--calcite-slider-handle-extension-color, var(--calcite-color-text-3))}.thumb:hover .handle{box-shadow:0 0 0 3px var(--calcite-color-brand) inset}.thumb:hover .handle-extension{background-color:var(--calcite-slider-accent-color, var(--calcite-color-brand))}.thumb:focus .handle{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))))}.thumb:focus .handle-extension{background-color:var(--calcite-slider-accent-color, var(--calcite-color-brand))}.thumb.thumb--minValue{transform:translate(calc(var(--calcite-slider-thumb-x-offset) * -1),var(--calcite-slider-thumb-y-offset))}.thumb.thumb--precise{--calcite-slider-thumb-y-offset: -.125rem}:host([label-handles]) .thumb{--calcite-slider-thumb-x-offset: 50%}:host([label-handles]):host(:not([has-histogram])) .scale--s .thumb:not(.thumb--precise){--calcite-slider-thumb-y-offset: -1.4375rem}:host([label-handles]):host(:not([has-histogram])) .scale--m .thumb:not(.thumb--precise){--calcite-slider-thumb-y-offset: -1.875rem}:host([label-handles]):host(:not([has-histogram])) .scale--l .thumb:not(.thumb--precise){--calcite-slider-thumb-y-offset: -2rem}:host([has-histogram][label-handles]) .handle__label,:host([label-handles]:not([has-histogram])) .thumb--minValue.thumb--precise .handle__label{margin-block-start:.5em}:host(:not([has-histogram]):not([precise])) .handle__label,:host([label-handles]:not([has-histogram])) .thumb--value .handle__label{margin-block-end:.5em}:host([label-handles][precise]):host(:not([has-histogram])) .scale--s .thumb--value{--calcite-slider-thumb-y-offset: -2.075rem}:host([label-handles][precise]):host(:not([has-histogram])) .scale--m .thumb--value{--calcite-slider-thumb-y-offset: -2.75rem}:host([label-handles][precise]):host(:not([has-histogram])) .scale--l .thumb--value{--calcite-slider-thumb-y-offset: -3.0625rem}.thumb:focus .handle,.thumb--active .handle{background-color:var(--calcite-slider-accent-color, var(--calcite-color-brand));box-shadow:0 0 8px #00000029}.thumb:hover.thumb--precise:after,.thumb:focus.thumb--precise:after,.thumb--active.thumb--precise:after{background-color:var(--calcite-slider-accent-color, var(--calcite-color-brand))}.track{position:relative;block-size:.125rem;border-radius:0;background-color:var(--calcite-slider-track-color, var(--calcite-color-border-2));transition:all var(--calcite-internal-animation-timing-medium) ease-in}.track__range{position:absolute;inset-block-start:0px;block-size:.125rem;background-color:var(--calcite-slider-track-fill-color, var(--calcite-color-brand))}.container--range .track__range:hover{cursor:ew-resize}.container--range .track__range:after{position:absolute;inline-size:100%;content:"";inset-block-start:calc(var(--calcite-slider-full-handle-height) * .5 * -1);block-size:calc(var(--calcite-slider-handle-size) + var(--calcite-slider-handle-extension-height))}@media (forced-colors: active){.thumb{outline-width:0;outline-offset:0}.handle{outline:2px solid transparent;outline-offset:2px}.thumb:focus .handle,.thumb .handle-extension,.thumb:hover .handle-extension,.thumb:focus .handle-extension,.thumb:active .handle-extension{background-color:canvasText}.track{background-color:canvasText}.track__range{background-color:highlight}}.tick{position:absolute;block-size:.25rem;inline-size:.125rem;border-width:1px;border-style:solid;background-color:var(--calcite-slider-tick-color, var(--calcite-color-border-input));border-color:var(--calcite-slider-tick-border-color, var(--calcite-color-foreground-1));inset-block-start:-2px;pointer-events:none;margin-inline-start:-.125rem}.tick--active{background-color:var(--calcite-slider-tick-selected-color, var(--calcite-color-brand))}.tick__label{pointer-events:none;margin-block-start:.875rem;display:flex;justify-content:center}.tick__label--min{transition:opacity var(--calcite-animation-timing)}.tick__label--max{transition:opacity var(--calcite-internal-animation-timing-fast)}:host([has-histogram][label-handles]) .tick__label--min,:host([has-histogram][label-handles]) .tick__label--max,:host([has-histogram][precise]) .tick__label--min,:host([has-histogram][precise]) .tick__label--max{font-weight:var(--calcite-font-weight-normal);color:var(--calcite-color-text-3)}.graph{color:var(--calcite-slider-graph-color, var(--calcite-color-foreground-3));block-size:48px}:host([label-ticks][ticks]) .container{padding-block-end:calc(.875rem + var(--calcite-slider-container-font-size))}:host([has-histogram]):host([precise][label-handles]) .container{padding-block-end:calc(var(--calcite-slider-full-handle-height) + 1em)}:host([has-histogram]):host([label-handles]:not([precise])) .container{padding-block-end:calc(var(--calcite-slider-handle-size) * .5 + 1em)}:host([has-histogram]):host([precise]:not([label-handles])) .container{padding-block-end:var(--calcite-slider-full-handle-height)}:host(:not([has-histogram])):host([precise]:not([label-handles])) .container{padding-block-start:var(--calcite-slider-full-handle-height)}:host(:not([has-histogram])):host([precise]:not([label-handles])) .container--range{padding-block-end:var(--calcite-slider-full-handle-height)}:host(:not([has-histogram])):host([label-handles]:not([precise])) .container{padding-block-start:calc(var(--calcite-slider-full-handle-height) + 4px)}:host(:not([has-histogram])):host([label-handles][precise]) .container{padding-block-start:calc(var(--calcite-slider-full-handle-height) + var(--calcite-slider-container-font-size) + 4px)}:host(:not([has-histogram])):host([label-handles][precise]) .container--range{padding-block-end:calc(var(--calcite-slider-full-handle-height) + var(--calcite-slider-container-font-size) + 4px)}.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}`; function isRange(value) { return Array.isArray(value); } class Slider extends LitElement { constructor() { super(); this.activeProp = "value"; this.dragEnd = (event) => { if (this.disabled) { return; } this.removeDragListeners(); this.focusActiveHandle(event.clientX); if (this.lastDragPropValue != this[this.dragProp]) { this.emitChange(); } this.dragProp = null; this.lastDragPropValue = null; this.minValueDragRange = null; this.maxValueDragRange = null; this.minMaxValueRange = null; }; this.dragUpdate = (event) => { if (this.disabled) { return; } event.preventDefault(); if (this.dragProp) { const value = this.mapToRange(event.clientX || event.pageX); if (isRange(this.value) && this.dragProp === "minMaxValue") { if (this.minValueDragRange && this.maxValueDragRange && this.minMaxValueRange) { const newMinValue = value - this.minValueDragRange; const newMaxValue = value + this.maxValueDragRange; if (newMaxValue <= this.max && newMinValue >= this.min && newMaxValue - newMinValue === this.minMaxValueRange) { this.setValue({ minValue: this.clamp(newMinValue, "minValue"), maxValue: this.clamp(newMaxValue, "maxValue") }); } } else { this.minValueDragRange = value - this.minValue; this.maxValueDragRange = this.maxValue - value; this.minMaxValueRange = this.maxValue - this.minValue; } } else { this.setValue({ [this.dragProp]: this.clamp(value, this.dragProp) }); } } }; this.formatValue = (value) => { numberStringFormatter.numberFormatOptions = { locale: this.messages._lang, numberingSystem: this.numberingSystem, useGrouping: this.groupSeparator }; return numberStringFormatter.localize(value.toString()); }; this.guid = `calcite-slider-${guid()}`; this.messages = useT9n({ name: null }); this.pointerUpDragEnd = (event) => { if (this.disabled || !isPrimaryPointerButton(event)) { return; } this.dragEnd(event); }; this.maxValueDragRange = null; this.minMaxValueRange = null; this.minValueDragRange = null; this.tickValues = []; this.disabled = false; this.fillPlacement = "start"; this.groupSeparator = false; this.hasHistogram = false; this.labelHandles = false; this.labelTicks = false; this.max = 100; this.min = 0; this.mirrored = false; this.precise = false; this.required = false; this.scale = "m"; this.snap = false; this.status = "idle"; this.step = 1; 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.value = 0; this.calciteSliderChange = createEvent({ cancelable: false }); this.calciteSliderInput = createEvent({ cancelable: false }); this.listen("pointerdown", this.pointerDownHandler); this.listen("keydown", this.handleKeyDown); this.listen("touchstart", this.handleTouchStart); } static { this.properties = { maxValueDragRange: [16, {}, { state: true }], minMaxValueRange: [16, {}, { state: true }], minValueDragRange: [16, {}, { state: true }], tickValues: [16, {}, { state: true }], disabled: [7, {}, { reflect: true, type: Boolean }], fillPlacement: [3, {}, { reflect: true }], form: [3, {}, { reflect: true }], groupSeparator: [7, {}, { reflect: true, type: Boolean }], hasHistogram: [7, {}, { reflect: true, type: Boolean }], histogram: [0, {}, { attribute: false }], histogramStops: [0, {}, { attribute: false }], labelFormatter: [0, {}, { attribute: false }], labelHandles: [7, {}, { reflect: true, type: Boolean }], labelTicks: [7, {}, { reflect: true, type: Boolean }], max: [11, {}, { reflect: true, type: Number }], maxLabel: 1, maxValue: [9, {}, { type: Number }], min: [11, {}, { reflect: true, type: Number }], minLabel: 1, minValue: [9, {}, { type: Number }], mirrored: [7, {}, { reflect: true, type: Boolean }], name: [3, {}, { reflect: true }], numberingSystem: 1, pageStep: [11, {}, { reflect: true, type: Number }], precise: [7, {}, { reflect: true, type: Boolean }], required: [7, {}, { reflect: true, type: Boolean }], scale: [3, {}, { reflect: true }], snap: [7, {}, { reflect: true, type: Boolean }], status: [3, {}, { reflect: true }], step: [11, {}, { reflect: true, type: Number }], ticks: [11, {}, { reflect: true, type: Number }], validationIcon: [3, { converter: stringOrBoolean }, { reflect: true }], validationMessage: 1, validity: [0, {}, { attribute: false }], value: [11, {}, { type: Number, reflect: true }] }; } static { this.shadowRootOptions = { mode: "open", delegatesFocus: true }; } static { this.styles = styles; } async setFocus() { await componentFocusable(this); const handle = this.minHandle ? this.minHandle : this.maxHandle; handle?.focus(); } connectedCallback() { super.connectedCallback(); this.setMinMaxFromValue(); this.setValueFromMinMax(); connectLabel(this); connectForm(this); } load() { if (!isRange(this.value)) { this.value = this.snap ? this.getClosestStep(this.value) : this.clamp(this.value); } afterConnectDefaultValueSet(this, this.value); } willUpdate(changes) { if (changes.has("histogram")) { this.hasHistogram = !!this.histogram; } if (changes.has("ticks")) { this.tickValues = this.generateTickValues(); } if (changes.has("value") && (this.hasUpdated || this.value !== 0)) { this.setMinMaxFromValue(); } if (changes.has("minValue") || changes.has("maxValue")) { this.setValueFromMinMax(); } } updated() { if (this.labelHandles) { this.adjustHostObscuredHandleLabel("value"); if (isRange(this.value)) { this.adjustHostObscuredHandleLabel("minValue"); if (!(this.precise && !this.hasHistogram)) { this.hyphenateCollidingRangeHandleLabels(); } } } this.hideObscuredBoundingTickLabels(); updateHostInteraction(this); } disconnectedCallback() { super.disconnectedCallback(); disconnectLabel(this); disconnectForm(this); this.removeDragListeners(); } handleKeyDown(event) { const mirror = this.shouldMirror(); const { activeProp, max, min, pageStep, step } = this; const value = this[activeProp]; const { key } = event; if (isActivationKey(key)) { event.preventDefault(); return; } let adjustment; if (key === "ArrowUp" || key === "ArrowRight") { const directionFactor = mirror && key === "ArrowRight" ? -1 : 1; adjustment = value + step * directionFactor; } else if (key === "ArrowDown" || key === "ArrowLeft") { const directionFactor = mirror && key === "ArrowLeft" ? -1 : 1; adjustment = value - step * directionFactor; } else if (key === "PageUp") { if (pageStep) { adjustment = value + pageStep; } } else if (key === "PageDown") { if (pageStep) { adjustment = value - pageStep; } } else if (key === "Home") { adjustment = min; } else if (key === "End") { adjustment = max; } if (isNaN(adjustment)) { return; } event.preventDefault(); const fixedDecimalAdjustment = Number(adjustment.toFixed(decimalPlaces(step))); this.setValue({ [activeProp]: this.clamp(fixedDecimalAdjustment, activeProp) }); } pointerDownHandler(event) { if (this.disabled || !isPrimaryPointerButton(event)) { return; } const x = event.clientX || event.pageX; const position = this.mapToRange(x); let prop = "value"; if (isRange(this.value)) { const inRange = position >= this.minValue && position <= this.maxValue; if (inRange && this.lastDragProp === "minMaxValue") { prop = "minMaxValue"; } else { const closerToMax = Math.abs(this.maxValue - position) < Math.abs(this.minValue - position); prop = closerToMax || position >= this.maxValue ? "maxValue" : "minValue"; } } this.lastDragPropValue = this[prop]; this.dragStart(prop); const isThumbActive = this.el.shadowRoot.querySelector(`.${CSS.thumb}:active`); if (!isThumbActive) { this.setValue({ [prop]: this.clamp(position, prop) }); } this.focusActiveHandle(x); } handleTouchStart(event) { event.preventDefault(); } buildThumbType(type) { const thumbTypeParts = [type]; if (this.labelHandles) { thumbTypeParts.push("labeled"); } if (this.precise) { thumbTypeParts.push("precise"); } if (this.hasHistogram) { thumbTypeParts.push("histogram"); } return thumbTypeParts.join("-"); } setValueFromMinMax() { const { minValue, maxValue } = this; if (typeof minValue === "number" && typeof maxValue === "number") { this.value = [minValue, maxValue]; } } setMinMaxFromValue() { const { value } = this; if (isRange(value)) { this.minValue = value[0]; this.maxValue = value[1]; } } onLabelClick() { this.setFocus(); } shouldMirror() { return this.mirrored && !this.hasHistogram; } shouldUseMinValue() { if (!isRange(this.value)) { return false; } return this.hasHistogram && this.maxValue === 0 || !this.hasHistogram && this.minValue === 0; } getTickDensity() { const density = (this.max - this.min) / this.ticks / maxTickElementThreshold; return density < 1 ? 1 : density; } generateTickValues() { const tickInterval = this.ticks ?? 0; if (tickInterval <= 0) { return []; } const ticks = [this.min]; const density = this.getTickDensity(); const tickOffset = tickInterval * density; let current = this.min; while (current < this.max) { current += tickOffset; ticks.push(Math.min(current, this.max)); } if (!ticks.includes(this.max)) { ticks.push(this.max); } return ticks; } onThumbBlur() { this.activeProp = null; } onThumbFocus(event) { const thumb = event.currentTarget; this.activeProp = thumb.getAttribute("data-value-prop"); } onThumbPointerDown(event) { const thumb = event.currentTarget; this.pointerDownDragStart(event, thumb.getAttribute("data-value-prop")); } onTrackPointerDown(event) { this.pointerDownDragStart(event, "minMaxValue"); } pointerDownDragStart(event, prop) { if (!isPrimaryPointerButton(event)) { return; } this.dragStart(prop); } dragStart(prop) { this.dragProp = prop; this.lastDragProp = this.dragProp; this.activeProp = prop; window.addEventListener("pointermove", this.dragUpdate); window.addEventListener("pointerup", this.pointerUpDragEnd); window.addEventListener("pointercancel", this.dragEnd); } focusActiveHandle(valueX) { if (this.dragProp === "minValue") { this.minHandle.focus(); } else if (this.dragProp === "maxValue" || this.dragProp === "value") { this.maxHandle.focus(); } else if (this.dragProp === "minMaxValue") { this.getClosestHandle(valueX).focus(); } } emitInput() { this.calciteSliderInput.emit(); } emitChange() { this.calciteSliderChange.emit(); } removeDragListeners() { window.removeEventListener("pointermove", this.dragUpdate); window.removeEventListener("pointerup", this.pointerUpDragEnd); window.removeEventListener("pointercancel", this.dragEnd); } setValue(values) { let valueChanged; Object.keys(values).forEach((propName) => { const newValue = values[propName]; if (!valueChanged) { const oldValue = this[propName]; valueChanged = oldValue !== newValue; } this[propName] = newValue; }); if (!valueChanged) { return; } const dragging = this.dragProp; if (!dragging) { this.emitChange(); } this.emitInput(); } storeTrackRef(node) { this.trackEl = node; } storeThumbRef(el) { if (!el) { return; } const valueProp = el.getAttribute("data-value-prop"); if (valueProp === "minValue") { this.minHandle = el; } else { this.maxHandle = el; } } clamp(value, prop) { value = clamp(value, this.min, this.max); if (prop === "maxValue") { value = Math.max(value, this.minValue); } if (prop === "minValue") { value = Math.min(value, this.maxValue); } return value; } mapToRange(x) { const range = this.max - this.min; const { left, width } = this.trackEl.getBoundingClientRect(); const percent = (x - left) / width; const mirror = this.shouldMirror(); const clampedValue = this.clamp(this.min + range * (mirror ? 1 - percent : percent)); const value = Number(clampedValue.toFixed(decimalPlaces(this.step))); return !(this.snap && this.step) ? value : this.getClosestStep(value); } getClosestStep(value) { const { max, min, step } = this; const bigDecimalString = new BigDecimal(`${Math.floor((value - min) / step)}`).multiply(`${step}`).add(`${min}`).toString(); let snappedValue = this.clamp(Number(bigDecimalString)); if (snappedValue > max) { snappedValue -= step; } return snappedValue; } getClosestHandle(valueX) { return this.getDistanceX(this.maxHandle, valueX) > this.getDistanceX(this.minHandle, valueX) ? this.minHandle : this.maxHandle; } getDistanceX(el, valueX) { return Math.abs(el.getBoundingClientRect().left - valueX); } getFontSizeForElement(element) { return Number(window.getComputedStyle(element).getPropertyValue("font-size").match(/\d+/)[0]); } getUnitInterval(num) { num = this.clamp(num); const range = this.max - this.min; return (num - this.min) / range; } adjustHostObscuredHandleLabel(name) { const label = this.el.shadowRoot.querySelector(`.handle__label--${name}`); const labelStatic = this.el.shadowRoot.querySelector(`.handle__label--${name}.static`); const labelTransformed = this.el.shadowRoot.querySelector(`.handle__label--${name}.transformed`); const labelStaticBounds = labelStatic.getBoundingClientRect(); const labelStaticOffset = this.getHostOffset(labelStaticBounds.left, labelStaticBounds.right); label.style.transform = `translateX(${labelStaticOffset}px)`; labelTransformed.style.transform = `translateX(${labelStaticOffset}px)`; } hyphenateCollidingRangeHandleLabels() { const { shadowRoot } = this.el; const mirror = this.shouldMirror(); const leftModifier = mirror ? "value" : "minValue"; const rightModifier = mirror ? "minValue" : "value"; const leftValueLabel = shadowRoot.querySelector(`.handle__label--${leftModifier}`); const leftValueLabelStatic = shadowRoot.querySelector(`.handle__label--${leftModifier}.static`); const leftValueLabelTransformed = shadowRoot.querySelector(`.handle__label--${leftModifier}.transformed`); const leftValueLabelStaticHostOffset = this.getHostOffset(leftValueLabelStatic.getBoundingClientRect().left, leftValueLabelStatic.getBoundingClientRect().right); const rightValueLabel = shadowRoot.querySelector(`.handle__label--${rightModifier}`); const rightValueLabelStatic = shadowRoot.querySelector(`.handle__label--${rightModifier}.static`); const rightValueLabelTransformed = shadowRoot.querySelector(`.handle__label--${rightModifier}.transformed`); const rightValueLabelStaticHostOffset = this.getHostOffset(rightValueLabelStatic.getBoundingClientRect().left, rightValueLabelStatic.getBoundingClientRect().right); const labelFontSize = this.getFontSizeForElement(leftValueLabel); const labelTransformedOverlap = this.getRangeLabelOverlap(leftValueLabelTransformed, rightValueLabelTransformed); const hyphenLabel = leftValueLabel; const labelOffset = labelFontSize / 2; if (labelTransformedOverlap > 0) { hyphenLabel.classList.add(CSS.hyphen, CSS.hyphenWrap); if (rightValueLabelStaticHostOffset === 0 && leftValueLabelStaticHostOffset === 0) { let leftValueLabelTranslate = labelTransformedOverlap / 2 - labelOffset; leftValueLabelTranslate = Math.sign(leftValueLabelTranslate) === -1 ? Math.abs(leftValueLabelTranslate) : -leftValueLabelTranslate; const leftValueLabelTransformedHostOffset = this.getHostOffset(leftValueLabelTransformed.getBoundingClientRect().left + leftValueLabelTranslate - labelOffset, leftValueLabelTransformed.getBoundingClientRect().right + leftValueLabelTranslate - labelOffset); let rightValueLabelTranslate = labelTransformedOverlap / 2; const rightValueLabelTransformedHostOffset = this.getHostOffset(rightValueLabelTransformed.getBoundingClientRect().left + rightValueLabelTranslate, rightValueLabelTransformed.getBoundingClientRect().right + rightValueLabelTranslate); if (leftValueLabelTransformedHostOffset !== 0) { leftValueLabelTranslate += leftValueLabelTransformedHostOffset; rightValueLabelTranslate += leftValueLabelTransformedHostOffset; } if (rightValueLabelTransformedHostOffset !== 0) { leftValueLabelTranslate += rightValueLabelTransformedHostOffset; rightValueLabelTranslate += rightValueLabelTransformedHostOffset; } leftValueLabel.style.transform = `translateX(${leftValueLabelTranslate}px)`; leftValueLabelTransformed.style.transform = `translateX(${leftValueLabelTranslate - labelOffset}px)`; rightValueLabel.style.transform = `translateX(${rightValueLabelTranslate}px)`; rightValueLabelTransformed.style.transform = `translateX(${rightValueLabelTranslate}px)`; } else if (leftValueLabelStaticHostOffset > 0 || rightValueLabelStaticHostOffset > 0) { leftValueLabel.style.transform = `translateX(${leftValueLabelStaticHostOffset + labelOffset}px)`; rightValueLabel.style.transform = `translateX(${labelTransformedOverlap + rightValueLabelStaticHostOffset}px)`; rightValueLabelTransformed.style.transform = `translateX(${labelTransformedOverlap + rightValueLabelStaticHostOffset}px)`; } else if (leftValueLabelStaticHostOffset < 0 || rightValueLabelStaticHostOffset < 0) { let leftValueLabelTranslate = Math.abs(leftValueLabelStaticHostOffset) + labelTransformedOverlap - labelOffset; leftValueLabelTranslate = Math.sign(leftValueLabelTranslate) === -1 ? Math.abs(leftValueLabelTranslate) : -leftValueLabelTranslate; leftValueLabel.style.transform = `translateX(${leftValueLabelTranslate}px)`; leftValueLabelTransformed.style.transform = `translateX(${leftValueLabelTranslate - labelOffset}px)`; } } else { hyphenLabel.classList.remove(CSS.hyphen, CSS.hyphenWrap); leftValueLabel.style.transform = `translateX(${leftValueLabelStaticHostOffset}px)`; leftValueLabelTransformed.style.transform = `translateX(${leftValueLabelStaticHostOffset}px)`; rightValueLabel.style.transform = `translateX(${rightValueLabelStaticHostOffset}px)`; rightValueLabelTransformed.style.transform = `translateX(${rightValueLabelStaticHostOffset}px)`; } } hideObscuredBoundingTickLabels() { const valueIsRange = isRange(this.value); if (!this.hasHistogram && !valueIsRange && !this.labelHandles && !this.precise) { return; } if (!this.hasHistogram && !valueIsRange && this.labelHandles && !this.precise) { return; } if (!this.hasHistogram && !valueIsRange && !this.labelHandles && this.precise) { return; } if (!this.hasHistogram && !valueIsRange && this.labelHandles && this.precise) { return; } if (!this.hasHistogram && valueIsRange && !this.precise) { return; } if (this.hasHistogram && !this.precise && !this.labelHandles) { return; } const minHandle = this.el.shadowRoot.querySelector(`.${CSS.thumbMinValue}`); const maxHandle = this.el.shadowRoot.querySelector(`.${CSS.thumbValue}`); const minTickLabel = this.el.shadowRoot.querySelector(`.${CSS.tickMin}`); const maxTickLabel = this.el.shadowRoot.querySelector(`.${CSS.tickMax}`); if (!minHandle && maxHandle && minTickLabel && maxTickLabel) { minTickLabel.style.opacity = this.isMinTickLabelObscured(minTickLabel, maxHandle) ? "0" : "1"; maxTickLabel.style.opacity = this.isMaxTickLabelObscured(maxTickLabel, maxHandle) ? "0" : "1"; } if (minHandle && maxHandle && minTickLabel && maxTickLabel) { minTickLabel.style.opacity = this.isMinTickLabelObscured(minTickLabel, minHandle) || this.isMinTickLabelObscured(minTickLabel, maxHandle) ? "0" : "1"; maxTickLabel.style.opacity = this.isMaxTickLabelObscured(maxTickLabel, minHandle) || this.isMaxTickLabelObscured(maxTickLabel, maxHandle) && this.hasHistogram ? "0" : "1"; } } getHostOffset(leftBounds, rightBounds) { const { left, right } = this.el.getBoundingClientRect(); if (leftBounds < left) { return left - leftBounds; } if (rightBounds > right) { return -(rightBounds - right); } return 0; } getRangeLabelOverlap(leftLabel, rightLabel) { const leftLabelBounds = leftLabel.getBoundingClientRect(); const rightLabelBounds = rightLabel.getBoundingClientRect(); const leftLabelFontSize = this.getFontSizeForElement(leftLabel); const rangeLabelOverlap = leftLabelBounds.right + leftLabelFontSize - rightLabelBounds.left; return Math.max(rangeLabelOverlap, 0); } isMinTickLabelObscured(minLabel, handle) { const minLabelBounds = minLabel.getBoundingClientRect(); const handleBounds = handle.getBoundingClientRect(); return intersects(minLabelBounds, handleBounds); } isMaxTickLabelObscured(maxLabel, handle) { const maxLabelBounds = maxLabel.getBoundingClientRect(); const handleBounds = handle.getBoundingClientRect(); return intersects(maxLabelBounds, handleBounds); } internalLabelFormatter(value, type) { const customFormatter = this.labelFormatter; if (!customFormatter) { return this.formatValue(value); } const formattedValue = customFormatter(value, type, this.formatValue); if (formattedValue == null) { return this.formatValue(value); } return formattedValue; } render() { const id = this.el.id || this.guid; const value = isRange(this.value) ? this.maxValue : this.value; const min = this.minValue || this.min; const useMinValue = this.shouldUseMinValue(); const minInterval = this.getUnitInterval(useMinValue ? this.minValue : min) * 100; const maxInterval = this.getUnitInterval(value) * 100; const mirror = this.shouldMirror(); const valueIsRange = isRange(this.value); const thumbTypes = this.buildThumbType("max"); const thumb = this.renderThumb({ type: thumbTypes, thumbPlacement: thumbTypes.includes("histogram") ? "below" : "above", maxInterval, minInterval, mirror }); const minThumbTypes = this.buildThumbType("min"); const minThumb = valueIsRange ? this.renderThumb({ type: minThumbTypes, thumbPlacement: minThumbTypes.includes("histogram") || minThumbTypes.includes("precise") ? "below" : "above", maxInterval, minInterval, mirror }) : null; const fillPlacement = valueIsRange ? "start" : this.fillPlacement; const trackRangePlacementStyles = fillPlacement === "none" ? { left: `unset`, right: `unset` } : fillPlacement === "end" ? { left: `${mirror ? minInterval : maxInterval}%`, right: `${mirror ? maxInterval : minInterval}%` } : ( /* default */ { left: `${mirror ? 100 - maxInterval : minInterval}%`, right: `${mirror ? minInterval : 100 - maxInterval}%` } ); setAttribute(this.el, "id", id); return InteractiveContainer({ disabled: this.disabled, children: html`<div aria-errormessage=${IDS.validationMessage} .ariaInvalid=${this.status === "invalid"} .ariaLabel=${getLabelText(this)} class=${safeClassMap({ [CSS.container]: true, [CSS.containerRange]: valueIsRange, [`scale--${this.scale}`]: true })}>${this.renderGraph()}<div class=${safeClassMap(CSS.track)} ${ref(this.storeTrackRef)}><div class=${safeClassMap(CSS.trackRange)} @pointerdown=${this.onTrackPointerDown} style=${safeStyleMap(trackRangePlacementStyles)}></div><div class=${safeClassMap(CSS.ticks)}>${this.tickValues.map((tick) => { const tickOffset = `${this.getUnitInterval(tick) * 100}%`; let activeTicks = false; if (fillPlacement === "start" || fillPlacement === "end") { if (useMinValue) { activeTicks = tick >= this.minValue && tick <= this.maxValue; } else { const rangeStart = fillPlacement === "start" ? min : value; const rangeEnd = fillPlacement === "start" ? value : this.max; activeTicks = tick >= rangeStart && tick <= rangeEnd; } } return html`<span class=${safeClassMap({ [CSS.tick]: true, [CSS.tickActive]: activeTicks })} style=${safeStyleMap({ left: mirror ? "" : tickOffset, right: mirror ? tickOffset : "" })}>${this.renderTickLabel(tick)}</span>`; })}</div></div><div class=${safeClassMap(CSS.thumbContainer)}>${minThumb}${thumb}${HiddenFormInputSlot({ component: this })}</div></div>${this.validationMessage && this.status === "invalid" ? Validation({ icon: this.validationIcon, id: IDS.validationMessage, message: this.validationMessage, scale: this.scale, status: this.status }) : null}` }); } renderThumb({ type, mirror, thumbPlacement, minInterval, maxInterval }) { const isLabeled = type.includes("labeled"); const isPrecise = type.includes("precise"); const isMinThumb = type.includes("min"); const valueIsRange = isRange(this.value); const value = isMinThumb ? this.minValue : valueIsRange ? this.maxValue : this.value; const valueProp = isMinThumb ? "minValue" : valueIsRange ? "maxValue" : "value"; const ariaLabel = isMinThumb ? this.minLabel : valueIsRange ? this.maxLabel : this.minLabel; const ariaValuenow = isMinThumb ? this.minValue : value; const displayedValue = valueProp === "minValue" ? this.internalLabelFormatter(this.minValue, "min") : valueProp === "maxValue" ? this.internalLabelFormatter(this.maxValue, "max") : this.internalLabelFormatter(value, "value"); const thumbStyle = isMinThumb ? { left: `${mirror ? 100 - minInterval : minInterval}%` } : { right: `${mirror ? maxInterval : 100 - maxInterval}%` }; const thumbLabelClasses = `${CSS.handleLabel} ${isMinThumb ? CSS.handleLabelMinValue : CSS.handleLabelValue}`; const labels = isLabeled ? [ html`<span aria-hidden=true class=${safeClassMap(thumbLabelClasses)}>${displayedValue}</span>`, html`<span aria-hidden=true class=${`${thumbLabelClasses} ${CSS.static}`}>${displayedValue}</span>`, html`<span aria-hidden=true class=${`${thumbLabelClasses} ${CSS.transformed}`}>${displayedValue}</span>` ] : []; const thumbContent = [ ...labels, html`<div class=${safeClassMap(CSS.handle)}></div>`, isPrecise && html`<div class=${safeClassMap(CSS.handleExtension)}></div>` || "" ]; if (thumbPlacement === "below") { thumbContent.reverse(); } return keyed(type, html`<div .ariaLabel=${ariaLabel} aria-orientation=horizontal .ariaValueMax=${this.max} .ariaValueMin=${this.min} .ariaValueNow=${ariaValuenow} class=${safeClassMap({ [CSS.thumb]: true, [CSS.thumbValue]: !isMinThumb, [CSS.thumbActive]: this.lastDragProp !== "minMaxValue" && this.dragProp === valueProp, [CSS.thumbPrecise]: isPrecise, [CSS.thumbMinValue]: isMinThumb })} data-value-prop=${valueProp ?? nothing} @blur=${this.onThumbBlur} @focus=${this.onThumbFocus} @pointerdown=${this.onThumbPointerDown} role=slider style=${safeStyleMap(thumbStyle)} tabindex=0 ${ref(this.storeThumbRef)}>${thumbContent}</div>`); } renderGraph() { return this.histogram ? html`<calcite-graph class=${safeClassMap(CSS.graph)} .colorStops=${this.histogramStops} .data=${this.histogram} .highlightMax=${isRange(this.value) ? this.maxValue : this.value} .highlightMin=${isRange(this.value) ? this.minValue : this.min} .max=${this.max} .min=${this.min}></calcite-graph>` : null; } renderTickLabel(tick) { const { hasHistogram, labelHandles, labelTicks, max, min, precise, value } = this; const valueIsRange = isRange(value); const isMinTickLabel = tick === min; const isMaxTickLabel = tick === max; const isAtEdge = isMinTickLabel || isMaxTickLabel; const shouldDisplayLabel = labelTicks && (!hasHistogram && (isAtEdge || !precise || !valueIsRange) || hasHistogram && (isAtEdge || !precise && !labelHandles)); return shouldDisplayLabel ? html`<span class=${safeClassMap({ [CSS.tickLabel]: true, [CSS.tickMin]: isMinTickLabel, [CSS.tickMax]: isMaxTickLabel })}>${this.internalLabelFormatter(tick, "tick")}</span>` : null; } } customElement("calcite-slider", Slider); export { Slider };