Skip to content

[WC-3322]: Fix slider and range-slider decimal places formatting#2220

Open
samuelreichert wants to merge 12 commits into
mainfrom
worktree-fix+slider-decimal-places-formatting
Open

[WC-3322]: Fix slider and range-slider decimal places formatting#2220
samuelreichert wants to merge 12 commits into
mainfrom
worktree-fix+slider-decimal-places-formatting

Conversation

@samuelreichert
Copy link
Copy Markdown
Contributor

@samuelreichert samuelreichert commented May 19, 2026

Pull request type

Bug fix (non-breaking change which fixes an issue)


Description

When decimalPlaces is configured on the Slider or Range Slider widget, displayed values were silently stripping trailing zeros and ignoring the user's locale:

  • Mark labels: 10.00 rendered as 10, 9.20 rendered as 9.2
  • Tooltip: raw RC Slider number passed through with no formatting applied

Root causes:

  1. marks.tsparseFloat(value.toFixed(n)).toString() round-tripped through a number, discarding trailing zeros. Fixed by keeping the formatted string as the label and using parseFloat only for the numeric key (so rc-slider positions dots correctly even when the decimal separator is a comma).

  2. createHandleRender.tsx (Slider) / Container.tsx (Range Slider) — tooltip overlay received restProps.value (raw RC Slider number). Fixed by passing a ValueFormatter derived from the attribute's own Mendix NumberFormatter, so the decimal separator and thousands grouping follow the user's session locale.

Shared logic extracted to widget-plugin-platform:

createValueFormatter and createMarks were duplicated across both widgets. They have been moved to packages/shared/widget-plugin-platform/src/utils/ so both widgets consume a single implementation. Tests live in the shared package only.

Range Slider specifics:

Uses a single formatter derived from lowerBoundAttribute for both marks and tooltips (both handles). The attribute formatter already carries the correct locale settings so there is no need for a separate formatter per handle.


What should be covered while testing?

Slider:

  1. Configure a Slider widget with decimalPlaces = 2 and Number of markers > 0.
  2. Set min/max so some markers land on whole numbers (e.g. min=0, max=10, markers=2).
  3. Mark labels should show 0.00, 5.00, 10.00 — not 0, 5, 10.
  4. Drag the handle to a whole number — tooltip should show 10.00, not 10.
  5. Drag to a value with one decimal (e.g. 9.5) — tooltip should show 9.50.
  6. Set decimalPlaces = 0 — labels and tooltip show integers, no regression.
  7. Switch app locale to one with comma decimal separator — marks and tooltip use , not ..

Range Slider:

  1. Same mark label checks as Slider above.
  2. Drag the lower handle to a whole number — tooltip shows 10.00.
  3. Drag the upper handle — tooltip also formats correctly (uses same formatter as lower handle).
  4. Custom text tooltip type — still shows custom text unchanged.
  5. Set decimalPlaces = 0 — no regression.

@samuelreichert samuelreichert requested a review from a team as a code owner May 19, 2026 11:35
@github-actions

This comment was marked as outdated.

@samuelreichert samuelreichert changed the title Worktree fix+slider decimal places formatting [WC-3322]: Fix slider decimal places formatting May 19, 2026
@github-actions

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

@samuelreichert samuelreichert force-pushed the worktree-fix+slider-decimal-places-formatting branch from 4e14487 to 945997f Compare May 22, 2026 12:23
@github-actions

This comment was marked as outdated.

@samuelreichert samuelreichert force-pushed the worktree-fix+slider-decimal-places-formatting branch from 945997f to 98f491a Compare May 26, 2026 07:37
@github-actions

This comment was marked as outdated.

@samuelreichert samuelreichert force-pushed the worktree-fix+slider-decimal-places-formatting branch 3 times, most recently from 07fc177 to 691462a Compare May 26, 2026 13:20
@github-actions

This comment was marked as outdated.

