Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
32a1249
feat(ui5-range-slider): refactor ui5-range-slider component
ndeshev Mar 23, 2026
e440176
Merge branch 'main' into range-slider-refactoring
ndeshev Mar 23, 2026
f0189fc
Merge branch 'main' into range-slider-refactoring
ndeshev Mar 30, 2026
7500c73
feat(ui5-range-slider): refactor ui5-range-slider component
ndeshev Mar 31, 2026
493960a
feat(ui5-range-slider): refactor ui5-range-slider component
ndeshev Mar 31, 2026
a6b9496
feat(ui5-range-slider): refactor ui5-range-slider component
ndeshev Apr 8, 2026
73dce64
refactor(ui5-range-slider): refactor ui5-range-slider component
ndeshev Apr 23, 2026
4d1cbee
Merge branch 'main' into range-slider-refactoring
ndeshev Apr 23, 2026
7720ce6
refactor(ui5-range-slider): refactor ui5-range-slider component
ndeshev Apr 23, 2026
9d893c4
refactor(ui5-range-slider): refactor ui5-range-slider component
ndeshev Apr 23, 2026
4dac128
refactor(ui5-range-slider): refactor ui5-range-slider component
ndeshev Apr 23, 2026
50cae53
refactor(ui5-range-slider): refactor ui5-range-slider component
ndeshev Apr 23, 2026
c399768
refactor(ui5-range-slider): refactor ui5-range-slider component
ndeshev Apr 23, 2026
3e6cbb9
Merge branch 'main' into range-slider-refactoring
ndeshev Apr 23, 2026
af680a8
refactor(ui5-range-slider): refactor ui5-range-slider component
ndeshev Apr 27, 2026
f952efb
Merge branch 'range-slider-refactoring' of https://github.com/SAP/ui5…
ndeshev Apr 27, 2026
66e9342
Merge branch 'main' into range-slider-refactoring
ndeshev Apr 27, 2026
a0187d1
refactor(ui5-range-slider): refactor ui5-range-slider component
ndeshev Apr 27, 2026
b51b9f0
refactor(ui5-range-slider): refactor ui5-range-slider component
ndeshev Apr 27, 2026
9f97af2
refactor(ui5-range-slider): refactor ui5-range-slider component
ndeshev Apr 27, 2026
8c3ed0c
refactor(ui5-range-slider): refactor ui5-range-slider component
ndeshev Apr 28, 2026
07e587c
refactor(ui5-range-slider): refactor ui5-range-slider component
ndeshev Apr 28, 2026
470fcab
refactor(ui5-range-slider): refactor ui5-range-slider component
ndeshev Apr 28, 2026
56ff769
refactor(ui5-range-slider): refactor ui5-range-slider component
ndeshev Apr 28, 2026
8901bac
refactor(ui5-range-slider): refactor ui5-range-slider component
ndeshev Apr 28, 2026
0d29acf
refactor(ui5-range-slider): refactor ui5-range-slider component
ndeshev Apr 29, 2026
a05f13a
refactor(ui5-range-slider): refactor ui5-range-slider component
ndeshev Apr 29, 2026
c6b5c94
refactor(ui5-range-slider): refactor ui5-range-slider component
ndeshev Apr 29, 2026
c740d39
refactor(ui5-range-slider): refactor ui5-range-slider component
ndeshev Apr 29, 2026
c463576
refactor(ui5-range-slider): refactor ui5-range-slider component
ndeshev Apr 30, 2026
e417159
refactor(ui5-range-slider): refactor ui5-range-slider component
ndeshev Apr 30, 2026
f5101da
refactor(ui5-range-slider): refactor ui5-range-slider component
ndeshev Apr 30, 2026
81e5852
refactor(ui5-range-slider): refactor ui5-range-slider component
ndeshev Apr 30, 2026
bf3abef
Merge branch 'main' into range-slider-refactoring
ndeshev Apr 30, 2026
3c88732
refactor(ui5-range-slider): refactor ui5-range-slider component
ndeshev Apr 30, 2026
170bb5c
Merge branch 'range-slider-refactoring' of https://github.com/SAP/ui5…
ndeshev Apr 30, 2026
840f0ab
refactor(ui5-range-slider): refactor ui5-range-slider component
ndeshev Apr 30, 2026
e71eec3
refactor(ui5-range-slider): refactor ui5-range-slider componen
ndeshev May 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,680 changes: 1,865 additions & 1,815 deletions packages/main/cypress/specs/RangeSlider.cy.tsx

