diff --git a/packages/main/src/RangeSlider.hbs b/packages/main/src/RangeSlider.hbs index 867637360b0a..6ac3e9b8e866 100644 --- a/packages/main/src/RangeSlider.hbs +++ b/packages/main/src/RangeSlider.hbs @@ -1,8 +1,8 @@ {{>include "./SliderBase.hbs"}} {{#*inline "handlesAriaText"}} - {{_ariaHandlesText.startHandleText}} - {{_ariaHandlesText.endHandleText}} + {{_ariaHandlesText.startHandleText}} + {{_ariaHandlesText.endHandleText}} {{/inline}} {{#*inline "progressBar"}} @@ -22,56 +22,88 @@ aria-valuemax="{{max}}" aria-valuenow="{{_ariaValueNow}}" aria-valuetext="From {{startValue}} to {{endValue}}" - aria-labelledby="{{_ariaLabelledByProgressBarRefs}}" + aria-labelledby="ui5-slider-sliderDesc" aria-disabled="{{_ariaDisabled}}" > {{/inline}} {{#*inline "handles"}} -
- +
+ +
+ +
{{#if showTooltip}} -
+
+ {{#if editableTooltip}} + + {{else}} {{tooltipStartValue}} -
+ {{/if}} +
{{/if}}
- -
- +
+
+ +
{{#if showTooltip}} -
+
+ {{#if editableTooltip}} + + {{else}} {{tooltipEndValue}} -
+ {{/if}} +
{{/if}}
{{/inline}} diff --git a/packages/main/src/RangeSlider.ts b/packages/main/src/RangeSlider.ts index 2a877b2da726..3ea110070748 100644 --- a/packages/main/src/RangeSlider.ts +++ b/packages/main/src/RangeSlider.ts @@ -5,18 +5,22 @@ import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js"; import type { IFormInputElement } from "@ui5/webcomponents-base/dist/features/InputElementsFormSupport.js"; import { isEscape, + isEnter, isHome, isEnd, } from "@ui5/webcomponents-base/dist/Keys.js"; import SliderBase from "./SliderBase.js"; import Icon from "./Icon.js"; import RangeSliderTemplate from "./generated/templates/RangeSliderTemplate.lit.js"; +import Input from "./Input.js"; // Texts import { RANGE_SLIDER_ARIA_DESCRIPTION, RANGE_SLIDER_START_HANDLE_DESCRIPTION, RANGE_SLIDER_END_HANDLE_DESCRIPTION, + SLIDER_TOOLTIP_INPUT_LABEL, + SLIDER_TOOLTIP_INPUT_DESCRIPTION, } from "./generated/i18n/i18n-defaults.js"; // Styles @@ -88,7 +92,7 @@ type AffectedValue = "startValue" | "endValue"; languageAware: true, formAssociated: true, template: RangeSliderTemplate, - dependencies: [Icon], + dependencies: [Icon, Input], styles: [SliderBase.styles, rangeSliderStyles], }) class RangeSlider extends SliderBase implements IFormInputElement { @@ -115,6 +119,12 @@ class RangeSlider extends SliderBase implements IFormInputElement { @property({ type: Boolean }) rangePressed = false; + @property({ type: Boolean }) + _isStartValueValid = false; + + @property({ type: Boolean }) + _isEndValueValid = false; + _startValueInitial?: number; _endValueInitial?: number; _valueAffected?: AffectedValue; @@ -128,6 +138,9 @@ class RangeSlider extends SliderBase implements IFormInputElement { _secondHandlePositionFromStart?: number; _selectedRange?: number; _reversedValues = false; + _lastValidStartValue: string; + _lastValidEndValue: string; + _areInputValuesSwapped = false; @i18n("@ui5/webcomponents") static i18nBundle: I18nBundle; @@ -149,6 +162,8 @@ class RangeSlider extends SliderBase implements IFormInputElement { super(); this._stateStorage.startValue = undefined; this._stateStorage.endValue = undefined; + this._lastValidStartValue = this.min.toString(); + this._lastValidEndValue = this.max.toString(); } get tooltipStartValue() { @@ -210,6 +225,10 @@ class RangeSlider extends SliderBase implements IFormInputElement { this.update(affectedValue, this.startValue, this.endValue); } + if (this.editableTooltip) { + this._saveInputValues(); + } + if (!this.isCurrentStateOutdated()) { return; } @@ -217,6 +236,7 @@ class RangeSlider extends SliderBase implements IFormInputElement { this.notResized = true; this.syncUIAndState(); this._updateHandlesAndRange(0); + this.update(this._valueAffected, this.startValue, this.endValue); } syncUIAndState() { @@ -279,7 +299,7 @@ class RangeSlider extends SliderBase implements IFormInputElement { * Resets the stored Range Slider's initial values saved when it was first focused * @private */ - _onfocusout() { + _onfocusout(e: FocusEvent) { if (this._isFocusing()) { this._preventFocusOut(); return; @@ -289,20 +309,56 @@ class RangeSlider extends SliderBase implements IFormInputElement { this._startValueInitial = undefined; this._endValueInitial = undefined; - if (this.showTooltip) { + if (this.showTooltip && !(e.relatedTarget as HTMLInputElement)?.hasAttribute("ui5-input")) { this._tooltipVisibility = SliderBase.TOOLTIP_VISIBILITY.HIDDEN; } } + _onInputFocusOut(e: FocusEvent) { + const tooltipInput = e.target as Input; + const oppositeTooltipInput: Input = tooltipInput.hasAttribute("data-sap-ui-start-value") ? this.shadowRoot!.querySelector("ui5-input[data-sap-ui-end-value]")! : this.shadowRoot!.querySelector("ui5-input[data-sap-ui-start-value]")!; + const relatedTarget = e.relatedTarget as HTMLElement; + + if (this.startValue > this.endValue) { + this._areInputValuesSwapped = true; + oppositeTooltipInput.focus(); + return; + } + + if (tooltipInput.hasAttribute("data-sap-ui-start-value")) { + this._setAffectedValue("startValue"); + } else { + this._setAffectedValue("endValue"); + } + + if (!this._areInputValuesSwapped || !this.shadowRoot!.contains(relatedTarget)) { + this._tooltipVisibility = SliderBase.TOOLTIP_VISIBILITY.HIDDEN; + } + + this._updateValueFromInput(e); + this._updateInputValue(); + this.update(this._valueAffected, parseFloat(this._lastValidStartValue), parseFloat(this._lastValidEndValue)); + + const isTooltipInputValueValid = parseFloat(tooltipInput.value) >= this.min && parseFloat(tooltipInput.value) <= this.max; + + if (!isTooltipInputValueValid) { + tooltipInput.value = tooltipInput.hasAttribute("data-sap-ui-start-value") ? this._lastValidStartValue : this._lastValidEndValue; + tooltipInput.valueState = "None"; + } + } + /** * Handles keyup logic. If one of the handles came across the other * swap the start and end values. Reset the affected value by the finished * user interaction. * @private */ - _onkeyup() { - super._onkeyup(); - this._setAffectedValue(undefined); + _onkeyup(e: KeyboardEvent) { + super._onKeyupBase(); + + if (!isEnter(e)) { + this._setAffectedValue(undefined); + } if (this.startValue !== this._startValueAtBeginningOfAction || this.endValue !== this._endValueAtBeginningOfAction) { this.fireEvent("change"); @@ -429,7 +485,7 @@ class RangeSlider extends SliderBase implements IFormInputElement { _onmousedown(e: TouchEvent | MouseEvent) { // 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) { + if (this.disabled || this._effectiveStep === 0 || (e.target as HTMLElement).hasAttribute("ui5-input")) { return; } @@ -484,7 +540,7 @@ class RangeSlider extends SliderBase implements IFormInputElement { e.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) { + if (this.disabled || this._effectiveStep === 0 || (e.target as HTMLElement).hasAttribute("ui5-input")) { return; } @@ -525,7 +581,11 @@ class RangeSlider extends SliderBase implements IFormInputElement { this.update(undefined, newValues[0], newValues[1]); } - _handleUp() { + _handleUp(e: MouseEvent) { + if ((e.target as HTMLElement).hasAttribute("ui5-input")) { + return; + } + this._setAffectedValueByFocusedElement(); this._setAffectedValue(undefined); @@ -541,6 +601,31 @@ class RangeSlider extends SliderBase implements IFormInputElement { this._endValueAtBeginningOfAction = undefined; } + _updateValueFromInput(e: Event) { + if (this._areInputValuesSwapped) { + return; + } + + const input = e.target as HTMLInputElement; + const inputValue = parseFloat(input.value); + const isValueValid = inputValue >= this._effectiveMin && inputValue <= this._effectiveMax; + + if (!isValueValid) { + return; + } + + if (input.hasAttribute("data-sap-ui-start-value")) { + this.startValue = inputValue; + return; + } + + this.endValue = inputValue; + + if (this.startValue > this.endValue) { + this._areInputValuesSwapped = true; + } + } + /** * Determines where the press occured and which values of the Range Slider * handles should be updated on further interaction. @@ -635,6 +720,10 @@ class RangeSlider extends SliderBase implements IFormInputElement { * @protected */ focusInnerElement() { + if (this.editableTooltip && this._tooltipVisibility === SliderBase.TOOLTIP_VISIBILITY.HIDDEN) { + return; + } + const isReversed = this._areValuesReversed(); const affectedValue = this._valueAffected; @@ -750,6 +839,111 @@ class RangeSlider extends SliderBase implements IFormInputElement { } } + _onInputKeydown(e: KeyboardEvent): void { + const targetedInput = e.target as Input; + const startValueInput = this.shadowRoot!.querySelector("ui5-input[data-sap-ui-start-value]") as Input; + const endValueInput = this.shadowRoot!.querySelector("ui5-input[data-sap-ui-end-value]") as Input; + + const startValue = parseFloat(startValueInput.value); + const endValue = parseFloat(endValueInput.value); + const affectedValue = targetedInput.hasAttribute("data-sap-ui-start-value") ? "startValue" : "endValue"; + + super._onInputKeydown(e); + + if (isEnter(e) && startValue > endValue) { + const swappedInput = affectedValue === "startValue" ? endValueInput : startValueInput; + const isValueValid = parseFloat(targetedInput.value) >= this.min && parseFloat(startValueInput.value) <= this.max; + + if (!isValueValid) { + targetedInput.valueState = "Negative"; + return; + } + + this._isEndValueValid = parseFloat(endValueInput.value) >= this.min && parseFloat(endValueInput.value) <= this.max; + + this._areInputValuesSwapped = true; + this._setAffectedValue(affectedValue === "startValue" ? "endValue" : "startValue"); + + startValueInput.value = this._getFormattedValue(this.endValue.toString()); + endValueInput.value = this._getFormattedValue(this.startValue.toString()); + swappedInput.focus(); + + return; + } + + this._setAffectedValue(affectedValue); + } + + _updateInputValue() { + const startValueInput = this.shadowRoot!.querySelector("ui5-input[data-sap-ui-start-value]") as Input; + const endValueInput = this.shadowRoot!.querySelector("ui5-input[data-sap-ui-end-value]") as Input; + + if (!startValueInput && !endValueInput) { + return; + } + + this._isStartValueValid = parseFloat(startValueInput.value) >= this.min && parseFloat(startValueInput.value) <= this.max; + this._isEndValueValid = parseFloat(endValueInput.value) >= this.min && parseFloat(endValueInput.value) <= this.max; + + if (!this._isStartValueValid) { + startValueInput.valueState = "Negative"; + return; + } + + if (!this._isEndValueValid) { + endValueInput.valueState = "Negative"; + return; + } + + this._lastValidStartValue = startValueInput.value; + this._lastValidEndValue = endValueInput.value; + + startValueInput.valueState = "None"; + endValueInput.valueState = "None"; + } + + _saveInputValues() { + const startValueInput = this.shadowRoot!.querySelector("ui5-input[data-sap-ui-start-value]") as Input; + const endValueInput = this.shadowRoot!.querySelector("ui5-input[data-sap-ui-end-value]") as Input; + + if (this.editableTooltip && startValueInput && endValueInput) { + const inputStartValue = parseFloat(startValueInput.value); + const inputEndValue = parseFloat(endValueInput.value); + + const isStartValueValid = inputStartValue >= this.min && inputStartValue <= this.max; + const isEndValueValid = inputEndValue >= this.min && inputEndValue <= this.max; + + if (this._isUserInteraction) { + startValueInput.value = isStartValueValid ? this._getFormattedValue(this.startValue.toString()) : this._getFormattedValue(this._lastValidStartValue); + endValueInput.value = isEndValueValid ? this._getFormattedValue(this.endValue.toString()) : this._getFormattedValue(this._lastValidEndValue); + + this.startValue = parseFloat(this._getFormattedValue(this.startValue.toString())); + this.endValue = parseFloat(this._getFormattedValue(this.endValue.toString())); + + this.syncUIAndState(); + this._updateHandlesAndRange(0); + this.update(this._valueAffected, this.startValue, this.endValue); + return; + } + + this._lastValidStartValue = isStartValueValid ? this._getFormattedValue(inputStartValue.toString()) : this._getFormattedValue(this._lastValidStartValue); + this._lastValidEndValue = isEndValueValid ? this._getFormattedValue(inputEndValue.toString()) : this._getFormattedValue(this._lastValidEndValue); + + if (startValueInput.valueState !== "Negative" && endValueInput.valueState !== "Negative") { + startValueInput.value = isStartValueValid ? this._getFormattedValue(inputStartValue.toString()) : this._getFormattedValue(this._lastValidStartValue); + endValueInput.value = isEndValueValid ? this._getFormattedValue(inputEndValue.toString()) : this._getFormattedValue(this._lastValidEndValue); + } + } + } + + _getFormattedValue(value: string) { + const valueNumber = parseFloat(value); + const ctor = this.constructor as typeof RangeSlider; + const stepPrecision = ctor._getDecimalPrecisionOfNumber(this._effectiveStep); + + return valueNumber.toFixed(stepPrecision).toString(); + } + /** * Swaps the start and end values of the handles if one came accros the other: * - If the start value is greater than the endValue swap them and their handles @@ -781,8 +975,13 @@ class RangeSlider extends SliderBase implements IFormInputElement { this._setValuesAreReversed(); this._updateHandlesAndRange(this[affectedValue]); - this.focusInnerElement(); + + if (!this._areInputValuesSwapped) { + this.focusInnerElement(); + } + this.syncUIAndState(); + this._areInputValuesSwapped = false; } /** @@ -830,16 +1029,12 @@ class RangeSlider extends SliderBase implements IFormInputElement { return this.shadowRoot!.querySelector(".ui5-slider-progress")!; } - get _ariaLabelledByStartHandleRefs() { - return [`${this._id}-accName`, `${this._id}-startHandleDesc`].join(" ").trim(); - } - - get _ariaLabelledByEndHandleRefs() { - return [`${this._id}-accName`, `${this._id}-endHandleDesc`].join(" ").trim(); + get _ariaLabelledByInputText() { + return RangeSlider.i18nBundle.getText(SLIDER_TOOLTIP_INPUT_LABEL); } - get _ariaLabelledByProgressBarRefs() { - return [`${this._id}-accName`, `${this._id}-sliderDesc`].join(" ").trim(); + get _ariaDescribedByInputText() { + return RangeSlider.i18nBundle.getText(SLIDER_TOOLTIP_INPUT_DESCRIPTION); } get styles() { diff --git a/packages/main/src/Slider.hbs b/packages/main/src/Slider.hbs index 00e0940882b3..21bb8cf6c1fe 100644 --- a/packages/main/src/Slider.hbs +++ b/packages/main/src/Slider.hbs @@ -17,26 +17,45 @@ {{/inline}} {{#*inline "handles"}} -
- +
+
+ +
{{#if showTooltip}} -
+
+ {{#if editableTooltip}} + + {{else}} {{tooltipValue}} -
+ {{/if}} +
{{/if}}
{{/inline}} diff --git a/packages/main/src/Slider.ts b/packages/main/src/Slider.ts index 699eb71d7f2e..743c3aa7e3e6 100644 --- a/packages/main/src/Slider.ts +++ b/packages/main/src/Slider.ts @@ -6,6 +6,7 @@ import { isEscape } from "@ui5/webcomponents-base/dist/Keys.js"; import type { IFormInputElement } from "@ui5/webcomponents-base/dist/features/InputElementsFormSupport.js"; import SliderBase from "./SliderBase.js"; import Icon from "./Icon.js"; +import Input from "./Input.js"; // Template import SliderTemplate from "./generated/templates/SliderTemplate.lit.js"; @@ -13,6 +14,8 @@ import SliderTemplate from "./generated/templates/SliderTemplate.lit.js"; // Texts import { SLIDER_ARIA_DESCRIPTION, + SLIDER_TOOLTIP_INPUT_DESCRIPTION, + SLIDER_TOOLTIP_INPUT_LABEL, } from "./generated/i18n/i18n-defaults.js"; /** @@ -74,7 +77,7 @@ import { languageAware: true, formAssociated: true, template: SliderTemplate, - dependencies: [Icon], + dependencies: [Icon, Input], }) class Slider extends SliderBase implements IFormInputElement { /** @@ -91,6 +94,9 @@ class Slider extends SliderBase implements IFormInputElement { _valueOnInteractionStart?: number; _progressPercentage = 0; _handlePositionFromStart = 0; + _lastValidInputValue: string; + _tooltipInputValue: string = this.value.toString(); + _tooltipInputValueState: string = "None"; get formFormattedValue() { return this.value.toString(); @@ -102,6 +108,7 @@ class Slider extends SliderBase implements IFormInputElement { constructor() { super(); this._stateStorage.value = undefined; + this._lastValidInputValue = this.min.toString(); } /** @@ -116,6 +123,10 @@ class Slider extends SliderBase implements IFormInputElement { * */ onBeforeRendering() { + if (this.editableTooltip) { + this._updateInputValue(); + } + if (!this.isCurrentStateOutdated()) { return; } @@ -162,7 +173,7 @@ class Slider extends SliderBase implements IFormInputElement { _onmousedown(e: TouchEvent | MouseEvent) { // 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.step === 0) { + if (this.disabled || this.step === 0 || (e.target as HTMLElement).hasAttribute("ui5-input")) { return; } @@ -196,7 +207,7 @@ class Slider extends SliderBase implements IFormInputElement { } } - _onfocusout() { + _onfocusout(e: FocusEvent) { // Prevent focusout when the focus is getting set within the slider internal // element (on the handle), before the Slider' customElement itself is finished focusing if (this._isFocusing()) { @@ -208,7 +219,7 @@ class Slider extends SliderBase implements IFormInputElement { // value that was saved when it was first focused in this._valueInitial = undefined; - if (this.showTooltip) { + if (this.showTooltip && !(e.relatedTarget as HTMLInputElement)?.hasAttribute("ui5-input")) { this._tooltipVisibility = SliderBase.TOOLTIP_VISIBILITY.HIDDEN; } } @@ -218,6 +229,10 @@ class Slider extends SliderBase implements IFormInputElement { * @private */ _handleMove(e: TouchEvent | MouseEvent) { + if ((e.target as HTMLElement).hasAttribute("ui5-input")) { + return; + } + e.preventDefault(); // If step is 0 no interaction is available because there is no constant @@ -237,7 +252,11 @@ class Slider extends SliderBase implements IFormInputElement { /** Called when the user finish interacting with the slider * @private */ - _handleUp() { + _handleUp(e: TouchEvent | MouseEvent) { + if ((e.target as HTMLElement).hasAttribute("ui5-input")) { + return; + } + if (this._valueOnInteractionStart !== this.value) { this.fireEvent("change"); } @@ -246,6 +265,41 @@ class Slider extends SliderBase implements IFormInputElement { this._valueOnInteractionStart = undefined; } + _onInputFocusOut(e: FocusEvent) { + const tooltipInput = this.shadowRoot!.querySelector("ui5-input") as Input; + + this._tooltipVisibility = SliderBase.TOOLTIP_VISIBILITY.HIDDEN; + this._updateValueFromInput(e); + + if (!this._isInputValueValid) { + tooltipInput.value = this._lastValidInputValue; + this._isInputValueValid = true; + this._tooltipInputValueState = "None"; + } + } + + _updateInputValue() { + const tooltipInput = this.shadowRoot!.querySelector("ui5-input") as Input; + + if (!tooltipInput) { + return; + } + + this._isInputValueValid = parseFloat(tooltipInput.value) >= this.min && parseFloat(tooltipInput.value) <= this.max; + + if (!this._isInputValueValid) { + this._tooltipInputValue = this._lastValidInputValue; + this._isInputValueValid = true; + this._tooltipInputValueState = "Negative"; + + return; + } + + this._tooltipInputValue = this.value.toString(); + this._lastValidInputValue = this._tooltipInputValue; + this._tooltipInputValueState = "None"; + } + /** Determines if the press is over the handle * @private */ @@ -281,6 +335,10 @@ class Slider extends SliderBase implements IFormInputElement { } } + get inputValue() { + return this.value.toString(); + } + get styles() { return { progress: { @@ -321,6 +379,14 @@ class Slider extends SliderBase implements IFormInputElement { return Slider.i18nBundle.getText(SLIDER_ARIA_DESCRIPTION); } + get _ariaDescribedByInputText() { + return Slider.i18nBundle.getText(SLIDER_TOOLTIP_INPUT_DESCRIPTION); + } + + get _ariaLabelledByInputText() { + return Slider.i18nBundle.getText(SLIDER_TOOLTIP_INPUT_LABEL); + } + get tickmarksObject() { const count = this._tickmarksCount; const arr = []; diff --git a/packages/main/src/SliderBase.hbs b/packages/main/src/SliderBase.hbs index f4795d150338..da75fbd468a0 100644 --- a/packages/main/src/SliderBase.hbs +++ b/packages/main/src/SliderBase.hbs @@ -5,7 +5,7 @@ @mouseover="{{_onmouseover}}" @mouseout="{{_onmouseout}}" @keydown="{{_onkeydown}}" - @keyup="{{_onkeyup}}" + @keyup="{{_onKeyupBase}}" part="root-container" > {{> handlesAriaText}} @@ -37,11 +37,20 @@ {{> handles}}
- {{accessibleName}} - {{_ariaLabelledByText}} + {{#if accessibleName}} + {{accessibleName}} + {{/if}} + + {{_ariaLabelledByText}} + + {{#if editableTooltip}} + {{_ariaDescribedByInputText}} + {{_ariaLabelledByInputText}} + {{/if}} +
{{#*inline "handlesAriaText"}}{{/inline}} {{#*inline "progressBar"}}{{/inline}} -{{#*inline "handles"}}{{/inline}} +{{#*inline "handles"}}{{/inline}} \ No newline at end of file diff --git a/packages/main/src/SliderBase.ts b/packages/main/src/SliderBase.ts index c401cc6c2ae1..2dce681bc11b 100644 --- a/packages/main/src/SliderBase.ts +++ b/packages/main/src/SliderBase.ts @@ -8,7 +8,8 @@ import type { ResizeObserverCallback } from "@ui5/webcomponents-base/dist/delega import type { PassiveEventListenerObject } from "@ui5/webcomponents-base/dist/types.js"; import "@ui5/webcomponents-icons/dist/direction-arrows.js"; import { - isEscape, isHome, isEnd, isUp, isDown, isRight, isLeft, isUpCtrl, isDownCtrl, isRightCtrl, isLeftCtrl, isPlus, isMinus, isPageUp, isPageDown, + isEscape, isHome, isEnd, isUp, isDown, isRight, isLeft, isUpCtrl, isDownCtrl, isRightCtrl, isLeftCtrl, isPlus, isMinus, isPageUp, isPageDown, isF2, + isEnter, } from "@ui5/webcomponents-base/dist/Keys.js"; // Styles @@ -108,6 +109,18 @@ abstract class SliderBase extends UI5Element { @property({ type: Boolean }) showTooltip = false; + /** + * + * Indicates whether input fields should be used as tooltips for the handles. + * + * **Note:** Setting this option to true will only work if showTooltip is set to true. + * **Note:** In order for the component to comply with the accessibility standard, it is recommended to set the editableTooltip property to true. + * @default false + * @public + */ + @property({ type: Boolean }) + editableTooltip = false; + /** * Defines whether the slider is in disabled state. * @default false @@ -125,6 +138,12 @@ abstract class SliderBase extends UI5Element { @property() accessibleName?: string; + /** + * @private + */ + @property({ type: Number }) + value = 0; + /** * @private */ @@ -137,9 +156,12 @@ abstract class SliderBase extends UI5Element { @property({ type: Boolean }) _hiddenTickmarks = false; + @property({ type: Boolean }) + _isInputValueValid = false; + _resizeHandler: ResizeObserverCallback; _moveHandler: (e: TouchEvent | MouseEvent) => void; - _upHandler: () => void; + _upHandler: (e: TouchEvent | MouseEvent) => void; _stateStorage: StateStorage; _ontouchstart: PassiveEventListenerObject; notResized = false; @@ -150,6 +172,7 @@ abstract class SliderBase extends UI5Element { _oldMax?: number; _labelWidth = 0; _labelValues?: Array; + _valueOnInteractionStart?: number; async formElementAnchor() { return this.getFocusDomRefAsync(); @@ -180,12 +203,14 @@ abstract class SliderBase extends UI5Element { _handleMove(e: TouchEvent | MouseEvent) {} // eslint-disable-line - _handleUp() {} + _handleUp(e: TouchEvent | MouseEvent) {} // eslint-disable-line _onmousedown(e: TouchEvent | MouseEvent) {} // eslint-disable-line _handleActionKeyPress(e: Event) {} // eslint-disable-line + _updateInputValue() {} + // used in base template, but implemented in subclasses abstract styles: { label: object, @@ -281,11 +306,17 @@ abstract class SliderBase extends UI5Element { } _onkeydown(e: KeyboardEvent) { - if (this.disabled || this._effectiveStep === 0) { + const target = e.target as HTMLElement; + + if (isF2(e) && target.classList.contains("ui5-slider-handle")) { + (target.parentNode!.querySelector(".ui5-slider-handle-container ui5-input") as HTMLElement).focus(); + } + + if (this.disabled || this._effectiveStep === 0 || target.hasAttribute("ui5-slider-handle")) { return; } - if (SliderBase._isActionKey(e)) { + if (SliderBase._isActionKey(e) && target && !target.hasAttribute("ui5-input")) { e.preventDefault(); this._isUserInteraction = true; @@ -293,7 +324,42 @@ abstract class SliderBase extends UI5Element { } } - _onkeyup() { + _onInputKeydown(e: KeyboardEvent) { + const target = e.target as HTMLElement; + + if (isF2(e) && target.hasAttribute("ui5-input")) { + (target.parentNode!.parentNode!.querySelector(".ui5-slider-handle") as HTMLElement).focus(); + } + + if (isEnter(e)) { + this._updateInputValue(); + this._updateValueFromInput(e); + } + } + + _onInputChange() { + if (this._valueOnInteractionStart !== this.value) { + this.fireEvent("change"); + } + } + + _onInputInput() { + this.fireEvent("input"); + } + + _updateValueFromInput(e: Event) { + const input = e.target as HTMLInputElement; + const value = parseFloat(input.value); + this._isInputValueValid = value >= this._effectiveMin && value <= this._effectiveMax; + + if (!this._isInputValueValid) { + return; + } + + this.value = value; + } + + _onKeyupBase() { if (this.disabled) { return; } @@ -403,9 +469,10 @@ abstract class SliderBase extends UI5Element { * @private */ _handleFocusOnMouseDown(e: TouchEvent | MouseEvent) { - const focusedElement = this.shadowRoot!.activeElement; + const currentlyFocusedElement = this.shadowRoot!.activeElement; + const elementToBeFocused = e.target as HTMLElement; - if (!focusedElement || focusedElement !== e.target) { + if ((!currentlyFocusedElement || currentlyFocusedElement !== elementToBeFocused) && !elementToBeFocused.hasAttribute("ui5-input")) { this._preserveFocus(true); this.focusInnerElement(); } @@ -725,8 +792,20 @@ abstract class SliderBase extends UI5Element { return this.disabled ? "-1" : "0"; } - get _ariaLabelledByHandleRefs() { - return [`${this._id}-accName`, `${this._id}-sliderDesc`].join(" ").trim(); + get _ariaDescribedByHandleText() { + return this.editableTooltip ? "ui5-slider-accName ui5-slider-InputDesc" : undefined; + } + + get _ariaLabelledByHandleText() { + return this.accessibleName ? "ui5-slider-accName" : undefined; + } + + get _ariaDescribedByInputText() { + return ""; + } + + get _ariaLabelledByInputText() { + return ""; } } diff --git a/packages/main/src/i18n/messagebundle.properties b/packages/main/src/i18n/messagebundle.properties index a600a72135b3..eadb967ade56 100644 --- a/packages/main/src/i18n/messagebundle.properties +++ b/packages/main/src/i18n/messagebundle.properties @@ -498,6 +498,12 @@ MONTH_PICKER_DESCRIPTION = Month Picker #XACT: ARIA description for year picker YEAR_PICKER_DESCRIPTION = Year Picker +#XACT: ARIA description for slider tooltip input +SLIDER_TOOLTIP_INPUT_DESCRIPTION = Press F2 to enter a value + +#XACT: ARIA label for slider tooltip input +SLIDER_TOOLTIP_INPUT_LABEL = Current Value + #XTOL: tooltip for decrease button of the StepInput STEPINPUT_DEC_ICON_TITLE=Decrease diff --git a/packages/main/src/themes/ColorPicker.css b/packages/main/src/themes/ColorPicker.css index 7896930463ca..5bc5a98ae79b 100644 --- a/packages/main/src/themes/ColorPicker.css +++ b/packages/main/src/themes/ColorPicker.css @@ -62,9 +62,17 @@ width: 0.9375rem; height: 1.5rem; background: transparent; + box-sizing: border-box; +} + +[ui5-slider]::part(handle-container) { margin-inline-start: -2px; margin-top: var(--_ui5_color_picker_slider_handle_margin_top); - box-sizing: border-box; +} + +[ui5-slider]::part(handle-container):focus-within { + margin-inline-start: unset; + margin-top: unset; } [ui5-slider]::part(handle)::after { diff --git a/packages/main/src/themes/SliderBase.css b/packages/main/src/themes/SliderBase.css index c3bcb2053b9e..1e642f9ca3a2 100644 --- a/packages/main/src/themes/SliderBase.css +++ b/packages/main/src/themes/SliderBase.css @@ -100,9 +100,7 @@ background: var(--_ui5_slider_handle_background); border: var(--_ui5_slider_handle_border); border-radius: var(--_ui5_slider_handle_border_radius); - margin-inline-start: calc(-1 * var(--_ui5_slider_handle_width) / 2); - top: var(--_ui5_slider_handle_top); - position: absolute; + position: relative; outline: none; height: var(--_ui5_slider_handle_height); width: var(--_ui5_slider_handle_width); @@ -148,13 +146,17 @@ border: var(--_ui5_slider_handle_focus_border); } -.ui5-slider-tooltip { +.ui5-slider-handle-container { + position: absolute; + margin-inline-start: calc(-1 * var(--_ui5_slider_handle_width) / 2); + top: var(--_ui5_slider_handle_top); +} + +:host(:not([hidden])) .ui5-slider-handle-container .ui5-slider-tooltip { display: flex; justify-content: center; align-items: center; visibility: hidden; - pointer-events: none; - line-height: 1rem; position: absolute; left: 50%; transform: translate(-50%); @@ -171,6 +173,22 @@ box-sizing: var(--_ui5_slider_tooltip_border_box); } +:host(:not([hidden])):host([editable-tooltip]) .ui5-slider-handle-container .ui5-slider-tooltip { + border: none; + background: none; + box-shadow: none; +} + +:host([editable-tooltip]) .ui5-slider-tooltip { + padding: 0; + box-shadow: none; +} + +.ui5-slider-tooltip [ui5-input] { + width: 100%; + text-align: center; +} + .ui5-slider-tooltip-value { position: relative; display: flex; @@ -236,4 +254,4 @@ position: absolute; border-radius: var(--_ui5_slider_handle_border_radius); pointer-events: none; -} +} \ No newline at end of file diff --git a/packages/main/test/pages/RangeSlider.html b/packages/main/test/pages/RangeSlider.html index 57f1b929379b..94282d41d2b9 100644 --- a/packages/main/test/pages/RangeSlider.html +++ b/packages/main/test/pages/RangeSlider.html @@ -40,8 +40,8 @@

Range Slider with tickmarks

Disabled Range Slider

-

Range Slider with steps, tooltips, tickmarks and labels

- +

Range Slider with steps, input tooltips, tickmarks and labels

+
@@ -71,6 +71,7 @@

Event Testing Result Slider

diff --git a/packages/main/test/pages/Slider.html b/packages/main/test/pages/Slider.html index 28f27c13d6a6..caa84a7e9042 100644 --- a/packages/main/test/pages/Slider.html +++ b/packages/main/test/pages/Slider.html @@ -44,8 +44,8 @@

Slider with many steps and small width

Inactive slider with no steps and tooltip

-

Slider with steps, tooltips, tickmarks and labels

- +

Slider with steps, input tooltips, tickmarks and labels

+

Slider with float number step (1.25), tooltips, tickmarks and labels between 3 steps (3.75 value)

@@ -64,6 +64,7 @@

Event Testing Result Slider

diff --git a/packages/main/test/specs/RangeSlider.spec.js b/packages/main/test/specs/RangeSlider.spec.js index b836a929fdab..751b45314fe7 100644 --- a/packages/main/test/specs/RangeSlider.spec.js +++ b/packages/main/test/specs/RangeSlider.spec.js @@ -7,7 +7,7 @@ describe("Testing Range Slider interactions", () => { await browser.setWindowSize(1257, 2000); const rangeSlider = await browser.$("#range-slider-tickmarks"); - const startHandle = await rangeSlider.shadow$(".ui5-slider-handle--start"); + const startHandle = await rangeSlider.shadow$(".ui5-slider-handle-container"); assert.strictEqual((await startHandle.getAttribute("style")).replace(" ", ""), "left:0%;", "Initially if no value is set, the Range Slider start-handle is at the beginning of the Range Slider"); @@ -28,7 +28,7 @@ describe("Testing Range Slider interactions", () => { it("Changing the endValue is reflected", async () => { const rangeSlider = await browser.$("#range-slider-tickmarks"); - const endHandle = await rangeSlider.shadow$(".ui5-slider-handle--end"); + const endHandle = await rangeSlider.shadow$$(".ui5-slider-inner .ui5-slider-handle-container")[1]; assert.strictEqual((await endHandle.getAttribute("style")).replace(" ", ""), "left:50%;", "Range Slider end-handle is should be 50% from the start the Range Slider"); await rangeSlider.setProperty("endValue", 10); @@ -67,9 +67,6 @@ describe("Testing Range Slider interactions", () => { it("Dragging the selected range should change both values and handles", async () => { const rangeSlider = await browser.$("#range-slider-tickmarks"); - const startHandle = await rangeSlider.shadow$(".ui5-slider-handle--start"); - const endHandle = await rangeSlider.shadow$(".ui5-slider-handle--end"); - await rangeSlider.dragAndDrop({ x: 90, y: 1 }); assert.strictEqual(await rangeSlider.getProperty("startValue"), 8, "startValue should be 8"); @@ -132,8 +129,6 @@ describe("Range Slider elements - tooltip, step, tickmarks, labels", () => { const rangeSliderStartTooltipValue = await rangeSliderStartTooltip.shadow$(".ui5-slider-tooltip-value"); const rangeSliderEndTooltip = await rangeSlider.shadow$(".ui5-slider-tooltip--end"); const rangeSliderEndTooltipValue = await rangeSliderEndTooltip.shadow$(".ui5-slider-tooltip-value"); - const startHandle = await rangeSlider.shadow$(".ui5-slider-handle--start"); - const endHandle = await rangeSlider.shadow$(".ui5-slider-handle--end"); await rangeSlider.moveTo(); @@ -152,7 +147,258 @@ describe("Range Slider elements - tooltip, step, tickmarks, labels", () => { assert.strictEqual(await rangeSliderEndTooltipValue.getText(), "115", "Range Slider end tooltip should display value of 65"); }); + it("Tooltip input is displayed showing the current value", async () => { + const rangeSlider = await browser.$("#range-slider-tickmarks-labels"); + const rangeSliderStartTooltipInput = await rangeSlider.shadow$(".ui5-slider-tooltip--start ui5-input"); + const rangeSliderEndTooltipInput = await rangeSlider.shadow$(".ui5-slider-tooltip--end ui5-input"); + + await rangeSlider.setProperty("startValue", 8); + const rangeSliderStartTooltipInputValue = await rangeSliderStartTooltipInput.getProperty("value"); + + await rangeSlider.setProperty("endValue", 12); + const rangeSliderEndTooltipInputValue = await rangeSliderEndTooltipInput.getProperty("value"); + + assert.strictEqual(await rangeSliderStartTooltipInputValue, "8", "Slider input has the correct value"); + assert.strictEqual(await rangeSliderEndTooltipInputValue, "12", "Slider input has the correct value"); + }); + + it("Tooltip input should not be closed on focusout if input tooltip is clicked", async () => { + const rangeSlider = await browser.$("#range-slider-tickmarks-labels"); + const rangeSliderStartTooltipInput = await rangeSlider.shadow$(".ui5-slider-tooltip--start ui5-input"); + + await rangeSlider.click(); + assert.strictEqual(await rangeSlider.getProperty("_tooltipVisibility"), "visible", "Slider tooltip visibility property should be 'visible'"); + + await rangeSliderStartTooltipInput.click(); + + assert.strictEqual(await rangeSliderStartTooltipInput.getProperty("focused"), true, "The tooltip is not closed and the input is focused"); + }); + + it("Input tooltips value change should change the range slider's value", async () => { + const rangeSlider = await browser.$("#range-slider-tickmarks-labels"); + const rangeSliderStartTooltipInput = await rangeSlider.shadow$(".ui5-slider-tooltip--start ui5-input"); + const rangeSliderHandle = await rangeSlider.shadow$(".ui5-slider-handle--start"); + + await rangeSlider.setProperty("startValue", 4); + + await rangeSliderHandle.click(); + await rangeSliderStartTooltipInput.click(); + await rangeSliderStartTooltipInput.setProperty("value", "8"); + + await browser.keys("Enter"); + + assert.strictEqual(await rangeSlider.getProperty("startValue"), 8, "The input value is reflected in the slider"); + }); + + it("Input tooltip value change should fire change event", async () => { + const rangeSlider = await browser.$("#range-slider-tickmarks-labels"); + const rangeSliderStartTooltipInput = await rangeSlider.shadow$(".ui5-slider-tooltip--start ui5-input"); + const rangeSliderHandle = await rangeSlider.shadow$(".ui5-slider-handle--start"); + const eventResultSlider = await browser.$("#test-result-slider"); + + await rangeSlider.setProperty("startValue", 1); + + await rangeSliderHandle.click(); + await rangeSliderStartTooltipInput.click(); + await eventResultSlider.setProperty("endValue", 2); + await browser.keys("2"); + await browser.keys("Enter"); + + assert.strictEqual(await eventResultSlider.getProperty("endValue") , 4, "'change' and 'input' events are fired"); + }); + + it("Input tooltips value state should change to 'Negative' if value is invalid", async () => { + const rangeSlider = await browser.$("#range-slider-tickmarks-labels"); + const rangeSliderStartTooltipInput = await rangeSlider.shadow$(".ui5-slider-tooltip--start ui5-input"); + const rangeSliderHandle = await rangeSlider.shadow$(".ui5-slider-handle--start"); + + await rangeSlider.setProperty("startValue", 1); + + await rangeSliderHandle.click(); + await rangeSliderStartTooltipInput.click(); + await browser.keys(["2", "3"]); + + await browser.keys("Enter"); + + assert.strictEqual(await rangeSliderStartTooltipInput.getProperty("valueState"), "Negative", "The input value state is negative when the value is invalid"); + }); + + it("Input tooltip should become hidden when input is looses focus", async () => { + const rangeSlider = await browser.$("#range-slider-tickmarks-labels"); + const anotherSlider = await browser.$("#basic-range-slider"); + const rangeSliderStartTooltipInput = await rangeSlider.shadow$(".ui5-slider-tooltip--start ui5-input"); + + await rangeSlider.click(); + await rangeSliderStartTooltipInput.click(); + + assert.strictEqual(await rangeSlider.getProperty("_tooltipVisibility"), "visible", "Slider tooltip visibility property should be 'visible'"); + + await anotherSlider.click(); + + assert.strictEqual(await rangeSlider.getProperty("_tooltipVisibility"), "hidden", "Slider tooltip visibility property should be 'visible'"); + }); + + it("F2 should switch the focus between the handle and the tooltip input", async () => { + const rangeSlider = await browser.$("#range-slider-tickmarks-labels"); + const rangeSliderStartTooltipInput = await rangeSlider.shadow$(".ui5-slider-tooltip--start ui5-input"); + const rangeSliderHandle = await rangeSlider.shadow$(".ui5-slider-handle--start"); + + + await rangeSliderHandle.click(); + await browser.keys("F2"); + + assert.strictEqual(await rangeSliderStartTooltipInput.getProperty("focused"), true, "Slider tooltip is focused on F2"); + + await browser.keys("F2"); + + const isHandleFocused = await browser.executeAsync(done => { + const focusedElement = document.getElementById("range-slider-tickmarks-labels").shadowRoot.activeElement; + const isHandleFocused = focusedElement.classList.contains("ui5-slider-handle"); + done(isHandleFocused); + }); + + assert.strictEqual(await isHandleFocused, true, "Slider tooltip visibility property should be 'visible'"); + }); + + it("Arrow up/down should not increase/decrease the value of the input", async () => { + const rangeSlider = await browser.$("#range-slider-tickmarks-labels"); + const rangeSliderStartTooltipInput = await rangeSlider.shadow$(".ui5-slider-tooltip--start ui5-input"); + const rangeSliderStartHandle = await rangeSlider.shadow$(".ui5-slider-handle--start"); + const rangeSliderEndHandle = await rangeSlider.shadow$(".ui5-slider-handle--start"); + const rangeSliderEndTooltipInput = await rangeSlider.shadow$(".ui5-slider-tooltip--end ui5-input"); + + await rangeSlider.setProperty("startValue", 1); + await rangeSliderStartHandle.click(); + await rangeSliderStartTooltipInput.click(); + + await browser.keys("ArrowUp"); + assert.strictEqual(await rangeSlider.getProperty("startValue"), 1, "The start value is not changed on arrow up"); + + await browser.keys("ArrowDown"); + assert.strictEqual(await rangeSlider.getProperty("startValue"), 1, "The start value is not changed on arrow down"); + + await rangeSlider.setProperty("endValue", 10); + await rangeSliderEndHandle.click(); + await rangeSliderEndTooltipInput.click(); + + await browser.keys("ArrowUp"); + assert.strictEqual(await rangeSlider.getProperty("endValue"), 10, "The end value is not changed on arrow up"); + + await browser.keys("ArrowDown"); + assert.strictEqual(await rangeSlider.getProperty("endValue"), 10, "The end value is not changed on arrow down"); + }); + + it("Tab on slider handle should not move the focus to the tooltip input", async () => { + const rangeSlider = await browser.$("#range-slider-tickmarks-labels"); + const rangeSliderStartTooltipInput = await rangeSlider.shadow$(".ui5-slider-tooltip--start ui5-input"); + const rangeSliderEndTooltipInput = await rangeSlider.shadow$(".ui5-slider-tooltip--end ui5-input"); + + assert.strictEqual(await rangeSliderStartTooltipInput.getAttribute("tabindex"), "-1", "The tooltip input is not tabbable"); + assert.strictEqual(await rangeSliderEndTooltipInput.getAttribute("tabindex"), "-1", "The tooltip input is not tabbable"); + }); + + it("Focus out with invalid value should reset it", async () => { + const rangeSlider = await browser.$("#range-slider-tickmarks-labels"); + const rangeSliderStartHandle = await rangeSlider.shadow$(".ui5-slider-handle--start"); + const rangeSliderStartTooltipInput = await rangeSlider.shadow$(".ui5-slider-tooltip--start ui5-input"); + const nextSlider = await browser.$("#test-slider"); + + await rangeSlider.setProperty("startValue", 2); + await rangeSliderStartHandle.click(); + await rangeSliderStartTooltipInput.click(); + await browser.keys(["2", "3"]); + + await nextSlider.click(); + assert.strictEqual(await rangeSliderStartTooltipInput.getProperty("value"), "2", "Value is reset to the last valid one"); + }); + + it("Input values should be swapped if the start value is bigger than the end value", async () => { + const rangeSlider = await browser.$("#range-slider-tickmarks-labels"); + const rangeSliderStartHandle = await rangeSlider.shadow$(".ui5-slider-handle--start"); + const rangeSliderEndHandle = await rangeSlider.shadow$(".ui5-slider-handle--end"); + + const rangeSliderEndTooltipInput = await rangeSlider.shadow$(".ui5-slider-tooltip--end ui5-input"); + const rangeSliderStartTooltipInput = await rangeSlider.shadow$(".ui5-slider-tooltip--start ui5-input"); + + await rangeSlider.setProperty("startValue", 0); + await rangeSlider.setProperty("endValue", 1); + + await rangeSliderStartHandle.click(); + await rangeSliderStartTooltipInput.click(); + await browser.keys("2"); + await browser.keys("Enter"); + + assert.strictEqual(await rangeSlider.getProperty("endValue"), 20, "The start value is now end value"); + assert.strictEqual(await rangeSliderEndTooltipInput.getProperty("value"), "20", "The start input value is now end value"); + + await rangeSliderEndHandle.click(); + await rangeSliderEndTooltipInput.click(); + await rangeSliderEndTooltipInput.setProperty("value", "3"); + + await browser.keys("Enter"); + + assert.strictEqual(await rangeSlider.getProperty("endValue"), 3, "Slider value is changed on a followup input after initial swap interaction"); + + await browser.keys("ArrowDown"); + await browser.keys("Enter"); + + await browser.keys("ArrowDown"); + await browser.keys("Enter"); + + await browser.keys("ArrowDown"); + await browser.keys("Enter"); + + assert.strictEqual(await rangeSlider.getProperty("endValue"), 1, "Slider value is changed on a followup keyboard actions after initial swap interaction"); + assert.strictEqual(await rangeSlider.getProperty("startValue"), 0, "Slider value is changed on a followup keyboard actions after initial swap interaction"); + + assert.strictEqual(await rangeSliderEndTooltipInput.getProperty("value"), "1", "Slider end value is changed on a followup keyboard actions after initial swap interaction"); + assert.strictEqual(await rangeSliderStartTooltipInput.getProperty("value"), "0", "Slider start value is changed on a followup keyboard actions after initial swap interaction"); + }); + + + it("Input values should be swapped if the end value is lower than the start value", async () => { + await browser.url(`test/pages/RangeSlider.html`); + + const rangeSlider = await browser.$("#range-slider-tickmarks-labels"); + const rangeSliderEndHandle = await rangeSlider.shadow$(".ui5-slider-handle--end"); + + const rangeSliderEndTooltipInput = await rangeSlider.shadow$(".ui5-slider-tooltip--end ui5-input"); + const rangeSliderStartTooltipInput = await rangeSlider.shadow$(".ui5-slider-tooltip--start ui5-input"); + + await rangeSlider.setProperty("startValue", 2); + await rangeSlider.setProperty("endValue", 3); + + await rangeSliderEndHandle.click(); + await rangeSliderEndTooltipInput.click(); + await browser.keys("Delete"); + await browser.keys("1"); + await browser.keys("Enter"); + + assert.strictEqual(await rangeSlider.getProperty("startValue"), 1, "The end value is now start value"); + assert.strictEqual(await rangeSliderStartTooltipInput.getProperty("value"), "1", "The end input value is now start value"); + }); + + it("Invalid tooltip value should not be changed on 'Enter'", async () => { + const rangeSlider = await browser.$("#range-slider-tickmarks-labels"); + const rangeSliderTooltipInput = await rangeSlider.shadow$(".ui5-slider-tooltip--end ui5-input"); + const rangeSliderHandle = await rangeSlider.shadow$(".ui5-slider-handle--end"); + + await rangeSlider.setProperty("endValue", 12); + + await rangeSliderHandle.click(); + await rangeSliderTooltipInput.click(); + await rangeSliderTooltipInput.setProperty("value", "60"); + + await browser.keys("Enter"); + + assert.strictEqual(await rangeSlider.getProperty("endValue"), 12, "The slider's value is not changed when invalid"); + assert.strictEqual(await rangeSliderTooltipInput.getProperty("valueState"), "Negative", "The input value is not changed when invalid"); + assert.strictEqual(await rangeSliderTooltipInput.getProperty("value"), "60", "The input value is not changed when invalid"); + }); + it("Range Slider tooltips should become visible when range slider is focused", async () => { + await browser.url(`test/pages/RangeSlider.html`); + const rangeSlider = await browser.$("#basic-range-slider-with-tooltip"); const rangeSliderStartTooltip = await rangeSlider.shadow$(".ui5-slider-tooltip--start"); const rangeSliderEndTooltip = await rangeSlider.shadow$(".ui5-slider-tooltip--end"); @@ -283,6 +529,9 @@ describe("Testing events", () => { const rangeSlider = await browser.$("#test-slider"); const eventResultRangeSlider = await browser.$("#test-result-slider"); + await eventResultRangeSlider.setProperty("startValue", 1); + await eventResultRangeSlider.setProperty("endValue", 2); + await rangeSlider.click(); assert.strictEqual(await eventResultRangeSlider.getProperty("endValue") , 4, "Both input event and change event are fired after user interaction"); @@ -359,7 +608,7 @@ describe("Accessibility", async () => { const rangeSliderId = await rangeSlider.getProperty("_id"); assert.strictEqual(await rangeSliderProgressBar.getAttribute("aria-labelledby"), - `${rangeSliderId}-accName ${rangeSliderId}-sliderDesc`, "aria-labelledby is set correctly"); + "ui5-slider-sliderDesc", "aria-labelledby is set correctly"); assert.strictEqual(await rangeSliderProgressBar.getAttribute("aria-valuemin"), `${await rangeSlider.getProperty("min")}`, "aria-valuemin is set correctly"); assert.strictEqual(await rangeSliderProgressBar.getAttribute("aria-valuemax"), @@ -375,7 +624,7 @@ describe("Accessibility", async () => { const rangeSliderId = await rangeSlider.getProperty("_id"); assert.strictEqual(await startHandle.getAttribute("aria-labelledby"), - `${rangeSliderId}-accName ${rangeSliderId}-startHandleDesc`, "aria-labelledby is set correctly"); + "ui5-slider-startHandleDesc", "aria-labelledby is set correctly"); assert.strictEqual(await startHandle.getAttribute("aria-valuemin"), `${await rangeSlider.getProperty("min")}`, "aria-valuemin is set correctly"); assert.strictEqual(await startHandle.getAttribute("aria-valuemax"), @@ -390,7 +639,7 @@ describe("Accessibility", async () => { const rangeSliderId = await rangeSlider.getProperty("_id"); assert.strictEqual(await endHandle.getAttribute("aria-labelledby"), - `${rangeSliderId}-accName ${rangeSliderId}-endHandleDesc`, "aria-labelledby is set correctly"); + "ui5-slider-endHandleDesc", "aria-labelledby is set correctly"); assert.strictEqual(await endHandle.getAttribute("aria-valuemin"), `${await rangeSlider.getProperty("min")}`, "aria-valuemin is set correctly"); assert.strictEqual(await endHandle.getAttribute("aria-valuemax"), @@ -399,12 +648,20 @@ describe("Accessibility", async () => { `${await rangeSlider.getProperty("endValue")}`, "aria-valuenow is set correctly"); }); + it("Aria attributes are set correctly to the tooltip input", async () => { + const rangeSlider = await browser.$("#range-slider-tickmarks-labels"); + const rangeSliderStartTooltipInput = await rangeSlider.shadow$(".ui5-slider-tooltip--start ui5-input"); + + assert.strictEqual(await rangeSliderStartTooltipInput.getAttribute("accessible-name-ref"), + "ui5-slider-InputLabel"); + }); + it("Aria-labelledby text is mapped correctly when values are swapped", async () => { const rangeSlider = await browser.$("#range-slider-tickmarks"); const rangeSliderId = await rangeSlider.getProperty("_id"); const startHandle = await rangeSlider.shadow$(".ui5-slider-handle--start"); - const rangeSliderStartHandleSpan = await rangeSlider.shadow$(`#${rangeSliderId}-startHandleDesc`); - const rangeSliderEndHandleSpan = await rangeSlider.shadow$(`#${rangeSliderId}-endHandleDesc`); + const rangeSliderStartHandleSpan = await rangeSlider.shadow$(`#ui5-slider-startHandleDesc`); + const rangeSliderEndHandleSpan = await rangeSlider.shadow$(`#ui5-slider-endHandleDesc`); await rangeSlider.setProperty("endValue", 9); await startHandle.dragAndDrop({ x: 100, y: 1 }); @@ -956,8 +1213,8 @@ describe("Accessibility: Testing keyboard handling", async () => { describe("Testing resize handling and RTL support", () => { it("Testing RTL support", async () => { const rangeSlider = await browser.$("#range-slider-tickmarks-labels"); - const startHandle = await rangeSlider.shadow$(".ui5-slider-handle--start"); - const endHandle = await rangeSlider.shadow$(".ui5-slider-handle--end"); + const startHandle = await rangeSlider.shadow$(".ui5-slider-handle-container"); + const endHandle = await rangeSlider.shadow$$(".ui5-slider-inner .ui5-slider-handle-container")[1]; await rangeSlider.setAttribute("dir", "rtl"); await rangeSlider.setProperty("min", 0); @@ -995,12 +1252,16 @@ describe("Testing resize handling and RTL support", () => { }); it("Testing RTL KBH support", async () => { + await browser.url(`test/pages/RangeSlider.html`); + const rangeSlider = await browser.$("#range-slider-tickmarks-labels"); - const startHandle = await rangeSlider.shadow$(".ui5-slider-handle--start"); - const endHandle = await rangeSlider.shadow$(".ui5-slider-handle--end"); + const startHandle = await rangeSlider.shadow$(".ui5-slider-handle-container"); + const endHandle = await rangeSlider.shadow$$(".ui5-slider-inner .ui5-slider-handle-container")[1]; const rangeSliderSelection = await rangeSlider.shadow$(".ui5-slider-progress"); await rangeSlider.setAttribute("dir", "rtl"); + await rangeSlider.scrollIntoView(); + await rangeSlider.setProperty("min", 0); await rangeSlider.setProperty("max", 10); await rangeSlider.setProperty("step", 1); diff --git a/packages/main/test/specs/Slider.spec.js b/packages/main/test/specs/Slider.spec.js index 85f22efbf0f3..8faa2d6afbed 100644 --- a/packages/main/test/specs/Slider.spec.js +++ b/packages/main/test/specs/Slider.spec.js @@ -6,33 +6,34 @@ describe("Slider basic interactions", () => { await browser.url(`test/pages/Slider.html`); const slider = await browser.$("#basic-slider"); + const sliderHandleContainer = await slider.shadow$(".ui5-slider-handle-container"); const sliderHandle = await slider.shadow$(".ui5-slider-handle"); - assert.strictEqual((await sliderHandle.getCSSProperty("left")).value, "0px", "Initially if no value is set, the Slider handle is at the beginning of the Slider"); + assert.strictEqual((await sliderHandleContainer.getCSSProperty("left")).value, "0px", "Initially if no value is set, the Slider handle is at the beginning of the Slider"); await browser.setWindowSize(1257, 2000); await slider.setProperty("value", 3); - assert.strictEqual(await sliderHandle.getAttribute("style"), "left: 30%;", "Slider handle should be 30% from the start"); + assert.strictEqual(await sliderHandleContainer.getAttribute("style"), "left: 30%;", "Slider handle should be 30% from the start"); await slider.click(); - assert.strictEqual(await sliderHandle.getAttribute("style"), "left: 50%;", "Slider handle should be in the middle of the slider"); + assert.strictEqual(await sliderHandleContainer.getAttribute("style"), "left: 50%;", "Slider handle should be in the middle of the slider"); assert.strictEqual(await slider.getProperty("value"), 5, "Slider current value should be 5"); await sliderHandle.dragAndDrop({ x: 300, y: 1 }); - assert.strictEqual(await sliderHandle.getAttribute("style"), "left: 80%;", "Slider handle should be 80% from the start of the slider"); + assert.strictEqual(await sliderHandleContainer.getAttribute("style"), "left: 80%;", "Slider handle should be 80% from the start of the slider"); assert.strictEqual(await slider.getProperty("value"), 8, "Slider current value should be 8"); await sliderHandle.dragAndDrop({ x: 100, y: 1 }); - assert.strictEqual(await sliderHandle.getAttribute("style"), "left: 90%;", "Slider handle should be 90% from the start"); + assert.strictEqual(await sliderHandleContainer.getAttribute("style"), "left: 90%;", "Slider handle should be 90% from the start"); assert.strictEqual(await slider.getProperty("value"), 9, "Slider current value should be 9"); await sliderHandle.dragAndDrop({ x:-100, y: 1 }); - assert.strictEqual(await sliderHandle.getAttribute("style"), "left: 80%;", "Slider handle should be at the end of the slider and not beyond its boundaries"); + assert.strictEqual(await sliderHandleContainer.getAttribute("style"), "left: 80%;", "Slider handle should be at the end of the slider and not beyond its boundaries"); assert.strictEqual(await slider.getProperty("value"), 8, "Slider current value should be 8"); }); @@ -157,6 +158,16 @@ describe("Slider elements - tooltip, step, tickmarks, labels", () => { assert.strictEqual(await sliderTooltipValue.getText(), "2", "Slider tooltip should display value of 2"); }); + it("Tooltip input is displayed showing the current value", async () => { + const slider = await browser.$("#slider-tickmarks-labels"); + const sliderTooltipInput = await slider.shadow$(".ui5-slider-tooltip ui5-input"); + + await slider.setProperty("value", 8); + const sliderTooltipInputValue = await sliderTooltipInput.getProperty("value"); + + assert.strictEqual(await sliderTooltipInputValue, "8", "Slider input has the correct value"); + }); + it("Slider Tooltip should become visible when slider is focused", async () => { const slider = await browser.$("#basic-slider-with-tooltip"); const sliderTooltip = await slider.shadow$(".ui5-slider-tooltip"); @@ -175,6 +186,134 @@ describe("Slider elements - tooltip, step, tickmarks, labels", () => { assert.strictEqual((await sliderTooltip.getCSSProperty("visibility")).value, "visible", "Slider tooltip should be shown"); }); + it("Slider Tooltip should not be closed on focusout if input tooltip is clicked", async () => { + const slider = await browser.$("#slider-tickmarks-labels"); + const sliderTooltipInput = await slider.shadow$(".ui5-slider-tooltip ui5-input"); + + await slider.click(); + assert.strictEqual(await slider.getProperty("_tooltipVisibility"), "visible", "Slider tooltip visibility property should be 'visible'"); + + await sliderTooltipInput.click(); + + assert.strictEqual(await sliderTooltipInput.getProperty("focused"), true, "The tooltip is not closed and the input is focused"); + }); + + it("Input tooltip value change should change the slider's value", async () => { + const slider = await browser.$("#slider-tickmarks-labels"); + const sliderTooltipInput = await slider.shadow$(".ui5-slider-tooltip ui5-input"); + const sliderHandle = await slider.shadow$(".ui5-slider-handle"); + + await slider.setProperty("value", 1); + + await sliderHandle.click(); + await sliderTooltipInput.click(); + await browser.keys(["2"]); + await browser.keys("Enter"); + + assert.strictEqual(await slider.getProperty("value"), 21, "The input value is reflected in the slider"); + }); + + it("Input tooltip value change should fire change event", async () => { + const slider = await browser.$("#slider-tickmarks-labels"); + const sliderTooltipInput = await slider.shadow$(".ui5-slider-tooltip ui5-input"); + const sliderHandle = await slider.shadow$(".ui5-slider-handle"); + const eventResultSlider = await browser.$("#test-result-slider"); + + await slider.setProperty("value", 1); + await eventResultSlider.setProperty("value", 1); + + await sliderHandle.click(); + await sliderTooltipInput.click(); + await browser.keys(["2"]); + await browser.keys("Enter"); + + assert.strictEqual(await slider.getProperty("value"), 21, "The input value is reflected in the slider"); + assert.strictEqual(await eventResultSlider.getProperty("value") , 3, "'input' and 'change' events are fired on input 'change' and 'input' events"); + }); + + it("Input tooltip should change the value state to error if it is invalid", async () => { + const slider = await browser.$("#slider-tickmarks-labels"); + const sliderTooltipInput = await slider.shadow$(".ui5-slider-tooltip ui5-input"); + const sliderHandle = await slider.shadow$(".ui5-slider-handle"); + + await slider.setProperty("value", 16); + await sliderTooltipInput.setProperty("value", ""); + + await sliderHandle.click(); + await sliderTooltipInput.click(); + await browser.keys(["1", "2", "3"]); + await browser.keys("Enter"); + + assert.strictEqual(await sliderTooltipInput.getProperty("valueState"), "Negative", "Value state is changed to negative when the value is invalid"); + }); + + it("F2 should switch the focus between the handle and the tooltip input", async () => { + await browser.url(`test/pages/Slider.html`); + + const slider = await browser.$("#slider-tickmarks-labels"); + const sliderTooltipInput = await slider.shadow$(".ui5-slider-tooltip ui5-input"); + const sliderHandle = await slider.shadow$(".ui5-slider-handle"); + + await sliderHandle.click(); + await browser.keys("F2"); + + assert.strictEqual(await sliderTooltipInput.getProperty("focused"), true, "The focus is on the input"); + + await browser.keys("F2"); + + const isHandleFocused = await browser.executeAsync(done => { + const focusedElement = document.getElementById("slider-tickmarks-labels").shadowRoot.activeElement; + const isHandleFocused = focusedElement.classList.contains("ui5-slider-handle"); + done(isHandleFocused); + }); + + assert.strictEqual(isHandleFocused, true, "The focus is on the handle"); + }); + + it("Arrow up/down should not increase/decrease the value of the input", async () => { + const slider = await browser.$("#slider-tickmarks-labels"); + const sliderTooltipInput = await slider.shadow$(".ui5-slider-tooltip ui5-input"); + const sliderHandle = await slider.shadow$(".ui5-slider-handle"); + + await slider.setProperty("value", 1); + await sliderHandle.click(); + await sliderTooltipInput.click(); + + await browser.keys("ArrowUp"); + + assert.strictEqual(await slider.getProperty("value"), 1, "The value is not changed on arrow up"); + + await browser.keys("ArrowDown"); + + assert.strictEqual(await slider.getProperty("value"), 1, "The value is not changed on arrow down"); + }); + + it("Tab on slider handle should not move the focus to the tooltip input", async () => { + const slider = await browser.$("#slider-tickmarks-labels"); + const sliderHandle = await slider.shadow$(".ui5-slider-handle"); + const nextSlider = await browser.$("#slider-tickmarks-tooltips-labels"); + + await sliderHandle.click(); + await browser.keys("Tab"); + + assert.strictEqual(await nextSlider.isFocused(), true, "The next component is focused and not the tooltip input"); + }); + + it("Focus out with invalid value should reset it", async () => { + const slider = await browser.$("#slider-tickmarks-labels"); + const sliderHandle = await slider.shadow$(".ui5-slider-handle"); + const sliderTooltipInput = await slider.shadow$(".ui5-slider-tooltip ui5-input"); + const nextSlider = await browser.$("#slider-tickmarks-tooltips-labels"); + + await slider.setProperty("value", 10); + await sliderHandle.click(); + await sliderTooltipInput.click(); + await browser.keys(["1", "2", "3"]); + + await nextSlider.click(); + assert.strictEqual(await sliderTooltipInput.getProperty("value"), "10", "Value is reset to the last valid one"); + }); + it("Slider Tooltip should stay visible when slider is focused and mouse moves away", async () => { const slider = await browser.$("#basic-slider-with-tooltip"); const sliderTooltip = await slider.shadow$(".ui5-slider-tooltip"); @@ -210,6 +349,21 @@ describe("Slider elements - tooltip, step, tickmarks, labels", () => { assert.strictEqual((await sliderTooltip.getCSSProperty("visibility")).value, "hidden", "Slider tooltip should be shown"); }); + it("Input tooltip should become hidden when input is looses focus", async () => { + const slider = await browser.$("#slider-tickmarks-labels"); + const anotherSlider = await browser.$("#basic-slider"); + const sliderTooltipInput = await slider.shadow$(".ui5-slider-tooltip ui5-input"); + + await slider.click(); + await sliderTooltipInput.click(); + + assert.strictEqual(await slider.getProperty("_tooltipVisibility"), "visible", "Slider tooltip visibility property should be 'visible'"); + + await anotherSlider.click(); + + assert.strictEqual(await slider.getProperty("_tooltipVisibility"), "hidden", "Slider tooltip visibility property should be 'visible'"); + }); + it("Slider have correct number of labels and tickmarks based on the defined step and labelInterval properties", async () => { const slider = await browser.$("#slider-tickmarks-tooltips-labels"); const labelsContainer = await slider.shadow$(".ui5-slider-labels"); @@ -233,6 +387,7 @@ describe("Testing events", () => { const slider = await browser.$("#test-slider"); const eventResultSlider = await browser.$("#test-result-slider"); + await eventResultSlider.setProperty("value", 1); await slider.click(); assert.strictEqual(await eventResultSlider.getProperty("value") , 3, "Both input event and change event are fired after user interaction"); @@ -252,10 +407,9 @@ describe("Accessibility", async () => { it("Aria attributes are set correctly", async () => { const slider = await browser.$("#basic-slider"); const sliderHandle = await slider.shadow$(".ui5-slider-handle"); - const sliderId = await slider.getProperty("_id"); assert.strictEqual(await sliderHandle.getAttribute("aria-labelledby"), - `${sliderId}-accName ${sliderId}-sliderDesc`, "aria-labelledby is set correctly"); + "ui5-slider-accName ui5-sliderDesc", "aria-labelledby is set correctly"); assert.strictEqual(await sliderHandle.getAttribute("aria-valuemin"), `${await slider.getProperty("min")}`, "aria-valuemin is set correctly"); assert.strictEqual(await sliderHandle.getAttribute("aria-valuemax"), @@ -264,6 +418,15 @@ describe("Accessibility", async () => { `${await slider.getProperty("value")}`, "aria-valuenow is set correctly"); }); + it("Aria attributes are set correctly to the tooltip input", async () => { + const slider = await browser.$("#slider-tickmarks-labels"); + const sliderHandle = await slider.shadow$(".ui5-slider-handle"); + const sliderTooltipInput = await slider.shadow$(".ui5-slider-tooltip ui5-input"); + + assert.strictEqual(await sliderTooltipInput.getAttribute("accessible-name-ref"), + "ui5-slider-InputLabel"); + }); + it("Click anywhere in the Slider should focus the Slider's handle", async () => { await browser.url(`test/pages/Slider.html`); @@ -444,49 +607,50 @@ describe("Testing resize handling and RTL support", () => { it("Testing RTL support", async () => { const slider = await browser.$("#basic-slider-rtl"); const sliderHandle = await slider.shadow$(".ui5-slider-handle"); + const sliderHandleContainer = await slider.shadow$(".ui5-slider-handle-container"); - assert.strictEqual((await sliderHandle.getCSSProperty("right")).value, "0px", "Initially if no value is set, the Slider handle is at the right of the Slider"); + assert.strictEqual((await sliderHandleContainer.getCSSProperty("right")).value, "0px", "Initially if no value is set, the Slider handle is at the right of the Slider"); await slider.setProperty("value", 3); - assert.strictEqual(await sliderHandle.getAttribute("style"), "right: 30%;", "Slider handle should be 30% from the right"); + assert.strictEqual(await sliderHandleContainer.getAttribute("style"), "right: 30%;", "Slider handle should be 30% from the right"); await slider.click(); - assert.strictEqual(await sliderHandle.getAttribute("style"), "right: 50%;", "Slider handle should be in the middle of the slider"); + assert.strictEqual(await sliderHandleContainer.getAttribute("style"), "right: 50%;", "Slider handle should be in the middle of the slider"); assert.strictEqual(await slider.getProperty("value"), 5, "Slider current value should be 5"); await sliderHandle.dragAndDrop({ x: -300, y: 1 }); - assert.strictEqual(await sliderHandle.getAttribute("style"), "right: 80%;", "Slider handle should be 80% from the right of the slider"); + assert.strictEqual(await sliderHandleContainer.getAttribute("style"), "right: 80%;", "Slider handle should be 80% from the right of the slider"); assert.strictEqual(await slider.getProperty("value"), 8, "Slider current value should be 8"); await sliderHandle.dragAndDrop({ x: -100, y: 1 }); - assert.strictEqual(await sliderHandle.getAttribute("style"), "right: 90%;", "Slider handle should be 90% from the right"); + assert.strictEqual(await sliderHandleContainer.getAttribute("style"), "right: 90%;", "Slider handle should be 90% from the right"); assert.strictEqual(await slider.getProperty("value"), 9, "Slider current value should be 9"); await sliderHandle.dragAndDrop({ x: -150, y: 1 }); - assert.strictEqual(await sliderHandle.getAttribute("style"), "right: 100%;", "Slider handle should be at the left of the slider and not beyond its boundaries"); + assert.strictEqual(await sliderHandleContainer.getAttribute("style"), "right: 100%;", "Slider handle should be at the left of the slider and not beyond its boundaries"); assert.strictEqual(await slider.getProperty("value"), 10, "Slider current value should be 10"); }); it("Testing RTL KBH support", async () => { const slider = await browser.$("#basic-slider-rtl"); - const sliderHandle = await slider.shadow$(".ui5-slider-handle"); + const sliderHandleContainer = await slider.shadow$(".ui5-slider-handle-container"); await slider.setProperty("value", 0); - assert.strictEqual((await sliderHandle.getCSSProperty("right")).value, "0px", "Initially if no value is set, the Slider handle is at the right of the Slider"); + assert.strictEqual((await sliderHandleContainer.getCSSProperty("right")).value, "0px", "Initially if no value is set, the Slider handle is at the right of the Slider"); await slider.keys("ArrowLeft"); await slider.keys("ArrowLeft"); - assert.strictEqual(await sliderHandle.getAttribute("style"), "right: 20%;", "Slider handle should be 20% from the right of the slider"); + assert.strictEqual(await sliderHandleContainer.getAttribute("style"), "right: 20%;", "Slider handle should be 20% from the right of the slider"); assert.strictEqual(await slider.getProperty("value"), 2, "Slider current value should be 2"); await slider.keys("ArrowRight"); - assert.strictEqual(await sliderHandle.getAttribute("style"), "right: 10%;", "Slider handle should be 10% from the right of the slider"); + assert.strictEqual(await sliderHandleContainer.getAttribute("style"), "right: 10%;", "Slider handle should be 10% from the right of the slider"); assert.strictEqual(await slider.getProperty("value"), 1, "Slider current value should be 1"); });