iobuhov
iobuhov previously approved these changes May 26, 2026
Copy link
Copy Markdown
Collaborator

@iobuhov iobuhov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment thread packages/pluggableWidgets/slider-web/src/utils/createHandleRender.tsx Outdated
@samuelreichert samuelreichert force-pushed the worktree-fix+slider-decimal-places-formatting branch from fce3bc3 to 955eda7 Compare May 28, 2026 10:49
@github-actions

This comment was marked as outdated.

@samuelreichert samuelreichert requested review from iobuhov and r0b1n May 28, 2026 10:55
@samuelreichert samuelreichert force-pushed the worktree-fix+slider-decimal-places-formatting branch from 955eda7 to 0fbf863 Compare May 28, 2026 13:16
@github-actions

This comment was marked as outdated.

@samuelreichert samuelreichert force-pushed the worktree-fix+slider-decimal-places-formatting branch from 0fbf863 to 457a610 Compare May 28, 2026 15:12
@github-actions

This comment was marked as outdated.

@samuelreichert samuelreichert force-pushed the worktree-fix+slider-decimal-places-formatting branch from 457a610 to 5a05fd6 Compare May 29, 2026 07:41
@github-actions

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

Comment thread packages/pluggableWidgets/slider-web/CHANGELOG.md Outdated
samuelreichert and others added 6 commits June 1, 2026 14:42
…eparator

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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 <noreply@anthropic.com>
@samuelreichert samuelreichert force-pushed the worktree-fix+slider-decimal-places-formatting branch from 6b578ae to f4aefab Compare June 1, 2026 12:47
…imal places

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 <noreply@anthropic.com>
@github-actions

This comment was marked as outdated.

…o 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 <noreply@anthropic.com>
@samuelreichert samuelreichert force-pushed the worktree-fix+slider-decimal-places-formatting branch from 92cb4a7 to 04fed6d Compare June 2, 2026 11:47
@github-actions

This comment was marked as outdated.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 2, 2026

AI Code Review

🔶 Changes requested — one or more medium-severity items must be addressed


What was reviewed

File Change
packages/shared/widget-plugin-platform/src/utils/number-formatter.ts New shared utility — locale-aware number formatter
packages/shared/widget-plugin-platform/src/utils/slider-marks.ts New shared utility — mark generation moved from widget-local
packages/shared/widget-plugin-platform/src/utils/__tests__/number-formatter.spec.ts Unit tests for number-formatter
packages/shared/widget-plugin-platform/src/utils/__tests__/slider-marks.spec.ts Unit tests for slider-marks
packages/shared/widget-plugin-platform/package.json Added big.js to devDependencies
packages/shared/widget-plugin-platform/jest.config.cjs Added big.js module name mapper
packages/pluggableWidgets/slider-web/src/utils/marks.ts Deleted — replaced by shared util
packages/pluggableWidgets/slider-web/src/utils/useMarks.ts Now consumes shared createMarks + accepts format fn
packages/pluggableWidgets/slider-web/src/utils/createHandleRender.tsx Accepts ValueFormatter; formats tooltip value
packages/pluggableWidgets/slider-web/src/components/Container.tsx Creates format fn; passes to marks + handleRender
packages/pluggableWidgets/slider-web/src/Slider.editorPreview.tsx Uses shared createMarks; passes simple toFixed format
packages/pluggableWidgets/slider-web/src/utils/__tests__/marks.spec.ts Unit tests for createMarks (via shared package)
packages/pluggableWidgets/slider-web/src/utils/__tests__/createHandleRender.spec.tsx Unit tests for tooltip formatting
packages/pluggableWidgets/slider-web/src/utils/__tests__/helpers.spec.ts Tests for createValueFormatter (duplicates shared tests)
packages/pluggableWidgets/slider-web/e2e/Slider.spec.js Updated assertions to expect formatted decimal strings
packages/pluggableWidgets/slider-web/package.json Moved widget-plugin-platform to dependencies
packages/pluggableWidgets/range-slider-web/src/utils/marks.ts Deleted — replaced by shared util
packages/pluggableWidgets/range-slider-web/src/utils/useMarks.ts Now consumes shared createMarks + accepts format fn
packages/pluggableWidgets/range-slider-web/src/components/Container.tsx Creates format fn; passes to marks and inline handleRender
packages/pluggableWidgets/range-slider-web/src/RangeSlider.editorPreview.tsx Uses shared createMarks; passes simple toFixed format
packages/pluggableWidgets/range-slider-web/package.json Moved widget-plugin-platform to dependencies
packages/pluggableWidgets/range-slider-web/CHANGELOG.md Fix entry added
packages/pluggableWidgets/slider-web/CHANGELOG.md Fix entry added
packages/pluggableWidgets/slider-web/typings/declare-svg.ts Added declare module "*.css"

