From 37ea6a05badf156f4767786d3b5e16dc5625db56 Mon Sep 17 00:00:00 2001 From: "Martin R. Hristov" Date: Tue, 4 Mar 2025 09:50:54 +0200 Subject: [PATCH 1/2] fix(ui5-slider): implement tooltips with Popover API --- packages/main/src/InputTemplate.tsx | 2 +- packages/main/src/RangeSlider.ts | 197 +++----------------- packages/main/src/RangeSliderTemplate.tsx | 63 +++---- packages/main/src/Slider.ts | 70 ++----- packages/main/src/SliderBase.ts | 46 ++--- packages/main/src/SliderBaseTemplate.tsx | 1 - packages/main/src/SliderTemplate.tsx | 35 ++-- packages/main/src/SliderTooltip.ts | 168 +++++++++++++++++ packages/main/src/SliderTooltipTemplate.tsx | 26 +++ packages/main/src/themes/SliderTooltip.css | 34 ++++ 10 files changed, 321 insertions(+), 321 deletions(-) create mode 100644 packages/main/src/SliderTooltip.ts create mode 100644 packages/main/src/SliderTooltipTemplate.tsx create mode 100644 packages/main/src/themes/SliderTooltip.css diff --git a/packages/main/src/InputTemplate.tsx b/packages/main/src/InputTemplate.tsx index adbc28a47b55..505d634a4fa8 100644 --- a/packages/main/src/InputTemplate.tsx +++ b/packages/main/src/InputTemplate.tsx @@ -27,7 +27,7 @@ export default function InputTemplate(this: Input, hooks?: { preContent: Templat style={this.styles.innerInput} type={this.inputNativeType} inner-input - inner-input-with-icon={this.icon.length} + inner-input-with-icon={!!this.icon.length} disabled={this.disabled} readonly={this._readonly} value={this._innerValue} diff --git a/packages/main/src/RangeSlider.ts b/packages/main/src/RangeSlider.ts index c1ff3ded949a..c6d3e9ffc4cf 100644 --- a/packages/main/src/RangeSlider.ts +++ b/packages/main/src/RangeSlider.ts @@ -223,10 +223,6 @@ class RangeSlider extends SliderBase implements IFormInputElement { this.update(affectedValue, this.startValue, this.endValue); } - if (this.editableTooltip) { - this._saveInputValues(); - } - if (!this.isCurrentStateOutdated()) { return; } @@ -281,9 +277,7 @@ class RangeSlider extends SliderBase implements IFormInputElement { this._endValueInitial = this.endValue; } - if (this.showTooltip) { - this._tooltipVisibility = SliderBase.TOOLTIP_VISIBILITY.VISIBLE; - } + this._tooltipsOpen = this.showTooltip; } /** @@ -307,41 +301,8 @@ class RangeSlider extends SliderBase implements IFormInputElement { this._startValueInitial = undefined; this._endValueInitial = undefined; - 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"; + if (this.showTooltip && !(e.relatedTarget as HTMLInputElement)?.hasAttribute("ui5-slider-tooltip")) { + this._tooltipsOpen = false; } } @@ -487,7 +448,7 @@ class RangeSlider extends SliderBase implements IFormInputElement { // 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 || (e.target as HTMLElement).hasAttribute("ui5-input")) { + if (this.disabled || this._effectiveStep === 0 || (e.target as HTMLElement).hasAttribute("ui5-slider-tooltip")) { return; } @@ -542,7 +503,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 || (e.target as HTMLElement).hasAttribute("ui5-input")) { + if (this.disabled || this._effectiveStep === 0) { return; } @@ -583,11 +544,7 @@ class RangeSlider extends SliderBase implements IFormInputElement { this.update(undefined, newValues[0], newValues[1]); } - _handleUp(e: MouseEvent) { - if ((e.target as HTMLElement).hasAttribute("ui5-input")) { - return; - } - + _handleUp() { this._setAffectedValueByFocusedElement(); this._setAffectedValue(undefined); @@ -603,30 +560,30 @@ class RangeSlider extends SliderBase implements IFormInputElement { this._endValueAtBeginningOfAction = undefined; } - _updateValueFromInput(e: Event) { - if (this._areInputValuesSwapped) { - return; - } + // _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; + // const input = e.target as HTMLInputElement; + // const inputValue = parseFloat(input.value); + // const isValueValid = inputValue >= this._effectiveMin && inputValue <= this._effectiveMax; - if (!isValueValid) { - return; - } + // if (!isValueValid) { + // return; + // } - if (input.hasAttribute("data-sap-ui-start-value")) { - this.startValue = inputValue; - return; - } + // if (input.hasAttribute("data-sap-ui-start-value")) { + // this.startValue = inputValue; + // return; + // } - this.endValue = inputValue; + // this.endValue = inputValue; - if (this.startValue > this.endValue) { - this._areInputValuesSwapped = true; - } - } + // if (this.startValue > this.endValue) { + // this._areInputValuesSwapped = true; + // } + // } /** * Determines where the press occured and which values of the Range Slider @@ -722,10 +679,6 @@ 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; @@ -841,102 +794,7 @@ 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); - } - } - } + _onInputKeydown(): void {} _getFormattedValue(value: string) { const valueNumber = parseFloat(value); @@ -1067,9 +925,6 @@ class RangeSlider extends SliderBase implements IFormInputElement { "width": `100%`, [this.directionStart]: `-${this._labelWidth / 2}%`, }, - tooltip: { - "visibility": `${this._tooltipVisibility}`, - }, }; } } diff --git a/packages/main/src/RangeSliderTemplate.tsx b/packages/main/src/RangeSliderTemplate.tsx index f4688d385afa..2b99f1b42b97 100644 --- a/packages/main/src/RangeSliderTemplate.tsx +++ b/packages/main/src/RangeSliderTemplate.tsx @@ -3,6 +3,7 @@ import type RangeSlider from "./RangeSlider.js"; import Icon from "./Icon.js"; import Input from "./Input.js"; import SliderBaseTemplate from "./SliderBaseTemplate.js"; +import SliderTooltip from "./SliderTooltip.js"; export default function RangeSliderTemplate(this: RangeSlider) { return SliderBaseTemplate.call(this, { @@ -67,26 +68,6 @@ export function handles(this: RangeSlider) { slider-icon > - - {this.showTooltip && -
- {this.editableTooltip ? - - : - {this.tooltipStartValue} - } -
- }
- {this.showTooltip && -
- {this.editableTooltip ? - - : - {this.tooltipEndValue} - } -
- } + + + + +
); } diff --git a/packages/main/src/Slider.ts b/packages/main/src/Slider.ts index 284bb0ae14b9..15cbffd4ee36 100644 --- a/packages/main/src/Slider.ts +++ b/packages/main/src/Slider.ts @@ -6,7 +6,7 @@ import { isEscape } from "@ui5/webcomponents-base/dist/Keys.js"; import type { IFormInputElement } from "@ui5/webcomponents-base/dist/features/InputElementsFormSupport.js"; import type ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js"; import SliderBase from "./SliderBase.js"; -import type Input from "./Input.js"; +import type SliderTooltip from "./SliderTooltip.js"; // Template import SliderTemplate from "./SliderTemplate.js"; @@ -122,10 +122,6 @@ class Slider extends SliderBase implements IFormInputElement { * */ onBeforeRendering() { - if (this.editableTooltip) { - this._updateInputValue(); - } - if (!this.isCurrentStateOutdated()) { return; } @@ -172,7 +168,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 || (e.target as HTMLElement).hasAttribute("ui5-input")) { + if (this.disabled || this.step === 0 || (e.target as HTMLElement).hasAttribute("ui5-slider-tooltip")) { return; } @@ -201,9 +197,7 @@ class Slider extends SliderBase implements IFormInputElement { this._valueInitial = this.value; } - if (this.showTooltip) { - this._tooltipVisibility = SliderBase.TOOLTIP_VISIBILITY.VISIBLE; - } + this._tooltipsOpen = this.showTooltip; } _onfocusout(e: FocusEvent) { @@ -218,8 +212,8 @@ class Slider extends SliderBase implements IFormInputElement { // value that was saved when it was first focused in this._valueInitial = undefined; - if (this.showTooltip && !(e.relatedTarget as HTMLInputElement)?.hasAttribute("ui5-input")) { - this._tooltipVisibility = SliderBase.TOOLTIP_VISIBILITY.HIDDEN; + if (this.showTooltip && !(e.relatedTarget as HTMLInputElement)?.hasAttribute("ui5-slider-tooltip")) { + this._tooltipsOpen = false; } } @@ -228,10 +222,6 @@ 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 @@ -251,11 +241,7 @@ class Slider extends SliderBase implements IFormInputElement { /** Called when the user finish interacting with the slider * @private */ - _handleUp(e: TouchEvent | MouseEvent) { - if ((e.target as HTMLElement).hasAttribute("ui5-input")) { - return; - } - + _handleUp() { if (this._valueOnInteractionStart !== this.value) { this.fireDecoratorEvent("change"); } @@ -264,40 +250,7 @@ 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"; - } + _updateInputValue() {} /** Determines if the press is over the handle * @private @@ -334,6 +287,12 @@ class Slider extends SliderBase implements IFormInputElement { } } + _onTooltopForwardFocus(e: CustomEvent) { + const tooltip = e.target as SliderTooltip; + + tooltip.followRef?.focus(); + } + get inputValue() { return this.value.toString(); } @@ -354,9 +313,6 @@ class Slider extends SliderBase implements IFormInputElement { "width": `100%`, [this.directionStart]: `-${this._labelWidth / 2}%`, }, - tooltip: { - "visibility": `${this._tooltipVisibility}`, - }, }; } diff --git a/packages/main/src/SliderBase.ts b/packages/main/src/SliderBase.ts index 459ed2de3136..b2ed80805d69 100644 --- a/packages/main/src/SliderBase.ts +++ b/packages/main/src/SliderBase.ts @@ -12,6 +12,7 @@ import { // Styles import sliderBaseStyles from "./generated/themes/SliderBase.css.js"; +import type { SliderTooltipChangeEventDetails } from "./SliderTooltip.js"; type StateStorage = { [key: string]: number | undefined, @@ -153,8 +154,8 @@ abstract class SliderBase extends UI5Element { /** * @private */ - @property() - _tooltipVisibility = "hidden"; + @property({ type: Boolean }) + _tooltipsOpen = false; @property({ type: Boolean }) _labelsOverlapping = false; @@ -205,7 +206,7 @@ abstract class SliderBase extends UI5Element { _handleActionKeyPress(e: Event) {} // eslint-disable-line - _updateInputValue() {} + _updateInputValue(value: string) {} // eslint-disable-line // used in base template, but implemented in subclasses abstract styles: { @@ -286,9 +287,7 @@ abstract class SliderBase extends UI5Element { * @private */ _onmouseover() { - if (this.showTooltip) { - this._tooltipVisibility = SliderBase.TOOLTIP_VISIBILITY.VISIBLE; - } + this._tooltipsOpen = this.showTooltip; } /** @@ -297,7 +296,7 @@ abstract class SliderBase extends UI5Element { */ _onmouseout() { if (this.showTooltip && !this.shadowRoot!.activeElement) { - this._tooltipVisibility = SliderBase.TOOLTIP_VISIBILITY.HIDDEN; + this._tooltipsOpen = false; } } @@ -305,14 +304,14 @@ abstract class SliderBase extends UI5Element { 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(); + (target.parentNode!.querySelector("[ui5-slider-tooltip]") as HTMLElement).focus(); } if (this.disabled || this._effectiveStep === 0 || target.hasAttribute("ui5-slider-handle")) { return; } - if (SliderBase._isActionKey(e) && target && !target.hasAttribute("ui5-input")) { + if (SliderBase._isActionKey(e) && target && !target.hasAttribute("ui5-slider-tooltip")) { e.preventDefault(); this._isUserInteraction = true; @@ -320,32 +319,17 @@ abstract class SliderBase extends UI5Element { } } - _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(); - } + _onInputKeydown() {} - if (isEnter(e)) { - this._updateInputValue(); - this._updateValueFromInput(e); - } - } - - _onInputChange() { - if (this._valueOnInteractionStart !== this.value) { - this.fireDecoratorEvent("change"); - } - } + _onTooltipChange(e: CustomEvent) { + const value = e.detail.value; - _onInputInput() { - this.fireDecoratorEvent("input"); + this._updateInputValue(value); + this._updateValueFromInput(value); } - _updateValueFromInput(e: Event) { - const input = e.target as HTMLInputElement; - const value = parseFloat(input.value); + _updateValueFromInput(fieldValue: string) { + const value = parseFloat(fieldValue); this._isInputValueValid = value >= this._effectiveMin && value <= this._effectiveMax; if (!this._isInputValueValid) { diff --git a/packages/main/src/SliderBaseTemplate.tsx b/packages/main/src/SliderBaseTemplate.tsx index c97542a8f62d..440f33e3378a 100644 --- a/packages/main/src/SliderBaseTemplate.tsx +++ b/packages/main/src/SliderBaseTemplate.tsx @@ -61,7 +61,6 @@ export default function SliderBaseTemplate(this: SliderBase, hooks?: { {this.editableTooltip && <> {this._ariaDescribedByInputText} - {this._ariaLabelledByInputText} } diff --git a/packages/main/src/SliderTemplate.tsx b/packages/main/src/SliderTemplate.tsx index 0ba28fdf1a57..0d1907beb87d 100644 --- a/packages/main/src/SliderTemplate.tsx +++ b/packages/main/src/SliderTemplate.tsx @@ -1,8 +1,8 @@ import directionArrows from "@ui5/webcomponents-icons/dist/direction-arrows.js"; import type Slider from "./Slider.js"; import Icon from "./Icon.js"; -import Input from "./Input.js"; import SliderBaseTemplate from "./SliderBaseTemplate.js"; +import SliderTooltip from "./SliderTooltip.js"; export default function SliderTemplate(this: Slider) { return SliderBaseTemplate.call(this, { @@ -47,6 +47,7 @@ export function handles(this: Slider) { aria-describedby={this._ariaDescribedByHandleText} data-sap-focus-ref part="handle" + id="handle1" > - {this.showTooltip && -
- {this.editableTooltip ? - - : - {this.tooltipValue} - } -
- } + + + ); } diff --git a/packages/main/src/SliderTooltip.ts b/packages/main/src/SliderTooltip.ts new file mode 100644 index 000000000000..0785a92d3c11 --- /dev/null +++ b/packages/main/src/SliderTooltip.ts @@ -0,0 +1,168 @@ +import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; +import { customElement, i18n, property } from "@ui5/webcomponents-base/dist/decorators.js"; +import ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js"; + +import SliderTooltipTemplate from "./SliderTooltipTemplate.js"; +import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; +import SliderTooltipCss from "./generated/themes/SliderTooltip.css.js"; +import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js"; +import { isEnter, isF2, isTabNext } from "@ui5/webcomponents-base/dist/Keys.js"; +import type Input from "./Input.js"; + +import { + SLIDER_TOOLTIP_INPUT_LABEL, +} from "./generated/i18n/i18n-defaults.js"; +import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js"; +import type { Interval } from "@ui5/webcomponents-base/dist/types.js"; + +type SliderTooltipChangeEventDetails = { + value: string, +}; + +/** + * @class + * + * ### Overview + * @constructor + * @extends UI5Element + * @private + */ +@customElement({ + tag: "ui5-slider-tooltip", + renderer: jsxRenderer, + template: SliderTooltipTemplate, + styles: SliderTooltipCss, +}) + +@event("change") + +@event("forward-focus") + +class SliderTooltip extends UI5Element { + eventDetails!: { + "change": SliderTooltipChangeEventDetails, + "forward-focus": void + }; + + @property() + value?: string; + + @property() + inputValue?: string; + + @property({ type: Boolean }) + open = false; + + @property({ type: Number }) + min = 0; + + @property({ type: Number }) + max = 100; + + @property({ type: Boolean }) + editable = false; + + @property() + valueState: `${ValueState}` = "None"; + + @property({ type: Object }) + followRef?: HTMLElement; + + _repoisitionInterval?: Interval; + _repositionTooltipBound: () => void; + + @i18n("@ui5/webcomponents") + static i18nBundle: I18nBundle; + + constructor() { + super(); + + this._repositionTooltipBound = this.repositionTooltip.bind(this); + } + + onBeforeRendering(): void { + this.setAttribute("popover", "manual"); + this.togglePopover(this.open); + + if (this.open) { + this.repositionTooltip(); + this.attachGlobalScrollHandler(); + } else { + this.detachGlobalScrollHandler(); + } + } + + repositionTooltip(): void { + const followRefRect = this.followRef?.getBoundingClientRect(); + if (!followRefRect) { + return; + } + + this.style.top = `${followRefRect.top}px`; + + // center the tooltip's mid and opener's mid + const tooltipWidth = this.offsetWidth; + const followRefWidth = followRefRect.width; + + const tooltipMidX = tooltipWidth / 2; + const followRefMidX = followRefWidth / 2; + + this.style.left = `${followRefRect.left + followRefMidX - tooltipMidX}px`; + } + + isValueValid(value: string): boolean { + return parseFloat(value) >= this.min && parseFloat(value) <= this.max; + } + + attachGlobalScrollHandler() { + document.addEventListener("scroll", this._repositionTooltipBound, { capture: true }); + } + + detachGlobalScrollHandler() { + document.removeEventListener("scroll", this._repositionTooltipBound, { capture: true }); + } + + _keydown(e: KeyboardEvent) { + if (isF2(e) || isTabNext(e)) { + e.preventDefault(); + + if (!this.isValueValid(this.inputRef.value)) { + const value = this.value; + this.inputRef.value = value || ""; + } + + this.valueState = ValueState.None; + + this.fireDecoratorEvent("change", { value: this.inputRef.value }); + this.fireDecoratorEvent("forward-focus"); + } + + if (isEnter(e)) { + if (!this.isValueValid(this.inputRef.value)) { + this.valueState = ValueState.Negative; + + return; + } + + this.valueState = ValueState.None; + + this.fireDecoratorEvent("change", { value: this.inputRef.value }); + } + } + + get inputRef() { + return this.shadowRoot?.querySelector("ui5-input") as Input; + } + + get _ariaLabelledByInputText() { + return SliderTooltip.i18nBundle.getText(SLIDER_TOOLTIP_INPUT_LABEL); + } +} + +SliderTooltip.define(); + +export type { + SliderTooltipChangeEventDetails, +}; + +export default SliderTooltip; diff --git a/packages/main/src/SliderTooltipTemplate.tsx b/packages/main/src/SliderTooltipTemplate.tsx new file mode 100644 index 000000000000..aa43a6c3be9c --- /dev/null +++ b/packages/main/src/SliderTooltipTemplate.tsx @@ -0,0 +1,26 @@ +import Input from "./Input.js"; +import type SliderTooltip from "./SliderTooltip.js"; + +export default function SliderTooltipTemplate(this: SliderTooltip) { + return ( +
+ {this.editable ? + : + {this.value} + } + + {this.editable && <> + {this._ariaLabelledByInputText} + } +
+ ); +} diff --git a/packages/main/src/themes/SliderTooltip.css b/packages/main/src/themes/SliderTooltip.css new file mode 100644 index 000000000000..c91a5b1581ea --- /dev/null +++ b/packages/main/src/themes/SliderTooltip.css @@ -0,0 +1,34 @@ +@import "./InvisibleTextStyles.css"; + +:host(:popover-open) { + margin: 0; + display: inline-block; + margin-top: -28px; + height: 1rem; + background: var(--sapBackgroundColor); + border: none; + box-shadow: var(--sapContent_Shadow1); + border-radius: 0.0625rem; + padding: 0 0.5rem; + font-family: var(--sapFontFamily); + font-size: var(--sapFontSmallSize); + color: var(--sapTextColor); + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; +} + +:host([editable]:popover-open) { + cursor: pointer; + height: fit-content; + border: none; + padding: 0; + margin-top: -3.25rem; + box-shadow: none; +} + +:host([editable]:popover-open) [ui5-input] { + width: min-content; + text-align: center; +} \ No newline at end of file From ec55c301013df2cf412b4f9f6101bbeafa5ee9a9 Mon Sep 17 00:00:00 2001 From: "Martin R. Hristov" Date: Tue, 4 Mar 2025 09:51:39 +0200 Subject: [PATCH 2/2] chore: improvements --- packages/main/test/pages/Slider.html | 63 ++++++---------------------- 1 file changed, 12 insertions(+), 51 deletions(-) diff --git a/packages/main/test/pages/Slider.html b/packages/main/test/pages/Slider.html index caa84a7e9042..83eb54604437 100644 --- a/packages/main/test/pages/Slider.html +++ b/packages/main/test/pages/Slider.html @@ -24,63 +24,24 @@ -
-

Basic Slider

- -

Basic Slider with tooltip

- + -

Disabled Slider with tickmarks and labels

- +
+
+
+ + -

Slider with tickmarks

- -

Slider with many steps and small width

- - + -

Inactive slider with no steps and tooltip

- +
+
+
+ + -

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)

- - -

Basic RTL Slider

- -
- -
-

Event Testing Slider

- -

Event Testing Result Slider

- -
- -