import { PropertyValues, css as litCss, LitElement } from 'lit'; import { customElement, property, query } from 'lit/decorators.js'; import { nothing, html } from 'lit/html.js'; import { css, theme } from 'twind/css'; import { createUUID } from './utils/UniqueId'; import { MonoTextComp } from './MonoTextComp'; import { fromOptionalConverter, spread } from './utils/LitHelper'; import { SpreadController } from './utils/SpreadController'; import { TailwindStylesController } from './utils/TailwindStylesController'; import { RoundedCorners } from './utils/CommonTypes'; @customElement('mono-rangeslider') export class MonoRangeSliderComp extends LitElement { static styles = litCss` ::slotted(p:first-of-type) { margin: 0; } `; private _spreadController: SpreadController = new SpreadController(this); private __stylesController: TailwindStylesController = new TailwindStylesController( this, ); @property({ type: String, reflect: true }) value: string = ''; @property({ type: String, reflect: true }) min: string = ''; @property({ type: String, reflect: true }) max: string = ''; @property({ type: String, reflect: true }) step: string = ''; @property({ type: String, reflect: true }) id: string = createUUID(); @property({ type: String, reflect: true }) name: string = ''; @property({ type: String, reflect: true, converter: fromOptionalConverter }) error?: string; @property({ type: Boolean, reflect: true }) disabled: boolean = false; @property({ type: Boolean, reflect: true }) required: boolean = false; @property({ type: Boolean, reflect: true }) currency: boolean = false; @property({ type: String, reflect: true }) corners: RoundedCorners = 'none'; @query('input', true) __inputEl!: HTMLInputElement; @query('label', true) __labelEl!: HTMLLabelElement; @query('[id*="-errors"]', true) __errorEl!: MonoTextComp; private __currencyFormatter = new Intl.NumberFormat('en-US', { style: 'currency', currencyDisplay: 'symbol', currency: 'USD', maximumSignificantDigits: 3, }); private __onInput(_event: Event) { this.value = this.__inputEl.value; } private __onChange(event: Event) { event.preventDefault(); event.stopPropagation(); this.value = this.__inputEl.value; const changeEvent = new CustomEvent('change', { detail: { value: this.value }, bubbles: true, composed: true, }); this.dispatchEvent(changeEvent); } private __hasError() { return this.error && this.error.length > 0; } private __renderError(ariaDescribedBy: string) { if (this.__hasError()) { return html` ${this.error} `; } return nothing; } private __computeCurrentValue() { const minValue = Number(this.min); const maxValue = Number(this.max); let value = Number(this.value); value = Math.max(value, minValue); value = Math.min(value, maxValue); return ((value - minValue) / (maxValue - minValue)) * 100; } private __computeToneColor() { if (this.disabled) { return theme('colors.neutral-3'); } if (this.__hasError()) { return theme('colors.alert'); } return theme('colors.highlight-2'); } private __computeTooltipColor() { if (this.disabled) { return 'neutral-3'; } if (this.__hasError()) { return 'alert'; } return 'highlight-2'; } private __computeHighlightThumbColor() { if (this.disabled) { return theme('colors.neutral-3'); } if (this.__hasError()) { return theme('colors.primary-dark'); } return theme('colors.highlight-2-dark'); } private __formatValue(value: string | number | undefined) { return this.currency ? this.__currencyFormatter.format(Number(value)) : value; } private __renderTooltip(currentValue: number) { const tooltipPosition = 10 - currentValue * 0.2; return html`
${this.__formatValue(this.value)}
`; } firstUpdated(changedProperties: PropertyValues) { super.firstUpdated(changedProperties); // reuse platform default this.min = this.__inputEl.min; this.max = this.__inputEl.max; this.step = this.__inputEl.step; if (this.value && this.value !== '') { // the input will have the adjusted value, will make it fit into the step value this.value = this.__inputEl.value; } else { // by default when provided no value, the input will set itself to 50 this.value = this.min; } } render() { const webkitRangeOverrides = css({ '&::-webkit-slider-thumb': { border: 'none', width: '18px', height: '18px', 'border-radius': '50%', 'background-color': this.__computeToneColor(), '-webkit-appearance': 'none', cursor: 'pointer', }, '&:not(:disabled)::-webkit-slider-thumb:hover': { background: this.__computeHighlightThumbColor(), }, '&:not(:disabled):focus::-webkit-slider-thumb': { background: this.__computeHighlightThumbColor(), outline: '2px dotted black', 'outline-offset': '2px', }, '&:not(:disabled)::-webkit-slider-thumb:active': { 'background-color': this.__computeToneColor(), cursor: 'grabbing', }, '&:disabled::-webkit-slider-thumb': { cursor: 'not-allowed', }, }); const mozRangeOverrides = css({ '&::-moz-focus-outer': { border: '0', }, '&::-moz-range-thumb': { border: 'none', width: '18px', height: '18px', 'border-radius': '50%', 'background-color': this.__computeToneColor(), '-webkit-appearance': 'none', cursor: 'pointer', }, '&:not(:disabled)::-moz-range-thumb:hover': { background: this.__computeHighlightThumbColor(), }, '&:not(:disabled):focus::-moz-range-thumb': { background: this.__computeHighlightThumbColor(), }, '&:not(:disabled)::-moz-range-thumb:active': { 'background-color': this.__computeToneColor(), cursor: 'grabbing', }, '&:disabled::-moz-range-thumb': { cursor: 'not-allowed', }, }); const currentValue = this.__computeCurrentValue(); const rangeBackgroudOverrides = css` background: linear-gradient( to right, ${this.__computeToneColor()} 0%, ${this.__computeToneColor()} ${currentValue + 0.0001}%, ${theme('colors.neutral-3')} ${currentValue + 0.0001}%, ${theme('colors.neutral-3')} 100% ); `; const ariaDescribedBy = this.__hasError() ? `${this.id}-errors` : ''; const attributesToSpread = this._spreadController.buildSpreadAttributesIgnoring( [ 'as', 'style', 'class', 'slot', 'value', 'min', 'max', 'step', 'id', 'name', 'error', 'disabled', 'required', 'currency', 'type', 'aria-describedby', 'corners', ], ); return html`
${this.disabled ? nothing : this.__renderTooltip(currentValue)}
${this.__formatValue(this.min)} ${this.__formatValue(this.max)}
${this.__renderError(ariaDescribedBy)}
`; } } declare global { interface HTMLElementTagNameMap { 'mono-rangeslider': MonoRangeSliderComp; } }