diff --git a/packages/main/bundle.esm.js b/packages/main/bundle.esm.js index d603486a69ea..52652d8f8fc2 100644 --- a/packages/main/bundle.esm.js +++ b/packages/main/bundle.esm.js @@ -66,6 +66,7 @@ import ResponsivePopover from "./dist/ResponsivePopover.js"; import SegmentedButton from "./dist/SegmentedButton.js"; import Select from "./dist/Select.js"; import Slider from "./dist/Slider.js"; +import RangeSlider from "./dist/RangeSlider.js"; import Switch from "./dist/Switch.js"; import MessageStrip from "./dist/MessageStrip.js"; import MultiComboBox from "./dist/MultiComboBox.js"; diff --git a/packages/main/src/RangeSlider.hbs b/packages/main/src/RangeSlider.hbs new file mode 100644 index 000000000000..0c01161c6fee --- /dev/null +++ b/packages/main/src/RangeSlider.hbs @@ -0,0 +1,18 @@ +{{>include "./SliderBase.hbs"}} + +{{#*inline "handles"}} +
+ {{#if showTooltip}} +
+ {{tooltipStartValue}} +
+ {{/if}} +
+
+ {{#if showTooltip}} +
+ {{tooltipEndValue}} +
+ {{/if}} +
+{{/inline}} \ No newline at end of file diff --git a/packages/main/src/RangeSlider.js b/packages/main/src/RangeSlider.js new file mode 100644 index 000000000000..b1170a52e23e --- /dev/null +++ b/packages/main/src/RangeSlider.js @@ -0,0 +1,459 @@ +import Float from "@ui5/webcomponents-base/dist/types/Float.js"; +import { fetchI18nBundle, getI18nBundle } from "@ui5/webcomponents-base/dist/i18nBundle.js"; +import SliderBase from "./SliderBase.js"; + +// Template +import RangeSliderTemplate from "./generated/templates/RangeSliderTemplate.lit.js"; + +/** + * @public + */ +const metadata = { + tag: "ui5-range-slider", + languageAware: true, + managedSlots: true, + properties: /** @lends sap.ui.webcomponents.main.RangeSlider.prototype */ { + /** + * Defines start point of a selection - position of a first handle on the slider. + *

+ * + * @type {Float} + * @defaultvalue 0 + * @public + */ + startValue: { + type: Float, + defaultValue: 0, + }, + /** + * Defines end point of a selection - position of a second handle on the slider. + *

+ * + * @type {Float} + * @defaultvalue 100 + * @public + */ + endValue: { + type: Float, + defaultValue: 100, + }, + }, +}; + +/** + * @class + * + * Represents a numerical interval and two handles (grips) to select a sub-range within it. + * + *

Overview

+ * The purpose of the component to enable visual selection of sub-ranges within a given interval. + * + *

Structure

+ * The most important properties of the Range Slider are: + * + *

Notes:

+ * + *

Usage

+ * The most common usecase is to select and move sub-ranges on a continuous numerical scale. + * + *

Responsive Behavior

+ * You can move the currently selected range by clicking on it and dragging it along the interval. + * + *

ES6 Module Import

+ * + * import "@ui5/webcomponents/dist/RangeSlider"; + * + * + * @constructor + * @author SAP SE + * @alias sap.ui.webcomponents.main.RangeSlider + * @extends sap.ui.webcomponents.main.SliderBase + * @tagname ui5-range-slider + * @since 1.0.0-rc.11 + * @appenddocs SliderBase + * @public + */ +class RangeSlider extends SliderBase { + static get metadata() { + return metadata; + } + + static get template() { + return RangeSliderTemplate; + } + + constructor() { + super(); + this._stateStorage.startValue = null; + this._stateStorage.endValue = null; + this.i18nBundle = getI18nBundle("@ui5/webcomponents"); + } + + get tooltipStartValue() { + const stepPrecision = this.constructor._getDecimalPrecisionOfNumber(this._effectiveStep); + return this.startValue.toFixed(stepPrecision); + } + + get tooltipEndValue() { + const stepPrecision = this.constructor._getDecimalPrecisionOfNumber(this._effectiveStep); + return this.endValue.toFixed(stepPrecision); + } + + /** + * Check if the previously saved state is outdated. That would mean + * either it is the initial rendering or that a property has been changed + * programatically - because the previous state is always updated in + * the interaction handlers. + * + * Normalize current properties, update the previously stored state. + * Update the visual UI representation of the Slider. + * + */ + onBeforeRendering() { + if (!this.isCurrentStateOutdated()) { + return; + } + + this.notResized = true; + this.syncUIAndState("startValue", "endValue"); + this._updateHandlesAndRange(null); + } + + /** + * Called when the user starts interacting with the slider + * + * @private + */ + _onmousedown(event) { + // If step is 0 no interaction is available because there is no constant + // (equal for all user environments) quantitative representation of the value + if (this.disabled || this._effectiveStep === 0) { + return; + } + + // Calculate the new value from the press position of the event + const newValue = this.handleDownBase(event, this._effectiveMin, this._effectiveMax); + + // Determine the rest of the needed details from the start of the interaction. + this._saveInteractionStartData(event, newValue); + + // Do not yet update the RangeSlider if press is in range or over a handle. + if (this._inCurrentRange || this._handeIsPressed) { + this._handeIsPressed = false; + return; + } + + // Update Slider UI and internal state + this._updateHandlesAndRange(newValue); + this.updateValue(this._valueAffected, newValue); + this.storePropertyState(this._valueAffected); + } + + + /** + * Determines and saves needed values from the start of the interaction: + * + * Is the value calculated is within the currently selected range; + * Initial pageX position of the start handle affected by the interaction; + * Initial pageX value of the pressed postion; + * Affected value property by the action; + * + * @private + */ + _saveInteractionStartData(event, newValue) { + const progressBarDom = this.shadowRoot.querySelector(".ui5-slider-progress").getBoundingClientRect(); + + // Save the state of the value properties on the start of the interaction + this._prevStartValue = this.startValue; + this._prevEndValue = this.endValue; + + // Check if the new value is in the current select range of values + this._inCurrentRange = newValue > this._prevStartValue && newValue < this._prevEndValue; + // Save the initial press point coordinates (position) + this._initialPageXPosition = this.constructor.getPageXValueFromEvent(event); + // Which element of the Range Slider is pressed and which value property to be modified on further interaction + this._pressTargetAndAffectedValue(this._initialPageXPosition, newValue); + + // Use the progress bar to save the initial coordinates of the start-handle when the interaction begins. + // We will use it as a reference to calculate a moving offset if the whole range selection is dragged. + this._initialStartHandlePageX = this.directionStart === "left" ? progressBarDom.left : progressBarDom.right; + } + + + /** + * Called when the user moves the slider + * + * @private + */ + _handleMove(event) { + event.preventDefault(); + + // If 'step' is 0 no interaction is available as there is no constant quantitative representation of the value + if (this.disabled || this._effectiveStep === 0) { + return; + } + + // Update UI and state when dragging a single Range Slider handle + if (!this._inCurrentRange) { + this._updateValueOnHandleDrag(event); + return; + } + + // Updates UI and state when dragging of the whole selected range + this._updateValueOnRangeDrag(event); + } + + /** + * Updates UI and state when dragging a single Range Slider handle + * + * @private + */ + _updateValueOnHandleDrag(event) { + const newValue = this.constructor.getValueFromInteraction(event, this._effectiveStep, this._effectiveMin, this._effectiveMax, this.getBoundingClientRect(), this.directionStart); + + this._updateHandlesAndRange(newValue); + this.updateValue(this._valueAffected, newValue); + this.storePropertyState(this._valueAffected); + } + + /** + * Updates UI and state when dragging of the whole selected range + * + * @private + */ + _updateValueOnRangeDrag(event) { + // Calculate the new 'start' and 'end' values from the offset between the original press point and the current position of the mouse + const currentPageXPos = this.constructor.getPageXValueFromEvent(event); + const newValues = this._calculateRangeOffset(currentPageXPos, this._initialStartHandlePageX); + + // No matter the which value is set as the one to be modified (this._valueAffected) we want to modify both of them + this._valueAffected = null; + + // Update the UI and the state acccording to the calculated new values + this.updateValue("startValue", newValues[0]); + this.updateValue("endValue", newValues[1]); + this._updateHandlesAndRange(null); + this.storePropertyState("startValue", "endValue"); + } + + _handleUp() { + if (this.startValue !== this._prevStartValue || this.endValue !== this._prevEndValue) { + this.fireEvent("change"); + } + + this._swapValues(); + this.handleUpBase(); + + this._valueAffected = null; + this._prevStartValue = null; + this._prevEndValue = null; + } + + /** + * Determines where the press occured and which values of the Range Slider + * handles should be updated on further interaction. + * + * If the press is not in the selected range or over one of the Range Slider handles + * determines which one from the value/endValue properties has to be updated + * after the user action (based on closest handle). + * + * Set flags if the press is over a handle or in the selected range, + * in such cases no values are changed on interaction start, but could be + * updated later when dragging. + * + * @private + */ + _pressTargetAndAffectedValue(clientX, value) { + const startHandle = this.shadowRoot.querySelector(".ui5-slider-handle--start"); + const endHandle = this.shadowRoot.querySelector(".ui5-slider-handle--end"); + + // Check if the press point is in the bounds of any of the Range Slider handles + const handleStartDomRect = startHandle.getBoundingClientRect(); + const handleEndDomRect = endHandle.getBoundingClientRect(); + const inHandleStartDom = clientX >= handleStartDomRect.left && clientX <= handleStartDomRect.right; + const inHandleEndDom = clientX >= handleEndDomRect.left && clientX <= handleEndDomRect.right; + + // Remove the flag for value in current range if the press action is over one of the handles + if (inHandleEndDom || inHandleStartDom) { + this._inCurrentRange = false; + this._handeIsPressed = true; + } + + // Return that handle that is closer to the press point + if (inHandleEndDom || value > this.endValue) { + this._valueAffected = "endValue"; + } + + // If one of the handle is pressed return that one + if (inHandleStartDom || value < this.startValue) { + this._valueAffected = "startValue"; + } + } + + /** + * Calculates startValue/endValue properties when the whole range is moved. + * + * Uses the change of the position of the start handle and adds the initially + * selected range to it, to determine the whole range offset. + * + * @param {Integer} currentPageXPos The current horizontal position of the cursor/touch + * @param {Integer} initialStartHandlePageXPos The initial horizontal position of the start handle + * + * @private + */ + _calculateRangeOffset(currentPageXPos, initialStartHandlePageXPos) { + // Return the current values if there is no difference in the + // possitions of the initial press and the current pointer + if (this._initialPageXPosition === currentPageXPos) { + return [this.startValue, this.endValue]; + } + + const min = this._effectiveMin; + const max = this._effectiveMax; + const selectedRange = this.endValue - this.startValue; + + // Computes the new value based on the difference of the current cursor location from the start of the interaction + let startValue = this._calculateStartValueByOffset(currentPageXPos, initialStartHandlePageXPos); + + // When the end handle reaches the max possible value prevent the start handle from moving + // And the opposite - if the start handle reaches the beginning of the slider keep the initially selected range. + startValue = this.constructor.clipValue(startValue, min, max - selectedRange); + + return [startValue, startValue + selectedRange]; + } + + /** + * Computes the new value based on the difference of the current cursor location from the + * start of the interaction. + * + * @param {Integer} currentPageXPos The current horizontal position of the cursor/touch + * @param {Integer} initialStartHandlePageXPos The initial horizontal position of the start handle + * + * @private + */ + _calculateStartValueByOffset(currentPageXPos, initialStartHandlePageXPos) { + const min = this._effectiveMin; + const max = this._effectiveMax; + const step = this._effectiveStep; + const dom = this.getBoundingClientRect(); + + let startValue; + let startValuePageX; + let positionOffset; + + /* Depending on the dragging direction: + - calculate the new position of the start handle from its old pageX value combined with the movement offset; + - calculate the start value based on its new pageX coordinates; + - 'stepify' the calculated value based on the specified step property; */ + if (currentPageXPos > this._initialPageXPosition) { + // Difference between the new position of the pointer and when the press event initial occured + positionOffset = currentPageXPos - this._initialPageXPosition; + + startValuePageX = initialStartHandlePageXPos + positionOffset; + startValue = this.constructor.computedValueFromPageX(startValuePageX, min, max, dom, this.directionStart); + startValue = this.constructor.getSteppedValue(startValue, step, min); + } else { + positionOffset = this._initialPageXPosition - currentPageXPos; + startValuePageX = initialStartHandlePageXPos - positionOffset; + startValue = this.constructor.computedValueFromPageX(startValuePageX, min, max, dom, this.directionStart); + startValue = this.constructor.getSteppedValue(startValue, step, min); + } + + return startValue; + } + + _updateHandlesAndRange(newValue) { + const max = this._effectiveMax; + const min = this._effectiveMin; + const prevStartValue = this.getStoredPropertyState("startValue"); + const prevEndValue = this.getStoredPropertyState("endValue"); + + // The value according to which we update the UI can be either the startValue + // or the endValue property. It is determined in _getClosestHandle() + // depending on to which handle is closer the user interaction. + if (this._valueAffected === "startValue") { + // When the value changing is the start value: + this._selectedRange = (prevEndValue - newValue) / (max - min); + this._firstHandlePositionFromStart = ((newValue - min) / (max - min)) * 100; + } else if (this._valueAffected === "endValue") { + // Wen the value changing is the end value: + this._selectedRange = ((newValue - prevStartValue)) / (max - min); + this._secondHandlePositionFromStart = (newValue - min) / (max - min) * 100; + } else { + // When both values are changed - UI sync or moving the whole selected range: + this._selectedRange = ((this.endValue - this.startValue)) / (max - min); + this._firstHandlePositionFromStart = ((this.startValue - min) / (max - min)) * 100; + this._secondHandlePositionFromStart = (this.endValue - min) / (max - min) * 100; + } + } + + /** + * Swaps start and end values and handles (thumbs), if one came accros the other + * + * @private + */ + _swapValues() { + // If the start value is greater than the endValue swap them and their handles + if (this._valueAffected === "startValue" && this.startValue > this.endValue) { + const oldEndValue = this.endValue; + this.endValue = this.startValue; + this.startValue = oldEndValue; + return; + } + + // If the endValue become less than the start value swap them and their handles + if (this._valueAffected === "endValue" && this.endValue < this.startValue) { + const oldStartValue = this.startValue; + this.startValue = this.endValue; + this.endValue = oldStartValue; + } + } + + get styles() { + return { + progress: { + "transform": `scaleX(${this._selectedRange})`, + "transform-origin": `${this.directionStart} top`, + [this.directionStart]: `${this._firstHandlePositionFromStart}%`, + }, + startHandle: { + [this.directionStart]: `${this._firstHandlePositionFromStart}%`, + }, + endHandle: { + [this.directionStart]: `${this._secondHandlePositionFromStart}%`, + }, + tickmarks: { + "background": `${this._tickmarks}`, + }, + label: { + "width": `${this._labelWidth}%`, + }, + labelContainer: { + "width": `100%`, + [this.directionStart]: `-${this._labelWidth / 2}%`, + }, + tooltip: { + "visibility": `${this._tooltipVisibility}`, + }, + }; + } + + static async onDefine() { + await fetchI18nBundle("@ui5/webcomponents"); + } +} + +RangeSlider.define(); + +export default RangeSlider; diff --git a/packages/main/src/Slider.js b/packages/main/src/Slider.js index f73adaea270e..6739ed8cf12f 100644 --- a/packages/main/src/Slider.js +++ b/packages/main/src/Slider.js @@ -120,6 +120,7 @@ class Slider extends SliderBase { } const newValue = this.handleDownBase(event, this._effectiveMin, this._effectiveMax); + this._valueOnInteractionStart = this.value; // Do not yet update the Slider if press is over a handle. It will be updated if the user drags the mouse. if (!this._isHandlePressed(this.constructor.getPageXValueFromEvent(event))) { @@ -153,7 +154,12 @@ class Slider extends SliderBase { * @private */ _handleUp(event) { + if (this._valueOnInteractionStart !== this.value) { + this.fireEvent("change"); + } + this.handleUpBase(); + this._valueOnInteractionStart = null; } /** Determines if the press is over the handle diff --git a/packages/main/src/SliderBase.js b/packages/main/src/SliderBase.js index 43db710dfd6b..2fff49f31513 100644 --- a/packages/main/src/SliderBase.js +++ b/packages/main/src/SliderBase.js @@ -3,6 +3,8 @@ import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js"; import Float from "@ui5/webcomponents-base/dist/types/Float.js"; import Integer from "@ui5/webcomponents-base/dist/types/Integer.js"; import ResizeHandler from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js"; +import { isPhone } from "@ui5/webcomponents-base/dist/Device.js"; + import { getTheme } from "@ui5/webcomponents-base/dist/config/Theme.js"; // Styles @@ -300,6 +302,10 @@ class SliderBase extends UI5Element { * @protected */ handleDownBase(event, min, max) { + if (isPhone() && this.showTooltip) { + this._tooltipVisibility = "visible"; + } + // Only allow one type of move event to be listened to (the first one registered after the down event) this._moveEventType = !this._moveEventType ? SliderBase.MOVE_EVENT_MAP[event.type] : this._moveEventType; @@ -309,7 +315,6 @@ class SliderBase extends UI5Element { this._boundingClientRect = this.getBoundingClientRect(); const newValue = SliderBase.getValueFromInteraction(event, this.step, min, max, this._boundingClientRect, this.directionStart); - this._valueOnInteractionStart = newValue; return newValue; } @@ -319,16 +324,15 @@ class SliderBase extends UI5Element { * * @protected */ - handleUpBase() { - if (this._valueOnInteractionStart !== this.value) { - this.fireEvent("change"); + handleUpBase(valueType) { + if (isPhone() && this.showTooltip) { + this._tooltipVisibility = "hidden"; } SliderBase.UP_EVENTS.forEach(upEventType => window.removeEventListener(upEventType, this._upHandler)); window.removeEventListener(this._moveEventType, this._moveHandler); this._moveEventType = null; - this._valueOnInteractionStart = null; } /** @@ -401,7 +405,7 @@ class SliderBase extends UI5Element { /** * Computes the new value (in %) from the pageX position of the cursor. - * Returns the value with rounded to a precision of at most 2 digits after decimal point. + * Returns the value rounded to a precision of at most 2 digits after decimal point. * * @protected */ @@ -632,10 +636,6 @@ class SliderBase extends UI5Element { get _effectiveStep() { let step = this.step; - if (step === 0) { - return; - } - if (step < 0) { step = Math.abs(step); } diff --git a/packages/main/src/themes/SliderBase.css b/packages/main/src/themes/SliderBase.css index dd46d1ae22c9..8cf91592ca91 100644 --- a/packages/main/src/themes/SliderBase.css +++ b/packages/main/src/themes/SliderBase.css @@ -68,15 +68,27 @@ width: var(--_ui5_slider_handle_width); } +.ui5-slider-handle--start, .ui5-slider-handle--end { + background: var(--_ui5_range_slider_handle_background); +} + + [dir="rtl"] .ui5-slider-handle { margin-right: var(--_ui5_slider_handle_margin_left); } -.ui5-slider-root:hover .ui5-slider-handle { +.ui5-slider-root:hover .ui5-slider-handle, +.ui5-slider-root:active .ui5-slider-handle, .ui5-slider-handle:active { background: var(--_ui5_slider_handle_hover_background); border-color: var(--_ui5_slider_handle_hover_border); } +.ui5-slider-root:hover .ui5-slider-handle--start, .ui5-slider-root:hover .ui5-slider-handle--end, +.ui5-slider-root:active .ui5-slider-handle--start, .ui5-slider-root:active .ui5-slider-handle--end, +.ui5-slider-handle--start:active, .ui5-slider-handle--end:active { + background: var(--_ui5_range_slider_handle_hover_background); +} + .ui5-slider-tooltip { text-align: center; visibility: hidden; diff --git a/packages/main/src/themes/base/SliderBase-parameters.css b/packages/main/src/themes/base/SliderBase-parameters.css index 6f4c45a78e11..f656c1dce3fe 100644 --- a/packages/main/src/themes/base/SliderBase-parameters.css +++ b/packages/main/src/themes/base/SliderBase-parameters.css @@ -9,10 +9,12 @@ --_ui5_slider_handle_width: 1.25rem; --_ui5_slider_handle_border: solid 0.125rem var(--sapField_BorderColor); --_ui5_slider_handle_background: var(--sapButton_Background); + --_ui5_range_slider_handle_background: rgba(var(--sapButton_Background), 0.25); --_ui5_slider_handle_top: -0.6425rem; --_ui5_slider_handle_margin_left: -0.8125rem; --_ui5_slider_handle_hover_background: var(--sapButton_Hover_Background); --_ui5_slider_handle_hover_border: var(--sapButton_Hover_BorderColor); + --_ui5_range_slider_handle_hover_background: rgba(var(--sapButton_Background), 0.25); --_ui5_slider_tickmark_color: #89919a; --_ui5_slider_tickmark_top: -0.375rem; --_ui5_slider_disabled_opacity: 0.4; diff --git a/packages/main/test/pages/RangeSlider.html b/packages/main/test/pages/RangeSlider.html new file mode 100644 index 000000000000..d66e1617b34e --- /dev/null +++ b/packages/main/test/pages/RangeSlider.html @@ -0,0 +1,63 @@ + + + + + + + + UI5 Range Slider + + + + + + + + + + + + + + + + +
+

Basic Range Slider

+ + +

Range Slider with custom min and max properties and tooltip

+ + +

Range Slider with small step and tooltip

+ + +

Range Slider with tickmarks

+ + +

Disabled Range Slider

+ + +

Range Slider with steps, tooltips, tickmarks and labels

+ +
+ + diff --git a/packages/main/test/samples/RangeSlider.sample.html b/packages/main/test/samples/RangeSlider.sample.html new file mode 100644 index 000000000000..01f95f2ed102 --- /dev/null +++ b/packages/main/test/samples/RangeSlider.sample.html @@ -0,0 +1,63 @@ +
+

Range Slider

+
+ +
+
+ +
@ui5/webcomponents
+ +
<ui5-range-slider>
+ +
+

Basic Range Slider

+
+ +
+ +

+<ui5-range-slider end-value="20"></ui5-range-slider>
+	
+
+ +
+

Range Slider with Custom 'min', 'max', 'startValue' and 'endValue' Properties

+
+ +
+

+<ui5-range-slider  min="100" max="200" start-value="120" end-value="150"></ui5-range-slider>
+	
+
+ +
+

Range Slider with Tooltips

+
+ +
+

+<ui5-range-slider start-value="3" end-value="13" show-tooltip></ui5-range-slider>
+	
+
+ +
+

Range Slider with Tickmarks and Custom Step

+
+ +
+

+<ui5-range-slider step="2" start-value="4" end-value="12" show-tickmarks></ui5-range-slider>
+	
+
+ +
+

Range Slider with Tooltips, Tickmarks and Labels

+
+ +
+

+<ui5-range-slider min="0" max="112" step="2" start-value="4" end-value="12" show-tooltip label-interval="2" show-tickmarks></ui5-range-slider>
+	
+
+ + \ No newline at end of file diff --git a/packages/main/test/samples/Slider.sample.html b/packages/main/test/samples/Slider.sample.html index 76661a7de7f8..59a245aa3f5f 100644 --- a/packages/main/test/samples/Slider.sample.html +++ b/packages/main/test/samples/Slider.sample.html @@ -34,6 +34,7 @@

Slider with Tooltip

Disabled Slider with Tickmarks and Labels

+

 <ui5-slider min="20" max="100" label-interval="5" disabled show-tickmarks></ui5-slider>
@@ -49,5 +50,8 @@ <h3>Slider tooltip, tickmarks and labels</h3>
 <ui5-slider min="-20" max="20" step="2" value="12" show-tooltip label-interval="2" show-tickmarks></ui5-slider>
 	
+ + + diff --git a/packages/main/test/specs/RangeSlider.spec.js b/packages/main/test/specs/RangeSlider.spec.js new file mode 100644 index 000000000000..04eedd27e9d6 --- /dev/null +++ b/packages/main/test/specs/RangeSlider.spec.js @@ -0,0 +1,256 @@ +const assert = require("chai").assert; + +describe("Testing Range Slider interactions", () => { + + it("Changing the current startValue is reflected", () => { + browser.url("http://localhost:8080/test-resources/pages/RangeSlider.html"); + browser.setWindowSize(1257, 2000); + + const rangeSlider = browser.$("#range-slider-tickmarks"); + const startHandle = rangeSlider.shadow$(".ui5-slider-handle--start"); + + assert.strictEqual(startHandle.getAttribute("style"), "left: 0%;", "Initially if no value is set, the Range Slider start-handle is at the beginning of the Range Slider"); + + rangeSlider.setProperty("startValue", 5); + + assert.strictEqual(startHandle.getAttribute("style"), "left: 12.5%;", "Start-handle should be 12.5% from the start"); + + startHandle.dragAndDrop({ x: 100, y: 1 }); + + assert.strictEqual(startHandle.getAttribute("style"), "left: 20%;", "Start-handle should be 20% from the start of the Range Slider"); + assert.strictEqual(rangeSlider.getProperty("startValue"), 8, "Range Slider startValue should be 8"); + + startHandle.click({ x: -100 }); + + assert.strictEqual(startHandle.getAttribute("style"), "left: 12.5%;", "Start-handle should be again 12.5% from the start"); + assert.strictEqual(rangeSlider.getProperty("startValue"), 5, "Current startValue should be again 5"); + }); + + it("Changing the endValue is reflected", () => { + const rangeSlider = browser.$("#range-slider-tickmarks"); + const endHandle = rangeSlider.shadow$(".ui5-slider-handle--end"); + + assert.strictEqual(endHandle.getAttribute("style"), "left: 50%;", "Range Slider end-handle is should be 50% from the start the Range Slider"); + rangeSlider.setProperty("endValue", 10); + + assert.strictEqual(endHandle.getAttribute("style"), "left: 25%;", "End-handle should be 25% from the start"); + + rangeSlider.click(); + + assert.strictEqual(endHandle.getAttribute("style"), "left: 50%;", "Range Slider end-handle should be in the middle of the slider"); + assert.strictEqual(rangeSlider.getProperty("endValue"), 20, "Range Slider endValue should be 20"); + + endHandle.click({ x: 100 }); + + assert.strictEqual(endHandle.getAttribute("style"), "left: 57.5%;", "End-handle should be 57.5%% from the start of the Range slider"); + assert.strictEqual(rangeSlider.getProperty("endValue"), 23, "Range Slider current endValue should be 23"); + + endHandle.dragAndDrop({ x: -100, y: 1 }); + + assert.strictEqual(endHandle.getAttribute("style"), "left: 50%;", "End-handle should be back to 50% from the start of the Range Slider"); + assert.strictEqual(rangeSlider.getProperty("endValue"), 20, "Current endValue should be 20"); + }); + + it("Click within the selected range should not change any value", () => { + const rangeSlider = browser.$("#range-slider-tickmarks"); + + rangeSlider.setProperty("endValue", 30); + + assert.strictEqual(rangeSlider.getProperty("startValue"), 5, "startValue should be 5"); + assert.strictEqual(rangeSlider.getProperty("endValue"), 30, "endValue value should be 30"); + + rangeSlider.click(); + + assert.strictEqual(rangeSlider.getProperty("startValue"), 5, "startValue should still be 5"); + assert.strictEqual(rangeSlider.getProperty("endValue"), 30, "endValue value should still be 30"); + }); + + it("Dragging the selected range should change both values and handles", () => { + const rangeSlider = browser.$("#range-slider-tickmarks"); + const startHandle = rangeSlider.shadow$(".ui5-slider-handle--start"); + const endHandle = rangeSlider.shadow$(".ui5-slider-handle--end"); + + rangeSlider.dragAndDrop({ x: 100, y: 1 }); + + assert.strictEqual(rangeSlider.getProperty("startValue"), 8, "startValue should be 8"); + assert.strictEqual(rangeSlider.getProperty("endValue"), 33, "endValue should be 33"); + }); + + it("Dragging the start-handle pass the end-handle should swap the values", () => { + const rangeSlider = browser.$("#range-slider-tickmarks"); + const startHandle = rangeSlider.shadow$(".ui5-slider-handle--start"); + const endHandle = rangeSlider.shadow$(".ui5-slider-handle--end"); + + rangeSlider.setProperty("endValue", 9); + + startHandle.dragAndDrop({ x: 100, y: 1 }); + + assert.strictEqual(rangeSlider.getProperty("startValue"), 9, "startValue should swapped with the endValue and should be 9"); + assert.strictEqual(rangeSlider.getProperty("endValue"), 11, "endValue should swapped with the startValue and should be 11"); + }); + + it("Dragging the whole range selection should always keep the initially selected range and be within min/max values", () => { + const rangeSlider = browser.$("#range-slider-tickmarks"); + const startHandle = rangeSlider.shadow$(".ui5-slider-handle--start"); + const endHandle = rangeSlider.shadow$(".ui5-slider-handle--end"); + + rangeSlider.setProperty("endValue", 30); + + rangeSlider.dragAndDrop({ x: -500, y: 1 }); + + assert.strictEqual(rangeSlider.getProperty("startValue"), 0, "startValue should be 0 as the selected range has reached the start of the Range Slider"); + assert.strictEqual(rangeSlider.getProperty("endValue"), 21, "endValue should be 21 and no less, the initially selected range should be preserved"); + + rangeSlider.dragAndDrop({ x: 600, y: 1 }); + + assert.strictEqual(rangeSlider.getProperty("startValue"), 19, "startValue should be 19 and no more, the initially selected range should be preserved"); + assert.strictEqual(rangeSlider.getProperty("endValue"), 40, "endValue should be 40 as the selected range has reached the end of the Range Slider"); + }); + + it("Range Slider should not be interactive if the step property is 0", () => { + const rangeSlider = browser.$("#basic-range-slider"); + + rangeSlider.setProperty("step", 0); + rangeSlider.click(); + + assert.strictEqual(rangeSlider.getProperty("endValue"), 20, "Range Slider endValue should be the same"); + }); + + it("Disabled Range Slider is not interactive", () => { + const rangeSlider = browser.$("#disabled-range-slider"); + + assert.strictEqual(rangeSlider.isClickable(), false, "Range Slider should be disabled"); + }); +}); + +describe("Range Slider elements - tooltip, step, tickmarks, labels", () => { + + it("Range Slider tooltips are displayed showing the current value", () => { + const rangeSlider = browser.$("#basic-range-slider-with-tooltip"); + const rangeSliderStartTooltip = rangeSlider.shadow$(".ui5-slider-tooltip--start"); + const rangeSliderStartTooltipValue = rangeSliderStartTooltip.shadow$(".ui5-slider-tooltip-value"); + const rangeSliderEndTooltip = rangeSlider.shadow$(".ui5-slider-tooltip--end"); + const rangeSliderEndTooltipValue = rangeSliderEndTooltip.shadow$(".ui5-slider-tooltip-value"); + const startHandle = rangeSlider.shadow$(".ui5-slider-handle--start"); + const endHandle = rangeSlider.shadow$(".ui5-slider-handle--end"); + + rangeSlider.moveTo(); + + assert.strictEqual(rangeSlider.getProperty("_tooltipVisibility"), "visible", "Range Slider tooltips visibility property should be 'visible'"); + assert.strictEqual(rangeSliderStartTooltip.getAttribute("style"), "visibility: visible;", "Range Slider start tooltip should be shown"); + assert.strictEqual(rangeSliderEndTooltip.getAttribute("style"), "visibility: visible;", "Range Slider end tooltip should be shown"); + + rangeSlider.setProperty("startValue", 65); + rangeSlider.moveTo(); + + assert.strictEqual(rangeSliderStartTooltipValue.getText(), "65", "Range Slider start tooltip should display value of 65"); + + rangeSlider.setProperty("endValue", 115); + rangeSlider.moveTo(); + + assert.strictEqual(rangeSliderEndTooltipValue.getText(), "115", "Range Slider end tooltip should display value of 65"); + }); + + it("Range Slider have correct number of labels and tickmarks based on the defined step and labelInterval properties", () => { + const rangeSlider = browser.$("#range-slider-tickmarks-labels"); + const labelsContainer = rangeSlider.shadow$(".ui5-slider-labels"); + const numberOfLabels = labelsContainer.$$("li").length; + + assert.strictEqual(numberOfLabels, 12, "12 labels should be rendered, 1 between every 4 tickmarks"); + }); +}); + +describe("Properties synchronization and normalization", () => { + + it("Should fallback to default value of 1 if step property is not a valid number", () => { + const rangeSlider = browser.$("#range-slider-tickmarks-labels"); + + rangeSlider.setProperty("step", "String value"); + + assert.strictEqual(rangeSlider.getProperty("step"), 1, "Step value should be its default value"); + }); + + it("Should keep the current values between the boundaries of min and max properties", () => { + const rangeSlider = browser.$("#range-slider-tickmarks-labels"); + + rangeSlider.setProperty("min", 100); + rangeSlider.setProperty("max", 200); + + rangeSlider.setProperty("endValue", 300); + + assert.strictEqual(rangeSlider.getProperty("endValue"), 200, "value prop should always be lower than the max value"); + + rangeSlider.setProperty("startValue", 99); + + assert.strictEqual(rangeSlider.getProperty("startValue"), 100, "value prop should always be greater than the min value"); + }); + + it("Should not 'stepify' current value if it is not in result of user interaction", () => { + const rangeSlider = browser.$("#range-slider-tickmarks-labels"); + + rangeSlider.setProperty("min", 0); + rangeSlider.setProperty("max", 44); + rangeSlider.setProperty("step", 1.25); + rangeSlider.setProperty("startValue", 14); + rangeSlider.setProperty("endValue", 24); + + assert.strictEqual(rangeSlider.getProperty("startValue"), 14, "startValue should not be stepped to the next step (15)"); + assert.strictEqual(rangeSlider.getProperty("endValue"), 24, "endValue should not be stepped to the next step (25)"); + }); +}); + +describe("Testing resize handling and RTL support", () => { + it("Testing RTL support", () => { + const rangeSlider = browser.$("#range-slider-tickmarks-labels"); + const startHandle = rangeSlider.shadow$(".ui5-slider-handle--start"); + const endHandle = rangeSlider.shadow$(".ui5-slider-handle--end"); + + rangeSlider.setAttribute("dir", "rtl"); + rangeSlider.setProperty("min", 0); + rangeSlider.setProperty("max", 10); + rangeSlider.setProperty("step", 1); + rangeSlider.setProperty("startValue", 0); + rangeSlider.setProperty("endValue", 4); + + assert.strictEqual(startHandle.getAttribute("style"), "right: 0%;", "Initially if no value is set, the start-handle is 0% from the right side of the Range Slider"); + assert.strictEqual(endHandle.getAttribute("style"), "right: 40%;", "End-handle should be 40 percent from the right side of the Range Slider"); + + rangeSlider.setProperty("startValue", 3); + + assert.strictEqual(startHandle.getAttribute("style"), "right: 30%;", "Start-handle should be 30% from the right end of the Range Slider"); + + rangeSlider.click(); + + assert.strictEqual(endHandle.getAttribute("style"), "right: 50%;", "End-handle should be at the middle of the Range Slider"); + assert.strictEqual(rangeSlider.getProperty("endValue"), 5, "endValue should be 5"); + + endHandle.dragAndDrop({ x: -200, y: 1 }); + + assert.strictEqual(endHandle.getAttribute("style"), "right: 80%;", "End-handle should be 80% from the right of the slider"); + assert.strictEqual(rangeSlider.getProperty("endValue"), 8, "endValue should be 8"); + + endHandle.dragAndDrop({ x: -100, y: 1 }); + + assert.strictEqual(endHandle.getAttribute("style"), "right: 90%;", "End-handle should be 90% from the right of the slider"); + assert.strictEqual(rangeSlider.getProperty("endValue"), 9, "endValue should be 9"); + + startHandle.dragAndDrop({ x: 350, y: 1 }); + + assert.strictEqual(startHandle.getAttribute("style"), "right: 0%;", "Slider handle should be 0% at the right of the Range Slider"); + assert.strictEqual(rangeSlider.getProperty("startValue"), 0, "startValue should be 0"); + }); + + it("Should hide all labels except the first and the last one, if there is not enough space for all of them", () => { + const rangeSlider = browser.$("#range-slider-tickmarks-labels"); + + rangeSlider.setAttribute("dir", "ltr"); + rangeSlider.setProperty("min", 0); + rangeSlider.setProperty("max", 44); + rangeSlider.setProperty("step", 1.25); + + browser.setWindowSize(400, 2000); + + assert.strictEqual(rangeSlider.getProperty("_labelsOverlapping"), true, "state should reflect if any of the labels is overlapping with another"); + assert.strictEqual(rangeSlider.getProperty("_hiddenTickmarks"), true, "state should reflect if the tickmarks has less than 8px space between each of them"); + }); +});