From 0dcf0a26ac3df3c515707b27178e739c6ad4874c Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Tue, 17 Oct 2023 17:04:58 +0200 Subject: [PATCH] Fix: Range slider corrections (#569) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Niels Lyngsø --- .../lib/uui-range-slider.element.ts | 1407 ++++++++--------- .../lib/uui-range-slider.story.ts | 18 +- .../lib/uui-range-slider.test.ts | 56 +- 3 files changed, 707 insertions(+), 774 deletions(-) diff --git a/packages/uui-range-slider/lib/uui-range-slider.element.ts b/packages/uui-range-slider/lib/uui-range-slider.element.ts index 8f69b27ab..aa6da61ab 100644 --- a/packages/uui-range-slider/lib/uui-range-slider.element.ts +++ b/packages/uui-range-slider/lib/uui-range-slider.element.ts @@ -1,14 +1,23 @@ -import { UUIHorizontalPulseKeyframes } from '@umbraco-ui/uui-base/lib/animations'; - -import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins'; import { defineElement } from '@umbraco-ui/uui-base/lib/registration'; -import { css, html, LitElement, nothing, svg } from 'lit'; +import { css, html, LitElement, svg } from 'lit'; +import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins'; import { property, query, state } from 'lit/decorators.js'; import { UUIRangeSliderEvent } from './UUIRangeSliderEvent'; +import { clamp } from '@umbraco-ui/uui-base/lib/utils'; + +const Z_INDEX = { + TOP: 3, + CENTER: 2, + BACK: 1, +}; +const THUMB_SIZE = 18; const TRACK_PADDING = 12; const STEP_MIN_WIDTH = 24; +// TODO: ability to focus on the range, to enable keyboard interaction to move the range. +// TODO: Ability to click outside a range, to move the range if the maxGap has been reached. +// TODO: . /** * @element uui-range-slider * @description - Range slider with two handles. Use uui-slider for a single handle. @@ -19,6 +28,14 @@ const STEP_MIN_WIDTH = 24; export class UUIRangeSliderElement extends FormControlMixin(LitElement) { static readonly formAssociated = true; + /** + * Label to be used for aria-label and eventually as visual label. Adds "low-end value" and "high-end value" endings for the two values. + * @type {string} + * @attr + */ + @property({ type: String }) + label!: String; + /** * Disables the input. * @type {boolean} @@ -29,21 +46,36 @@ export class UUIRangeSliderElement extends FormControlMixin(LitElement) { disabled = false; /** - * Label to be used for aria-label and eventually as visual label. Adds " low value" and " high value" endings for the two values. - * @type {string} - * @attr + * Sets the minimum allowed value. + * @type {number} + * @attr min + * @default 0 */ - @property({ type: String }) - label!: String; + @property({ type: Number }) + get min() { + return this._min; + } + set min(newVal) { + this._min = newVal; + this.#transferValueToInternalValues(); + } + _min = 0; /** - * This reflects the behavior of a native input step attribute. + * Sets the maximum allowed value. * @type {number} - * @attr - * @default 1 + * @attr max + * @default 100 */ @property({ type: Number }) - step = 1; + get max() { + return this._max; + } + set max(newVal) { + this._max = newVal; + this.#transferValueToInternalValues(); + } + _max = 0; /** * Hides the numbers representing the value of each steps. Dots will still be visible @@ -55,22 +87,20 @@ export class UUIRangeSliderElement extends FormControlMixin(LitElement) { hideStepValues = false; /** - * Sets the minimum allowed value. - * @type {number} - * @attr min - * @default 0 - */ - @property({ type: Number }) - min = 0; - - /** - * Sets the maximum allowed value. + * This reflects the behavior of a native input step attribute. * @type {number} - * @attr max - * @default 100 + * @attr + * @default 1 */ @property({ type: Number }) - max = 100; + get step() { + return this._step; + } + set step(newVal) { + this._step = newVal; + this.#transferValueToInternalValues(); + } + _step = 1; /** * Minimum value gap between the the two picked values. Cannot be lower than the step value and cannot be higher than the maximum gap @@ -79,7 +109,14 @@ export class UUIRangeSliderElement extends FormControlMixin(LitElement) { * @default undefined */ @property({ type: Number, attribute: 'min-gap' }) - minGap?: number; + get minGap() { + return this._minGap; + } + set minGap(newVal) { + this._minGap = newVal; + this.#transferValueToInternalValues(); + } + _minGap?: number; /** * Maximum value gap between the the two picked values. Cannot be lower than the minimum gap. @@ -88,941 +125,843 @@ export class UUIRangeSliderElement extends FormControlMixin(LitElement) { * @default undefined */ @property({ type: Number, attribute: 'max-gap' }) - maxGap?: number; + get maxGap() { + return this._maxGap; + } + set maxGap(newVal) { + this._maxGap = newVal; + this.#transferValueToInternalValues(); + } + _maxGap?: number; /** - * This is a value property of the uui-range-slider. Split the two values with comma, forexample 10,50 sets the values to 10 and 50. + * This is a value property of the uui-range-slider. Split the two values with comma, for example 10,50 sets the values to 10 and 50. * @type {string} * @attr - * @default 0,100 + * @default 0,0 */ @property({ type: String }) get value() { return this._value; } set value(newVal) { - if (newVal instanceof String) { - super.value = newVal; - const values = newVal.split(','); - this.valueLow = parseInt(values[0]); - this.valueHigh = parseInt(values[1]); - } + super.value = newVal; + this.#transferValueToInternalValues(); } - private _valueLow = 0; - /** - * The lower picked value. - * @type {number} - * @attr value-low - * @default 0 - */ - @property({ type: Number, attribute: 'value-low' }) - set valueLow(newLow) { - const old = this._valueHigh; - if (newLow <= this.min) { - this._valueLow = this.min; - super.value = `${this.min},${this.valueHigh}`; - this.requestUpdate('valueLow', old); - return; - } - if (newLow >= this.valueHigh - this.step) { - this._valueLow = this.valueHigh - this.step; - super.value = `${this.valueHigh - this.step},${this.valueHigh}`; - this.requestUpdate('valueLow', old); - return; - } - this._valueLow = newLow; - super.value = `${newLow},${this.valueHigh}`; - this.requestUpdate('valueLow', old); - } - get valueLow() { - return this._valueLow; - } - - private _valueHigh = 100; - /** - * The higher picked value. - * @type {number} - * @attr value-high - * @default 100 - */ - @property({ type: Number, attribute: 'value-high' }) - set valueHigh(newHigh) { - const old = this._valueHigh; - if (newHigh >= this.max) { - this._valueHigh = this.max; - super.value = `${this.valueLow},${this.max}`; - this.requestUpdate('valueHigh', old); - return; - } - if (newHigh <= this.valueLow + this.step) { - this._valueHigh = this.valueLow + this.step; - super.value = `${this.valueLow},${this.valueLow + this.step}`; - this.requestUpdate('valueHigh', old); - return; - } - this._valueHigh = newHigh; - super.value = `${this.valueLow},${newHigh}`; - this.requestUpdate('valueHigh', old); - } - get valueHigh() { - return this._valueHigh; - } + private _currentFocus?: HTMLInputElement; @state() - private _trackWidth = 0; + private _movement = false; - @state() - private _currentInputFocus?: HTMLInputElement; + private startPoint = { + mouse: 0, + low: 0, + high: 0, + }; @state() - private _currentThumbFocus: 'high' | 'low' = 'low'; + private _lowInputValue = 0; @state() - private _grabbingBoth?: boolean; + private _highInputValue = 0; @state() - private _startPos = 0; + private _trackWidth = 0; @state() - private _startLow = 0; - + _lowValuePercentStart = 0; @state() - private _startHigh = 0; + _highValuePercentEnd = 100; + + protected setValueLow(low: number) { + // Clamp value to ensure it fits within its restrictions + low = clamp( + low, + this.maxGap + ? this._highInputValue - this.maxGap > this.min + ? this._highInputValue - this.maxGap + : this.min + : this.min, + this.minGap + ? this._highInputValue - this.minGap + : this._highInputValue - this.step + ); + this.setValue(low, this._highInputValue); + } + + protected setValueHigh(high: number) { + // Clamp value to ensure it fits within its restrictions + high = clamp( + high, + this.minGap + ? this._lowInputValue + this.minGap + : this._lowInputValue + this.step, + this.maxGap + ? this.maxGap + this._lowInputValue < this.max + ? this.maxGap + this._lowInputValue + : this.max + : this.max + ); + this.setValue(this._lowInputValue, high); + } - @query('#low-input') - private _inputLow!: HTMLInputElement; + protected setValue(low: number, high: number, lockValueRange?: boolean) { + if (lockValueRange) { + // Get the length of the range + const length = this.startPoint.high - this.startPoint.low; - @query('#high-input') - private _inputHigh!: HTMLInputElement; + // Clamp values to make sure it keeps its length: + low = clamp(low, this.min, this.max - length); + high = clamp(high, this.min + length, this.max); + } - @query('#range-slider') - private _outerTrack!: HTMLElement; + // Overwrite input value, to enforce the calculated value and avoid the native slider moving to a invalid position: + this._inputLow.value = low.toString(); + this._inputHigh.value = high.toString(); - @query('#inner-track') - private _innerTrack!: HTMLElement; + this.value = `${low},${high}`; + } - @query('#low-thumb') - private _thumbLow!: HTMLElement; + #transferValueToInternalValues() { + const valueSplit = (this.value as string).split(','); + let low = Number(valueSplit[0]) || 0; + let high = Number(valueSplit[1]) || 0; - @query('#high-thumb') - private _thumbHigh!: HTMLElement; + // First secure that the high value are within range (low does not need as its being handled below) + high = clamp(high, this._min, this._max); - @query('.color') - private _innerColor!: HTMLElement; + // Make sure it matches the steps: + low = this._min + Math.round((low - this._min) / this._step) * this._step; + high = this._min + Math.round((high - this._min) / this._step) * this._step; - @query('.color-target') - private _bothThumbsTarget!: HTMLElement; + // Fit with gaps: + this._lowInputValue = clamp( + low, + this._min, + this._minGap ? high - this._minGap : high - this._step + ); + this._highInputValue = clamp( + high, + this._minGap + ? this._lowInputValue + this._minGap + : this._lowInputValue + this._step, + Math.min(this._maxGap ? low + this._maxGap : this._max, this._max) + ); - #setValue(val?: string) { - this._value = val ? val : `${this.valueLow},${this.valueHigh}`; + this._updateInnerColor(); + this.requestUpdate(); } protected getFormElement(): HTMLInputElement { - return this._currentInputFocus ? this._currentInputFocus : this._inputLow; + return this._currentFocus ? this._currentFocus : this._inputLow; } - public focus() { - this._currentInputFocus - ? this._currentInputFocus.focus() - : this._inputLow.focus(); - } + /** Elements */ - private _onKeypress(e: KeyboardEvent) { - if (e.key == 'Enter') { - this.submit(); - } - } - - /** Thumb position */ + @query('#range-slider') + private _outerTrack!: HTMLElement; - private _sliderLowThumbPosition() { - const ratio = (this.valueLow - this.min) / (this.max - this.min); - const valueLowPercent = `${Math.floor(ratio * 100000) / 1000}%`; - return valueLowPercent; - } + @query('#inputLow') + private _inputLow!: HTMLInputElement; - private _sliderHighThumbPosition() { - const ratio = (this.valueHigh - this.min) / (this.max - this.min); - const valueHighPercent = `${Math.floor(ratio * 100000) / 1000}%`; - return valueHighPercent; - } + @query('#inputHigh') + private _inputHigh!: HTMLInputElement; - /** Coloring of the line between thumbs */ + @query('.color') + private _innerColor!: HTMLElement; - private _fillColor() { - const percentStart = - ((this.valueLow - this.min) / (this.max - this.min)) * 100; - const percentEnd = - ((this.valueHigh - this.min) / (this.max - this.min)) * 100; + @query('#inner-color-thumb') + private _innerColorThumb!: HTMLElement; - this._innerColor.style.left = `${percentStart}%`; - this._innerColor.style.right = `${100 - percentEnd}%`; - } + /** Constructor and Validator */ - /** Moving thumb */ + constructor() { + super(); + // Keyboard + this.addEventListener('keypress', this._onKeypress); + // Mouse + this.addEventListener('mousedown', this._onMouseDown); + // Touch + this.addEventListener('touchstart', this._onTouchStart); - private _moveThumb(pageX: number) { - const value = this._getValue(pageX); - if (value >= this.valueHigh) this._setThumb(this._thumbHigh); - if (value <= this.valueLow) this._setThumb(this._thumbLow); - this._setValueBasedOnCurrentThumb( - this._validateValueBasedOnCurrentThumb(value) - ); + window.addEventListener('resize', () => { + this._trackWidth = this._outerTrack?.offsetWidth; + }); } - /** Mouse events */ + connectedCallback(): void { + super.connectedCallback(); + if (!this.value) { + // Lack value. Defaulting to the min and max attributes + this.value = `${this._min},${this._max}`; + } + } - private _onMouseDown = (e: MouseEvent) => { - e.preventDefault(); - if (this.disabled) return; - window.addEventListener('mouseup', this._onMouseUp); - window.addEventListener('mousemove', this._onMouseMove); + firstUpdated(changedProperties: Map) { + super.updated(changedProperties); + this._trackWidth = this._outerTrack.offsetWidth; + this._runPropertiesChecks(); + } - const target = e.composedPath()[0]; - const pageX = e.pageX; + private _runPropertiesChecks() { + // Note: We are checking if the attributes set makes any sense. - target == this._bothThumbsTarget - ? (this._grabbingBoth = true) - : (this._grabbingBoth = false); + const regex = new RegExp(/^-?\d+(\.\d+)?,-?\d+(\.\d+)?$/); // regex: Number Comma Number (optional: decimals and negatives) + if (!regex.test(this.value as string)) + console.error(`Range slider (Value error occurred): Bad input`); - if (this._grabbingBoth) { - this._saveStartPoint(pageX, this.valueLow, this.valueHigh); - return; + if (this._highInputValue === this._lowInputValue) { + console.error( + `Range slider (Value error occurred): Low-end and high-end value should never be equal. Use instead.` + ); } - this._moveThumb(pageX); - }; - - private _onMouseMove = (e: MouseEvent) => { - e.preventDefault(); - const pageX = e.pageX; - const val = this._getValue(pageX); - if (!this._grabbingBoth) - this._setValueBasedOnCurrentThumb( - this._validateValueBasedOnCurrentThumb(val) + if (this._lowInputValue > this._highInputValue) { + console.error( + `Range slider (Value error occurred): Low-end value should never be higher than high-end value.` ); - else this._moveBoth(pageX); - - this.dispatchEvent(new UUIRangeSliderEvent(UUIRangeSliderEvent.INPUT)); - }; - - private _onMouseUp = () => { - this._stop(); - window.removeEventListener('mouseup', this._onMouseUp); - window.removeEventListener('mousemove', this._onMouseMove); - }; - - /** Touch / mobile events */ - - private _onTouchStart = (e: TouchEvent) => { - e.preventDefault(); - if (this.disabled) return; + } - window.addEventListener('touchend', this._onTouchEnd); - window.addEventListener('touchmove', this._onTouchMove); + if (this._highInputValue > this._max || this._highInputValue < this._min) { + this.setValueHigh(this._max); + console.warn( + `Conflict with the high-end value and max value. High-end value has been converted to the max value (${this._max})` + ); + } - const target = e.composedPath()[0]; - const pageX = e.touches[0].pageX; + if (this._lowInputValue < this._min || this._lowInputValue > this._max) { + this.setValueLow(this._min); + console.warn( + `Conflict with the low-end value and min value. Low-end value has been converted to the min value (${this._min})` + ); + } - target == this._bothThumbsTarget - ? (this._grabbingBoth = true) - : (this._grabbingBoth = false); + // Step vs value logic + if (this._step <= 0) { + this._step = 1; + console.warn( + `Property step needs a value higher than 0. Converted the step value to 1 (default)` + ); + } - if (this._grabbingBoth) { - this._saveStartPoint(pageX, this.valueLow, this.valueHigh); - return; + if (((this._max - this._min) / this._step) % 1 !== 0) { + console.error( + `Conflict with step value and the min and max values. May experience bad side effects` + ); } - this._moveThumb(pageX); - }; - private _onTouchMove = (e: TouchEvent) => { - const pageX = e.touches[0].pageX; - const val = this._getValue(pageX); - if (!this._grabbingBoth) - this._setValueBasedOnCurrentThumb( - this._validateValueBasedOnCurrentThumb(val) + if (this._minGap && this._minGap < this._step) { + this._minGap = undefined; + console.warn( + `Conflict with min-gap and step value. The min-gap needs to be higher than the step value. Removed the min-gap property.` ); - else this._moveBoth(pageX); + } - this.dispatchEvent(new UUIRangeSliderEvent(UUIRangeSliderEvent.INPUT)); - }; + // Gaps + if (this._minGap && this._maxGap && this._minGap > this._maxGap) { + this._minGap = undefined; + this._maxGap = undefined; + console.warn( + `Conflict with min-gap and max-gap. Removed the min-gap and max-gap properties.` + ); + } - private _onTouchEnd = () => { - this._stop(); - window.removeEventListener('touchend', this._onTouchEnd); - window.removeEventListener('touchmove', this._onTouchMove); - }; + if (this._minGap && this._max - this._min < this._minGap) { + this._minGap = undefined; + console.warn( + `Conflict with the min-gap and the total range. Removed the min-gap.` + ); + } - /** */ + if ( + this._maxGap && + this._highInputValue - this._lowInputValue > this._maxGap + ) { + this.setValueHigh(this._lowInputValue + this._maxGap); + console.warn( + `Conflict with value and max-gap. High-end value has been converted to the highest possible value based on the low-end value and the max gap value (${this._highInputValue})` + ); + } - private _stop() { - this._grabbingBoth = false; - this.pristine = false; - this.dispatchEvent(new UUIRangeSliderEvent(UUIRangeSliderEvent.CHANGE)); + if ( + this._minGap && + this._highInputValue - this._lowInputValue < this._minGap + ) { + const minGap = this._minGap; + if (this._highInputValue - minGap < this._min) { + this.setValueHigh(this._lowInputValue + minGap); + console.warn( + `Conflict with value and min gap. High-end value has been converted to the lowest possible value based on the low-end value and the min gap value (${this._highInputValue}).` + ); + } else { + this.setValueLow(this._highInputValue - minGap); + console.warn( + `Conflict with value and min gap. Low-end value has been converted to the highest possible value based on the high-end value and the min gap value (${this._lowInputValue}).` + ); + } + } } - /** The latest thumb in use */ + private _updateInnerColor() { + const scopeLength = this._max - this._min; + const scopedLow = this._lowInputValue - this._min; + const scopedHigh = this._highInputValue - this._min; - private _setThumb(target: EventTarget | HTMLElement) { - this._currentThumbFocus = target === this._thumbLow ? 'low' : 'high'; + const leftPercent = (scopedLow / scopeLength) * 100; + const rightPercent = 100 - (scopedHigh / scopeLength) * 100; - this._currentThumbFocus === 'low' - ? (this._currentInputFocus = this._inputLow) - : (this._currentInputFocus = this._inputHigh); - - this.focus(); + this._lowValuePercentStart = clamp(leftPercent, 0, 100); + this._highValuePercentEnd = clamp(rightPercent, 0, 100); } - private _setValueBasedOnCurrentThumb(val: number) { - this._currentThumbFocus === 'low' - ? (this.valueLow = val) - : (this.valueHigh = val); - } - - /** Get the value depends on where clicked/touched */ - - private _getValue(pageX: number) { - const mouseXPosition = - pageX - this._innerTrack.getBoundingClientRect().left; + private _getClickedValue(pageX: number) { + const outerTrackMargin = this._outerTrack.getBoundingClientRect().left; + const mouseXPosition = pageX - outerTrackMargin - TRACK_PADDING; const clickPercent = mouseXPosition / (this._trackWidth - TRACK_PADDING * 2); - const clickedValue = clickPercent * (this.max - this.min) + this.min; - const newValue = Math.round(clickedValue / this.step) * this.step; - - return newValue; + const clickedValue = clickPercent * (this._max - this._min) + this._min; + return Math.round(clickedValue / this._step) * this._step; } - private _validateLowByMinGap(value: number) { - if (!this.minGap || this.minGap <= this.step) return value; - return value + this.minGap >= this.valueHigh - ? this.valueHigh - this.minGap - : value; - } + /** Events */ - private _validateLowByMaxGap(value: number) { - if (!this.maxGap) return value; - return this.valueHigh - value >= this.maxGap - ? this.valueHigh - this.maxGap - : value; - } + private _onKeypress = (e: KeyboardEvent) => { + if (e.key == 'Enter') { + this.submit(); + } + }; - private _validateHighByMinGap(value: number) { - if (!this.minGap || this.minGap <= this.step) return value; - return value <= this.valueLow + this.minGap - ? this.valueLow + this.minGap - : value; - } + /** Touch Event */ + private _onTouchStart = (e: TouchEvent) => { + if (this.disabled) return; - private _validateHighByMaxGap(value: number) { - if (!this.maxGap) return value; - return value >= this.valueLow + this.maxGap - ? this.valueLow + this.maxGap - : value; - } + const target = e.composedPath()[0]; + if (target === this._outerTrack) return; + //Clicked on the inner track. - private _validateValueBasedOnCurrentThumb(newValue: number): number { - if (this._currentThumbFocus == 'low') { - let newLow: number; - newLow = - newValue < this.valueHigh - this.step - ? newValue - : this.valueHigh - this.step; - newLow = newLow >= this.min ? newLow : this.min; + if (target === this._innerColor || target === this._innerColorThumb) { + // If we clicked in the colored area - newLow = this.minGap ? this._validateLowByMinGap(newLow) : newLow; - newLow = this.maxGap ? this._validateLowByMaxGap(newLow) : newLow; + window.addEventListener('touchend', this._onTouchEnd); + window.addEventListener('touchcancel', this._onTouchEnd); + window.addEventListener('touchmove', this._onTouchMove); - return newLow; + this._movement = true; + this._saveStartPoints(e.touches[0].pageX); + } else { + // Else move just 1 thumb + const value = this._getClickedValue(e.touches[0].pageX); + const diffLow = Math.abs(this._lowInputValue - value); + const diffHigh = Math.abs(this._highInputValue - value); + if (diffLow < diffHigh) { + this.setValueLow(value); + } else { + this.setValueHigh(value); + } } + }; - let newHigh: number; - newHigh = - newValue > this.valueLow + this.step - ? newValue - : this.valueLow + this.step; - newHigh = newHigh <= this.max ? newHigh : this.max; - - newHigh = this.minGap ? this._validateHighByMinGap(newHigh) : newHigh; - newHigh = this.maxGap ? this._validateHighByMaxGap(newHigh) : newHigh; + private _onTouchMove = (e: TouchEvent) => { + this._dragBothValuesByMousePos(e.touches[0].pageX); + }; - return newHigh; - } + private _onTouchEnd = () => { + this._movement = false; + this.onChanged(); + window.removeEventListener('touchend', this._onTouchEnd); + window.removeEventListener('touchcancel', this._onTouchEnd); + window.removeEventListener('touchmove', this._onTouchMove); + }; - /** Methods when moving both thumbs */ + /** Mouse Event */ + private _onMouseDown = (e: MouseEvent) => { + if (this.disabled) return; - private _saveStartPoint(pageX: number, lowVal: number, highVal: number) { - this._startPos = pageX; - this._startLow = lowVal; - this._startHigh = highVal; - } + const target = e.composedPath()[0]; + if (target === this._outerTrack) return; + //Clicked on the inner track. - private _moveBoth(pageX: number) { - const drag = pageX - this._startPos; - const trackDiff = this.max - this.min; + if (target === this._innerColor || target === this._innerColorThumb) { + // If we clicked in the colored area + window.addEventListener('mouseup', this._onMouseUp); + window.addEventListener('mousemove', this._onMouseMove); - const dragPercent = drag / (this._trackWidth + TRACK_PADDING * 2); - const dragValue = - Math.round((dragPercent * trackDiff) / this.step) * this.step; + this._movement = true; + this._saveStartPoints(e.pageX); + } else { + // Else move just 1 thumb + const value = this._getClickedValue(e.pageX); + const diffLow = Math.abs(this._lowInputValue - value); + const diffHigh = Math.abs(this._highInputValue - value); + if (diffLow < diffHigh) { + this.setValueLow(value); + } else { + this.setValueHigh(value); + } + } + }; - const newValueLow = this._startLow + dragValue; - const newValueHigh = this._startHigh + dragValue; + private _onMouseMove = (e: MouseEvent) => { + e.preventDefault(); + this._dragBothValuesByMousePos(e.pageX); + }; - const value = this._getValidatedValues(newValueLow, newValueHigh); + private _onMouseUp = () => { + this._movement = false; + this.onChanged(); + window.removeEventListener('mouseup', this._onMouseUp); + window.removeEventListener('mousemove', this._onMouseMove); + }; - if (newValueLow === value.low && newValueHigh === value.high) { - this.valueLow = newValueLow; - this.valueHigh = newValueHigh; - this.dispatchEvent(new UUIRangeSliderEvent(UUIRangeSliderEvent.INPUT)); - } + /** Drag both thumbs logics */ + private _saveStartPoints(pageX: number) { + this.startPoint = { + mouse: pageX, + low: this._lowInputValue, + high: this._highInputValue, + }; } - private _getValidatedValues(low: number, high: number) { - const validatedLow = low > this.min ? low : this.min; - const validatedHigh = high < this.max ? high : this.max; - return { low: validatedLow, high: validatedHigh }; - } + private _dragBothValuesByMousePos(pageX: number) { + const trackWidth = this._outerTrack.offsetWidth; - /** CHANGE AND INPUT EVENT LISTENERS */ + const drag = pageX - this.startPoint.mouse; + const trackDiff = this._max - this._min; - private _onChange(e: Event) { - e.stopPropagation(); - this.pristine = false; - this.dispatchEvent(new UUIRangeSliderEvent(UUIRangeSliderEvent.CHANGE)); - } + const dragPercent = drag / (trackWidth + TRACK_PADDING * 2); + const dragValue = + Math.round((dragPercent * trackDiff) / this._step) * this._step; - private _onLowInput(e: Event) { - e.stopPropagation(); - let value = parseInt(this._inputLow.value); + const newValueLow = this.startPoint.low + dragValue; + const newValueHigh = this.startPoint.high + dragValue; - value = this._validateLowByMinGap(value); - value = this._validateLowByMaxGap(value); + this.setValue(newValueLow, newValueHigh, true); + this.dispatchEvent(new UUIRangeSliderEvent(UUIRangeSliderEvent.INPUT)); + } - this._inputLow.value = String(value); - this.valueLow = value; + /** Input Events */ + private _onLowInput(e: Event) { + if (this.disabled) e.preventDefault(); + this._currentFocus = this._inputLow; + const newValue = Number((e.target as HTMLInputElement).value); + this.setValueLow(newValue); this.dispatchEvent(new UUIRangeSliderEvent(UUIRangeSliderEvent.INPUT)); } private _onHighInput(e: Event) { - e.stopPropagation(); - let value = parseInt(this._inputHigh.value); - - value = this._validateHighByMinGap(value); - value = this._validateHighByMaxGap(value); - - this._inputHigh.value = String(value); - this.valueHigh = value; + if (this.disabled) e.preventDefault(); + this._currentFocus = this._inputHigh; + const newValue = Number((e.target as HTMLInputElement).value); + this.setValueHigh(newValue); this.dispatchEvent(new UUIRangeSliderEvent(UUIRangeSliderEvent.INPUT)); } - /** Constructor */ - - constructor() { - super(); - // Keyboard - this.addEventListener('keypress', this._onKeypress); - // Mouse - this.addEventListener('mousedown', this._onMouseDown); - // Touch - this.addEventListener('touchstart', this._onTouchStart); - - this.addValidator( - 'stepMismatch', - () => `Step property needs to be higher than 0`, - () => this.step <= 0 - ); - - this.addValidator( - 'stepMismatch', - () => `Maxmimum gap needs to be higher than minimum gap`, - () => !!this.maxGap && !!this.minGap && this.maxGap <= this.minGap - ); - - this.addValidator( - 'rangeUnderflow', - () => - `The lower end value (${this.valueLow}) cannot be below the the minimum value setting (${this.min})`, - () => this.valueLow < this.min - ); - this.addValidator( - 'rangeOverflow', - () => - `The higher end value (${this.valueHigh}) cannot be above the the maximum value setting (${this.max})`, - () => this.valueHigh > this.max - ); + /** Change Events */ + private _onLowChange() { + this.setValueLow(Number(this._inputLow.value)); + this.onChanged(); } - connectedCallback(): void { - super.connectedCallback(); - this.#setValue(); - window.addEventListener('resize', () => { - this._trackWidth = this._outerTrack.offsetWidth; - }); + private _onHighChange() { + this.setValueHigh(Number(this._inputHigh.value)); + this.onChanged(); } - updated(changedProperties: Map) { - super.updated(changedProperties); - this._trackWidth = this._outerTrack.offsetWidth; - this._fillColor(); + protected onChanged() { + this.pristine = false; + this.dispatchEvent(new UUIRangeSliderEvent(UUIRangeSliderEvent.CHANGE)); } - /** RENDER */ + /** Render */ render() { return html`
- ${this.renderNativeInputs()} + ${this._renderNativeInputs()}
- - ${this.renderStepsOutput()} ${this.renderThumbs()} +
+ ${this._renderThumbValues()} +
+
+ ${this._renderSteps()}
-
${this.renderStepValues()}
`; } - renderNativeInputs() { - return html` - `; + private _renderThumbValues() { + return html`
+ ${this._lowInputValue} + ${this._highInputValue} +
`; } - renderThumbs() { - return html`
-
${this.valueLow}
-
-
-
${this.valueHigh}
-
`; - } + private _renderSteps() { + const stepAmount = (this._max - this._min) / this._step; + const stepWidth = (this._trackWidth - TRACK_PADDING * 2) / stepAmount; - /** RENDER STEPS & STEP VALUES */ - renderStepsOutput() { - return html`
+ if (stepWidth < STEP_MIN_WIDTH) return; + if (stepAmount % 1 !== 0) return; + + let index = 0; + const stepPositions = new Array(stepAmount + 1) + .fill(stepWidth) + .map(stepWidth => stepWidth * index++); + + return html`
- ${this.renderSteps()} + ${this._renderStepCircles(stepPositions)} + ${this._renderStepValues(stepAmount)}
`; } - renderSteps() { - const stepAmount = (this.max - this.min) / this.step; - const stepWidth = (this._trackWidth - TRACK_PADDING * 2) / stepAmount; - const trackValue = this._trackWidth / (this.max - this.min); + private _renderStepValues(stepAmount: number) { + if (this.hideStepValues || stepAmount > 20) return; - if (stepWidth >= STEP_MIN_WIDTH) { - let i = 0; - const steps = []; - for (i; i <= stepAmount; i++) { - steps.push(i * stepWidth); - } - const colorClass = this.disabled == false ? `filled` : `filled-disabled`; - return svg` - ${steps.map(position => { - const x = position + TRACK_PADDING; - if ( - x / trackValue > this.valueLow - this.min && - x / trackValue < this.valueHigh - this.min - ) { - return svg``; - } else { - return svg``; - } - })}`; - } else { - return svg``; - } + let index = 0; + const stepValues = new Array(stepAmount + 1) + .fill(this._step) + .map(step => this._min + step * index++); + + return html`
+ ${stepValues.map(value => html`${value}`)} +
`; } - renderStepValues() { - if (this.hideStepValues) return nothing; - const stepAmount = (this.max - this.min) / this.step; - const stepWidth = (this._trackWidth - TRACK_PADDING * 2) / stepAmount; + private _renderStepCircles(stepPositionArray: Array) { + const trackValue = this._trackWidth / (this._max - this._min); - if (stepWidth >= STEP_MIN_WIDTH && stepAmount <= 20) { - let i = 0; - const stepValues = []; - for (i; i <= stepAmount; i++) { - stepValues.push(i * this.step + this.min); + return svg`${stepPositionArray.map(position => { + const cx = position + TRACK_PADDING; + const colorStart = this._lowInputValue - this._min; + const colorEnd = this._highInputValue - this._min; + + if (cx / trackValue >= colorStart && cx / trackValue <= colorEnd) { + return svg``; + } else { + return svg``; } - return html` ${stepValues.map(stepValue => { - return html`${stepValue}`; - })}`; - } else { - return nothing; - } + })}`; + } + + private _renderNativeInputs() { + return html`
+ + +
`; } + /** Style */ static styles = [ - UUIHorizontalPulseKeyframes, css` :host { - display: block; - min-height: 50px; - width: 100%; - place-items: center; - -webkit-user-select: none; /* Safari */ - -moz-user-select: none; /* Firefox */ - -ms-user-select: none; /* IE10+/Edge */ - user-select: none; - cursor: pointer; + --color-interactive: var(--uui-color-selected); + --color-hover: var(--uui-color-selected-emphasis); + --color-focus: var(--uui-color-focus); + min-height: 30px; } - :host([disabled]) { - cursor: default; + :host([error]) { + --color-interactive: var(--uui-color-danger-standalone); + --color-hover: var(--uui-color-danger); } - /** NATIVE INPUT STYLING */ - - input { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - position: absolute; - top: 0; - background-color: transparent; - pointer-events: none; - left: 0; - right: 0; - border-radius: 20px; - } - - input::-webkit-slider-thumb { - pointer-events: all; + #range-slider { + min-height: 30px; + box-sizing: border-box; position: relative; - z-index: 1; - outline: 0; + display: flex; + align-items: center; } - input::-moz-range-thumb { - pointer-events: all; - position: relative; - z-index: 10; - -moz-appearance: none; - background: linear-gradient(to bottom, #ededed 0%, #dedede 100%); - width: 11px; - } + /** Track */ - input::-moz-range-track { - position: relative; - z-index: -1; - background-color: rgba(0, 0, 0, 0.15); - border: 0; + #inner-track { + user-select: none; + background-color: var(--uui-color-border-standalone); + position: absolute; + height: 3px; + left: ${TRACK_PADDING}px; /* Match TRACK_MARGIN */ + right: ${TRACK_PADDING}px; /* Match TRACK_MARGIN */ } - input:last-of-type::-moz-range-track { - -moz-appearance: none; - background: none transparent; - border: 0; + :host(:not([disabled]):hover) #inner-track, + :host(:not([disabled]):active) #inner-track { + background-color: var(--uui-color-border-emphasis); } - /** TRACK */ - - #inner-track .color-target { + #inner-color-thumb { + margin: -9px 0 0; position: absolute; - z-index: 2; - left: 0; - right: 0; - height: 25px; - transform: translateY(-50%); + display: flex; + flex-direction: column; + justify-content: center; + height: ${THUMB_SIZE}px; + cursor: grab; + user-select: none; + z-index: ${Z_INDEX.CENTER}; } - #inner-track .color { - height: 3px; - position: absolute; - transition: background-color 320ms ease-out; + :host([disabled]) #inner-color-thumb { + cursor: default; } - :host(:not([disabled])) - #range-slider - #inner-track - .color:has(.color-target:hover), - :host(:not([disabled])) - #range-slider - #inner-track - .color:has(.color-target:active) { - background-color: var(--uui-color-focus); - } + /** Colored part of track */ - :host(:not([disabled])) #range-slider .color { - background-color: var(--uui-color-selected); + :host([disabled]) #range-slider #inner-color-thumb .color { + background-color: var(--uui-palette-mine-grey) !important; } - :host([disabled]) #range-slider .color { - background-color: #555; + #inner-color-thumb:focus .color { + background-color: var(--color-focus); } - - #range-slider { - transform: translateY(50%); - position: relative; - height: 18px; - display: flex; - flex-direction: column; - width: 100%; + #inner-color-thumb:hover .color, + #inner-color-thumb .color.active { + background-color: var(--color-hover); + } + #inner-color-thumb:hover .color { + height: 5px; + background-color: var(--color-hover); } - #inner-track { - border-radius: 10px; + .color { + user-select: none; position: absolute; + inset-inline: 0; height: 3px; - background-color: var(--uui-color-border-standalone); - left: ${TRACK_PADDING}px; /* Match TRACK_MARGIN */ - right: ${TRACK_PADDING}px; /* Match TRACK_MARGIN */ + transform: translateY(2px); + background-color: var(--color-interactive); + transition: height 60ms; } - #range-slider:hover #inner-track, - #range-slider:active #inner-track { - background-color: #a1a1a1; + :host([error]) .color { + background-color: var(--uui-color-danger-standalone); + } + :host([error]) #inner-color-thumb:hover ~ .color, + :host([error]) #inner-color-thumb:focus ~ .color { + background-color: var(--uui-color-danger-emphasis); } - /** STEP VALUES */ + /** Steps */ + .step-wrapper { + margin: 0 ${-1 * TRACK_PADDING}px; + height: 11px; + position: absolute; + left: 0; + right: 0; + top: -10px; + } + /** Step circles */ .track-step { fill: var(--uui-color-border); } + :host(:not([disabled]):hover) #inner-track .track-step.regular, + :host(:not([disabled]):active) #inner-track .track-step.regular { + fill: var(--uui-color-border-emphasis); + } + :host .track-step.filled { - fill: var(--uui-color-selected) !important; + fill: var(--color-interactive); } - :host .track-step.filled-disabled { - fill: var(--uui-palette-mine-grey) !important; + :host([error]) .track-step.filled { + fill: var(--uui-color-danger-emphasis); } - #range-slider:hover .track-step, - #range-slider:active .track-step { - fill: #a1a1a1; + :host #inner-color-thumb.active ~ .step-wrapper .track-step.filled, + :host #inner-color-thumb:hover ~ .step-wrapper .track-step.filled { + fill: var(--color-hover); } - #step-values { + :host([disabled]) #range-slider .track-step.filled { + fill: var(--uui-palette-mine-grey) !important; + } + + /** Step values */ + + .step-values { + box-sizing: border-box; margin: 0 ${TRACK_PADDING}px; /* Match TRACK_MARGIN */ - padding-top: ${TRACK_PADDING + 3}px; display: flex; - align-items: flex-end; - box-sizing: border-box; + justify-content: space-between; + font-size: var(--uui-type-small-size); } - #step-values > span { - flex-basis: 0; - flex-grow: 1; + .step-values > span { + position: relative; color: var(--uui-color-disabled-contrast); } - #step-values > span > span { + .step-values > span > span { + position: absolute; transform: translateX(-50%); - display: inline-block; - text-align: center; - font-size: var(--uui-type-small-size); - } - - #step-values > span:last-child { - width: 0; - flex-grow: 0; } - .svg-wrapper { - margin: 0 ${-1 * TRACK_PADDING}px; - height: 18px; - transform: translateY(-75%); - } + /** Input */ - .svg-wrapper svg { - margin-top: ${TRACK_PADDING / 2}px; + .native-wrapper { + position: relative; + width: 100%; + inset-inline: 0px; + margin: -22px 5px 0 1px; } - /** FOCUS */ - - input[type='range'] { + input { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + pointer-events: none; position: absolute; - left: 0; - right: 0; - top: -50%; + cursor: pointer; + background-color: transparent; + inset-inline: 0; + width: 100%; + border-radius: 20px; } - input[type='range']:focus-visible { + input:focus-within { outline: none; } - #low-input:focus-visible ~ #inner-track #low-thumb, - #high-input:focus-visible ~ #inner-track #high-thumb, - #low-input:focus ~ #inner-track #low.thumb, - #high-input:focus ~ #inner-track #high-thumb, - #low-input:active ~ #inner-track #low.thumb, - #high-input:active ~ #inner-track #high-thumb { - outline: calc(2px * var(--uui-show-focus-outline, 1)) solid - var(--uui-color-focus); - } - - input[type='range']:focus + .thumb { - outline: calc(2px * var(--uui-show-focus-outline, 1)) solid - var(--uui-color-focus); + /** Thumb values */ + .thumb-values { + box-sizing: border-box; + display: flex; + justify-content: space-between; + color: var(--color-interactive); + font-weight: bold; + transition: 120ms opacity; + opacity: 0; } - :host(:not([disabled])) - #range-slider - #inner-track - .color:has(.color-target:hover) - ~ #low-thumb, - :host(:not([disabled])) - #range-slider - #inner-track - .color:has(.color-target:active) - ~ #low-thumb, - :host(:not([disabled])) - #range-slider - #inner-track - .color:has(.color-target:hover) - ~ #high-thumb, - :host(:not([disabled])) - #range-slider - #inner-track - .color:has(.color-target:active) - ~ #high-thumb { - outline: calc(2px * var(--uui-show-focus-outline, 1)) solid - var(--uui-color-focus); + .thumb-values > span { + width: 0; } - /** THUMBS */ - - .thumb { - z-index: 3; - transform: translateY(-50%); + .thumb-values > span > span { + bottom: 15px; position: absolute; - top: 2px; - bottom: 0px; - left: 0px; - height: 17px; - width: 17px; - margin-left: -8px; - margin-right: -8px; - border-radius: 50%; - box-sizing: border-box; - background-color: var(--uui-color-surface, #fff); - border: 2px solid var(--uui-color-selected, #3544b1); - transition: left 120ms ease 0s; + transform: translateX(-50%); } - .thumb:after { - content: ''; - position: absolute; - top: 2px; - left: 2px; - height: 9px; - width: 9px; - border-radius: 50%; - background-color: var(--uui-color-selected); + :host([disabled]) .thumb-values { + color: var(--uui-palette-mine-grey); } - :host([disabled]) .thumb { - background-color: var(--uui-color-disabled); - border-color: var(--uui-palette-mine-grey); - } - :host([disabled]) .thumb:after { - background-color: var(--uui-palette-mine-grey); + #range-slider:hover .thumb-values { + opacity: 1; } - .thumb .value { - position: absolute; - box-sizing: border-box; - font-weight: 700; - bottom: 15px; - left: 50%; - width: 40px; - margin-left: -20px; - text-align: center; - opacity: 1; - transition: 120ms opacity; - color: var(--uui-color-selected); - visibility: hidden; - opacity: 0; + /** Native thumbs */ + /** Chrome */ + input::-webkit-slider-thumb { + -webkit-appearance: none; + pointer-events: all; + cursor: grab; + position: relative; + z-index: ${Z_INDEX.TOP}; + width: ${THUMB_SIZE}px; + height: ${THUMB_SIZE}px; + border-radius: 24px; + border: none; + background-color: var(--color-interactive); + overflow: visible; + box-shadow: inset 0 0 0 2px var(--color-interactive), + inset 0 0 0 4px var(--uui-color-surface); + } + :host([disabled]) input::-webkit-slider-thumb { + cursor: default; } - :host([disabled]) .thumb .value { - color: var(--uui-palette-mine-grey); + input:focus-within::-webkit-slider-thumb, + input.focus::-webkit-slider-thumb { + background-color: var(--color-focus); + box-shadow: inset 0 0 0 2px var(--color-focus), + inset 0 0 0 4px var(--uui-color-surface), 0 0 0 2px var(--color-focus); + } + input::-webkit-slider-thumb:hover { + background-color: var(--color-hover); + box-shadow: inset 0 0 0 2px var(--color-hover), + inset 0 0 0 4px var(--uui-color-surface); } - #range-slider:active .thumb .value, - #range-slider:focus .thumb .value, - #range-slider:hover .thumb .value { - visibility: visible; - opacity: 1; + :host([disabled]) #range-slider input::-webkit-slider-thumb { + background-color: var(--uui-palette-mine-grey); + box-shadow: inset 0 0 0 2px var(--uui-palette-mine-grey), + inset 0 0 0 4px var(--uui-color-surface); } - /** NATIVE THUMB STYLING */ + /** Mozilla */ - input[type='range']::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 17px; - height: 17px; - background-color: transparent; - display: block; - border-radius: 100%; - pointer-events: auto; - cursor: pointer; - } - input[type='range']:disabled::-webkit-slider-thumb { + input::-moz-range-thumb { + -moz-appearance: none; + pointer-events: all; + cursor: grab; + position: relative; + z-index: ${Z_INDEX.TOP}; + width: ${THUMB_SIZE}px; + height: ${THUMB_SIZE}px; + border-radius: 24px; + border: none; + background-color: var(--color-interactive); + overflow: visible; + box-shadow: inset 0 0 0 2px var(--color-interactive), + inset 0 0 0 4px var(--uui-color-surface); + } + :host([disabled]) input::-moz-range-thumb { cursor: default; } - input[type='range']::-moz-range-thumb { - -moz-appearance: none; - appearance: none; - width: 17px; - height: 17px; - background-color: transparent; - display: block; - border-radius: 100%; - pointer-events: auto; - cursor: pointer; + input:focus-within::-moz-range-thumb, + input.focus::-moz-range-thumb { + background-color: var(--color-focus); + box-shadow: inset 0 0 0 2px var(--color-focus), + inset 0 0 0 4px var(--uui-color-surface), 0 0 0 2px var(--color-focus); } - input[type='range']:disabled::-moz-range-thumb { - cursor: default; + input::-moz-range-thumb:hover { + background-color: var(--color-hover); + box-shadow: inset 0 0 0 2px var(--color-hover), + inset 0 0 0 4px var(--uui-color-surface); } - input[type='range']::-ms-thumb { - appearance: none; - width: 17px; - height: 17px; - background-color: transparent; - display: block; - border-radius: 100%; - pointer-events: auto; - cursor: pointer; - } - input[type='range']:disabled::-ms-thumb { - cursor: default; + :host([disabled]) #range-slider input::-moz-range-thumb { + background-color: var(--uui-palette-mine-grey); + box-shadow: inset 0 0 0 2px var(--uui-palette-mine-grey), + inset 0 0 0 4px var(--uui-color-surface); } `, ]; diff --git a/packages/uui-range-slider/lib/uui-range-slider.story.ts b/packages/uui-range-slider/lib/uui-range-slider.story.ts index 395255aec..fc4b9f3aa 100644 --- a/packages/uui-range-slider/lib/uui-range-slider.story.ts +++ b/packages/uui-range-slider/lib/uui-range-slider.story.ts @@ -11,11 +11,13 @@ export default { component: 'uui-range-slider', args: { step: 10, - minGap: 10, - maxGap: 0, - valueLow: 0, - valueHigh: 70, + min: 0, + max: 100, + minGap: undefined, + maxGap: undefined, + value: '0,20', disabled: false, + error: false, hideStepValues: false, label: 'range', }, @@ -33,13 +35,13 @@ const Template: Story = props => html` diff --git a/packages/uui-range-slider/lib/uui-range-slider.test.ts b/packages/uui-range-slider/lib/uui-range-slider.test.ts index 0b8ff2d48..ed938e3fc 100644 --- a/packages/uui-range-slider/lib/uui-range-slider.test.ts +++ b/packages/uui-range-slider/lib/uui-range-slider.test.ts @@ -19,12 +19,14 @@ describe('UUIRangeSliderElement', () => { let inputHigh: HTMLInputElement; beforeEach(async () => { - element = await fixture(html` `); + element = await fixture( + html`` + ); inputLow = element.shadowRoot?.querySelector( - '#low-input' + '#inputLow' ) as HTMLInputElement; inputHigh = element.shadowRoot?.querySelector( - '#high-input' + '#inputHigh' ) as HTMLInputElement; }); @@ -46,14 +48,12 @@ describe('UUIRangeSliderElement', () => { expect(inputLow.disabled).to.be.true; expect(inputHigh.disabled).to.be.true; }); + it('has a label property', () => { expect(element).to.have.property('label'); }); - it('has a valueLow property', () => { - expect(element).to.have.property('valueLow'); - }); - it('has a valueHigh property', () => { - expect(element).to.have.property('valueHigh'); + it('has a value property', () => { + expect(element).to.have.property('value'); }); it('has a min property', () => { expect(element).to.have.property('min'); @@ -111,15 +111,18 @@ describe('UUIRangeSliderElement', () => { expect(event.type).to.equal(UUIRangeSliderEvent.INPUT); expect(event!.target).to.equal(element); }); - it('changes the valueLow to the input value when input event is emitted on inputLow', async () => { - inputLow.value = '10'; + + it('changes the value when the low-end value changes', async () => { + const LowEnd = '30'; + inputLow.value = LowEnd; inputLow.dispatchEvent(new Event('input')); - expect(element.valueLow).to.equal(10); + expect(element.value).to.equal(`${LowEnd},${inputHigh.value}`); }); - it('changes the valueHigh to the input value when input event is emitted on inputHigh', async () => { - inputHigh.value = '50'; + it('changes the value when the high-end value changes', async () => { + const HighEnd = '80'; + inputHigh.value = HighEnd; inputHigh.dispatchEvent(new Event('input')); - expect(element.valueHigh).to.equal(50); + expect(element.value).to.equal(`${inputLow.value},${HighEnd}`); }); }); }); @@ -132,42 +135,31 @@ describe('UUIRangeSlider in a form', () => { formElement = await fixture( html`
` ); element = formElement.querySelector('uui-range-slider') as any; }); - it('valueLow is correct', async () => { - await expect(element.valueLow).to.be.equal(20); - }); - it('valueHigh is correct', async () => { - await expect(element.valueHigh).to.be.equal(50); + it('Value is correct', async () => { + await expect(element.value).to.be.equal('10,90'); }); it('form output', async () => { - const formData = new FormData(formElement); - await expect(formData.get('slider')).to.be.equal('20,50'); - }); - - it('change low and high values and check output', async () => { - element.valueLow = 10; - element.valueHigh = 90; const formData = new FormData(formElement); await expect(formData.get('slider')).to.be.equal('10,90'); }); - it('change component value and check output', async () => { - formElement.value = '20,50'; + it('change low and high values and check output', async () => { + element.value = '50,60'; const formData = new FormData(formElement); - await expect(formData.get('slider')).to.be.equal('20,50'); + await expect(formData.get('slider')).to.be.equal('50,60'); }); describe('submit', () => {