Large diffs are not rendered by default.

22 changes: 0 additions & 22 deletions packages/main/cypress/specs/Slider.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -512,28 +512,6 @@ describe("Accessibility", () => {
.invoke('css', 'padding', '100px')
})

it("aria-keyshortcuts should not be set on regular slider", () => {
cy.mount(
<Slider min={0} max={20}></Slider>
);

cy.get("[ui5-slider]")
.shadow()
.find("[ui5-slider-handle]")
.should("not.have.attr", "aria-keyshortcuts");
});

it("aria-keyshortcuts should be set on slider with editable tooltips", () => {
cy.mount(
<Slider editableTooltip={true} min={0} max={20}></Slider>
);

cy.get("[ui5-slider]")
.shadow()
.find("[ui5-slider-handle]")
.should("have.attr", "aria-keyshortcuts");
});

it("should apply associated label text as aria-label on the slider element", () => {
const labelText = "label for slider";
cy.mount(
Expand Down
2 changes: 1 addition & 1 deletion packages/main/cypress/specs/VerticalAlignment.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,6 @@ describe("Vertical Alignment", () => {
cy.get("#container").should("have.css", "height", "44px");
cy.get("#container2").should("have.css", "height", "44px");
cy.get("#container3").should("have.css", "height", "48px");
cy.get("#container4").should("have.css", "height", "53px");
cy.get("#container4").should("have.css", "height", "44px");
});
});
156 changes: 126 additions & 30 deletions packages/main/src/RangeSlider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
isEnd,
isF2,
} from "@ui5/webcomponents-base/dist/Keys.js";
import { getAssociatedLabelForTexts } from "@ui5/webcomponents-base/dist/util/AccessibilityTextsHelper.js";
import SliderBase from "./SliderBase.js";
import RangeSliderTemplate from "./RangeSliderTemplate.js";
import type SliderTooltip from "./SliderTooltip.js";
Expand Down Expand Up @@ -93,7 +94,7 @@ type AffectedValue = "startValue" | "endValue";
languageAware: true,
formAssociated: true,
template: RangeSliderTemplate,
styles: [SliderBase.styles, rangeSliderStyles],
styles: [rangeSliderStyles],
})
class RangeSlider extends SliderBase implements IFormInputElement {
/**
Expand All @@ -106,7 +107,7 @@ class RangeSlider extends SliderBase implements IFormInputElement {
@property({ type: Number })
set startValue(value: number) {
this._startValue = value;
this.tooltipStartValue = value.toString();
this.tooltipStartValue = value?.toString() ?? "";
}

get startValue(): number {
Expand All @@ -123,7 +124,7 @@ class RangeSlider extends SliderBase implements IFormInputElement {
@property({ type: Number })
set endValue(value: number) {
this._endValue = value;
this.tooltipEndValue = value.toString();
this.tooltipEndValue = value?.toString() ?? "";
}

get endValue(): number {
Expand All @@ -145,6 +146,9 @@ class RangeSlider extends SliderBase implements IFormInputElement {
@property({ type: Boolean })
rangePressed = false;

@property({ type: Boolean })
_progressFocused = false;

@property({ type: Boolean })
_isStartValueValid = false;

Expand All @@ -169,6 +173,7 @@ class RangeSlider extends SliderBase implements IFormInputElement {
_lastValidStartValue: string;
_lastValidEndValue: string;
_areInputValuesSwapped = false;
_onDocumentClickBound: (e: MouseEvent) => void;

@i18n("@ui5/webcomponents")
static i18nBundle: I18nBundle;
Expand All @@ -192,6 +197,29 @@ class RangeSlider extends SliderBase implements IFormInputElement {
this._stateStorage.endValue = undefined;
this._lastValidStartValue = this.min.toString();
this._lastValidEndValue = this.max.toString();
this._onDocumentClickBound = this._onDocumentClick.bind(this);
}

onEnterDOM() {
document.addEventListener("mousedown", this._onDocumentClickBound, true);
}

onExitDOM() {
document.removeEventListener("mousedown", this._onDocumentClickBound, true);
}

/**
* Handles document-level clicks to clear progress focus when clicking outside.
* @private
*/
_onDocumentClick(e: MouseEvent) {
const clickedInside = e.composedPath().includes(this);

if (!clickedInside) {
if (this._tooltipsOpen) {
this._tooltipsOpen = false;
}
}
}

get _ariaDisabled() {
Expand Down Expand Up @@ -326,6 +354,7 @@ class RangeSlider extends SliderBase implements IFormInputElement {
this._setAffectedValue(undefined);
this._startValueInitial = undefined;
this._endValueInitial = undefined;
this._progressFocused = false;

if (this.showTooltip && !(e.relatedTarget as HTMLInputElement)?.hasAttribute("ui5-slider-tooltip")) {
this._tooltipsOpen = false;
Expand Down Expand Up @@ -358,7 +387,9 @@ class RangeSlider extends SliderBase implements IFormInputElement {
this._endValueAtBeginningOfAction = this.endValue;

if (isEscape(e)) {
this.update(undefined, this._startValueInitial, this._endValueInitial);
if (this._startValueInitial !== undefined && this._endValueInitial !== undefined) {
this.update(undefined, this._startValueInitial, this._endValueInitial);
}
return;
}

Expand Down Expand Up @@ -387,13 +418,14 @@ class RangeSlider extends SliderBase implements IFormInputElement {

// Update a single value if one of the handles is focused or the range if not already at min or max
const ctor = this.constructor as typeof RangeSlider;
const stepPrecision = ctor._getDecimalPrecisionOfNumber(this._effectiveStep);
if (affectedValue && !this._isPressInCurrentRange) {
const propValue = this[affectedValue as keyof RangeSlider] as number;
const newValue = ctor.clipValue(newValueOffset + propValue, min, max);
const newValue = Number(ctor.clipValue(newValueOffset + propValue, min, max).toFixed(stepPrecision));
this.update(affectedValue, newValue, undefined);
} else if ((newValueOffset < 0 && this.startValue > min) || (newValueOffset > 0 && this.endValue < max)) {
const newStartValue = ctor.clipValue(newValueOffset + this.startValue, min, max);
const newEndValue = ctor.clipValue(newValueOffset + this.endValue, min, max);
const newStartValue = Number(ctor.clipValue(newValueOffset + this.startValue, min, max).toFixed(stepPrecision));
const newEndValue = Number(ctor.clipValue(newValueOffset + this.endValue, min, max).toFixed(stepPrecision));
this.update(affectedValue, newStartValue, newEndValue);
}

Expand All @@ -415,7 +447,9 @@ class RangeSlider extends SliderBase implements IFormInputElement {
this._setAffectedValue("endValue");
}

if (this.shadowRoot!.activeElement === this._progressBar) {
// Progress bar is inside SliderScale's shadow DOM, so check the nested activeElement
const sliderScale = this.shadowRoot!.querySelector<HTMLElement>("[ui5-slider-scale]");
if (sliderScale?.shadowRoot?.activeElement === this._progressBar) {
this._setAffectedValue(undefined);
}

Expand All @@ -430,8 +464,9 @@ class RangeSlider extends SliderBase implements IFormInputElement {
_homeEndForSelectedRange(e: KeyboardEvent, affectedValue: string, min: number, max: number) {
const newValueOffset = this._handleActionKeyPressBase(e, affectedValue);
const ctor = this.constructor as typeof RangeSlider;
const newStartValue = ctor.clipValue(newValueOffset + this.startValue, min, max);
const newEndValue = ctor.clipValue(newValueOffset + this.endValue, min, max);
const stepPrecision = ctor._getDecimalPrecisionOfNumber(this._effectiveStep);
const newStartValue = Number(ctor.clipValue(newValueOffset + this.startValue, min, max).toFixed(stepPrecision));
const newEndValue = Number(ctor.clipValue(newValueOffset + this.endValue, min, max).toFixed(stepPrecision));

this.update(undefined, newStartValue, newEndValue);
}
Expand Down Expand Up @@ -481,15 +516,30 @@ class RangeSlider extends SliderBase implements IFormInputElement {
return;
}

// Calculate the new value from the press position of the event
// Pre-calculate whether the press is in the current range before handleDownBase
// This is needed so focusInnerElement() knows where to focus
const ctor = this.constructor as typeof RangeSlider;
const pageX = ctor.getPageXValueFromEvent(e);
const tempValue = ctor.getValueFromInteraction(e, this._effectiveStep, this._effectiveMin, this._effectiveMax, this.getBoundingClientRect(), this.directionStart);
const isInRange = tempValue >= this.startValue && tempValue <= this.endValue;
const startHandle = this._startHandle;
const endHandle = this._endHandle;
const inStartHandle = startHandle && pageX >= startHandle.getBoundingClientRect().left && pageX <= startHandle.getBoundingClientRect().right;
const inEndHandle = endHandle && pageX >= endHandle.getBoundingClientRect().left && pageX <= endHandle.getBoundingClientRect().right;

const newValue = this.handleDownBase(e);

// Determine the rest of the needed details from the start of the interaction.
this._saveInteractionStartData(e, newValue);
if (isInRange && !inStartHandle && !inEndHandle) {
this._setIsPressInCurrentRange(true);
this._progressFocused = true;
this.rangePressed = true;
} else {
this._progressFocused = false;
this.rangePressed = false;
}

this.rangePressed = this._isPressInCurrentRange;
this._saveInteractionStartData(e, newValue);

// Do not yet update the RangeSlider if press is in range or over a handle.
if (this._isPressInCurrentRange || this._handeIsPressed) {
this._handeIsPressed = false;
return;
Expand All @@ -509,7 +559,7 @@ class RangeSlider extends SliderBase implements IFormInputElement {
* @private
*/
_saveInteractionStartData(e: TouchEvent | MouseEvent, newValue: number) {
const progressBarDom = this.shadowRoot!.querySelector(".ui5-slider-progress")!.getBoundingClientRect();
const progressBarDom = this._progressBar?.getBoundingClientRect();

// Save the state of the value properties on the start of the interaction
this._startValueAtBeginningOfAction = this.startValue;
Expand All @@ -521,7 +571,9 @@ class RangeSlider extends SliderBase implements IFormInputElement {
// 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.
this._initialStartHandlePageX = this.directionStart === "left" ? progressBarDom.left : progressBarDom.right;
if (progressBarDom) {
this._initialStartHandlePageX = this.directionStart === "left" ? progressBarDom.left : progressBarDom.right;
}
}

/**
Expand Down Expand Up @@ -606,8 +658,8 @@ class RangeSlider extends SliderBase implements IFormInputElement {
* @private
*/
_pressTargetAndAffectedValue(clientX: number, value: number) {
const startHandle = this.shadowRoot!.querySelector(".ui5-slider-handle--start")!;
const endHandle = this.shadowRoot!.querySelector(".ui5-slider-handle--end")!;
const startHandle = this._startHandle;
const endHandle = this._endHandle;

// Check if the press point is in the bounds of any of the Range Slider handles
const handleStartDomRect = startHandle.getBoundingClientRect();
Expand Down Expand Up @@ -690,16 +742,16 @@ class RangeSlider extends SliderBase implements IFormInputElement {
const affectedValue = this._valueAffected;

if (this._isPressInCurrentRange || !affectedValue) {
this._progressBar.focus();
this._progressBar?.focus();
}

if ((affectedValue === "startValue" && !isReversed) || (affectedValue === "endValue" && isReversed)) {
this._startHandle.focus();
this._startHandle?.focus();
this.bringToFrontTooltip("start");
}

if ((affectedValue === "endValue" && !isReversed) || (affectedValue === "startValue" && isReversed)) {
this._endHandle.focus();
this._endHandle?.focus();
this.bringToFrontTooltip("end");
}
}
Expand Down Expand Up @@ -732,7 +784,11 @@ class RangeSlider extends SliderBase implements IFormInputElement {
const ctor = this.constructor as typeof RangeSlider;
startValue = ctor.clipValue(startValue, min, max - selectedRange);

return [startValue, startValue + selectedRange];
const stepPrecision = ctor._getDecimalPrecisionOfNumber(this._effectiveStep);
const endValue = Number((startValue + selectedRange).toFixed(stepPrecision));
startValue = Number(startValue.toFixed(stepPrecision));

return [startValue, endValue];
}

/**
Expand Down Expand Up @@ -830,6 +886,12 @@ class RangeSlider extends SliderBase implements IFormInputElement {
}

_onTooltipChange(e: CustomEvent) {
// Skip if this is a focusout change event triggered by the swap focus change
if (this._areInputValuesSwapped) {
this._areInputValuesSwapped = false;
return;
}

const tooltip = e.target as SliderTooltip;
const isStart = tooltip.hasAttribute("data-sap-ui-start-value");
const inputValue = parseFloat(e.detail.value as string);
Expand All @@ -849,9 +911,11 @@ class RangeSlider extends SliderBase implements IFormInputElement {
const clampedValue = Math.min(this.max, Math.max(this.min, inputValue));

if (isStart) {
this.tooltipStartValueState = ValueState.None;
this.startValue = clampedValue;
this._lastValidStartValue = clampedValue.toString();
} else {
this.tooltipEndValueState = ValueState.None;
this.endValue = clampedValue;
this._lastValidEndValue = clampedValue.toString();
}
Expand Down Expand Up @@ -898,10 +962,12 @@ class RangeSlider extends SliderBase implements IFormInputElement {
}

_onTooltipOpen() {
const ctor = this.constructor as typeof RangeSlider;
const stepPrecision = ctor._getDecimalPrecisionOfNumber(this._effectiveStep);
this.tooltipStartValue = this.startValue.toFixed(stepPrecision);
this.tooltipEndValue = this.endValue.toFixed(stepPrecision);
if (!this.startValue || !this.endValue) {
return;
}

this.tooltipStartValue = this.startValue.toString();
this.tooltipEndValue = this.endValue.toString();
}

_onTooltipInput(e: CustomEvent) {
Expand Down Expand Up @@ -1004,15 +1070,16 @@ class RangeSlider extends SliderBase implements IFormInputElement {
}

get _startHandle() {
return this.shadowRoot!.querySelector<HTMLElement>(".ui5-slider-handle--start")!;
return this.shadowRoot!.querySelector<HTMLElement>("[ui5-slider-handle][handle-type='Start']")!;
}

get _endHandle() {
return this.shadowRoot!.querySelector<HTMLElement>(".ui5-slider-handle--end")!;
return this.shadowRoot!.querySelector<HTMLElement>("[ui5-slider-handle][handle-type='End']")!;
}

get _progressBar() {
return this.shadowRoot!.querySelector<HTMLElement>(".ui5-slider-progress")!;
const sliderScale = this.shadowRoot!.querySelector<HTMLElement>("[ui5-slider-scale]");
return sliderScale?.shadowRoot?.querySelector<HTMLElement>(".ui5-slider-progress") ?? null;
}

get _ariaLabelledByStartHandleText() {
Expand All @@ -1023,6 +1090,35 @@ class RangeSlider extends SliderBase implements IFormInputElement {
return this.accessibleName ? ["ui5-slider-accName", "ui5-slider-endHandleDesc"].join(" ").trim() : "ui5-slider-endHandleDesc";
}

/**
* @private
*/
get _ariaLabelStartHandle() {
Comment thread
ndeshev marked this conversation as resolved.
return this._getAriaLabelHandle(this._ariaHandlesText.startHandleText || "");
Comment thread
ndeshev marked this conversation as resolved.
}

/**
* @private
*/
get _ariaLabelEndHandle() {
return this._getAriaLabelHandle(this._ariaHandlesText.endHandleText || "");
}

_getAriaLabelHandle(handleDescription: string) {
const associatedLabelText = getAssociatedLabelForTexts(this);
const hasAccessibleName = !!this.accessibleName;

let labelText = hasAccessibleName
? `${this.accessibleName} ${handleDescription}`
: handleDescription;

if (!hasAccessibleName && associatedLabelText) {
labelText = `${associatedLabelText} ${labelText}`;
}

return labelText;
}

get _ariaLabelledByInputText() {
return RangeSlider.i18nBundle.getText(SLIDER_TOOLTIP_INPUT_LABEL);
}
Expand Down
Loading
Loading