From 91cc99cd52072d3524918cc1018763fdfd9a6594 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 28 May 2026 14:39:17 +0200 Subject: [PATCH 01/12] fix(slider-web): declare CSS module type to fix side-effect import error --- packages/pluggableWidgets/slider-web/typings/declare-svg.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/pluggableWidgets/slider-web/typings/declare-svg.ts b/packages/pluggableWidgets/slider-web/typings/declare-svg.ts index e6958d5a9f..d966c93688 100644 --- a/packages/pluggableWidgets/slider-web/typings/declare-svg.ts +++ b/packages/pluggableWidgets/slider-web/typings/declare-svg.ts @@ -2,3 +2,5 @@ declare module "*.svg" { const content: string; export = content; } + +declare module "*.css"; From 5ac14cada6cedcca9de0142d19bbe1af56d1c4d6 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 28 May 2026 18:03:33 +0200 Subject: [PATCH 02/12] fix(slider-web): format marks and tooltip with locale-aware decimal separator Co-Authored-By: Claude Sonnet 4.6 --- .../slider-web/src/Slider.editorPreview.tsx | 4 +- .../slider-web/src/components/Container.tsx | 44 ++++++++++++++----- .../src/utils/createHandleRender.tsx | 11 +++-- .../slider-web/src/utils/helpers.ts | 16 +++++++ .../slider-web/src/utils/marks.ts | 9 ++-- .../slider-web/src/utils/useMarks.ts | 6 ++- 6 files changed, 69 insertions(+), 21 deletions(-) diff --git a/packages/pluggableWidgets/slider-web/src/Slider.editorPreview.tsx b/packages/pluggableWidgets/slider-web/src/Slider.editorPreview.tsx index 8ad56d8f9d..2acbffe635 100644 --- a/packages/pluggableWidgets/slider-web/src/Slider.editorPreview.tsx +++ b/packages/pluggableWidgets/slider-web/src/Slider.editorPreview.tsx @@ -11,11 +11,13 @@ export function getPreviewCss(): string { export function preview(props: SliderPreviewProps): ReactNode { const values = getPreviewValues(props); + const decimalPlaces = props.decimalPlaces ?? 2; const marks = createMarks({ min: values.min, max: values.max, numberOfMarks: props.noOfMarkers ?? 2, - decimalPlaces: props.decimalPlaces ?? 2 + decimalPlaces, + decimalSeparator: "." }); const style = getStyleProp({ orientation: props.orientation, diff --git a/packages/pluggableWidgets/slider-web/src/components/Container.tsx b/packages/pluggableWidgets/slider-web/src/components/Container.tsx index b26135235b..23faa8c72c 100644 --- a/packages/pluggableWidgets/slider-web/src/components/Container.tsx +++ b/packages/pluggableWidgets/slider-web/src/components/Container.tsx @@ -1,15 +1,15 @@ +import { NumberFormatter } from "mendix"; import { ReactElement, useMemo, useRef } from "react"; -import { SliderContainerProps } from "../../typings/SliderProps"; +import { Slider as SliderComponent } from "./Slider"; +import { SliderContainerProps } from "../../typings/SliderProps"; import { createHandleRender } from "../utils/createHandleRender"; +import { getDecimalSeparator, getSliderLabel } from "../utils/helpers"; import { getStyleProp, isVertical, maxProp, minProp, stepProp } from "../utils/prop-utils"; import { useMarks } from "../utils/useMarks"; import { useNumber } from "../utils/useNumber"; -import { getSliderLabel } from "../utils/helpers"; import { useOnChangeDebounced } from "../utils/useOnChangeDebounced"; -import { Slider as SliderComponent } from "./Slider"; - export function Container(props: SliderContainerProps): ReactElement { const min = useNumber(minProp(props)); const max = useNumber(maxProp(props)); @@ -30,19 +30,39 @@ interface InnerContainerProps extends SliderContainerProps { function InnerContainer(props: InnerContainerProps): ReactElement { const sliderRef = useRef(null); - const handleRender = props.showTooltip - ? createHandleRender({ - tooltip: props.tooltip, - tooltipType: props.tooltipType, - tooltipAlwaysVisible: props.tooltipAlwaysVisible, - sliderRef - }) - : undefined; + + const decimalSeparator = useMemo( + () => getDecimalSeparator(props.valueAttribute.formatter as NumberFormatter), + [props.valueAttribute.formatter] + ); + + const handleRender = useMemo( + () => + props.showTooltip + ? createHandleRender({ + tooltip: props.tooltip, + tooltipType: props.tooltipType, + tooltipAlwaysVisible: props.tooltipAlwaysVisible, + sliderRef, + decimalPlaces: props.decimalPlaces, + decimalSeparator + }) + : undefined, + [ + props.showTooltip, + props.tooltip, + props.tooltipType, + props.tooltipAlwaysVisible, + props.decimalPlaces, + decimalSeparator + ] + ); const { onChange } = useOnChangeDebounced({ valueAttribute: props.valueAttribute, onChange: props.onChange }); const marks = useMarks({ noOfMarkers: props.noOfMarkers, decimalPlaces: props.decimalPlaces, + decimalSeparator, min: props.min, max: props.max }); diff --git a/packages/pluggableWidgets/slider-web/src/utils/createHandleRender.tsx b/packages/pluggableWidgets/slider-web/src/utils/createHandleRender.tsx index 5b1a86bdc4..09d37e5001 100644 --- a/packages/pluggableWidgets/slider-web/src/utils/createHandleRender.tsx +++ b/packages/pluggableWidgets/slider-web/src/utils/createHandleRender.tsx @@ -2,6 +2,7 @@ import { SliderProps as RcSliderProps } from "@rc-component/slider"; import RcTooltip from "@rc-component/tooltip"; import { DynamicValue } from "mendix"; import { RefObject } from "react"; +import { formatNumber } from "./helpers"; import "@rc-component/tooltip/assets/bootstrap.css"; @@ -10,26 +11,30 @@ type CreateHandleRenderProps = { tooltipType: "value" | "customText"; tooltipAlwaysVisible: boolean; sliderRef: RefObject; + decimalPlaces: number; + decimalSeparator: string; }; export function createHandleRender({ tooltip, tooltipType, tooltipAlwaysVisible, - sliderRef + sliderRef, + decimalPlaces, + decimalSeparator }: CreateHandleRenderProps): RcSliderProps["handleRender"] | undefined { const isCustomText = tooltipType === "customText"; const handleRender: RcSliderProps["handleRender"] = (node, props) => { const { dragging, index, ...restProps } = props; - const overlay =
{tooltip?.value ?? ""}
; + const overlay = isCustomText ?
{tooltip?.value ?? ""}
: null; return ( sliderRef.current ?? document.body} defaultVisible prefixCls="rc-slider-tooltip" - overlay={isCustomText ? overlay : restProps.value} + overlay={isCustomText ? overlay : formatNumber(restProps.value, decimalPlaces, decimalSeparator)} trigger={["hover", "click", "focus"]} visible={tooltipAlwaysVisible || dragging} placement="top" diff --git a/packages/pluggableWidgets/slider-web/src/utils/helpers.ts b/packages/pluggableWidgets/slider-web/src/utils/helpers.ts index df4a690418..42ea8571f6 100644 --- a/packages/pluggableWidgets/slider-web/src/utils/helpers.ts +++ b/packages/pluggableWidgets/slider-web/src/utils/helpers.ts @@ -1 +1,17 @@ +import { Big } from "big.js"; +import { NumberFormatter } from "mendix"; + export const getSliderLabel = (sliderId: string): Element | null => document.querySelector(`label[for="${sliderId}"]`); + +export function getDecimalSeparator(formatter: NumberFormatter): string { + const formatted = formatter.format(new Big("1.1")); + return formatted.charAt(1); +} + +export function formatNumber(value: number, decimalPlaces: number, decimalSeparator: string): string { + if (decimalPlaces === 0) { + return String(Math.round(value)); + } + const formatted = value.toFixed(decimalPlaces); + return decimalSeparator !== "." ? formatted.replace(".", decimalSeparator) : formatted; +} diff --git a/packages/pluggableWidgets/slider-web/src/utils/marks.ts b/packages/pluggableWidgets/slider-web/src/utils/marks.ts index 2c27eba8e6..e24ab46190 100644 --- a/packages/pluggableWidgets/slider-web/src/utils/marks.ts +++ b/packages/pluggableWidgets/slider-web/src/utils/marks.ts @@ -1,11 +1,13 @@ import { MarkObj } from "@rc-component/slider/lib/Marks"; import { ReactNode } from "react"; +import { formatNumber } from "./helpers"; export type Marks = Record; export interface CreateMarksParams { numberOfMarks: number; decimalPlaces: number; + decimalSeparator: string; min: number; max: number; } @@ -20,12 +22,13 @@ export function createMarks(params: CreateMarksParams): Marks | undefined { } const marks: Marks = {}; - const { numberOfMarks, decimalPlaces, min, max } = params; + const { numberOfMarks, decimalPlaces, decimalSeparator, min, max } = params; const interval = (max - min) / numberOfMarks; for (let i = 0; i <= numberOfMarks; i++) { - const value = parseFloat((min + i * interval).toFixed(decimalPlaces)); - marks[value] = value.toString(); + const rawValue = min + i * interval; + const key = parseFloat(rawValue.toFixed(decimalPlaces)); + marks[key] = formatNumber(rawValue, decimalPlaces, decimalSeparator); } return marks; diff --git a/packages/pluggableWidgets/slider-web/src/utils/useMarks.ts b/packages/pluggableWidgets/slider-web/src/utils/useMarks.ts index 4e8dd10dee..7604c249e5 100644 --- a/packages/pluggableWidgets/slider-web/src/utils/useMarks.ts +++ b/packages/pluggableWidgets/slider-web/src/utils/useMarks.ts @@ -4,21 +4,23 @@ import { createMarks } from "./marks"; type UseMarksParams = { noOfMarkers: number; decimalPlaces: number; + decimalSeparator: string; min?: number; max?: number; }; export function useMarks(props: UseMarksParams): ReturnType { - const { noOfMarkers, decimalPlaces, min = 0, max = 100 } = props; + const { noOfMarkers, decimalPlaces, decimalSeparator, min = 0, max = 100 } = props; return useMemo( () => createMarks({ numberOfMarks: noOfMarkers, decimalPlaces, + decimalSeparator, min, max }), - [min, max, noOfMarkers, decimalPlaces] + [min, max, noOfMarkers, decimalPlaces, decimalSeparator] ); } From 621875afe8b40d4fe5bdd19729efaf0a1f222f51 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 28 May 2026 18:03:46 +0200 Subject: [PATCH 03/12] test(slider-web): update unit and E2E tests for decimal formatting Co-Authored-By: Claude Sonnet 4.6 --- .../slider-web/e2e/Slider.spec.js | 12 ++- .../__tests__/createHandleRender.spec.tsx | 75 +++++++++++++++++++ .../src/utils/__tests__/marks.spec.ts | 57 ++++++++++++++ 3 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 packages/pluggableWidgets/slider-web/src/utils/__tests__/createHandleRender.spec.tsx create mode 100644 packages/pluggableWidgets/slider-web/src/utils/__tests__/marks.spec.ts diff --git a/packages/pluggableWidgets/slider-web/e2e/Slider.spec.js b/packages/pluggableWidgets/slider-web/e2e/Slider.spec.js index 02376b95e0..7d85e10064 100644 --- a/packages/pluggableWidgets/slider-web/e2e/Slider.spec.js +++ b/packages/pluggableWidgets/slider-web/e2e/Slider.spec.js @@ -6,19 +6,17 @@ test.describe("Slider", () => { await page.goto("/"); await waitForMendixApp(page); - const minimumValue = await page.inputValue(".mx-name-textBoxMinimumValue input"); const minimumValueText = await page .locator(".mx-name-sliderContext .rc-slider-mark > span") .first() .textContent(); - await expect(minimumValueText).toBe(minimumValue); + await expect(minimumValueText).toBe("0.000"); - const maximumValue = await page.inputValue(".mx-name-textBoxMaximumValue input"); const maximumValueText = await page .locator(".mx-name-sliderContext .rc-slider-mark > span") .nth(2) .textContent(); - await expect(maximumValueText).toBe(maximumValue); + await expect(maximumValueText).toBe("20.000"); const value = await page.inputValue(".mx-name-textBoxValue input"); await expect(value).toContain("10"); @@ -38,13 +36,13 @@ test.describe("Slider", () => { .locator(".mx-name-sliderNoContext .rc-slider-mark > span") .first() .textContent(); - await expect(minimumValueText).toBe("0"); + await expect(minimumValueText).toBe("0.0"); const maximumValueText = await page .locator(".mx-name-sliderNoContext .rc-slider-mark > span") .nth(2) .textContent(); - await expect(maximumValueText).toBe("100"); + await expect(maximumValueText).toBe("100.0"); const handleStyle = await page.locator(".mx-name-sliderNoContext .rc-slider-handle").getAttribute("style"); await expect(handleStyle).toContain("left: 0%;"); @@ -173,7 +171,7 @@ test.describe("Slider", () => { await waitForMendixApp(page); await expect(page.locator(".mx-name-slider")).toBeVisible(); - await expect(page.locator(".mx-name-slider .rc-slider-mark > span").nth(1)).toHaveText("140000"); + await expect(page.locator(".mx-name-slider .rc-slider-mark > span").nth(1)).toHaveText("140000.0"); await expect(page.locator(".mx-name-slider .rc-slider-mark > span").nth(1)).toHaveAttribute( "style", /left: 33.3333%;/ diff --git a/packages/pluggableWidgets/slider-web/src/utils/__tests__/createHandleRender.spec.tsx b/packages/pluggableWidgets/slider-web/src/utils/__tests__/createHandleRender.spec.tsx new file mode 100644 index 0000000000..6291a4e330 --- /dev/null +++ b/packages/pluggableWidgets/slider-web/src/utils/__tests__/createHandleRender.spec.tsx @@ -0,0 +1,75 @@ +import { SliderProps as RcSliderProps } from "@rc-component/slider"; +import { createRef, ReactElement } from "react"; +import { createHandleRender } from "../createHandleRender"; + +const defaultRenderProps = { + dragging: false, + index: 0, + prefixCls: "rc-slider-handle", + draggingDelete: false, + onFocus: jest.fn(), + onBlur: jest.fn() +}; + +const mockNode =
; + +function buildHandleRender( + decimalPlaces: number, + tooltipType: "value" | "customText" = "value", + decimalSeparator = "." +): NonNullable { + const sliderRef = createRef(); + return createHandleRender({ + tooltipType, + tooltipAlwaysVisible: true, + sliderRef, + decimalPlaces, + decimalSeparator + })!; +} + +describe("createHandleRender tooltip value formatting", () => { + it("formats whole number with trailing zeros when decimalPlaces=2", () => { + const result = buildHandleRender(2)(mockNode, { ...defaultRenderProps, value: 10 } as any) as ReactElement; + expect(result.props.overlay).toBe("10.00"); + }); + + it("formats partial decimal with trailing zero when decimalPlaces=2", () => { + const result = buildHandleRender(2)(mockNode, { + ...defaultRenderProps, + value: 9.2 + } as any) as ReactElement; + expect(result.props.overlay).toBe("9.20"); + }); + + it("formats value without decimals when decimalPlaces=0", () => { + const result = buildHandleRender(0)(mockNode, { ...defaultRenderProps, value: 10 } as any) as ReactElement; + expect(result.props.overlay).toBe("10"); + }); + + it("uses locale decimal separator", () => { + const result = buildHandleRender( + 2, + "value", + "," + )(mockNode, { + ...defaultRenderProps, + value: 9.2 + } as any) as ReactElement; + expect(result.props.overlay).toBe("9,20"); + }); + + it("renders custom text tooltip ignoring value formatting", () => { + const sliderRef = createRef(); + const handleRender = createHandleRender({ + tooltip: { value: "custom label" } as any, + tooltipType: "customText", + tooltipAlwaysVisible: true, + sliderRef, + decimalPlaces: 2, + decimalSeparator: "." + })!; + const result = handleRender(mockNode, { ...defaultRenderProps, value: 10 } as any) as ReactElement; + expect(result.props.overlay.props.children).toBe("custom label"); + }); +}); diff --git a/packages/pluggableWidgets/slider-web/src/utils/__tests__/marks.spec.ts b/packages/pluggableWidgets/slider-web/src/utils/__tests__/marks.spec.ts new file mode 100644 index 0000000000..ce90184943 --- /dev/null +++ b/packages/pluggableWidgets/slider-web/src/utils/__tests__/marks.spec.ts @@ -0,0 +1,57 @@ +import { createMarks } from "../marks"; + +describe("createMarks", () => { + it("forces trailing zeros when decimalPlaces > 0 and value is whole number", () => { + const marks = createMarks({ numberOfMarks: 2, decimalPlaces: 2, decimalSeparator: ".", min: 0, max: 10 }); + expect(marks).toBeDefined(); + expect(marks![0]).toBe("0.00"); + expect(marks![5]).toBe("5.00"); + expect(marks![10]).toBe("10.00"); + }); + + it("forces trailing zeros when decimalPlaces > 0 and value has fewer decimals", () => { + const marks = createMarks({ numberOfMarks: 2, decimalPlaces: 2, decimalSeparator: ".", min: 0, max: 9.2 }); + expect(marks).toBeDefined(); + expect(marks![4.6]).toBe("4.60"); + expect(marks![9.2]).toBe("9.20"); + }); + + it("does not add decimal places when decimalPlaces is 0", () => { + const marks = createMarks({ numberOfMarks: 4, decimalPlaces: 0, decimalSeparator: ".", min: 0, max: 100 }); + expect(marks).toBeDefined(); + expect(marks![0]).toBe("0"); + expect(marks![25]).toBe("25"); + expect(marks![100]).toBe("100"); + }); + + it("uses locale decimal separator", () => { + const marks = createMarks({ numberOfMarks: 2, decimalPlaces: 2, decimalSeparator: ",", min: 0, max: 10 }); + expect(marks![0]).toBe("0,00"); + expect(marks![5]).toBe("5,00"); + expect(marks![10]).toBe("10,00"); + }); + + it("uses correct numeric keys for fractional values with comma locale", () => { + const marks = createMarks({ numberOfMarks: 2, decimalPlaces: 2, decimalSeparator: ",", min: 0, max: 9.2 }); + expect(marks![4.6]).toBe("4,60"); + expect(marks![9.2]).toBe("9,20"); + }); + + it("returns undefined when numberOfMarks is 0", () => { + expect( + createMarks({ numberOfMarks: 0, decimalPlaces: 2, decimalSeparator: ".", min: 0, max: 100 }) + ).toBeUndefined(); + }); + + it("returns undefined when min equals max", () => { + expect( + createMarks({ numberOfMarks: 4, decimalPlaces: 2, decimalSeparator: ".", min: 5, max: 5 }) + ).toBeUndefined(); + }); + + it("returns undefined when min > max", () => { + expect( + createMarks({ numberOfMarks: 2, decimalPlaces: 1, decimalSeparator: ".", min: 10, max: 5 }) + ).toBeUndefined(); + }); +}); From 8894831438b2763525aa5508eff5ff31ee6b53f0 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 28 May 2026 18:03:52 +0200 Subject: [PATCH 04/12] chore(slider-web): add changelog entry for decimal places formatting fix Co-Authored-By: Claude Sonnet 4.6 --- packages/pluggableWidgets/slider-web/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/pluggableWidgets/slider-web/CHANGELOG.md b/packages/pluggableWidgets/slider-web/CHANGELOG.md index 20079df8d5..0753d662c3 100644 --- a/packages/pluggableWidgets/slider-web/CHANGELOG.md +++ b/packages/pluggableWidgets/slider-web/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Fixed + +- We fixed mark labels and tooltip values not preserving trailing zeros when decimal places are configured (e.g., `10` now displays as `10.00` and `9.2` as `9.20` when two decimal places are set). + ## [3.0.2] - 2026-02-19 ### Fixed From bbf482e5cb54ba91212436dbd54166f00ac0e24e Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Fri, 29 May 2026 15:17:32 +0200 Subject: [PATCH 05/12] refactor(slider-web): use built-in NumberFormatter for marks and tooltip Replace manual toFixed + decimal-separator swapping with the value attribute's own Mendix NumberFormatter, overriding only decimalPrecision via withConfig. The locale decimal separator and thousands grouping now follow the user's session locale and the attribute's groupDigits setting automatically. Mark keys remain rounded with parseFloat(rawValue.toFixed(dp)) (always "." based, locale-safe) so rc-slider positions dots where their labels read. Fix E2E context expectations to match the model's decimalPlaces=1 output (0.0 / 20.0). Co-Authored-By: Claude Opus 4.8 --- .../pluggableWidgets/slider-web/CHANGELOG.md | 2 +- .../slider-web/e2e/Slider.spec.js | 4 +- .../slider-web/src/Slider.editorPreview.tsx | 2 +- .../slider-web/src/components/Container.tsx | 22 +++---- .../__tests__/createHandleRender.spec.tsx | 14 +++-- .../src/utils/__tests__/helpers.spec.ts | 62 +++++++++++++++++++ .../src/utils/__tests__/marks.spec.ts | 33 +++++++--- .../src/utils/createHandleRender.tsx | 10 ++- .../slider-web/src/utils/helpers.ts | 22 ++++--- .../slider-web/src/utils/marks.ts | 11 ++-- .../slider-web/src/utils/useMarks.ts | 9 +-- 11 files changed, 136 insertions(+), 55 deletions(-) create mode 100644 packages/pluggableWidgets/slider-web/src/utils/__tests__/helpers.spec.ts diff --git a/packages/pluggableWidgets/slider-web/CHANGELOG.md b/packages/pluggableWidgets/slider-web/CHANGELOG.md index 0753d662c3..bf6d52181b 100644 --- a/packages/pluggableWidgets/slider-web/CHANGELOG.md +++ b/packages/pluggableWidgets/slider-web/CHANGELOG.md @@ -8,7 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Fixed -- We fixed mark labels and tooltip values not preserving trailing zeros when decimal places are configured (e.g., `10` now displays as `10.00` and `9.2` as `9.20` when two decimal places are set). +- We fixed mark labels and tooltip values not respecting the value attribute's number formatting. They now use the attribute's own formatter with the configured number of decimal places, so the decimal separator and thousands grouping follow the current locale and the attribute's settings (e.g., `10` displays as `10.00` and `9.2` as `9.20` with two decimal places, and grouping like `1,000,000` is preserved when enabled). ## [3.0.2] - 2026-02-19 diff --git a/packages/pluggableWidgets/slider-web/e2e/Slider.spec.js b/packages/pluggableWidgets/slider-web/e2e/Slider.spec.js index 7d85e10064..0681753473 100644 --- a/packages/pluggableWidgets/slider-web/e2e/Slider.spec.js +++ b/packages/pluggableWidgets/slider-web/e2e/Slider.spec.js @@ -10,13 +10,13 @@ test.describe("Slider", () => { .locator(".mx-name-sliderContext .rc-slider-mark > span") .first() .textContent(); - await expect(minimumValueText).toBe("0.000"); + await expect(minimumValueText).toBe("0.0"); const maximumValueText = await page .locator(".mx-name-sliderContext .rc-slider-mark > span") .nth(2) .textContent(); - await expect(maximumValueText).toBe("20.000"); + await expect(maximumValueText).toBe("20.0"); const value = await page.inputValue(".mx-name-textBoxValue input"); await expect(value).toContain("10"); diff --git a/packages/pluggableWidgets/slider-web/src/Slider.editorPreview.tsx b/packages/pluggableWidgets/slider-web/src/Slider.editorPreview.tsx index 2acbffe635..bf31ffcf8a 100644 --- a/packages/pluggableWidgets/slider-web/src/Slider.editorPreview.tsx +++ b/packages/pluggableWidgets/slider-web/src/Slider.editorPreview.tsx @@ -17,7 +17,7 @@ export function preview(props: SliderPreviewProps): ReactNode { max: values.max, numberOfMarks: props.noOfMarkers ?? 2, decimalPlaces, - decimalSeparator: "." + format: (value: number) => value.toFixed(decimalPlaces) }); const style = getStyleProp({ orientation: props.orientation, diff --git a/packages/pluggableWidgets/slider-web/src/components/Container.tsx b/packages/pluggableWidgets/slider-web/src/components/Container.tsx index 23faa8c72c..10a9b3105f 100644 --- a/packages/pluggableWidgets/slider-web/src/components/Container.tsx +++ b/packages/pluggableWidgets/slider-web/src/components/Container.tsx @@ -4,7 +4,7 @@ import { ReactElement, useMemo, useRef } from "react"; import { Slider as SliderComponent } from "./Slider"; import { SliderContainerProps } from "../../typings/SliderProps"; import { createHandleRender } from "../utils/createHandleRender"; -import { getDecimalSeparator, getSliderLabel } from "../utils/helpers"; +import { createValueFormatter, getSliderLabel } from "../utils/helpers"; import { getStyleProp, isVertical, maxProp, minProp, stepProp } from "../utils/prop-utils"; import { useMarks } from "../utils/useMarks"; import { useNumber } from "../utils/useNumber"; @@ -31,9 +31,9 @@ interface InnerContainerProps extends SliderContainerProps { function InnerContainer(props: InnerContainerProps): ReactElement { const sliderRef = useRef(null); - const decimalSeparator = useMemo( - () => getDecimalSeparator(props.valueAttribute.formatter as NumberFormatter), - [props.valueAttribute.formatter] + const format = useMemo( + () => createValueFormatter(props.valueAttribute.formatter as NumberFormatter, props.decimalPlaces), + [props.valueAttribute.formatter, props.decimalPlaces] ); const handleRender = useMemo( @@ -44,25 +44,17 @@ function InnerContainer(props: InnerContainerProps): ReactElement { tooltipType: props.tooltipType, tooltipAlwaysVisible: props.tooltipAlwaysVisible, sliderRef, - decimalPlaces: props.decimalPlaces, - decimalSeparator + format }) : undefined, - [ - props.showTooltip, - props.tooltip, - props.tooltipType, - props.tooltipAlwaysVisible, - props.decimalPlaces, - decimalSeparator - ] + [props.showTooltip, props.tooltip, props.tooltipType, props.tooltipAlwaysVisible, format] ); const { onChange } = useOnChangeDebounced({ valueAttribute: props.valueAttribute, onChange: props.onChange }); const marks = useMarks({ noOfMarkers: props.noOfMarkers, decimalPlaces: props.decimalPlaces, - decimalSeparator, + format, min: props.min, max: props.max }); diff --git a/packages/pluggableWidgets/slider-web/src/utils/__tests__/createHandleRender.spec.tsx b/packages/pluggableWidgets/slider-web/src/utils/__tests__/createHandleRender.spec.tsx index 6291a4e330..d29273207a 100644 --- a/packages/pluggableWidgets/slider-web/src/utils/__tests__/createHandleRender.spec.tsx +++ b/packages/pluggableWidgets/slider-web/src/utils/__tests__/createHandleRender.spec.tsx @@ -13,6 +13,14 @@ const defaultRenderProps = { const mockNode =
; +// Deterministic stand-in for createValueFormatter's output. +const formatWith = + (decimalPlaces: number, decimalSeparator = ".") => + (value: number): string => { + const fixed = value.toFixed(decimalPlaces); + return decimalSeparator === "." ? fixed : fixed.replace(".", decimalSeparator); + }; + function buildHandleRender( decimalPlaces: number, tooltipType: "value" | "customText" = "value", @@ -23,8 +31,7 @@ function buildHandleRender( tooltipType, tooltipAlwaysVisible: true, sliderRef, - decimalPlaces, - decimalSeparator + format: formatWith(decimalPlaces, decimalSeparator) })!; } @@ -66,8 +73,7 @@ describe("createHandleRender tooltip value formatting", () => { tooltipType: "customText", tooltipAlwaysVisible: true, sliderRef, - decimalPlaces: 2, - decimalSeparator: "." + format: formatWith(2) })!; const result = handleRender(mockNode, { ...defaultRenderProps, value: 10 } as any) as ReactElement; expect(result.props.overlay.props.children).toBe("custom label"); diff --git a/packages/pluggableWidgets/slider-web/src/utils/__tests__/helpers.spec.ts b/packages/pluggableWidgets/slider-web/src/utils/__tests__/helpers.spec.ts new file mode 100644 index 0000000000..80492644e5 --- /dev/null +++ b/packages/pluggableWidgets/slider-web/src/utils/__tests__/helpers.spec.ts @@ -0,0 +1,62 @@ +import { Big } from "big.js"; +import { NumberFormatter } from "mendix"; +import { createValueFormatter } from "../helpers"; + +/** + * Minimal stand-in for the Mendix runtime NumberFormatter. It mimics the two behaviours the + * widget relies on: `withConfig` returns a new formatter with the merged config, and `format` + * honours `decimalPrecision`, `groupDigits` and the configured locale separators. + */ +function fakeNumberFormatter( + config: { groupDigits: boolean; decimalPrecision?: number }, + locale: { decimal: string; group: string } = { decimal: ".", group: "," } +): NumberFormatter { + return { + type: "number", + config, + withConfig: (next: { groupDigits: boolean; decimalPrecision?: number }) => + fakeNumberFormatter({ ...config, ...next }, locale), + format: (value?: Big) => { + if (value == null) { + return ""; + } + const fixed = value.toNumber().toFixed(config.decimalPrecision ?? 0); + const [intPart, fracPart] = fixed.split("."); + const grouped = config.groupDigits ? intPart.replace(/\B(?=(\d{3})+(?!\d))/g, locale.group) : intPart; + return fracPart != null ? `${grouped}${locale.decimal}${fracPart}` : grouped; + }, + parse: () => ({ valid: false }) + } as unknown as NumberFormatter; +} + +describe("createValueFormatter", () => { + it("redefines the formatter's decimal precision (forces trailing zeros)", () => { + const format = createValueFormatter(fakeNumberFormatter({ groupDigits: false }), 2); + expect(format(10)).toBe("10.00"); + expect(format(9.2)).toBe("9.20"); + }); + + it("formats without decimals when decimalPlaces is 0", () => { + const format = createValueFormatter(fakeNumberFormatter({ groupDigits: false }), 0); + expect(format(10)).toBe("10"); + expect(format(9.7)).toBe("10"); + }); + + it("respects the locale decimal separator from the formatter", () => { + const format = createValueFormatter( + fakeNumberFormatter({ groupDigits: false }, { decimal: ",", group: "." }), + 2 + ); + expect(format(9.2)).toBe("9,20"); + }); + + it("keeps the attribute's thousands grouping when groupDigits is enabled", () => { + const format = createValueFormatter(fakeNumberFormatter({ groupDigits: true }), 0); + expect(format(1000000)).toBe("1,000,000"); + }); + + it("omits thousands grouping when groupDigits is disabled", () => { + const format = createValueFormatter(fakeNumberFormatter({ groupDigits: false }), 0); + expect(format(1000000)).toBe("1000000"); + }); +}); diff --git a/packages/pluggableWidgets/slider-web/src/utils/__tests__/marks.spec.ts b/packages/pluggableWidgets/slider-web/src/utils/__tests__/marks.spec.ts index ce90184943..9ba6e3106e 100644 --- a/packages/pluggableWidgets/slider-web/src/utils/__tests__/marks.spec.ts +++ b/packages/pluggableWidgets/slider-web/src/utils/__tests__/marks.spec.ts @@ -1,8 +1,16 @@ import { createMarks } from "../marks"; +// Simple deterministic formatter standing in for createValueFormatter's output. +const formatWith = + (decimalPlaces: number, decimalSeparator = ".") => + (value: number): string => { + const fixed = value.toFixed(decimalPlaces); + return decimalSeparator === "." ? fixed : fixed.replace(".", decimalSeparator); + }; + describe("createMarks", () => { it("forces trailing zeros when decimalPlaces > 0 and value is whole number", () => { - const marks = createMarks({ numberOfMarks: 2, decimalPlaces: 2, decimalSeparator: ".", min: 0, max: 10 }); + const marks = createMarks({ numberOfMarks: 2, decimalPlaces: 2, format: formatWith(2), min: 0, max: 10 }); expect(marks).toBeDefined(); expect(marks![0]).toBe("0.00"); expect(marks![5]).toBe("5.00"); @@ -10,14 +18,14 @@ describe("createMarks", () => { }); it("forces trailing zeros when decimalPlaces > 0 and value has fewer decimals", () => { - const marks = createMarks({ numberOfMarks: 2, decimalPlaces: 2, decimalSeparator: ".", min: 0, max: 9.2 }); + const marks = createMarks({ numberOfMarks: 2, decimalPlaces: 2, format: formatWith(2), min: 0, max: 9.2 }); expect(marks).toBeDefined(); expect(marks![4.6]).toBe("4.60"); expect(marks![9.2]).toBe("9.20"); }); it("does not add decimal places when decimalPlaces is 0", () => { - const marks = createMarks({ numberOfMarks: 4, decimalPlaces: 0, decimalSeparator: ".", min: 0, max: 100 }); + const marks = createMarks({ numberOfMarks: 4, decimalPlaces: 0, format: formatWith(0), min: 0, max: 100 }); expect(marks).toBeDefined(); expect(marks![0]).toBe("0"); expect(marks![25]).toBe("25"); @@ -25,33 +33,42 @@ describe("createMarks", () => { }); it("uses locale decimal separator", () => { - const marks = createMarks({ numberOfMarks: 2, decimalPlaces: 2, decimalSeparator: ",", min: 0, max: 10 }); + const marks = createMarks({ numberOfMarks: 2, decimalPlaces: 2, format: formatWith(2, ","), min: 0, max: 10 }); expect(marks![0]).toBe("0,00"); expect(marks![5]).toBe("5,00"); expect(marks![10]).toBe("10,00"); }); it("uses correct numeric keys for fractional values with comma locale", () => { - const marks = createMarks({ numberOfMarks: 2, decimalPlaces: 2, decimalSeparator: ",", min: 0, max: 9.2 }); + const marks = createMarks({ numberOfMarks: 2, decimalPlaces: 2, format: formatWith(2, ","), min: 0, max: 9.2 }); expect(marks![4.6]).toBe("4,60"); expect(marks![9.2]).toBe("9,20"); }); + it("rounds mark keys to the configured decimal places so dots align with their labels", () => { + // 9 intervals over 0..20 yields repeating decimals (e.g. 6.6667). The key must be the + // rounded value (6.7) so rc-slider positions the dot where the label reads. + const marks = createMarks({ numberOfMarks: 9, decimalPlaces: 1, format: formatWith(1), min: 0, max: 20 }); + expect(Object.keys(marks!)).toContain("6.7"); + expect(Object.keys(marks!)).not.toContain("6.666666666666667"); + expect(marks![6.7]).toBe("6.7"); + }); + it("returns undefined when numberOfMarks is 0", () => { expect( - createMarks({ numberOfMarks: 0, decimalPlaces: 2, decimalSeparator: ".", min: 0, max: 100 }) + createMarks({ numberOfMarks: 0, decimalPlaces: 2, format: formatWith(2), min: 0, max: 100 }) ).toBeUndefined(); }); it("returns undefined when min equals max", () => { expect( - createMarks({ numberOfMarks: 4, decimalPlaces: 2, decimalSeparator: ".", min: 5, max: 5 }) + createMarks({ numberOfMarks: 4, decimalPlaces: 2, format: formatWith(2), min: 5, max: 5 }) ).toBeUndefined(); }); it("returns undefined when min > max", () => { expect( - createMarks({ numberOfMarks: 2, decimalPlaces: 1, decimalSeparator: ".", min: 10, max: 5 }) + createMarks({ numberOfMarks: 2, decimalPlaces: 1, format: formatWith(1), min: 10, max: 5 }) ).toBeUndefined(); }); }); diff --git a/packages/pluggableWidgets/slider-web/src/utils/createHandleRender.tsx b/packages/pluggableWidgets/slider-web/src/utils/createHandleRender.tsx index 09d37e5001..2bbdb155a5 100644 --- a/packages/pluggableWidgets/slider-web/src/utils/createHandleRender.tsx +++ b/packages/pluggableWidgets/slider-web/src/utils/createHandleRender.tsx @@ -2,7 +2,7 @@ import { SliderProps as RcSliderProps } from "@rc-component/slider"; import RcTooltip from "@rc-component/tooltip"; import { DynamicValue } from "mendix"; import { RefObject } from "react"; -import { formatNumber } from "./helpers"; +import { ValueFormatter } from "./helpers"; import "@rc-component/tooltip/assets/bootstrap.css"; @@ -11,8 +11,7 @@ type CreateHandleRenderProps = { tooltipType: "value" | "customText"; tooltipAlwaysVisible: boolean; sliderRef: RefObject; - decimalPlaces: number; - decimalSeparator: string; + format: ValueFormatter; }; export function createHandleRender({ @@ -20,8 +19,7 @@ export function createHandleRender({ tooltipType, tooltipAlwaysVisible, sliderRef, - decimalPlaces, - decimalSeparator + format }: CreateHandleRenderProps): RcSliderProps["handleRender"] | undefined { const isCustomText = tooltipType === "customText"; @@ -34,7 +32,7 @@ export function createHandleRender({ getTooltipContainer={() => sliderRef.current ?? document.body} defaultVisible prefixCls="rc-slider-tooltip" - overlay={isCustomText ? overlay : formatNumber(restProps.value, decimalPlaces, decimalSeparator)} + overlay={isCustomText ? overlay : format(restProps.value)} trigger={["hover", "click", "focus"]} visible={tooltipAlwaysVisible || dragging} placement="top" diff --git a/packages/pluggableWidgets/slider-web/src/utils/helpers.ts b/packages/pluggableWidgets/slider-web/src/utils/helpers.ts index 42ea8571f6..3f896d8ee7 100644 --- a/packages/pluggableWidgets/slider-web/src/utils/helpers.ts +++ b/packages/pluggableWidgets/slider-web/src/utils/helpers.ts @@ -3,15 +3,17 @@ import { NumberFormatter } from "mendix"; export const getSliderLabel = (sliderId: string): Element | null => document.querySelector(`label[for="${sliderId}"]`); -export function getDecimalSeparator(formatter: NumberFormatter): string { - const formatted = formatter.format(new Big("1.1")); - return formatted.charAt(1); -} +export type ValueFormatter = (value: number) => string; -export function formatNumber(value: number, decimalPlaces: number, decimalSeparator: string): string { - if (decimalPlaces === 0) { - return String(Math.round(value)); - } - const formatted = value.toFixed(decimalPlaces); - return decimalSeparator !== "." ? formatted.replace(".", decimalSeparator) : formatted; +/** + * Builds a value formatter from the attribute's own Mendix NumberFormatter, overriding only the + * decimal precision. Reusing the runtime formatter means the decimal separator and thousands + * grouping follow the user's session locale and the attribute's `groupDigits` setting automatically. + */ +export function createValueFormatter(formatter: NumberFormatter, decimalPlaces: number): ValueFormatter { + const configured = formatter.withConfig({ + groupDigits: formatter.config.groupDigits, + decimalPrecision: decimalPlaces + }); + return (value: number) => configured.format(new Big(value)); } diff --git a/packages/pluggableWidgets/slider-web/src/utils/marks.ts b/packages/pluggableWidgets/slider-web/src/utils/marks.ts index e24ab46190..1f8eb53142 100644 --- a/packages/pluggableWidgets/slider-web/src/utils/marks.ts +++ b/packages/pluggableWidgets/slider-web/src/utils/marks.ts @@ -1,13 +1,13 @@ import { MarkObj } from "@rc-component/slider/lib/Marks"; import { ReactNode } from "react"; -import { formatNumber } from "./helpers"; +import { ValueFormatter } from "./helpers"; export type Marks = Record; export interface CreateMarksParams { numberOfMarks: number; decimalPlaces: number; - decimalSeparator: string; + format: ValueFormatter; min: number; max: number; } @@ -22,13 +22,16 @@ export function createMarks(params: CreateMarksParams): Marks | undefined { } const marks: Marks = {}; - const { numberOfMarks, decimalPlaces, decimalSeparator, min, max } = params; + const { numberOfMarks, decimalPlaces, format, min, max } = params; const interval = (max - min) / numberOfMarks; for (let i = 0; i <= numberOfMarks; i++) { const rawValue = min + i * interval; + // Round the key to the configured precision so rc-slider positions the dot where its + // label reads. toFixed always uses "." here, so parseFloat is locale-safe (unlike parsing + // the formatted label, which may contain a comma decimal separator). const key = parseFloat(rawValue.toFixed(decimalPlaces)); - marks[key] = formatNumber(rawValue, decimalPlaces, decimalSeparator); + marks[key] = format(rawValue); } return marks; diff --git a/packages/pluggableWidgets/slider-web/src/utils/useMarks.ts b/packages/pluggableWidgets/slider-web/src/utils/useMarks.ts index 7604c249e5..3626fb7921 100644 --- a/packages/pluggableWidgets/slider-web/src/utils/useMarks.ts +++ b/packages/pluggableWidgets/slider-web/src/utils/useMarks.ts @@ -1,26 +1,27 @@ import { useMemo } from "react"; +import { ValueFormatter } from "./helpers"; import { createMarks } from "./marks"; type UseMarksParams = { noOfMarkers: number; decimalPlaces: number; - decimalSeparator: string; + format: ValueFormatter; min?: number; max?: number; }; export function useMarks(props: UseMarksParams): ReturnType { - const { noOfMarkers, decimalPlaces, decimalSeparator, min = 0, max = 100 } = props; + const { noOfMarkers, decimalPlaces, format, min = 0, max = 100 } = props; return useMemo( () => createMarks({ numberOfMarks: noOfMarkers, decimalPlaces, - decimalSeparator, + format, min, max }), - [min, max, noOfMarkers, decimalPlaces, decimalSeparator] + [min, max, noOfMarkers, decimalPlaces, format] ); } From 3890b016b010b3a7f93e06cc2bc4d3e4fde1f7c9 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Mon, 1 Jun 2026 14:45:37 +0200 Subject: [PATCH 06/12] fix(changelog): update description for decimal places and locale formatting fix --- packages/pluggableWidgets/slider-web/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pluggableWidgets/slider-web/CHANGELOG.md b/packages/pluggableWidgets/slider-web/CHANGELOG.md index bf6d52181b..5d80c65204 100644 --- a/packages/pluggableWidgets/slider-web/CHANGELOG.md +++ b/packages/pluggableWidgets/slider-web/CHANGELOG.md @@ -8,7 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Fixed -- We fixed mark labels and tooltip values not respecting the value attribute's number formatting. They now use the attribute's own formatter with the configured number of decimal places, so the decimal separator and thousands grouping follow the current locale and the attribute's settings (e.g., `10` displays as `10.00` and `9.2` as `9.20` with two decimal places, and grouping like `1,000,000` is preserved when enabled). +- We fixed mark labels and tooltip values not respecting the decimal places and locale settings configured on the value attribute. Numbers now display with the correct decimal separator, digit grouping, and number of decimal places. ## [3.0.2] - 2026-02-19 From 1f0e386b820215a2fcafea4f715f44c80edcd8c4 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Mon, 1 Jun 2026 14:37:21 +0200 Subject: [PATCH 07/12] fix(range-slider-web): format marks and tooltip with locale-aware decimal places MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace raw .toString() / number in mark labels and numeric tooltips with createValueFormatter — the same pattern used in slider-web. Marks use lowerBoundAttribute.formatter; each tooltip handle uses its own formatter (lower for index 0, upper for index 1). The locale decimal separator, thousands grouping, and decimalPlaces are now all respected. Add unit tests for createValueFormatter (helpers.spec.ts) and createMarks (marks.spec.ts) mirroring the slider-web coverage. Update editorPreview to pass a deterministic preview formatter. Co-Authored-By: Claude Sonnet 4.6 --- .../range-slider-web/CHANGELOG.md | 4 + .../src/RangeSlider.editorPreview.tsx | 4 +- .../src/components/Container.tsx | 20 ++++- .../src/utils/__tests__/helpers.spec.ts | 57 +++++++++++++ .../src/utils/__tests__/marks.spec.ts | 80 +++++++++++++++++++ .../range-slider-web/src/utils/helpers.ts | 17 ++++ .../range-slider-web/src/utils/marks.ts | 11 ++- .../range-slider-web/src/utils/useMarks.ts | 7 +- 8 files changed, 193 insertions(+), 7 deletions(-) create mode 100644 packages/pluggableWidgets/range-slider-web/src/utils/__tests__/helpers.spec.ts create mode 100644 packages/pluggableWidgets/range-slider-web/src/utils/__tests__/marks.spec.ts create mode 100644 packages/pluggableWidgets/range-slider-web/src/utils/helpers.ts diff --git a/packages/pluggableWidgets/range-slider-web/CHANGELOG.md b/packages/pluggableWidgets/range-slider-web/CHANGELOG.md index e214f1de22..325ffd94cc 100644 --- a/packages/pluggableWidgets/range-slider-web/CHANGELOG.md +++ b/packages/pluggableWidgets/range-slider-web/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Fixed + +- Mark labels and numeric tooltips now respect the configured `decimalPlaces` and the user's session locale (decimal separator and thousands grouping). Previously, marks and tooltips rendered raw numbers via `.toString()`, ignoring locale and decimal precision settings. + ## [3.0.1] - 2026-02-10 ### Added diff --git a/packages/pluggableWidgets/range-slider-web/src/RangeSlider.editorPreview.tsx b/packages/pluggableWidgets/range-slider-web/src/RangeSlider.editorPreview.tsx index 91679d8cd3..2e4538d092 100644 --- a/packages/pluggableWidgets/range-slider-web/src/RangeSlider.editorPreview.tsx +++ b/packages/pluggableWidgets/range-slider-web/src/RangeSlider.editorPreview.tsx @@ -11,11 +11,13 @@ export function getPreviewCss(): string { export function preview(props: RangeSliderPreviewProps): ReactNode { const { min, max, step, value } = getPreviewValues(props); + const decimalPlaces = props.decimalPlaces ?? 0; const marks = createMarks({ min, max, numberOfMarks: props.noOfMarkers ?? 1, - decimalPlaces: props.decimalPlaces ?? 0 + decimalPlaces, + format: (v: number) => v.toFixed(decimalPlaces) }); const style = getStyleProp({ diff --git a/packages/pluggableWidgets/range-slider-web/src/components/Container.tsx b/packages/pluggableWidgets/range-slider-web/src/components/Container.tsx index 55990a2fa7..2be08fb8d4 100644 --- a/packages/pluggableWidgets/range-slider-web/src/components/Container.tsx +++ b/packages/pluggableWidgets/range-slider-web/src/components/Container.tsx @@ -1,5 +1,7 @@ +import { NumberFormatter } from "mendix"; import { ReactElement, useMemo, useRef } from "react"; import { RangeSliderContainerProps } from "../../typings/RangeSliderProps"; +import { createValueFormatter } from "../utils/helpers"; import { useNumber } from "../utils/useNumber"; import { RangeSlider as RangeComponent } from "./RangeSlider"; import { useOnChangeDebounced } from "../utils/useOnChangeDebounced"; @@ -41,9 +43,22 @@ function InnerContainer(props: InnerContainerProps): ReactElement { const { onChange } = useOnChangeDebounced({ lowerBoundAttribute, upperBoundAttribute, onChange: props.onChange }); + const formatLower = useMemo( + () => createValueFormatter(lowerBoundAttribute.formatter as NumberFormatter, props.decimalPlaces), + // eslint-disable-next-line react-hooks/exhaustive-deps + [lowerBoundAttribute.formatter, props.decimalPlaces] + ); + + const formatUpper = useMemo( + () => createValueFormatter(upperBoundAttribute.formatter as NumberFormatter, props.decimalPlaces), + // eslint-disable-next-line react-hooks/exhaustive-deps + [upperBoundAttribute.formatter, props.decimalPlaces] + ); + const marks = useMarks({ noOfMarkers: props.noOfMarkers, decimalPlaces: props.decimalPlaces, + format: formatLower, min, max }); @@ -73,7 +88,10 @@ function InnerContainer(props: InnerContainerProps): ReactElement { max={props.max} handleRender={(node, handleProps) => { const isCustomText = tooltipTypeCheck[handleProps.index] === "customText"; - const displayValue = isCustomText ? (tooltipValue[handleProps.index]?.value ?? "") : handleProps.value; + const fmt = handleProps.index === 0 ? formatLower : formatUpper; + const displayValue = isCustomText + ? (tooltipValue[handleProps.index]?.value ?? "") + : fmt(handleProps.value); return ( + fakeNumberFormatter({ ...config, ...next }, locale), + format: (value?: Big) => { + if (value == null) { + return ""; + } + const fixed = value.toNumber().toFixed(config.decimalPrecision ?? 0); + const [intPart, fracPart] = fixed.split("."); + const grouped = config.groupDigits ? intPart.replace(/\B(?=(\d{3})+(?!\d))/g, locale.group) : intPart; + return fracPart != null ? `${grouped}${locale.decimal}${fracPart}` : grouped; + }, + parse: () => ({ valid: false }) + } as unknown as NumberFormatter; +} + +describe("createValueFormatter", () => { + it("redefines the formatter's decimal precision (forces trailing zeros)", () => { + const format = createValueFormatter(fakeNumberFormatter({ groupDigits: false }), 2); + expect(format(10)).toBe("10.00"); + expect(format(9.2)).toBe("9.20"); + }); + + it("formats without decimals when decimalPlaces is 0", () => { + const format = createValueFormatter(fakeNumberFormatter({ groupDigits: false }), 0); + expect(format(10)).toBe("10"); + expect(format(9.7)).toBe("10"); + }); + + it("respects the locale decimal separator from the formatter", () => { + const format = createValueFormatter( + fakeNumberFormatter({ groupDigits: false }, { decimal: ",", group: "." }), + 2 + ); + expect(format(9.2)).toBe("9,20"); + }); + + it("keeps the attribute's thousands grouping when groupDigits is enabled", () => { + const format = createValueFormatter(fakeNumberFormatter({ groupDigits: true }), 0); + expect(format(1000000)).toBe("1,000,000"); + }); + + it("omits thousands grouping when groupDigits is disabled", () => { + const format = createValueFormatter(fakeNumberFormatter({ groupDigits: false }), 0); + expect(format(1000000)).toBe("1000000"); + }); +}); diff --git a/packages/pluggableWidgets/range-slider-web/src/utils/__tests__/marks.spec.ts b/packages/pluggableWidgets/range-slider-web/src/utils/__tests__/marks.spec.ts new file mode 100644 index 0000000000..a4c84c2341 --- /dev/null +++ b/packages/pluggableWidgets/range-slider-web/src/utils/__tests__/marks.spec.ts @@ -0,0 +1,80 @@ +import { createMarks } from "../marks"; + +// Simple deterministic formatter standing in for createValueFormatter's output. +const formatWith = + (decimalPlaces: number, decimalSeparator = ".") => + (value: number): string => { + const fixed = value.toFixed(decimalPlaces); + return decimalSeparator === "." ? fixed : fixed.replace(".", decimalSeparator); + }; + +describe("createMarks", () => { + it("forces trailing zeros when decimalPlaces > 0 and value is whole number", () => { + const marks = createMarks({ numberOfMarks: 2, decimalPlaces: 2, format: formatWith(2), min: 0, max: 10 }); + expect(marks).toBeDefined(); + expect(marks![0]).toBe("0.00"); + expect(marks![5]).toBe("5.00"); + expect(marks![10]).toBe("10.00"); + }); + + it("forces trailing zeros when decimalPlaces > 0 and value has fewer decimals", () => { + const marks = createMarks({ numberOfMarks: 2, decimalPlaces: 2, format: formatWith(2), min: 0, max: 9.2 }); + expect(marks).toBeDefined(); + expect(marks![4.6]).toBe("4.60"); + expect(marks![9.2]).toBe("9.20"); + }); + + it("does not add decimal places when decimalPlaces is 0", () => { + const marks = createMarks({ numberOfMarks: 4, decimalPlaces: 0, format: formatWith(0), min: 0, max: 100 }); + expect(marks).toBeDefined(); + expect(marks![0]).toBe("0"); + expect(marks![25]).toBe("25"); + expect(marks![100]).toBe("100"); + }); + + it("uses locale decimal separator", () => { + const marks = createMarks({ numberOfMarks: 2, decimalPlaces: 2, format: formatWith(2, ","), min: 0, max: 10 }); + expect(marks![0]).toBe("0,00"); + expect(marks![5]).toBe("5,00"); + expect(marks![10]).toBe("10,00"); + }); + + it("uses correct numeric keys for fractional values with comma locale", () => { + const marks = createMarks({ + numberOfMarks: 2, + decimalPlaces: 2, + format: formatWith(2, ","), + min: 0, + max: 9.2 + }); + expect(marks![4.6]).toBe("4,60"); + expect(marks![9.2]).toBe("9,20"); + }); + + it("rounds mark keys to the configured decimal places so dots align with their labels", () => { + // 9 intervals over 0..20 yields repeating decimals (e.g. 6.6667). The key must be the + // rounded value (6.7) so rc-slider positions the dot where the label reads. + const marks = createMarks({ numberOfMarks: 9, decimalPlaces: 1, format: formatWith(1), min: 0, max: 20 }); + expect(Object.keys(marks!)).toContain("6.7"); + expect(Object.keys(marks!)).not.toContain("6.666666666666667"); + expect(marks![6.7]).toBe("6.7"); + }); + + it("returns undefined when numberOfMarks is 0", () => { + expect( + createMarks({ numberOfMarks: 0, decimalPlaces: 2, format: formatWith(2), min: 0, max: 100 }) + ).toBeUndefined(); + }); + + it("returns undefined when min equals max", () => { + expect( + createMarks({ numberOfMarks: 4, decimalPlaces: 2, format: formatWith(2), min: 5, max: 5 }) + ).toBeUndefined(); + }); + + it("returns undefined when min > max", () => { + expect( + createMarks({ numberOfMarks: 2, decimalPlaces: 1, format: formatWith(1), min: 10, max: 5 }) + ).toBeUndefined(); + }); +}); diff --git a/packages/pluggableWidgets/range-slider-web/src/utils/helpers.ts b/packages/pluggableWidgets/range-slider-web/src/utils/helpers.ts new file mode 100644 index 0000000000..25ccb2dbee --- /dev/null +++ b/packages/pluggableWidgets/range-slider-web/src/utils/helpers.ts @@ -0,0 +1,17 @@ +import { Big } from "big.js"; +import { NumberFormatter } from "mendix"; + +export type ValueFormatter = (value: number) => string; + +/** + * Builds a value formatter from the attribute's own Mendix NumberFormatter, overriding only the + * decimal precision. Reusing the runtime formatter means the decimal separator and thousands + * grouping follow the user's session locale and the attribute's `groupDigits` setting automatically. + */ +export function createValueFormatter(formatter: NumberFormatter, decimalPlaces: number): ValueFormatter { + const configured = formatter.withConfig({ + groupDigits: formatter.config.groupDigits, + decimalPrecision: decimalPlaces + }); + return (value: number) => configured.format(new Big(value)); +} diff --git a/packages/pluggableWidgets/range-slider-web/src/utils/marks.ts b/packages/pluggableWidgets/range-slider-web/src/utils/marks.ts index 2c27eba8e6..aa4d529e36 100644 --- a/packages/pluggableWidgets/range-slider-web/src/utils/marks.ts +++ b/packages/pluggableWidgets/range-slider-web/src/utils/marks.ts @@ -1,11 +1,13 @@ import { MarkObj } from "@rc-component/slider/lib/Marks"; import { ReactNode } from "react"; +import { ValueFormatter } from "./helpers"; export type Marks = Record; export interface CreateMarksParams { numberOfMarks: number; decimalPlaces: number; + format: ValueFormatter; min: number; max: number; } @@ -20,12 +22,15 @@ export function createMarks(params: CreateMarksParams): Marks | undefined { } const marks: Marks = {}; - const { numberOfMarks, decimalPlaces, min, max } = params; + const { numberOfMarks, decimalPlaces, format, min, max } = params; const interval = (max - min) / numberOfMarks; for (let i = 0; i <= numberOfMarks; i++) { - const value = parseFloat((min + i * interval).toFixed(decimalPlaces)); - marks[value] = value.toString(); + const rawValue = min + i * interval; + // Round the key to the configured precision so rc-slider positions the dot where its + // label reads. toFixed always uses "." here, so parseFloat is locale-safe. + const key = parseFloat(rawValue.toFixed(decimalPlaces)); + marks[key] = format(rawValue); } return marks; diff --git a/packages/pluggableWidgets/range-slider-web/src/utils/useMarks.ts b/packages/pluggableWidgets/range-slider-web/src/utils/useMarks.ts index 4e8dd10dee..3626fb7921 100644 --- a/packages/pluggableWidgets/range-slider-web/src/utils/useMarks.ts +++ b/packages/pluggableWidgets/range-slider-web/src/utils/useMarks.ts @@ -1,24 +1,27 @@ import { useMemo } from "react"; +import { ValueFormatter } from "./helpers"; import { createMarks } from "./marks"; type UseMarksParams = { noOfMarkers: number; decimalPlaces: number; + format: ValueFormatter; min?: number; max?: number; }; export function useMarks(props: UseMarksParams): ReturnType { - const { noOfMarkers, decimalPlaces, min = 0, max = 100 } = props; + const { noOfMarkers, decimalPlaces, format, min = 0, max = 100 } = props; return useMemo( () => createMarks({ numberOfMarks: noOfMarkers, decimalPlaces, + format, min, max }), - [min, max, noOfMarkers, decimalPlaces] + [min, max, noOfMarkers, decimalPlaces, format] ); } From b7153bb40d5524eef1eb67a8d692aea9f114366e Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Tue, 2 Jun 2026 13:21:22 +0200 Subject: [PATCH 08/12] refactor(slider-web,range-slider-web): move shared formatting logic to widget-plugin-platform createValueFormatter and createMarks extracted to widget-plugin-platform/utils so both slider-web and range-slider-web consume a single implementation without duplication. Tests moved to platform; duplicate tests removed from widget packages. Range-slider-web now uses a single formatter derived from lowerBoundAttribute. Co-Authored-By: Claude Sonnet 4.6 --- .../range-slider-web/package.json | 2 +- .../src/components/Container.tsx | 13 ++---- .../range-slider-web/src/utils/helpers.ts | 18 +------- .../range-slider-web/src/utils/marks.ts | 42 +++--------------- .../pluggableWidgets/slider-web/package.json | 2 +- .../slider-web/src/Slider.editorPreview.tsx | 2 +- .../slider-web/src/components/Container.tsx | 3 +- .../__tests__/createHandleRender.spec.tsx | 1 - .../slider-web/src/utils/helpers.ts | 18 +------- .../slider-web/src/utils/marks.ts | 43 +++---------------- .../slider-web/src/utils/useMarks.ts | 1 + .../widget-plugin-platform/jest.config.cjs | 1 + .../widget-plugin-platform/package.json | 1 + .../utils/__tests__/number-formatter.spec.ts} | 2 +- .../src/utils/__tests__/slider-marks.spec.ts} | 5 +-- .../src/utils/number-formatter.ts | 12 ++++++ .../src/utils/slider-marks.ts | 33 ++++++++++++++ 17 files changed, 70 insertions(+), 129 deletions(-) rename packages/{pluggableWidgets/range-slider-web/src/utils/__tests__/helpers.spec.ts => shared/widget-plugin-platform/src/utils/__tests__/number-formatter.spec.ts} (97%) rename packages/{pluggableWidgets/range-slider-web/src/utils/__tests__/marks.spec.ts => shared/widget-plugin-platform/src/utils/__tests__/slider-marks.spec.ts} (90%) create mode 100644 packages/shared/widget-plugin-platform/src/utils/number-formatter.ts create mode 100644 packages/shared/widget-plugin-platform/src/utils/slider-marks.ts diff --git a/packages/pluggableWidgets/range-slider-web/package.json b/packages/pluggableWidgets/range-slider-web/package.json index ff46e5d998..224f32a04c 100644 --- a/packages/pluggableWidgets/range-slider-web/package.json +++ b/packages/pluggableWidgets/range-slider-web/package.json @@ -44,6 +44,7 @@ }, "dependencies": { "@mendix/widget-plugin-component-kit": "workspace:*", + "@mendix/widget-plugin-platform": "workspace:*", "@rc-component/slider": "^1.0.1", "@rc-component/tooltip": "^1.3.3", "classnames": "^2.5.1" @@ -55,7 +56,6 @@ "@mendix/prettier-config-web-widgets": "workspace:*", "@mendix/run-e2e": "workspace:*", "@mendix/widget-plugin-hooks": "workspace:*", - "@mendix/widget-plugin-platform": "workspace:*", "@types/rc-slider": "^8.6.6", "@types/rc-tooltip": "^3.7.7", "cross-env": "^7.0.3" diff --git a/packages/pluggableWidgets/range-slider-web/src/components/Container.tsx b/packages/pluggableWidgets/range-slider-web/src/components/Container.tsx index 2be08fb8d4..bc63039ae2 100644 --- a/packages/pluggableWidgets/range-slider-web/src/components/Container.tsx +++ b/packages/pluggableWidgets/range-slider-web/src/components/Container.tsx @@ -43,22 +43,16 @@ function InnerContainer(props: InnerContainerProps): ReactElement { const { onChange } = useOnChangeDebounced({ lowerBoundAttribute, upperBoundAttribute, onChange: props.onChange }); - const formatLower = useMemo( + const format = useMemo( () => createValueFormatter(lowerBoundAttribute.formatter as NumberFormatter, props.decimalPlaces), // eslint-disable-next-line react-hooks/exhaustive-deps [lowerBoundAttribute.formatter, props.decimalPlaces] ); - const formatUpper = useMemo( - () => createValueFormatter(upperBoundAttribute.formatter as NumberFormatter, props.decimalPlaces), - // eslint-disable-next-line react-hooks/exhaustive-deps - [upperBoundAttribute.formatter, props.decimalPlaces] - ); - const marks = useMarks({ noOfMarkers: props.noOfMarkers, decimalPlaces: props.decimalPlaces, - format: formatLower, + format, min, max }); @@ -88,10 +82,9 @@ function InnerContainer(props: InnerContainerProps): ReactElement { max={props.max} handleRender={(node, handleProps) => { const isCustomText = tooltipTypeCheck[handleProps.index] === "customText"; - const fmt = handleProps.index === 0 ? formatLower : formatUpper; const displayValue = isCustomText ? (tooltipValue[handleProps.index]?.value ?? "") - : fmt(handleProps.value); + : format(handleProps.value); return ( string; - -/** - * Builds a value formatter from the attribute's own Mendix NumberFormatter, overriding only the - * decimal precision. Reusing the runtime formatter means the decimal separator and thousands - * grouping follow the user's session locale and the attribute's `groupDigits` setting automatically. - */ -export function createValueFormatter(formatter: NumberFormatter, decimalPlaces: number): ValueFormatter { - const configured = formatter.withConfig({ - groupDigits: formatter.config.groupDigits, - decimalPrecision: decimalPlaces - }); - return (value: number) => configured.format(new Big(value)); -} +export { createValueFormatter, type ValueFormatter } from "@mendix/widget-plugin-platform/utils/number-formatter"; diff --git a/packages/pluggableWidgets/range-slider-web/src/utils/marks.ts b/packages/pluggableWidgets/range-slider-web/src/utils/marks.ts index aa4d529e36..1b43c8ba8e 100644 --- a/packages/pluggableWidgets/range-slider-web/src/utils/marks.ts +++ b/packages/pluggableWidgets/range-slider-web/src/utils/marks.ts @@ -1,37 +1,5 @@ -import { MarkObj } from "@rc-component/slider/lib/Marks"; -import { ReactNode } from "react"; -import { ValueFormatter } from "./helpers"; - -export type Marks = Record; - -export interface CreateMarksParams { - numberOfMarks: number; - decimalPlaces: number; - format: ValueFormatter; - min: number; - max: number; -} - -export function isParamsValidToCalcMarks(params: CreateMarksParams): boolean { - return params.numberOfMarks > 0 && params.min < params.max; -} - -export function createMarks(params: CreateMarksParams): Marks | undefined { - if (!isParamsValidToCalcMarks(params)) { - return; - } - - const marks: Marks = {}; - const { numberOfMarks, decimalPlaces, format, min, max } = params; - const interval = (max - min) / numberOfMarks; - - for (let i = 0; i <= numberOfMarks; i++) { - const rawValue = min + i * interval; - // Round the key to the configured precision so rc-slider positions the dot where its - // label reads. toFixed always uses "." here, so parseFloat is locale-safe. - const key = parseFloat(rawValue.toFixed(decimalPlaces)); - marks[key] = format(rawValue); - } - - return marks; -} +export { + createMarks, + isParamsValidToCalcMarks, + type CreateMarksParams +} from "@mendix/widget-plugin-platform/utils/slider-marks"; diff --git a/packages/pluggableWidgets/slider-web/package.json b/packages/pluggableWidgets/slider-web/package.json index 746d7a1d39..1f20f33976 100644 --- a/packages/pluggableWidgets/slider-web/package.json +++ b/packages/pluggableWidgets/slider-web/package.json @@ -44,6 +44,7 @@ }, "dependencies": { "@mendix/widget-plugin-component-kit": "workspace:*", + "@mendix/widget-plugin-platform": "workspace:*", "@rc-component/slider": "^1.0.1", "@rc-component/tooltip": "^1.3.3", "classnames": "^2.5.1" @@ -55,7 +56,6 @@ "@mendix/prettier-config-web-widgets": "workspace:*", "@mendix/run-e2e": "workspace:*", "@mendix/widget-plugin-hooks": "workspace:*", - "@mendix/widget-plugin-platform": "workspace:*", "cross-env": "^7.0.3" } } diff --git a/packages/pluggableWidgets/slider-web/src/Slider.editorPreview.tsx b/packages/pluggableWidgets/slider-web/src/Slider.editorPreview.tsx index bf31ffcf8a..09622685c1 100644 --- a/packages/pluggableWidgets/slider-web/src/Slider.editorPreview.tsx +++ b/packages/pluggableWidgets/slider-web/src/Slider.editorPreview.tsx @@ -17,7 +17,7 @@ export function preview(props: SliderPreviewProps): ReactNode { max: values.max, numberOfMarks: props.noOfMarkers ?? 2, decimalPlaces, - format: (value: number) => value.toFixed(decimalPlaces) + format: (v: number) => v.toFixed(decimalPlaces) }); const style = getStyleProp({ orientation: props.orientation, diff --git a/packages/pluggableWidgets/slider-web/src/components/Container.tsx b/packages/pluggableWidgets/slider-web/src/components/Container.tsx index 10a9b3105f..ae2b324bef 100644 --- a/packages/pluggableWidgets/slider-web/src/components/Container.tsx +++ b/packages/pluggableWidgets/slider-web/src/components/Container.tsx @@ -1,8 +1,8 @@ import { NumberFormatter } from "mendix"; import { ReactElement, useMemo, useRef } from "react"; +import { SliderContainerProps } from "../../typings/SliderProps"; import { Slider as SliderComponent } from "./Slider"; -import { SliderContainerProps } from "../../typings/SliderProps"; import { createHandleRender } from "../utils/createHandleRender"; import { createValueFormatter, getSliderLabel } from "../utils/helpers"; import { getStyleProp, isVertical, maxProp, minProp, stepProp } from "../utils/prop-utils"; @@ -33,6 +33,7 @@ function InnerContainer(props: InnerContainerProps): ReactElement { const format = useMemo( () => createValueFormatter(props.valueAttribute.formatter as NumberFormatter, props.decimalPlaces), + // eslint-disable-next-line react-hooks/exhaustive-deps [props.valueAttribute.formatter, props.decimalPlaces] ); diff --git a/packages/pluggableWidgets/slider-web/src/utils/__tests__/createHandleRender.spec.tsx b/packages/pluggableWidgets/slider-web/src/utils/__tests__/createHandleRender.spec.tsx index d29273207a..33eef11b9d 100644 --- a/packages/pluggableWidgets/slider-web/src/utils/__tests__/createHandleRender.spec.tsx +++ b/packages/pluggableWidgets/slider-web/src/utils/__tests__/createHandleRender.spec.tsx @@ -13,7 +13,6 @@ const defaultRenderProps = { const mockNode =
; -// Deterministic stand-in for createValueFormatter's output. const formatWith = (decimalPlaces: number, decimalSeparator = ".") => (value: number): string => { diff --git a/packages/pluggableWidgets/slider-web/src/utils/helpers.ts b/packages/pluggableWidgets/slider-web/src/utils/helpers.ts index 3f896d8ee7..f4a9afeb49 100644 --- a/packages/pluggableWidgets/slider-web/src/utils/helpers.ts +++ b/packages/pluggableWidgets/slider-web/src/utils/helpers.ts @@ -1,19 +1,3 @@ -import { Big } from "big.js"; -import { NumberFormatter } from "mendix"; +export { createValueFormatter, type ValueFormatter } from "@mendix/widget-plugin-platform/utils/number-formatter"; export const getSliderLabel = (sliderId: string): Element | null => document.querySelector(`label[for="${sliderId}"]`); - -export type ValueFormatter = (value: number) => string; - -/** - * Builds a value formatter from the attribute's own Mendix NumberFormatter, overriding only the - * decimal precision. Reusing the runtime formatter means the decimal separator and thousands - * grouping follow the user's session locale and the attribute's `groupDigits` setting automatically. - */ -export function createValueFormatter(formatter: NumberFormatter, decimalPlaces: number): ValueFormatter { - const configured = formatter.withConfig({ - groupDigits: formatter.config.groupDigits, - decimalPrecision: decimalPlaces - }); - return (value: number) => configured.format(new Big(value)); -} diff --git a/packages/pluggableWidgets/slider-web/src/utils/marks.ts b/packages/pluggableWidgets/slider-web/src/utils/marks.ts index 1f8eb53142..1b43c8ba8e 100644 --- a/packages/pluggableWidgets/slider-web/src/utils/marks.ts +++ b/packages/pluggableWidgets/slider-web/src/utils/marks.ts @@ -1,38 +1,5 @@ -import { MarkObj } from "@rc-component/slider/lib/Marks"; -import { ReactNode } from "react"; -import { ValueFormatter } from "./helpers"; - -export type Marks = Record; - -export interface CreateMarksParams { - numberOfMarks: number; - decimalPlaces: number; - format: ValueFormatter; - min: number; - max: number; -} - -export function isParamsValidToCalcMarks(params: CreateMarksParams): boolean { - return params.numberOfMarks > 0 && params.min < params.max; -} - -export function createMarks(params: CreateMarksParams): Marks | undefined { - if (!isParamsValidToCalcMarks(params)) { - return; - } - - const marks: Marks = {}; - const { numberOfMarks, decimalPlaces, format, min, max } = params; - const interval = (max - min) / numberOfMarks; - - for (let i = 0; i <= numberOfMarks; i++) { - const rawValue = min + i * interval; - // Round the key to the configured precision so rc-slider positions the dot where its - // label reads. toFixed always uses "." here, so parseFloat is locale-safe (unlike parsing - // the formatted label, which may contain a comma decimal separator). - const key = parseFloat(rawValue.toFixed(decimalPlaces)); - marks[key] = format(rawValue); - } - - return marks; -} +export { + createMarks, + isParamsValidToCalcMarks, + type CreateMarksParams +} from "@mendix/widget-plugin-platform/utils/slider-marks"; diff --git a/packages/pluggableWidgets/slider-web/src/utils/useMarks.ts b/packages/pluggableWidgets/slider-web/src/utils/useMarks.ts index 3626fb7921..6466733bd8 100644 --- a/packages/pluggableWidgets/slider-web/src/utils/useMarks.ts +++ b/packages/pluggableWidgets/slider-web/src/utils/useMarks.ts @@ -1,6 +1,7 @@ import { useMemo } from "react"; import { ValueFormatter } from "./helpers"; import { createMarks } from "./marks"; +import type { ValueFormatter } from "./helpers"; type UseMarksParams = { noOfMarkers: number; diff --git a/packages/shared/widget-plugin-platform/jest.config.cjs b/packages/shared/widget-plugin-platform/jest.config.cjs index 1a6d9d630f..1e6e270219 100644 --- a/packages/shared/widget-plugin-platform/jest.config.cjs +++ b/packages/shared/widget-plugin-platform/jest.config.cjs @@ -15,6 +15,7 @@ module.exports = { ] }, moduleNameMapper: { + "^big\\.js$": "/../../../node_modules/big.js/big.js", "(.+)\\.js": "$1" }, extensionsToTreatAsEsm: [".ts"], diff --git a/packages/shared/widget-plugin-platform/package.json b/packages/shared/widget-plugin-platform/package.json index 8180132dbf..d6fcafbef3 100644 --- a/packages/shared/widget-plugin-platform/package.json +++ b/packages/shared/widget-plugin-platform/package.json @@ -36,6 +36,7 @@ "@mendix/tsconfig-web-widgets": "workspace:*", "@swc/core": "^1.7.26", "@swc/jest": "^0.2.36", + "big.js": "^6.2.1", "classnames": "^2.5.1", "jest-environment-jsdom": "^29.7.0" } diff --git a/packages/pluggableWidgets/range-slider-web/src/utils/__tests__/helpers.spec.ts b/packages/shared/widget-plugin-platform/src/utils/__tests__/number-formatter.spec.ts similarity index 97% rename from packages/pluggableWidgets/range-slider-web/src/utils/__tests__/helpers.spec.ts rename to packages/shared/widget-plugin-platform/src/utils/__tests__/number-formatter.spec.ts index 6ed2c84082..43007115a1 100644 --- a/packages/pluggableWidgets/range-slider-web/src/utils/__tests__/helpers.spec.ts +++ b/packages/shared/widget-plugin-platform/src/utils/__tests__/number-formatter.spec.ts @@ -1,6 +1,6 @@ import { Big } from "big.js"; import { NumberFormatter } from "mendix"; -import { createValueFormatter } from "../helpers"; +import { createValueFormatter } from "../number-formatter"; function fakeNumberFormatter( config: { groupDigits: boolean; decimalPrecision?: number }, diff --git a/packages/pluggableWidgets/range-slider-web/src/utils/__tests__/marks.spec.ts b/packages/shared/widget-plugin-platform/src/utils/__tests__/slider-marks.spec.ts similarity index 90% rename from packages/pluggableWidgets/range-slider-web/src/utils/__tests__/marks.spec.ts rename to packages/shared/widget-plugin-platform/src/utils/__tests__/slider-marks.spec.ts index a4c84c2341..b3b46edc93 100644 --- a/packages/pluggableWidgets/range-slider-web/src/utils/__tests__/marks.spec.ts +++ b/packages/shared/widget-plugin-platform/src/utils/__tests__/slider-marks.spec.ts @@ -1,6 +1,5 @@ -import { createMarks } from "../marks"; +import { createMarks } from "../slider-marks"; -// Simple deterministic formatter standing in for createValueFormatter's output. const formatWith = (decimalPlaces: number, decimalSeparator = ".") => (value: number): string => { @@ -52,8 +51,6 @@ describe("createMarks", () => { }); it("rounds mark keys to the configured decimal places so dots align with their labels", () => { - // 9 intervals over 0..20 yields repeating decimals (e.g. 6.6667). The key must be the - // rounded value (6.7) so rc-slider positions the dot where the label reads. const marks = createMarks({ numberOfMarks: 9, decimalPlaces: 1, format: formatWith(1), min: 0, max: 20 }); expect(Object.keys(marks!)).toContain("6.7"); expect(Object.keys(marks!)).not.toContain("6.666666666666667"); diff --git a/packages/shared/widget-plugin-platform/src/utils/number-formatter.ts b/packages/shared/widget-plugin-platform/src/utils/number-formatter.ts new file mode 100644 index 0000000000..1c365fe5e6 --- /dev/null +++ b/packages/shared/widget-plugin-platform/src/utils/number-formatter.ts @@ -0,0 +1,12 @@ +import { Big } from "big.js"; +import { NumberFormatter } from "mendix"; + +export type ValueFormatter = (value: number) => string; + +export function createValueFormatter(formatter: NumberFormatter, decimalPlaces: number): ValueFormatter { + const configured = formatter.withConfig({ + groupDigits: formatter.config.groupDigits, + decimalPrecision: decimalPlaces + }); + return (value: number) => configured.format(new Big(value)); +} diff --git a/packages/shared/widget-plugin-platform/src/utils/slider-marks.ts b/packages/shared/widget-plugin-platform/src/utils/slider-marks.ts new file mode 100644 index 0000000000..0d6e09893b --- /dev/null +++ b/packages/shared/widget-plugin-platform/src/utils/slider-marks.ts @@ -0,0 +1,33 @@ +import { ValueFormatter } from "./number-formatter"; + +export interface CreateMarksParams { + numberOfMarks: number; + decimalPlaces: number; + format: ValueFormatter; + min: number; + max: number; +} + +export function isParamsValidToCalcMarks(params: CreateMarksParams): boolean { + return params.numberOfMarks > 0 && params.min < params.max; +} + +export function createMarks(params: CreateMarksParams): Record | undefined { + if (!isParamsValidToCalcMarks(params)) { + return; + } + + const marks: Record = {}; + const { numberOfMarks, decimalPlaces, format, min, max } = params; + const interval = (max - min) / numberOfMarks; + + for (let i = 0; i <= numberOfMarks; i++) { + const rawValue = min + i * interval; + // Round the key to configured precision so rc-slider positions dot where label reads. + // toFixed always uses "." so parseFloat is locale-safe. + const key = parseFloat(rawValue.toFixed(decimalPlaces)); + marks[key] = format(rawValue); + } + + return marks; +} From 8f08218cf32ee6443deb18183e5b329532ee29e2 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Tue, 2 Jun 2026 14:01:05 +0200 Subject: [PATCH 09/12] refactor(slider-web,range-slider-web): migrate utility functions to widget-plugin-platform --- .../src/RangeSlider.editorPreview.tsx | 2 +- .../src/components/Container.tsx | 12 +-- .../range-slider-web/src/utils/helpers.ts | 1 - .../range-slider-web/src/utils/marks.ts | 5 -- .../range-slider-web/src/utils/useMarks.ts | 4 +- .../slider-web/src/Slider.editorPreview.tsx | 2 +- .../slider-web/src/components/Container.tsx | 6 +- .../src/utils/__tests__/helpers.spec.ts | 62 ---------------- .../src/utils/__tests__/marks.spec.ts | 74 ------------------- .../src/utils/createHandleRender.tsx | 2 +- .../slider-web/src/utils/helpers.ts | 2 - .../slider-web/src/utils/marks.ts | 5 -- .../slider-web/src/utils/useMarks.ts | 5 +- 13 files changed, 16 insertions(+), 166 deletions(-) delete mode 100644 packages/pluggableWidgets/range-slider-web/src/utils/helpers.ts delete mode 100644 packages/pluggableWidgets/range-slider-web/src/utils/marks.ts delete mode 100644 packages/pluggableWidgets/slider-web/src/utils/__tests__/helpers.spec.ts delete mode 100644 packages/pluggableWidgets/slider-web/src/utils/__tests__/marks.spec.ts delete mode 100644 packages/pluggableWidgets/slider-web/src/utils/marks.ts diff --git a/packages/pluggableWidgets/range-slider-web/src/RangeSlider.editorPreview.tsx b/packages/pluggableWidgets/range-slider-web/src/RangeSlider.editorPreview.tsx index 2e4538d092..e0e982931e 100644 --- a/packages/pluggableWidgets/range-slider-web/src/RangeSlider.editorPreview.tsx +++ b/packages/pluggableWidgets/range-slider-web/src/RangeSlider.editorPreview.tsx @@ -1,7 +1,7 @@ import { ReactNode } from "react"; +import { createMarks } from "@mendix/widget-plugin-platform/utils/slider-marks"; import { RangeSliderPreviewProps } from "../typings/RangeSliderProps"; import { RangeSlider } from "./components/RangeSlider"; -import { createMarks } from "./utils/marks"; import { getPreviewValues } from "./utils/getPreviewValues"; import { getStyleProp, isVertical } from "./utils/prop-utils"; diff --git a/packages/pluggableWidgets/range-slider-web/src/components/Container.tsx b/packages/pluggableWidgets/range-slider-web/src/components/Container.tsx index bc63039ae2..1e242c8a56 100644 --- a/packages/pluggableWidgets/range-slider-web/src/components/Container.tsx +++ b/packages/pluggableWidgets/range-slider-web/src/components/Container.tsx @@ -1,14 +1,14 @@ import { NumberFormatter } from "mendix"; import { ReactElement, useMemo, useRef } from "react"; +import { useScheduleUpdateOnce } from "@mendix/widget-plugin-hooks/useScheduleUpdateOnce"; +import { createValueFormatter } from "@mendix/widget-plugin-platform/utils/number-formatter"; +import { RangeSlider as RangeComponent } from "./RangeSlider"; +import { HandleTooltip } from "./TooltipHandler"; import { RangeSliderContainerProps } from "../../typings/RangeSliderProps"; -import { createValueFormatter } from "../utils/helpers"; +import { getStyleProp, isVertical, maxProp, minProp, stepProp } from "../utils/prop-utils"; +import { useMarks } from "../utils/useMarks"; import { useNumber } from "../utils/useNumber"; -import { RangeSlider as RangeComponent } from "./RangeSlider"; import { useOnChangeDebounced } from "../utils/useOnChangeDebounced"; -import { useMarks } from "../utils/useMarks"; -import { getStyleProp, isVertical, maxProp, minProp, stepProp } from "../utils/prop-utils"; -import { useScheduleUpdateOnce } from "@mendix/widget-plugin-hooks/useScheduleUpdateOnce"; -import { HandleTooltip } from "./TooltipHandler"; export function Container(props: RangeSliderContainerProps): ReactElement { const min = useNumber(minProp(props)); diff --git a/packages/pluggableWidgets/range-slider-web/src/utils/helpers.ts b/packages/pluggableWidgets/range-slider-web/src/utils/helpers.ts deleted file mode 100644 index 831aa958b1..0000000000 --- a/packages/pluggableWidgets/range-slider-web/src/utils/helpers.ts +++ /dev/null @@ -1 +0,0 @@ -export { createValueFormatter, type ValueFormatter } from "@mendix/widget-plugin-platform/utils/number-formatter"; diff --git a/packages/pluggableWidgets/range-slider-web/src/utils/marks.ts b/packages/pluggableWidgets/range-slider-web/src/utils/marks.ts deleted file mode 100644 index 1b43c8ba8e..0000000000 --- a/packages/pluggableWidgets/range-slider-web/src/utils/marks.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { - createMarks, - isParamsValidToCalcMarks, - type CreateMarksParams -} from "@mendix/widget-plugin-platform/utils/slider-marks"; diff --git a/packages/pluggableWidgets/range-slider-web/src/utils/useMarks.ts b/packages/pluggableWidgets/range-slider-web/src/utils/useMarks.ts index 3626fb7921..ab9790fa05 100644 --- a/packages/pluggableWidgets/range-slider-web/src/utils/useMarks.ts +++ b/packages/pluggableWidgets/range-slider-web/src/utils/useMarks.ts @@ -1,6 +1,6 @@ import { useMemo } from "react"; -import { ValueFormatter } from "./helpers"; -import { createMarks } from "./marks"; +import { type ValueFormatter } from "@mendix/widget-plugin-platform/utils/number-formatter"; +import { createMarks } from "@mendix/widget-plugin-platform/utils/slider-marks"; type UseMarksParams = { noOfMarkers: number; diff --git a/packages/pluggableWidgets/slider-web/src/Slider.editorPreview.tsx b/packages/pluggableWidgets/slider-web/src/Slider.editorPreview.tsx index 09622685c1..b8774909ae 100644 --- a/packages/pluggableWidgets/slider-web/src/Slider.editorPreview.tsx +++ b/packages/pluggableWidgets/slider-web/src/Slider.editorPreview.tsx @@ -1,8 +1,8 @@ import { ReactNode } from "react"; +import { createMarks } from "@mendix/widget-plugin-platform/utils/slider-marks"; import { Slider } from "./components/Slider"; import { SliderPreviewProps } from "../typings/SliderProps"; import { getPreviewValues } from "./utils/getPreviewValues"; -import { createMarks } from "./utils/marks"; import { getStyleProp, isVertical } from "./utils/prop-utils"; export function getPreviewCss(): string { diff --git a/packages/pluggableWidgets/slider-web/src/components/Container.tsx b/packages/pluggableWidgets/slider-web/src/components/Container.tsx index ae2b324bef..a1ad7092d2 100644 --- a/packages/pluggableWidgets/slider-web/src/components/Container.tsx +++ b/packages/pluggableWidgets/slider-web/src/components/Container.tsx @@ -1,10 +1,10 @@ import { NumberFormatter } from "mendix"; import { ReactElement, useMemo, useRef } from "react"; -import { SliderContainerProps } from "../../typings/SliderProps"; - +import { createValueFormatter } from "@mendix/widget-plugin-platform/utils/number-formatter"; import { Slider as SliderComponent } from "./Slider"; +import { SliderContainerProps } from "../../typings/SliderProps"; import { createHandleRender } from "../utils/createHandleRender"; -import { createValueFormatter, getSliderLabel } from "../utils/helpers"; +import { getSliderLabel } from "../utils/helpers"; import { getStyleProp, isVertical, maxProp, minProp, stepProp } from "../utils/prop-utils"; import { useMarks } from "../utils/useMarks"; import { useNumber } from "../utils/useNumber"; diff --git a/packages/pluggableWidgets/slider-web/src/utils/__tests__/helpers.spec.ts b/packages/pluggableWidgets/slider-web/src/utils/__tests__/helpers.spec.ts deleted file mode 100644 index 80492644e5..0000000000 --- a/packages/pluggableWidgets/slider-web/src/utils/__tests__/helpers.spec.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Big } from "big.js"; -import { NumberFormatter } from "mendix"; -import { createValueFormatter } from "../helpers"; - -/** - * Minimal stand-in for the Mendix runtime NumberFormatter. It mimics the two behaviours the - * widget relies on: `withConfig` returns a new formatter with the merged config, and `format` - * honours `decimalPrecision`, `groupDigits` and the configured locale separators. - */ -function fakeNumberFormatter( - config: { groupDigits: boolean; decimalPrecision?: number }, - locale: { decimal: string; group: string } = { decimal: ".", group: "," } -): NumberFormatter { - return { - type: "number", - config, - withConfig: (next: { groupDigits: boolean; decimalPrecision?: number }) => - fakeNumberFormatter({ ...config, ...next }, locale), - format: (value?: Big) => { - if (value == null) { - return ""; - } - const fixed = value.toNumber().toFixed(config.decimalPrecision ?? 0); - const [intPart, fracPart] = fixed.split("."); - const grouped = config.groupDigits ? intPart.replace(/\B(?=(\d{3})+(?!\d))/g, locale.group) : intPart; - return fracPart != null ? `${grouped}${locale.decimal}${fracPart}` : grouped; - }, - parse: () => ({ valid: false }) - } as unknown as NumberFormatter; -} - -describe("createValueFormatter", () => { - it("redefines the formatter's decimal precision (forces trailing zeros)", () => { - const format = createValueFormatter(fakeNumberFormatter({ groupDigits: false }), 2); - expect(format(10)).toBe("10.00"); - expect(format(9.2)).toBe("9.20"); - }); - - it("formats without decimals when decimalPlaces is 0", () => { - const format = createValueFormatter(fakeNumberFormatter({ groupDigits: false }), 0); - expect(format(10)).toBe("10"); - expect(format(9.7)).toBe("10"); - }); - - it("respects the locale decimal separator from the formatter", () => { - const format = createValueFormatter( - fakeNumberFormatter({ groupDigits: false }, { decimal: ",", group: "." }), - 2 - ); - expect(format(9.2)).toBe("9,20"); - }); - - it("keeps the attribute's thousands grouping when groupDigits is enabled", () => { - const format = createValueFormatter(fakeNumberFormatter({ groupDigits: true }), 0); - expect(format(1000000)).toBe("1,000,000"); - }); - - it("omits thousands grouping when groupDigits is disabled", () => { - const format = createValueFormatter(fakeNumberFormatter({ groupDigits: false }), 0); - expect(format(1000000)).toBe("1000000"); - }); -}); diff --git a/packages/pluggableWidgets/slider-web/src/utils/__tests__/marks.spec.ts b/packages/pluggableWidgets/slider-web/src/utils/__tests__/marks.spec.ts deleted file mode 100644 index 9ba6e3106e..0000000000 --- a/packages/pluggableWidgets/slider-web/src/utils/__tests__/marks.spec.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { createMarks } from "../marks"; - -// Simple deterministic formatter standing in for createValueFormatter's output. -const formatWith = - (decimalPlaces: number, decimalSeparator = ".") => - (value: number): string => { - const fixed = value.toFixed(decimalPlaces); - return decimalSeparator === "." ? fixed : fixed.replace(".", decimalSeparator); - }; - -describe("createMarks", () => { - it("forces trailing zeros when decimalPlaces > 0 and value is whole number", () => { - const marks = createMarks({ numberOfMarks: 2, decimalPlaces: 2, format: formatWith(2), min: 0, max: 10 }); - expect(marks).toBeDefined(); - expect(marks![0]).toBe("0.00"); - expect(marks![5]).toBe("5.00"); - expect(marks![10]).toBe("10.00"); - }); - - it("forces trailing zeros when decimalPlaces > 0 and value has fewer decimals", () => { - const marks = createMarks({ numberOfMarks: 2, decimalPlaces: 2, format: formatWith(2), min: 0, max: 9.2 }); - expect(marks).toBeDefined(); - expect(marks![4.6]).toBe("4.60"); - expect(marks![9.2]).toBe("9.20"); - }); - - it("does not add decimal places when decimalPlaces is 0", () => { - const marks = createMarks({ numberOfMarks: 4, decimalPlaces: 0, format: formatWith(0), min: 0, max: 100 }); - expect(marks).toBeDefined(); - expect(marks![0]).toBe("0"); - expect(marks![25]).toBe("25"); - expect(marks![100]).toBe("100"); - }); - - it("uses locale decimal separator", () => { - const marks = createMarks({ numberOfMarks: 2, decimalPlaces: 2, format: formatWith(2, ","), min: 0, max: 10 }); - expect(marks![0]).toBe("0,00"); - expect(marks![5]).toBe("5,00"); - expect(marks![10]).toBe("10,00"); - }); - - it("uses correct numeric keys for fractional values with comma locale", () => { - const marks = createMarks({ numberOfMarks: 2, decimalPlaces: 2, format: formatWith(2, ","), min: 0, max: 9.2 }); - expect(marks![4.6]).toBe("4,60"); - expect(marks![9.2]).toBe("9,20"); - }); - - it("rounds mark keys to the configured decimal places so dots align with their labels", () => { - // 9 intervals over 0..20 yields repeating decimals (e.g. 6.6667). The key must be the - // rounded value (6.7) so rc-slider positions the dot where the label reads. - const marks = createMarks({ numberOfMarks: 9, decimalPlaces: 1, format: formatWith(1), min: 0, max: 20 }); - expect(Object.keys(marks!)).toContain("6.7"); - expect(Object.keys(marks!)).not.toContain("6.666666666666667"); - expect(marks![6.7]).toBe("6.7"); - }); - - it("returns undefined when numberOfMarks is 0", () => { - expect( - createMarks({ numberOfMarks: 0, decimalPlaces: 2, format: formatWith(2), min: 0, max: 100 }) - ).toBeUndefined(); - }); - - it("returns undefined when min equals max", () => { - expect( - createMarks({ numberOfMarks: 4, decimalPlaces: 2, format: formatWith(2), min: 5, max: 5 }) - ).toBeUndefined(); - }); - - it("returns undefined when min > max", () => { - expect( - createMarks({ numberOfMarks: 2, decimalPlaces: 1, format: formatWith(1), min: 10, max: 5 }) - ).toBeUndefined(); - }); -}); diff --git a/packages/pluggableWidgets/slider-web/src/utils/createHandleRender.tsx b/packages/pluggableWidgets/slider-web/src/utils/createHandleRender.tsx index 2bbdb155a5..6f23854652 100644 --- a/packages/pluggableWidgets/slider-web/src/utils/createHandleRender.tsx +++ b/packages/pluggableWidgets/slider-web/src/utils/createHandleRender.tsx @@ -2,7 +2,7 @@ import { SliderProps as RcSliderProps } from "@rc-component/slider"; import RcTooltip from "@rc-component/tooltip"; import { DynamicValue } from "mendix"; import { RefObject } from "react"; -import { ValueFormatter } from "./helpers"; +import { type ValueFormatter } from "@mendix/widget-plugin-platform/utils/number-formatter"; import "@rc-component/tooltip/assets/bootstrap.css"; diff --git a/packages/pluggableWidgets/slider-web/src/utils/helpers.ts b/packages/pluggableWidgets/slider-web/src/utils/helpers.ts index f4a9afeb49..df4a690418 100644 --- a/packages/pluggableWidgets/slider-web/src/utils/helpers.ts +++ b/packages/pluggableWidgets/slider-web/src/utils/helpers.ts @@ -1,3 +1 @@ -export { createValueFormatter, type ValueFormatter } from "@mendix/widget-plugin-platform/utils/number-formatter"; - export const getSliderLabel = (sliderId: string): Element | null => document.querySelector(`label[for="${sliderId}"]`); diff --git a/packages/pluggableWidgets/slider-web/src/utils/marks.ts b/packages/pluggableWidgets/slider-web/src/utils/marks.ts deleted file mode 100644 index 1b43c8ba8e..0000000000 --- a/packages/pluggableWidgets/slider-web/src/utils/marks.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { - createMarks, - isParamsValidToCalcMarks, - type CreateMarksParams -} from "@mendix/widget-plugin-platform/utils/slider-marks"; diff --git a/packages/pluggableWidgets/slider-web/src/utils/useMarks.ts b/packages/pluggableWidgets/slider-web/src/utils/useMarks.ts index 6466733bd8..2e67f68e31 100644 --- a/packages/pluggableWidgets/slider-web/src/utils/useMarks.ts +++ b/packages/pluggableWidgets/slider-web/src/utils/useMarks.ts @@ -1,7 +1,6 @@ import { useMemo } from "react"; -import { ValueFormatter } from "./helpers"; -import { createMarks } from "./marks"; -import type { ValueFormatter } from "./helpers"; +import type { ValueFormatter } from "@mendix/widget-plugin-platform/utils/number-formatter"; +import { createMarks } from "@mendix/widget-plugin-platform/utils/slider-marks"; type UseMarksParams = { noOfMarkers: number; From 41e716dc1a4e09b5bf69d9c4cd55b2c397641e30 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Tue, 2 Jun 2026 14:16:34 +0200 Subject: [PATCH 10/12] fix(pnpm-lock): add missing widget-plugin-platform dependency links --- pnpm-lock.yaml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de09f6caeb..61152bb109 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2085,6 +2085,9 @@ importers: '@mendix/widget-plugin-component-kit': specifier: workspace:* version: link:../../shared/widget-plugin-component-kit + '@mendix/widget-plugin-platform': + specifier: workspace:* + version: link:../../shared/widget-plugin-platform '@rc-component/slider': specifier: ^1.0.1 version: 1.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -2113,9 +2116,6 @@ importers: '@mendix/widget-plugin-hooks': specifier: workspace:* version: link:../../shared/widget-plugin-hooks - '@mendix/widget-plugin-platform': - specifier: workspace:* - version: link:../../shared/widget-plugin-platform '@types/rc-slider': specifier: ^8.6.6 version: 8.6.6 @@ -2351,6 +2351,9 @@ importers: '@mendix/widget-plugin-component-kit': specifier: workspace:* version: link:../../shared/widget-plugin-component-kit + '@mendix/widget-plugin-platform': + specifier: workspace:* + version: link:../../shared/widget-plugin-platform '@rc-component/slider': specifier: ^1.0.1 version: 1.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -2379,9 +2382,6 @@ importers: '@mendix/widget-plugin-hooks': specifier: workspace:* version: link:../../shared/widget-plugin-hooks - '@mendix/widget-plugin-platform': - specifier: workspace:* - version: link:../../shared/widget-plugin-platform cross-env: specifier: ^7.0.3 version: 7.0.3 @@ -3031,6 +3031,9 @@ importers: '@swc/jest': specifier: ^0.2.36 version: 0.2.39(@swc/core@1.13.5) + big.js: + specifier: ^6.2.1 + version: 6.2.2 classnames: specifier: ^2.5.1 version: 2.5.1 From 3b2249233113116cd26a85d2ce4037989d668d98 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Tue, 2 Jun 2026 15:39:47 +0200 Subject: [PATCH 11/12] test(range-slider-web): update E2E mark assertions to match decimalPlaces=1 output Test project widget has decimalPlaces=1, so marks now render "0.0"/"100.0". Co-Authored-By: Claude Sonnet 4.6 --- .../pluggableWidgets/range-slider-web/e2e/dataTypes.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/pluggableWidgets/range-slider-web/e2e/dataTypes.spec.js b/packages/pluggableWidgets/range-slider-web/e2e/dataTypes.spec.js index aedfc6039c..e09aa65b83 100644 --- a/packages/pluggableWidgets/range-slider-web/e2e/dataTypes.spec.js +++ b/packages/pluggableWidgets/range-slider-web/e2e/dataTypes.spec.js @@ -12,11 +12,11 @@ test.describe("Range Slider", () => { }); test("renders slider min value text", async ({ page }) => { - await expect(page.locator(".mx-name-rangeSlider1 .rc-slider-mark-text").first()).toHaveText("0"); + await expect(page.locator(".mx-name-rangeSlider1 .rc-slider-mark-text").first()).toHaveText("0.0"); }); test("renders slider max value text", async ({ page }) => { - await expect(page.locator(".mx-name-rangeSlider1 .rc-slider-mark-text").nth(1)).toHaveText("100"); + await expect(page.locator(".mx-name-rangeSlider1 .rc-slider-mark-text").nth(1)).toHaveText("100.0"); }); test("upper bound value is higher than lower bound value", async ({ page }) => { From 474d698d697a4c9f30ada36126981a3fe8e327d1 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Tue, 2 Jun 2026 16:16:37 +0200 Subject: [PATCH 12/12] test(slider-web): update snapshot for slider styles in Chromium Linux --- .../sliderStyles-chromium-linux.png | Bin 1660 -> 1844 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/packages/pluggableWidgets/slider-web/e2e/Slider.spec.js-snapshots/sliderStyles-chromium-linux.png b/packages/pluggableWidgets/slider-web/e2e/Slider.spec.js-snapshots/sliderStyles-chromium-linux.png index 7e9275f52bd90c753b3d69916c684a5c451f57d5..1a666cffd1f58b6ef3f9f6afa497d18725c5a81c 100644 GIT binary patch literal 1844 zcmb`ISv1=T8^-@-YIoXxVoPXss8Ov+ZKZYzqbdj`W8b&fx3tlOs#YzvS7a*0pol@l z7W)>IYHguGW2rH=*1ptIW%RrI?!TM&{LXXnyf@D|zXYV2ksu$K4*&o`u)&hFe(n-~D#S}``zx5DPEP6zowJSDg{OB$!@hz>$TTNHvf`2|bZ&bN$EJrLm9 zrF&nWdp$W3`h1yxuYv-_FYtG&#Z+1$rzC`mtDnH--FpULjz(44wpj|Q)HHFNJ=5gZ z)^~L$8N2QEJ1iM@bla7qzqhp>n%YOEd|%X7e(w!u{ZD7Xj!7Cbl(vr8gs^iemfcWn4+$ZeJ?G?pY;JipvC!u&E5E~hYuTlw9I?+ zqj!eGOjC7aX!i;uLNqFdvBPT*RMkxU9(j9ZB4kb6;i*OpL?ddsiHgp41&L^=r}+8% zcQ;&KrTn#kU-)s4^Q*^VA;X_9jzwaO?Cfhg;+n2v9F62d%f0kS(D(r5ryR%)1E z*f~VV))bGdJ&+dcv?IuuOv&L_)^;bZ9J#YeGuTe}1znAtlR1Y#avQXaw(o>ljhL9* znZCxeXlxYr&zJF_;rNSB8DLoDQY}lLyT>}kmdj-*yFXvzwTg#lFl`%QCj5pH)V7>o zsu)S&NcXBKXKHLH$Vd#$hSO~Au9Y*C;ldWGVTMK3Tr{-lLZ`Q7Z`gOiDp$YW1g&&eXxPo{`lzas`}R zF6^`ab60PeW9X(cd#kGzn{?i_SHpw63Ls`^{t9nVfRKg1udlC1aBml#w|q#lDS(lP zK3LgekkE3(ditFUk{55ZMIsg#7e`mbv!zIFuViyk)sy~kI435=p%L!bpiL=8cS3@S zpV(U`_m>vc+xTUZ-+I_?5li%5&*q6-K+90%CYe{x2UT%J?@g+oVwXY_`t$*tJn#$H zbG9O&Cp_i^;sC59yfOhG9tHqERt7&yZv790M1A`}9HbZM&O9 zZ)>P3>ZpJ&9qq?GE&3)XRyxtQX>+qtz8nD|+U%cj@6UM~J!1ECAJ{NQ*Xyl=kLu>6 zd)*|tog)rQFQY>@#|T;E!GV@mCkp%!vZcGnO~%Sjow}Ht58ZAj{e{TYI33+cp=#qw zVcvy>#3WY7w5dt`2yHz!dEl<$|vgvVhL#JnwIWuTNA(79696P zU?T#u#~Teub93)pom3lq2M&K(gkYW9h=sHw;{>{fEs2khkCeh1>VIlJI?}XjfN3N~ zRayDR$b|UbQ=NdVL9$wDBO#V1H{26jrraorZ;aj0G0kEqus5?I)1{bbv&d%9d5uaY zPQs(L=GCBq_3DNt)XJv1S`2hd@=@{yUC1W`eUoc$?$RaPgkW_KP+%QB{CS`F=fuC2 zZ{qS#kcPn~HS8XmQb286cRzz8N&g9YKWn1pu6uhA_FY*7**Rj|;ENN*C`XEUF;%Gb zB&E$PFYEOyzWQpF>nkV`=eZhO;0!;8lICFojeFth`OzmbfnW(@`dH@czpjXxC)H!O zw`=Y2f0ituS*!LZ5*lZ1VOH%NM$Rg(u78yi60C}j~hajDGm+Uo=3YqVeN}-99t_~><-Rr*O%2I z`uUIizHJ6IPPVu3`t{+T`JCF#Nr|8baGz8UmnfthYNRJcP=UmIS$`fFoC4jM^nE@5%67w%=No~vk delta 1643 zcmZ{leLT~N1INEJPT6tJWAZpnCL+?DHSDlOWzIuI9)>(LLqarRh$A&}UF2yRH=Q5N z=gYj@XgeXm!F zr`{5EBlHv{_yji5iX9w?y$n5JVcFI=+U>{m=GZH1^wAz_rq}kohe(~%MUaoVP1|63 z{2~Oua9jCstF~5|hEic`$f=fA>r-k0+(#AH(fLrQ-ALm2flg=Az zYH9{O(i>FK+GatYF^CMHrqkpB$%KoB9AKmb0#tJWfKT!2?C$PnMqE5!AK09ejx|?35xQ|OD%j2Py29h zYPR|n`HXtEVNdPaV(E-9qsMpZos^D{ZKr>@J(B7|GlkV>{Jwf%Yh(Fj;MX;9v2mJ% zh^^xI^KS_;U!xPFQ&mJpkD%-~CMoA_aFoBtx{iEB+453$H5m5pQg#x;D&YtVL_=GS z{$}`s(aNwhh8Y01t*;aLDh|bUQl2lL-azG$0kw4M4MRm>G&rvI7-E4RhO*p|RjV~s zO0sx=WLdw*H{kZI*?wEAE%`ek%25ZIf66Xd!B7>=+lw#Fo5w|!csTla8eCa#-QQFu z8Qkxwv6dA2r?zjYM>w0sV&%Ol&Wli=s5q50nRA4&IZP5o9^?vN-0WF2V=oW=+DHzZ z<_Zy{%s^>mc_7Y8L&=~huUbu>MYlxhO+6w#*R|uJ8<1m(oJB1nuYGMx?pzN_(U?r8 zqUKM3CxDC_DzN;(Pp8GzPSabTY!Xy~jk=+lyUqGwAPFtSKmqjYP}luHCR`ps?*XP- zqY7kxCM;)9Fmi)_*y86W=J^3udTZ+FLmR1U&pn6OT~Y7HF5rrC zO>AB|;6D7+wjLT=@X0IdeZ58);qk1NQLD z9k)s1`at-R>(TK1WAX`dmM3Nq_q6J@qgf672 zYo7@S9#qDmQsy7x^-+kcXU_)to2jA?)rH=L&9J&F>*)geM7%BROrVB}!pHGDE6F}# z6IMAP+r2|wWGhMs)&GRE%PI)-gUi@k_+z?Ym_s?5Rv`P&=wPfy8uT2aGC$gl{$r%y z{FPlqP_xxUM{H!%Zo&$Z1pd@gJk~;Me)ChUUU5TukhjLO{(}gbh5TXoTaa;(?3X@7 zlFxVZ(S4@<1z}B#b!L1*yL&W8of*n#cAb4z#abO8I}SxZ;n@QCIrXMf3jQ{2#KpdO zF~*@d|23COlmaU1m$#qV#v#+|45(+mNO5QoX62>Tr@XmGFT-$RowD3X`$v-v29CuCil9oNb`B0FD+*1LSIljh{Ou|F zeLX2{XYN+7n~+h)p?JJ_^%wv)=kx*KzsQH~jqN_K@O=O}-x&a$|7Sb?_543e$Sw!~ lc6TX*Z`=U@_~P@CfB}=v$uj|V={b7};Eut$GMq!Q{{aKm`St(+