Skipped (out of scope): dist/, pnpm-lock.yaml

⚠️ CI checks could not be fetched (command required approval). Verify checks pass before merging.


Findings

🔶 Medium — big.js listed in devDependencies but used in production code

File: packages/shared/widget-plugin-platform/package.json line 50
Problem: number-formatter.ts imports Big from big.js and calls new Big(value) in the returned formatter closure — this executes at widget runtime, not just during tests or builds. Declaring it only in devDependencies is incorrect: any consumer that builds widget-plugin-platform in isolation will fail to resolve big.js, and bundlers that trace package boundaries may emit a warning or fail. The monorepo pnpm workspace masks the issue today because the root node_modules holds the install, but the package metadata is wrong.
Fix: Move big.js from devDependencies to dependencies in packages/shared/widget-plugin-platform/package.json:

"dependencies": {
    "big.js": "^6.2.1"
},
"devDependencies": {
    // remove big.js from here
}

⚠️ Low — helpers.spec.ts in slider-web duplicates number-formatter.spec.ts in the shared package verbatim

File: packages/pluggableWidgets/slider-web/src/utils/__tests__/helpers.spec.ts
Note: Every test case (fakeNumberFormatter helper, all five createValueFormatter assertions) is an exact copy of packages/shared/widget-plugin-platform/src/utils/__tests__/number-formatter.spec.ts. The file tests a function that lives in the shared package, not in slider-web/utils/helpers. Duplicated tests diverge silently over time and add no coverage value. Consider removing helpers.spec.ts — the shared package's tests already cover createValueFormatter completely.


⚠️ Low — Range-slider handleRender is recreated on every render (inconsistent with slider-web)

File: packages/pluggableWidgets/range-slider-web/src/components/Container.tsx line 83
Note: slider-web/Container.tsx wraps createHandleRender(...) in useMemo, giving rc-slider a stable handleRender reference. The range-slider defines the function inline in JSX, so a new closure is allocated on every render. While not a correctness bug, this inconsistency means range-slider handle nodes re-mount more often than needed. Consider wrapping it in useMemo for parity:

const handleRender = useMemo(
    () => (node: ReactElement, handleProps: SliderHandleProps) => {
        const isCustomText = tooltipTypeCheck[handleProps.index] === "customText";
        const displayValue = isCustomText
            ? (tooltipValue[handleProps.index]?.value ?? "")
            : format(handleProps.value);
        return (
            <HandleTooltip
                value={displayValue}
                index={handleProps.index}
                visible={handleProps.dragging && props.showTooltip}
                sliderRef={sliderRef}
                {...props}
            >
                {node}
            </HandleTooltip>
        );
    },
    [tooltipTypeCheck, tooltipValue, format, props]
);

⚠️ Low — eslint-disable-next-line react-hooks/exhaustive-deps with no explanation

File: packages/pluggableWidgets/slider-web/src/components/Container.tsx line 36
File: packages/pluggableWidgets/range-slider-web/src/components/Container.tsx line 48
Note: Per repo conventions (AGENTS.md), suppression comments should explain the non-obvious constraint — here, why formatter cannot be used as a stable dep. A one-line note helps reviewers understand this is intentional:

// Mendix formatter is a new object reference each render; identity-compare on formatter
// would recreate the closure on every tick. We intentionally omit it and rely on
// decimalPlaces changing as the meaningful signal.
// eslint-disable-next-line react-hooks/exhaustive-deps
[props.valueAttribute.formatter, props.decimalPlaces]

Positives

  • Correct root cause identified and fixed in both places: marks key uses parseFloat(rawValue.toFixed(n)) (locale-safe) while the label uses format(rawValue) — these are the right tools for each job.
  • Moving createMarks and createValueFormatter to the shared package is the right call; both slider-web and range-slider-web now consume a single implementation with a single test suite.
  • The useMemo wrapping of createHandleRender in slider-web Container (with correct deps including format) is a genuine improvement over the pre-PR inline creation.
  • Test coverage is thorough: locale separators, trailing-zero preservation, decimalPlaces=0 regression, repeating-decimal key rounding, and all three undefined-return guard cases are all covered.
  • CHANGELOG entries present in both packages in Keep a Changelog format.

@samuelreichert samuelreichert force-pushed the worktree-fix+slider-decimal-places-formatting branch from c49d36d to 0020512 Compare June 2, 2026 12:08
@samuelreichert samuelreichert changed the title [WC-3322]: Fix slider decimal places formatting [WC-3322]: Fix slider and range-slider decimal places formatting Jun 2, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 2, 2026

AI Code Review

⚠️ Approved with suggestions — low-severity items only, safe to merge


What was reviewed

File Change
packages/shared/widget-plugin-platform/src/utils/number-formatter.ts New shared createValueFormatter using Mendix NumberFormatter.withConfig
packages/shared/widget-plugin-platform/src/utils/slider-marks.ts createMarks extracted to shared package; format callback injected
packages/shared/widget-plugin-platform/src/utils/__tests__/number-formatter.spec.ts New unit tests for formatter
packages/shared/widget-plugin-platform/src/utils/__tests__/slider-marks.spec.ts New unit tests for marks
packages/shared/widget-plugin-platform/package.json Adds big.js to devDependencies
packages/shared/widget-plugin-platform/jest.config.cjs Adds big.js module mapper for ESM
packages/pluggableWidgets/slider-web/src/components/Container.tsx Wires format into useMarks and createHandleRender; wraps handleRender in useMemo
packages/pluggableWidgets/slider-web/src/utils/createHandleRender.tsx Accepts format: ValueFormatter; uses it for tooltip overlay
packages/pluggableWidgets/slider-web/src/utils/useMarks.ts Accepts and forwards format
packages/pluggableWidgets/slider-web/src/utils/marks.ts Deleted (moved to shared)
packages/pluggableWidgets/slider-web/src/Slider.editorPreview.tsx Imports createMarks from shared package
packages/pluggableWidgets/slider-web/src/utils/__tests__/createHandleRender.spec.tsx New unit tests for handle tooltip rendering
packages/pluggableWidgets/slider-web/e2e/Slider.spec.js Hardened assertions to expect formatted values ("0.0", "20.0", etc.)
packages/pluggableWidgets/slider-web/package.json Moves widget-plugin-platform from devDependencies to dependencies
packages/pluggableWidgets/slider-web/typings/declare-svg.ts Adds *.css module declaration
packages/pluggableWidgets/range-slider-web/src/components/Container.tsx Same format-injection pattern as slider
packages/pluggableWidgets/range-slider-web/src/utils/useMarks.ts Accepts and forwards format
packages/pluggableWidgets/range-slider-web/src/utils/marks.ts Deleted (moved to shared)
packages/pluggableWidgets/range-slider-web/src/RangeSlider.editorPreview.tsx Imports createMarks from shared package
packages/pluggableWidgets/range-slider-web/package.json Moves widget-plugin-platform from devDependencies to dependencies
packages/pluggableWidgets/slider-web/CHANGELOG.md ✅ Unreleased Fixed entry
packages/pluggableWidgets/range-slider-web/CHANGELOG.md ✅ Unreleased Fixed entry

