Skip to content

Commit 7f94de3

Browse files
authored
fix(ui5-slider): rework tooltips states (#12725)
FIXES: #12553
1 parent fc063f0 commit 7f94de3

File tree

7 files changed

+240
-101
lines changed

7 files changed

+240
-101
lines changed

packages/main/src/RangeSlider.ts

Lines changed: 106 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import property from "@ui5/webcomponents-base/dist/decorators/property.js";
33
import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";
44
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
55
import type { IFormInputElement } from "@ui5/webcomponents-base/dist/features/InputElementsFormSupport.js";
6+
import ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js";
67
import {
78
isEscape,
89
isEnter,
910
isHome,
1011
isEnd,
12+
isF2,
1113
} from "@ui5/webcomponents-base/dist/Keys.js";
1214
import SliderBase from "./SliderBase.js";
1315
import RangeSliderTemplate from "./RangeSliderTemplate.js";
@@ -102,7 +104,14 @@ class RangeSlider extends SliderBase implements IFormInputElement {
102104
* @public
103105
*/
104106
@property({ type: Number })
105-
startValue = 0;
107+
set startValue(value: number) {
108+
this._startValue = value;
109+
this.tooltipStartValue = value.toString();
110+
}
111+
112+
get startValue() {
113+
return this._startValue;
114+
}
106115

107116
/**
108117
* Defines end point of a selection - position of a second handle on the slider.
@@ -112,7 +121,26 @@ class RangeSlider extends SliderBase implements IFormInputElement {
112121
* @public
113122
*/
114123
@property({ type: Number })
115-
endValue = 100;
124+
set endValue(value: number) {
125+
this._endValue = value;
126+
this.tooltipEndValue = value.toString();
127+
}
128+
129+
get endValue() {
130+
return this._endValue;
131+
}
132+
133+
@property()
134+
tooltipStartValue = "";
135+
136+
@property()
137+
tooltipEndValue = "";
138+
139+
@property()
140+
tooltipStartValueState: `${ValueState}` = "None";
141+
142+
@property()
143+
tooltipEndValueState: `${ValueState}` = "None";
116144

117145
@property({ type: Boolean })
118146
rangePressed = false;
@@ -123,6 +151,8 @@ class RangeSlider extends SliderBase implements IFormInputElement {
123151
@property({ type: Boolean })
124152
_isEndValueValid = false;
125153

154+
_startValue: number = 0;
155+
_endValue: number = 100;
126156
_startValueInitial?: number;
127157
_endValueInitial?: number;
128158
_valueAffected?: AffectedValue;
@@ -164,18 +194,6 @@ class RangeSlider extends SliderBase implements IFormInputElement {
164194
this._lastValidEndValue = this.max.toString();
165195
}
166196

167-
get tooltipStartValue() {
168-
const ctor = this.constructor as typeof RangeSlider;
169-
const stepPrecision = ctor._getDecimalPrecisionOfNumber(this._effectiveStep);
170-
return this.startValue.toFixed(stepPrecision);
171-
}
172-
173-
get tooltipEndValue() {
174-
const ctor = this.constructor as typeof RangeSlider;
175-
const stepPrecision = ctor._getDecimalPrecisionOfNumber(this._effectiveStep);
176-
return this.endValue.toFixed(stepPrecision);
177-
}
178-
179197
get _ariaDisabled() {
180198
return this.disabled || undefined;
181199
}
@@ -233,6 +251,14 @@ class RangeSlider extends SliderBase implements IFormInputElement {
233251
this.update(this._valueAffected, this.startValue, this.endValue);
234252
}
235253

254+
onAfterRendering(): void {
255+
super.onAfterRendering();
256+
257+
[...this.getDomRef()!.querySelectorAll("[ui5-slider-tooltip]")].forEach(tooltip => {
258+
(tooltip as SliderTooltip).repositionTooltip();
259+
});
260+
}
261+
236262
syncUIAndState() {
237263
// Validate step and update the stored state for the step property.
238264
if (this.isPropertyUpdated("step")) {
@@ -370,6 +396,9 @@ class RangeSlider extends SliderBase implements IFormInputElement {
370396
const newEndValue = ctor.clipValue(newValueOffset + this.endValue, min, max);
371397
this.update(affectedValue, newStartValue, newEndValue);
372398
}
399+
400+
this.tooltipStartValue = this.startValue.toString();
401+
this.tooltipEndValue = this.endValue.toString();
373402
}
374403

375404
/**
@@ -515,6 +544,9 @@ class RangeSlider extends SliderBase implements IFormInputElement {
515544

516545
// Updates UI and state when dragging of the whole selected range
517546
this._updateValueOnRangeDrag(e);
547+
548+
this.tooltipStartValue = this.startValue.toString();
549+
this.tooltipEndValue = this.endValue.toString();
518550
}
519551

520552
/**
@@ -779,7 +811,12 @@ class RangeSlider extends SliderBase implements IFormInputElement {
779811
const tooltip = this.shadowRoot!.querySelector(tooltipSelector) as SliderTooltip;
780812

781813
if (tooltip?.hidePopover && tooltip?.showPopover) {
782-
requestAnimationFrame(() => {
814+
const frame = requestAnimationFrame(() => {
815+
if (tooltip.getDomRef()?.offsetParent === null) {
816+
cancelAnimationFrame(frame);
817+
return;
818+
}
819+
783820
tooltip.hidePopover();
784821
tooltip.showPopover();
785822
});
@@ -796,6 +833,18 @@ class RangeSlider extends SliderBase implements IFormInputElement {
796833
const tooltip = e.target as SliderTooltip;
797834
const isStart = tooltip.hasAttribute("data-sap-ui-start-value");
798835
const inputValue = parseFloat(e.detail.value as string);
836+
const isInvalid = inputValue > this._effectiveMax || inputValue < this._effectiveMin;
837+
838+
if (isInvalid) {
839+
if (isStart) {
840+
this.tooltipStartValueState = ValueState.Negative;
841+
this.tooltipStartValue = e.detail.value;
842+
} else {
843+
this.tooltipEndValueState = ValueState.Negative;
844+
this.tooltipEndValue = e.detail.value;
845+
}
846+
return;
847+
}
799848

800849
const clampedValue = Math.min(this.max, Math.max(this.min, inputValue));
801850

@@ -831,6 +880,48 @@ class RangeSlider extends SliderBase implements IFormInputElement {
831880
this.fireDecoratorEvent("change");
832881
}
833882

883+
_onTooltipFocusChange(e: CustomEvent) {
884+
const tooltip = e.target as SliderTooltip;
885+
const isStart = tooltip.hasAttribute("data-sap-ui-start-value");
886+
const value = isStart ? this.tooltipStartValue : this.tooltipEndValue;
887+
const isInvalid = parseFloat(value) > this._effectiveMax || parseFloat(value) < this._effectiveMin;
888+
889+
if (isInvalid) {
890+
if (isStart) {
891+
this.tooltipStartValueState = ValueState.None;
892+
this.tooltipStartValue = this.startValue.toString();
893+
} else {
894+
this.tooltipEndValueState = ValueState.None;
895+
this.tooltipEndValue = this.endValue.toString();
896+
}
897+
}
898+
}
899+
900+
_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);
905+
}
906+
907+
_onTooltipInput(e: CustomEvent) {
908+
const tooltip = e.target as SliderTooltip;
909+
const isStart = tooltip.hasAttribute("data-sap-ui-start-value");
910+
911+
if (isStart) {
912+
this.tooltipStartValue = e.detail.value;
913+
} else {
914+
this.tooltipEndValue = e.detail.value;
915+
}
916+
}
917+
918+
_onTooltipKeydown(e: KeyboardEvent) {
919+
if (isF2(e)) {
920+
e.preventDefault();
921+
(e.target as SliderTooltip).followRef?.focus();
922+
}
923+
}
924+
834925
_getFormattedValue(value: string) {
835926
const valueNumber = parseFloat(value);
836927
const ctor = this.constructor as typeof RangeSlider;

packages/main/src/RangeSliderTemplate.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,18 @@ export function handles(this: RangeSlider) {
7070

7171
<SliderTooltip
7272
open={this._tooltipsOpen}
73-
value={this.startValue.toString()}
73+
value={this.tooltipStartValue}
74+
valueState={this.tooltipStartValueState}
7475
min={this.min}
7576
max={this.max}
7677
data-sap-ui-start-value
7778
editable={this.editableTooltip}
7879
followRef={this._startHandle}
7980
onChange={this._onTooltipChange}
80-
onForwardFocus={this._onTooltopForwardFocus}
81+
onKeyDown={this._onTooltipKeydown}
82+
onFocusChange={this._onTooltipFocusChange}
83+
onOpen={this._onTooltipOpen}
84+
onInput={this._onTooltipInput}
8185
>
8286
</SliderTooltip>
8387
</div>
@@ -105,14 +109,18 @@ export function handles(this: RangeSlider) {
105109

106110
<SliderTooltip
107111
open={this._tooltipsOpen}
108-
value={this.endValue.toString()}
112+
value={this.tooltipEndValue}
113+
valueState={this.tooltipEndValueState}
109114
min={this.min}
110115
max={this.max}
111116
data-sap-ui-end-value
112117
editable={this.editableTooltip}
113118
followRef={this._endHandle}
114119
onChange={this._onTooltipChange}
115-
onForwardFocus={this._onTooltopForwardFocus}
120+
onKeyDown={this._onTooltipKeydown}
121+
onFocusChange={this._onTooltipFocusChange}
122+
onOpen={this._onTooltipOpen}
123+
onInput={this._onTooltipInput}
116124
>
117125
</SliderTooltip>
118126
</div>

packages/main/src/Slider.ts

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ import customElement from "@ui5/webcomponents-base/dist/decorators/customElement
22
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
33
import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";
44
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
5-
import { isEscape } from "@ui5/webcomponents-base/dist/Keys.js";
5+
import { isEscape, isF2 } from "@ui5/webcomponents-base/dist/Keys.js";
66
import type { IFormInputElement } from "@ui5/webcomponents-base/dist/features/InputElementsFormSupport.js";
77
import type ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js";
88
import SliderBase from "./SliderBase.js";
99
import type SliderTooltip from "./SliderTooltip.js";
1010

1111
// Template
1212
import SliderTemplate from "./SliderTemplate.js";
13+
import type { SliderTooltipChangeEventDetails } from "./SliderTooltip.js";
1314

1415
// Texts
1516
import {
@@ -89,13 +90,17 @@ class Slider extends SliderBase implements IFormInputElement {
8990
@property({ type: Number })
9091
value = 0;
9192

93+
@property()
94+
tooltipValueState: `${ValueState}` = "None";
95+
96+
@property()
97+
tooltipValue = "";
98+
9299
_valueInitial?: number;
93100
_valueOnInteractionStart?: number;
94101
_progressPercentage = 0;
95102
_handlePositionFromStart = 0;
96103
_lastValidInputValue: string;
97-
_tooltipInputValue: string = this.value.toString();
98-
_tooltipInputValueState: `${ValueState}` = "None";
99104

100105
get formFormattedValue() {
101106
return this.value.toString();
@@ -131,6 +136,12 @@ class Slider extends SliderBase implements IFormInputElement {
131136
this._updateHandleAndProgress(this.value);
132137
}
133138

139+
onAfterRendering(): void {
140+
super.onAfterRendering();
141+
142+
this.tooltip?.repositionTooltip();
143+
}
144+
134145
syncUIAndState() {
135146
// Validate step and update the stored state for the step property.
136147
if (this.isPropertyUpdated("step")) {
@@ -219,6 +230,47 @@ class Slider extends SliderBase implements IFormInputElement {
219230
}
220231
}
221232

233+
_onTooltipChange(e: CustomEvent<SliderTooltipChangeEventDetails>) {
234+
const value = parseFloat(e.detail.value);
235+
const isInvalid = value < this._effectiveMin || value > this._effectiveMax;
236+
237+
if (isInvalid) {
238+
this.tooltipValueState = "Negative";
239+
this.tooltipValue = `${value}`;
240+
return;
241+
}
242+
243+
this.value = value;
244+
this.fireDecoratorEvent("change");
245+
}
246+
247+
_onTooltipFocusChange() {
248+
const value = parseFloat(this.tooltipValue);
249+
const isInvalid = value < this._effectiveMin || value > this._effectiveMax;
250+
251+
if (isInvalid) {
252+
this.tooltipValueState = "None";
253+
this.tooltipValue = this.value.toString();
254+
}
255+
}
256+
257+
_onTooltipKeydown(e: KeyboardEvent) {
258+
if (isF2(e)) {
259+
e.preventDefault();
260+
this._sliderHandle.focus();
261+
}
262+
}
263+
264+
_onTooltipOpen() {
265+
const ctor = this.constructor as typeof Slider;
266+
const stepPrecision = ctor._getDecimalPrecisionOfNumber(this._effectiveStep);
267+
this.tooltipValue = this.value.toFixed(stepPrecision);
268+
}
269+
270+
_onTooltipInput(e: CustomEvent) {
271+
this.tooltipValue = e.detail.value;
272+
}
273+
222274
/**
223275
* Called when the user moves the slider
224276
* @private
@@ -237,6 +289,7 @@ class Slider extends SliderBase implements IFormInputElement {
237289

238290
this._updateHandleAndProgress(newValue);
239291
this.value = newValue;
292+
this.tooltipValue = newValue.toString();
240293
this.updateStateStorageAndFireInputEvent("value");
241294
}
242295

@@ -295,6 +348,7 @@ class Slider extends SliderBase implements IFormInputElement {
295348
if (newValue !== currentValue) {
296349
this._updateHandleAndProgress(newValue!);
297350
this.value = newValue!;
351+
this.tooltipValue = this.value.toString();
298352
this.updateStateStorageAndFireInputEvent("value");
299353
}
300354
}
@@ -309,6 +363,10 @@ class Slider extends SliderBase implements IFormInputElement {
309363
return this.value.toString();
310364
}
311365

366+
get tooltip() {
367+
return this.getDomRef()?.querySelector<SliderTooltip>("[ui5-slider-tooltip]");
368+
}
369+
312370
get styles() {
313371
return {
314372
progress: {
@@ -321,16 +379,10 @@ class Slider extends SliderBase implements IFormInputElement {
321379
};
322380
}
323381

324-
get _sliderHandle() {
382+
get _sliderHandle() : HTMLElement {
325383
return this.shadowRoot!.querySelector(".ui5-slider-handle")!;
326384
}
327385

328-
get tooltipValue() {
329-
const ctor = this.constructor as typeof Slider;
330-
const stepPrecision = ctor._getDecimalPrecisionOfNumber(this._effectiveStep);
331-
return this.value.toFixed(stepPrecision);
332-
}
333-
334386
get _ariaDisabled() {
335387
return this.disabled || undefined;
336388
}

0 commit comments

Comments
 (0)