Skip to content

Commit 9803e64

Browse files
Merge branch 'main' into badge-tooltip
2 parents ba404ab + 4727fb5 commit 9803e64

64 files changed

Lines changed: 2574 additions & 3054 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/main/cypress/specs/RangeSlider.cy.tsx

Lines changed: 1865 additions & 1815 deletions
Large diffs are not rendered by default.

packages/main/cypress/specs/Slider.cy.tsx

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -512,28 +512,6 @@ describe("Accessibility", () => {
512512
.invoke('css', 'padding', '100px')
513513
})
514514

515-
it("aria-keyshortcuts should not be set on regular slider", () => {
516-
cy.mount(
517-
<Slider min={0} max={20}></Slider>
518-
);
519-
520-
cy.get("[ui5-slider]")
521-
.shadow()
522-
.find("[ui5-slider-handle]")
523-
.should("not.have.attr", "aria-keyshortcuts");
524-
});
525-
526-
it("aria-keyshortcuts should be set on slider with editable tooltips", () => {
527-
cy.mount(
528-
<Slider editableTooltip={true} min={0} max={20}></Slider>
529-
);
530-
531-
cy.get("[ui5-slider]")
532-
.shadow()
533-
.find("[ui5-slider-handle]")
534-
.should("have.attr", "aria-keyshortcuts");
535-
});
536-
537515
it("should apply associated label text as aria-label on the slider element", () => {
538516
const labelText = "label for slider";
539517
cy.mount(

packages/main/cypress/specs/VerticalAlignment.cy.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,6 @@ describe("Vertical Alignment", () => {
7272
cy.get("#container").should("have.css", "height", "44px");
7373
cy.get("#container2").should("have.css", "height", "44px");
7474
cy.get("#container3").should("have.css", "height", "48px");
75-
cy.get("#container4").should("have.css", "height", "53px");
75+
cy.get("#container4").should("have.css", "height", "44px");
7676
});
7777
});

packages/main/src/RangeSlider.ts

Lines changed: 126 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
isEnd,
1212
isF2,
1313
} from "@ui5/webcomponents-base/dist/Keys.js";
14+
import { getAssociatedLabelForTexts } from "@ui5/webcomponents-base/dist/util/AccessibilityTextsHelper.js";
1415
import SliderBase from "./SliderBase.js";
1516
import RangeSliderTemplate from "./RangeSliderTemplate.js";
1617
import type SliderTooltip from "./SliderTooltip.js";
@@ -93,7 +94,7 @@ type AffectedValue = "startValue" | "endValue";
9394
languageAware: true,
9495
formAssociated: true,
9596
template: RangeSliderTemplate,
96-
styles: [SliderBase.styles, rangeSliderStyles],
97+
styles: [rangeSliderStyles],
9798
})
9899
class RangeSlider extends SliderBase implements IFormInputElement {
99100
/**
@@ -106,7 +107,7 @@ class RangeSlider extends SliderBase implements IFormInputElement {
106107
@property({ type: Number })
107108
set startValue(value: number) {
108109
this._startValue = value;
109-
this.tooltipStartValue = value.toString();
110+
this.tooltipStartValue = value?.toString() ?? "";
110111
}
111112

112113
get startValue(): number {
@@ -123,7 +124,7 @@ class RangeSlider extends SliderBase implements IFormInputElement {
123124
@property({ type: Number })
124125
set endValue(value: number) {
125126
this._endValue = value;
126-
this.tooltipEndValue = value.toString();
127+
this.tooltipEndValue = value?.toString() ?? "";
127128
}
128129

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

149+
@property({ type: Boolean })
150+
_progressFocused = false;
151+
148152
@property({ type: Boolean })
149153
_isStartValueValid = false;
150154

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

173178
@i18n("@ui5/webcomponents")
174179
static i18nBundle: I18nBundle;
@@ -192,6 +197,29 @@ class RangeSlider extends SliderBase implements IFormInputElement {
192197
this._stateStorage.endValue = undefined;
193198
this._lastValidStartValue = this.min.toString();
194199
this._lastValidEndValue = this.max.toString();
200+
this._onDocumentClickBound = this._onDocumentClick.bind(this);
201+
}
202+
203+
onEnterDOM() {
204+
document.addEventListener("mousedown", this._onDocumentClickBound, true);
205+
}
206+
207+
onExitDOM() {
208+
document.removeEventListener("mousedown", this._onDocumentClickBound, true);
209+
}
210+
211+
/**
212+
* Handles document-level clicks to clear progress focus when clicking outside.
213+
* @private
214+
*/
215+
_onDocumentClick(e: MouseEvent) {
216+
const clickedInside = e.composedPath().includes(this);
217+
218+
if (!clickedInside) {
219+
if (this._tooltipsOpen) {
220+
this._tooltipsOpen = false;
221+
}
222+
}
195223
}
196224

197225
get _ariaDisabled() {
@@ -326,6 +354,7 @@ class RangeSlider extends SliderBase implements IFormInputElement {
326354
this._setAffectedValue(undefined);
327355
this._startValueInitial = undefined;
328356
this._endValueInitial = undefined;
357+
this._progressFocused = false;
329358

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

360389
if (isEscape(e)) {
361-
this.update(undefined, this._startValueInitial, this._endValueInitial);
390+
if (this._startValueInitial !== undefined && this._endValueInitial !== undefined) {
391+
this.update(undefined, this._startValueInitial, this._endValueInitial);
392+
}
362393
return;
363394
}
364395

@@ -387,13 +418,14 @@ class RangeSlider extends SliderBase implements IFormInputElement {
387418

388419
// Update a single value if one of the handles is focused or the range if not already at min or max
389420
const ctor = this.constructor as typeof RangeSlider;
421+
const stepPrecision = ctor._getDecimalPrecisionOfNumber(this._effectiveStep);
390422
if (affectedValue && !this._isPressInCurrentRange) {
391423
const propValue = this[affectedValue as keyof RangeSlider] as number;
392-
const newValue = ctor.clipValue(newValueOffset + propValue, min, max);
424+
const newValue = Number(ctor.clipValue(newValueOffset + propValue, min, max).toFixed(stepPrecision));
393425
this.update(affectedValue, newValue, undefined);
394426
} else if ((newValueOffset < 0 && this.startValue > min) || (newValueOffset > 0 && this.endValue < max)) {
395-
const newStartValue = ctor.clipValue(newValueOffset + this.startValue, min, max);
396-
const newEndValue = ctor.clipValue(newValueOffset + this.endValue, min, max);
427+
const newStartValue = Number(ctor.clipValue(newValueOffset + this.startValue, min, max).toFixed(stepPrecision));
428+
const newEndValue = Number(ctor.clipValue(newValueOffset + this.endValue, min, max).toFixed(stepPrecision));
397429
this.update(affectedValue, newStartValue, newEndValue);
398430
}
399431

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

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

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

436471
this.update(undefined, newStartValue, newEndValue);
437472
}
@@ -481,15 +516,30 @@ class RangeSlider extends SliderBase implements IFormInputElement {
481516
return;
482517
}
483518

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

487-
// Determine the rest of the needed details from the start of the interaction.
488-
this._saveInteractionStartData(e, newValue);
532+
if (isInRange && !inStartHandle && !inEndHandle) {
533+
this._setIsPressInCurrentRange(true);
534+
this._progressFocused = true;
535+
this.rangePressed = true;
536+
} else {
537+
this._progressFocused = false;
538+
this.rangePressed = false;
539+
}
489540

490-
this.rangePressed = this._isPressInCurrentRange;
541+
this._saveInteractionStartData(e, newValue);
491542

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

514564
// Save the state of the value properties on the start of the interaction
515565
this._startValueAtBeginningOfAction = this.startValue;
@@ -521,7 +571,9 @@ class RangeSlider extends SliderBase implements IFormInputElement {
521571
// Which element of the Range Slider is pressed and which value property to be modified on further interaction
522572
this._pressTargetAndAffectedValue(this._initialPageXPosition, newValue);
523573
// Use the progress bar to save the initial coordinates of the start-handle when the interaction begins.
524-
this._initialStartHandlePageX = this.directionStart === "left" ? progressBarDom.left : progressBarDom.right;
574+
if (progressBarDom) {
575+
this._initialStartHandlePageX = this.directionStart === "left" ? progressBarDom.left : progressBarDom.right;
576+
}
525577
}
526578

527579
/**
@@ -606,8 +658,8 @@ class RangeSlider extends SliderBase implements IFormInputElement {
606658
* @private
607659
*/
608660
_pressTargetAndAffectedValue(clientX: number, value: number) {
609-
const startHandle = this.shadowRoot!.querySelector(".ui5-slider-handle--start")!;
610-
const endHandle = this.shadowRoot!.querySelector(".ui5-slider-handle--end")!;
661+
const startHandle = this._startHandle;
662+
const endHandle = this._endHandle;
611663

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

692744
if (this._isPressInCurrentRange || !affectedValue) {
693-
this._progressBar.focus();
745+
this._progressBar?.focus();
694746
}
695747

696748
if ((affectedValue === "startValue" && !isReversed) || (affectedValue === "endValue" && isReversed)) {
697-
this._startHandle.focus();
749+
this._startHandle?.focus();
698750
this.bringToFrontTooltip("start");
699751
}
700752

701753
if ((affectedValue === "endValue" && !isReversed) || (affectedValue === "startValue" && isReversed)) {
702-
this._endHandle.focus();
754+
this._endHandle?.focus();
703755
this.bringToFrontTooltip("end");
704756
}
705757
}
@@ -732,7 +784,11 @@ class RangeSlider extends SliderBase implements IFormInputElement {
732784
const ctor = this.constructor as typeof RangeSlider;
733785
startValue = ctor.clipValue(startValue, min, max - selectedRange);
734786

735-
return [startValue, startValue + selectedRange];
787+
const stepPrecision = ctor._getDecimalPrecisionOfNumber(this._effectiveStep);
788+
const endValue = Number((startValue + selectedRange).toFixed(stepPrecision));
789+
startValue = Number(startValue.toFixed(stepPrecision));
790+
791+
return [startValue, endValue];
736792
}
737793

738794
/**
@@ -830,6 +886,12 @@ class RangeSlider extends SliderBase implements IFormInputElement {
830886
}
831887

832888
_onTooltipChange(e: CustomEvent) {
889+
// Skip if this is a focusout change event triggered by the swap focus change
890+
if (this._areInputValuesSwapped) {
891+
this._areInputValuesSwapped = false;
892+
return;
893+
}
894+
833895
const tooltip = e.target as SliderTooltip;
834896
const isStart = tooltip.hasAttribute("data-sap-ui-start-value");
835897
const inputValue = parseFloat(e.detail.value as string);
@@ -849,9 +911,11 @@ class RangeSlider extends SliderBase implements IFormInputElement {
849911
const clampedValue = Math.min(this.max, Math.max(this.min, inputValue));
850912

851913
if (isStart) {
914+
this.tooltipStartValueState = ValueState.None;
852915
this.startValue = clampedValue;
853916
this._lastValidStartValue = clampedValue.toString();
854917
} else {
918+
this.tooltipEndValueState = ValueState.None;
855919
this.endValue = clampedValue;
856920
this._lastValidEndValue = clampedValue.toString();
857921
}
@@ -898,10 +962,12 @@ class RangeSlider extends SliderBase implements IFormInputElement {
898962
}
899963

900964
_onTooltipOpen() {
901-
const ctor = this.constructor as typeof RangeSlider;
902-
const stepPrecision = ctor._getDecimalPrecisionOfNumber(this._effectiveStep);
903-
this.tooltipStartValue = this.startValue.toFixed(stepPrecision);
904-
this.tooltipEndValue = this.endValue.toFixed(stepPrecision);
965+
if (!this.startValue || !this.endValue) {
966+
return;
967+
}
968+
969+
this.tooltipStartValue = this.startValue.toString();
970+
this.tooltipEndValue = this.endValue.toString();
905971
}
906972

907973
_onTooltipInput(e: CustomEvent) {
@@ -1004,15 +1070,16 @@ class RangeSlider extends SliderBase implements IFormInputElement {
10041070
}
10051071

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

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

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

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

1093+
/**
1094+
* @private
1095+
*/
1096+
get _ariaLabelStartHandle() {
1097+
return this._getAriaLabelHandle(this._ariaHandlesText.startHandleText || "");
1098+
}
1099+
1100+
/**
1101+
* @private
1102+
*/
1103+
get _ariaLabelEndHandle() {
1104+
return this._getAriaLabelHandle(this._ariaHandlesText.endHandleText || "");
1105+
}
1106+
1107+
_getAriaLabelHandle(handleDescription: string) {
1108+
const associatedLabelText = getAssociatedLabelForTexts(this);
1109+
const hasAccessibleName = !!this.accessibleName;
1110+
1111+
let labelText = hasAccessibleName
1112+
? `${this.accessibleName} ${handleDescription}`
1113+
: handleDescription;
1114+
1115+
if (!hasAccessibleName && associatedLabelText) {
1116+
labelText = `${associatedLabelText} ${labelText}`;
1117+
}
1118+
1119+
return labelText;
1120+
}
1121+
10261122
get _ariaLabelledByInputText() {
10271123
return RangeSlider.i18nBundle.getText(SLIDER_TOOLTIP_INPUT_LABEL);
10281124
}

0 commit comments

Comments
 (0)