Skipped (out of scope): dist/, pnpm-lock.yaml


Findings

⚠️ Low — big.js classified as devDependency in a runtime module

File: packages/shared/widget-plugin-platform/package.json line 39
Problem: number-formatter.ts compiles to dist/number-formatter.js with a live import { Big } from "big.js". The package is a library ("build": "tsc"; no bundling), so the import survives to dist. Listing big.js only under devDependencies is semantically wrong for a published/distributed shared package — a consumer building outside the monorepo's hoisted node_modules would fail to resolve it.

In this monorepo it works today because pnpm hoists devDeps to the root, and widget Rollup builds pick it up transitively. But it's one misconfigured install away from a silent build failure.
Fix: Move big.js to dependencies:

// package.json
"dependencies": {
    "big.js": "^6.2.1"
},
"devDependencies": {
    // remove big.js from here
}

⚠️ Low — eslint-disable-next-line react-hooks/exhaustive-deps without a why comment

File: packages/pluggableWidgets/slider-web/src/components/Container.tsx line 36, packages/pluggableWidgets/range-slider-web/src/components/Container.tsx line 47
Problem: The disable comment is on the deps array [props.valueAttribute.formatter, props.decimalPlaces]. This silences the linter without explaining whether the author is intentionally omitting props.valueAttribute from deps (to avoid recreating on every render since the formatter config, not the reference, is what matters) or something else. Per repo convention, suppression comments must explain the WHY.

Also worth a second look: if EditableValue.formatter is a new object reference on every render (common in Mendix), format will be recreated every render, which cascades to handleRender and marks. If that's acceptable (cheap to recreate), the useMemo can be removed entirely. If not, a stable key (e.g. formatter.config.decimalPrecision + formatter.config.groupDigits) should be used as the dependency.
Fix:

const format = useMemo(
    () => createValueFormatter(props.valueAttribute.formatter as NumberFormatter, props.decimalPlaces),
    // formatter is read by reference; config values are the actual change signal
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [props.valueAttribute.formatter, props.decimalPlaces]
);

⚠️ Low — E2E spec still missing test.afterEach session logout

File: packages/pluggableWidgets/slider-web/e2e/Slider.spec.js (pre-existing, in diff scope)
Problem: The file uses @mendix/run-e2e/fixtures (which may or may not bundle automatic cleanup) but has no explicit test.afterEach("Cleanup session", async ({ page }) => { await page.evaluate(() => window.mx.session.logout()); }). Per the E2E guidelines, every spec file must include this to avoid exceeding Mendix's 5-session license limit in CI.

This predates the PR, but since the file is in the diff it should be addressed here or tracked as follow-up.


Positives

  • Clean root-cause fix: separating the key (locale-agnostic parseFloat(rawValue.toFixed(n))) from the label (format(rawValue)) is exactly the right model.
  • Injecting format as a callback into createMarks keeps the shared utility decoupled from Mendix-specific types — editor preview works with a simple toFixed lambda, runtime uses the full NumberFormatter.
  • big.js module mapper in jest.config.cjs preemptively solves the ESM-in-Jest problem cleanly.
  • handleRender is now wrapped in useMemo in slider-web/Container.tsx — good performance improvement that was missing before.
  • Both widget changelogs updated; widget-plugin-platform moved from devDependencies to dependencies in both widget packages (correct direction).
  • Tests cover trailing-zero formatting, locale decimal separators, decimalPlaces=0, and the key-rounding alignment edge case — comprehensive for the new surface area.

samuelreichert and others added 3 commits June 2, 2026 14:16
…aces=1 output

Test project widget has decimalPlaces=1, so marks now render "0.0"/"100.0".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants