@marimo-team/frontend 0.25.3-dev135 → 0.25.3-dev136
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{CellStatus-Bu_cFOaf.js → CellStatus-B9hoYYSq.js} +1 -1
- package/dist/assets/{ErrorBoundary-Cy4lxM4U.js → ErrorBoundary-DIoi62xQ.js} +2 -2
- package/dist/assets/{JsonOutput-Dnd0uzN4.js → JsonOutput-f0zvWw7g.js} +3 -3
- package/dist/assets/{MarimoErrorOutput-DWgmERpO.js → MarimoErrorOutput-DFCWAXPj.js} +1 -1
- package/dist/assets/{RenderHTML-BuZ8DdVv.js → RenderHTML-CJOIsfDF.js} +1 -1
- package/dist/assets/{RunButton-BXXDV5w-.js → RunButton-BhbOSnBI.js} +1 -1
- package/dist/assets/{add-cell-with-ai-DYahRMb_.js → add-cell-with-ai-xzt8ZIZY.js} +1 -1
- package/dist/assets/{add-connection-dialog-CjRaQbDW.js → add-connection-dialog-CLcZdzEt.js} +1 -1
- package/dist/assets/{agent-panel-Cb9PZdDM.js → agent-panel-x_kvcauj.js} +1 -1
- package/dist/assets/{ai-model-dropdown-Bbr2oylS.js → ai-model-dropdown-DcOMJlL4.js} +1 -1
- package/dist/assets/{app-config-button-CPNc_---.js → app-config-button-gr-9epH3.js} +1 -1
- package/dist/assets/{blob-CQAkRxR1.js → blob-B25C4ORK.js} +1 -1
- package/dist/assets/{cache-panel-DuBsA6oO.js → cache-panel-BK4-1NJC.js} +1 -1
- package/dist/assets/{cell-editor-CjvWSYDB.js → cell-editor-CO_2xdmY.js} +1 -1
- package/dist/assets/{cell-link-DIOanEQB.js → cell-link-DBP_BcJY.js} +1 -1
- package/dist/assets/{cells-lwP9-xsQ.js → cells-B9RIIuYa.js} +1 -1
- package/dist/assets/{chat-display-DpCQoui-.js → chat-display-CvnF2GaH.js} +1 -1
- package/dist/assets/{chat-panel-C79_pWxF.js → chat-panel-CJFHiZ2R.js} +1 -1
- package/dist/assets/{chat-ui-DOOiaOcb.js → chat-ui-DbPlwime.js} +1 -1
- package/dist/assets/{column-preview-DIvrCqra.js → column-preview-j0kenrsq.js} +1 -1
- package/dist/assets/{command-palette-BYgBVslq.js → command-palette-CQ1DPGNl.js} +1 -1
- package/dist/assets/{common-B7U2CeDc.js → common-C9Gvbmz5.js} +1 -1
- package/dist/assets/{components-Vz1pD133.js → components-CMj235gS.js} +1 -1
- package/dist/assets/{datasource-Bp8TnG7a.js → datasource-BsklC6tp.js} +1 -1
- package/dist/assets/{dependency-graph-panel-B31-YcSF.js → dependency-graph-panel-BNqY5al9.js} +1 -1
- package/dist/assets/{documentation-panel-C2MJwSef.js → documentation-panel-UrRQcsOB.js} +1 -1
- package/dist/assets/{download-J6sWXUJR.js → download-BRqw2JzE.js} +1 -1
- package/dist/assets/{edit-page-DfxHDnWP.js → edit-page-Dqkmdmfj.js} +4 -4
- package/dist/assets/{error-entries-BcRbOxwK.js → error-entries-CuIzi5P7.js} +1 -1
- package/dist/assets/{error-panel-jZlFCjdy.js → error-panel-CFCs0M8e.js} +1 -1
- package/dist/assets/{feedback-button-C2XmZw_y.js → feedback-button-DZVOiUEf.js} +1 -1
- package/dist/assets/feedback-button-J9hRaX_K.js +1 -0
- package/dist/assets/{file-explorer-panel-Cg9UovsS.js → file-explorer-panel-Cqey5tMY.js} +1 -1
- package/dist/assets/{file-name-input-y9ggAMGJ.js → file-name-input-khW7mtt2.js} +1 -1
- package/dist/assets/{floating-outline-CE1olGl0.js → floating-outline-CSwtWNm9.js} +1 -1
- package/dist/assets/{formatting-DW2u7HXI.js → formatting-CMgDQsM2.js} +1 -1
- package/dist/assets/{gallery-page-ClEnoAT-.js → gallery-page-CcG0Msi3.js} +1 -1
- package/dist/assets/{glide-data-editor-Cp0q28Zz.js → glide-data-editor-B46-8C6x.js} +1 -1
- package/dist/assets/{home-page-DJsI6Od_.js → home-page-C5qcnnak.js} +1 -1
- package/dist/assets/{hooks-DGSryuSD.js → hooks-CiNPyjez.js} +1 -1
- package/dist/assets/index-D1hW_v1v.js +30 -0
- package/dist/assets/{input-DLA5Dhn3.js → input-BQBy_25D.js} +1 -1
- package/dist/assets/{layout-DRVIFud8.js → layout-DSsztKhq.js} +3 -3
- package/dist/assets/{logs-panel-Db38azhc.js → logs-panel-DZ8zJWNT.js} +1 -1
- package/dist/assets/{markdown-renderer-D2td9eFc.js → markdown-renderer-DszNRkaL.js} +1 -1
- package/dist/assets/{name-cell-input-CJpi0Kvu.js → name-cell-input-BIpvW2kg.js} +1 -1
- package/dist/assets/numbers-BB-lSqeY.js +1 -0
- package/dist/assets/{outline-panel-DxQKP4eN.js → outline-panel-CHwFhhFo.js} +1 -1
- package/dist/assets/{packages-panel-BFzN4mNb.js → packages-panel-qT6TuHGP.js} +1 -1
- package/dist/assets/{panel-accordion-state-D3RiRniG.js → panel-accordion-state-DROYE5RR.js} +1 -1
- package/dist/assets/{panels-CpUpLOy4.js → panels-3HImHlAU.js} +1 -1
- package/dist/assets/{process-output-B6v1FE29.js → process-output-CPh21qjo.js} +1 -1
- package/dist/assets/{readonly-code-display-Deg2hzFh.js → readonly-code-display-DKoAsBb-.js} +1 -1
- package/dist/assets/{readonly-python-code-DPGknu49.js → readonly-python-code-QqiDzhYb.js} +1 -1
- package/dist/assets/{reveal-component-CXkDpGxy.js → reveal-component-Dd29QoYx.js} +1 -1
- package/dist/assets/{run-page-qp2KKYgs.js → run-page-J_1gGmPH.js} +1 -1
- package/dist/assets/{runtime-details-state-BiGF299n.js → runtime-details-state-BlarRoM1.js} +1 -1
- package/dist/assets/{scratchpad-panel-QV0lacAe.js → scratchpad-panel-BPw7VxO6.js} +1 -1
- package/dist/assets/{secrets-panel-ConQpchl.js → secrets-panel-j7vcP2r2.js} +1 -1
- package/dist/assets/{session-panel-CrWluawa.js → session-panel-BnszHmkW.js} +1 -1
- package/dist/assets/{snippets-panel-CM7g9b6c.js → snippets-panel-a-A15D0n.js} +1 -1
- package/dist/assets/{state-lEPoZS4k.js → state-2U7LSUNv.js} +1 -1
- package/dist/assets/{state-DhMztz1E.js → state-B_SxQGvw.js} +1 -1
- package/dist/assets/{switch-D2gdC_ep.js → switch-DLzvDOLo.js} +1 -1
- package/dist/assets/{tracing-DRA8maZB.js → tracing-CpbvZwxP.js} +1 -1
- package/dist/assets/{tracing-panel-DLxPRadV.js → tracing-panel-Dxtm4-GJ.js} +2 -2
- package/dist/assets/{tree-actions-COtU5qy_.js → tree-actions-CIBdpNC-.js} +1 -1
- package/dist/assets/{useAddCell-BAEOA87M.js → useAddCell-B6my967m.js} +1 -1
- package/dist/assets/{useCellActionButton-BobjsJfu.js → useCellActionButton-BwBEO8zi.js} +1 -1
- package/dist/assets/{useDeleteCell-gedazkBF.js → useDeleteCell-DYRsSrAa.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-DAJ6BOUm.js → useDependencyPanelTab-lNbBYJZs.js} +1 -1
- package/dist/assets/{useInstallPackage-BfNKN33w.js → useInstallPackage-Q9agSKO7.js} +1 -1
- package/dist/assets/{useNotebookActions-CnLArLoz.js → useNotebookActions-CmG2irDr.js} +1 -1
- package/dist/assets/{useRunCells-CUknep7p.js → useRunCells-Bl702L84.js} +1 -1
- package/dist/assets/{useSplitCell-DeXniINv.js → useSplitCell-DQRIXpFK.js} +1 -1
- package/dist/assets/{write-secret-modal-CBi2vNK_.js → write-secret-modal-BujVAO67.js} +1 -1
- package/dist/index.html +25 -25
- package/package.json +1 -1
- package/src/components/ui/__tests__/range-slider.test.tsx +165 -0
- package/src/components/ui/range-slider.tsx +22 -9
- package/src/plugins/impl/RangeSliderPlugin.tsx +17 -2
- package/src/plugins/impl/__tests__/RangeSliderPlugin.test.tsx +109 -0
- package/src/utils/__tests__/numbers.test.ts +57 -0
- package/src/utils/numbers.ts +59 -9
- package/dist/assets/feedback-button-94vZrmBy.js +0 -1
- package/dist/assets/index-BjNf54kO.js +0 -30
- package/dist/assets/numbers-DJK15_4S.js +0 -1
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { render } from "@testing-library/react";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { RangeSlider } from "../range-slider";
|
|
7
|
+
|
|
8
|
+
vi.mock("react-aria", () => ({
|
|
9
|
+
useLocale: () => ({ locale: "en-US" }),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock("../tooltip", () => {
|
|
13
|
+
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
14
|
+
<div>{children}</div>
|
|
15
|
+
);
|
|
16
|
+
const Content = ({ children }: { children: React.ReactNode }) => (
|
|
17
|
+
<div>{children}</div>
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
TooltipContent: Content,
|
|
22
|
+
TooltipPortal: Wrapper,
|
|
23
|
+
TooltipProvider: Wrapper,
|
|
24
|
+
TooltipRoot: Wrapper,
|
|
25
|
+
TooltipTrigger: Wrapper,
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
vi.mock("radix-ui", () => {
|
|
30
|
+
const Primitive = React.forwardRef<
|
|
31
|
+
HTMLDivElement,
|
|
32
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
33
|
+
>(({ children, ...props }, ref) => (
|
|
34
|
+
<div ref={ref} {...props}>
|
|
35
|
+
{children}
|
|
36
|
+
</div>
|
|
37
|
+
));
|
|
38
|
+
Primitive.displayName = "Primitive";
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
Slider: {
|
|
42
|
+
Root: Primitive,
|
|
43
|
+
Track: Primitive,
|
|
44
|
+
Range: Primitive,
|
|
45
|
+
Thumb: Primitive,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("RangeSlider", () => {
|
|
51
|
+
it("formats both tooltip values without floating-point artifacts", () => {
|
|
52
|
+
const { getAllByText, queryByText } = render(
|
|
53
|
+
<RangeSlider
|
|
54
|
+
aria-label="Range"
|
|
55
|
+
min={0}
|
|
56
|
+
max={10}
|
|
57
|
+
step={0.01}
|
|
58
|
+
value={[1.2, 1.500000000000001]}
|
|
59
|
+
valueMap={(value) => value}
|
|
60
|
+
/>,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
expect(getAllByText("1.2")).toHaveLength(1);
|
|
64
|
+
expect(getAllByText("1.5")).toHaveLength(1);
|
|
65
|
+
expect(queryByText("1.500000000000001")).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("keeps the legacy precision when no step is supplied", () => {
|
|
69
|
+
const { getAllByText, queryByText } = render(
|
|
70
|
+
<RangeSlider
|
|
71
|
+
aria-label="Range"
|
|
72
|
+
min={0}
|
|
73
|
+
max={10}
|
|
74
|
+
value={[1.2, 1.500000000000001]}
|
|
75
|
+
valueMap={(value) => value}
|
|
76
|
+
/>,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
expect(getAllByText("1.2")).toHaveLength(1);
|
|
80
|
+
expect(getAllByText("1.5")).toHaveLength(1);
|
|
81
|
+
expect(queryByText("1.500000000000001")).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("retains value precision when no step is supplied", () => {
|
|
85
|
+
const { getByText } = render(
|
|
86
|
+
<RangeSlider
|
|
87
|
+
aria-label="Range"
|
|
88
|
+
min={0}
|
|
89
|
+
max={10}
|
|
90
|
+
value={[1.2345, 1.500000000000001]}
|
|
91
|
+
valueMap={(value) => value}
|
|
92
|
+
/>,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
expect(getByText("1.2345")).toBeDefined();
|
|
96
|
+
expect(getByText("1.5")).toBeDefined();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("preserves precision from finer-grained steps", () => {
|
|
100
|
+
const { getAllByText, queryByText } = render(
|
|
101
|
+
<RangeSlider
|
|
102
|
+
aria-label="Range"
|
|
103
|
+
min={0}
|
|
104
|
+
max={10}
|
|
105
|
+
step={0.001}
|
|
106
|
+
value={[1.234, 1.235000000000001]}
|
|
107
|
+
valueMap={(value) => value}
|
|
108
|
+
/>,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
expect(getAllByText("1.234")).toHaveLength(1);
|
|
112
|
+
expect(getAllByText("1.235")).toHaveLength(1);
|
|
113
|
+
expect(queryByText("1.23")).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("preserves precision for small tooltip values", () => {
|
|
117
|
+
const { getByText } = render(
|
|
118
|
+
<RangeSlider
|
|
119
|
+
aria-label="Range"
|
|
120
|
+
min={0}
|
|
121
|
+
max={0.01}
|
|
122
|
+
step={0.00001}
|
|
123
|
+
value={[0.00123, 0.0012400000000000002]}
|
|
124
|
+
valueMap={(value) => value}
|
|
125
|
+
/>,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
expect(getByText("1.23e-3")).toBeDefined();
|
|
129
|
+
expect(getByText("1.24e-3")).toBeDefined();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("infers precision from custom mapped steps", () => {
|
|
133
|
+
const steps = [1, 1.234, 2];
|
|
134
|
+
const { getAllByText } = render(
|
|
135
|
+
<RangeSlider
|
|
136
|
+
aria-label="Range"
|
|
137
|
+
min={0}
|
|
138
|
+
max={steps.length - 1}
|
|
139
|
+
step={1}
|
|
140
|
+
steps={steps}
|
|
141
|
+
value={[0, 1]}
|
|
142
|
+
valueMap={(value) => steps[value]}
|
|
143
|
+
/>,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
expect(getAllByText("1")).toHaveLength(1);
|
|
147
|
+
expect(getAllByText("1.234")).toHaveLength(1);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("preserves decimals from the slider origin", () => {
|
|
151
|
+
const { getAllByText } = render(
|
|
152
|
+
<RangeSlider
|
|
153
|
+
aria-label="Range"
|
|
154
|
+
min={0.5}
|
|
155
|
+
max={10.5}
|
|
156
|
+
step={1}
|
|
157
|
+
value={[0.5, 1.5]}
|
|
158
|
+
valueMap={(value) => value}
|
|
159
|
+
/>,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
expect(getAllByText("0.5")).toHaveLength(1);
|
|
163
|
+
expect(getAllByText("1.5")).toHaveLength(1);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -4,7 +4,10 @@ import { Slider as SliderPrimitive } from "radix-ui";
|
|
|
4
4
|
import * as React from "react";
|
|
5
5
|
import { useLocale } from "react-aria";
|
|
6
6
|
import { cn } from "@/utils/cn";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
fractionDigitsForSlider,
|
|
9
|
+
prettyScientificNumber,
|
|
10
|
+
} from "@/utils/numbers";
|
|
8
11
|
import { useBoolean } from "../../hooks/useBoolean";
|
|
9
12
|
import {
|
|
10
13
|
TooltipContent,
|
|
@@ -20,9 +23,15 @@ const RangeSlider = React.forwardRef<
|
|
|
20
23
|
valueMap: (sliderValue: number) => number;
|
|
21
24
|
steps?: number[];
|
|
22
25
|
}
|
|
23
|
-
>(({ className, valueMap, ...props }, ref) => {
|
|
26
|
+
>(({ className, steps, valueMap, ...props }, ref) => {
|
|
24
27
|
const [open, openActions] = useBoolean(false);
|
|
25
28
|
const { locale } = useLocale();
|
|
29
|
+
const fractionDigits = fractionDigitsForSlider(
|
|
30
|
+
props.step,
|
|
31
|
+
steps,
|
|
32
|
+
props.min,
|
|
33
|
+
props.value?.map(valueMap),
|
|
34
|
+
);
|
|
26
35
|
|
|
27
36
|
const isDraggingRange = React.useRef(false);
|
|
28
37
|
const dragStartX = React.useRef(0);
|
|
@@ -94,11 +103,9 @@ const RangeSlider = React.forwardRef<
|
|
|
94
103
|
const [origLeft, origRight] = dragStartValue.current;
|
|
95
104
|
const rangeWidth = origRight - origLeft;
|
|
96
105
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
? Math.min(...steps.slice(1).map((s, i) => s - steps[i]))
|
|
101
|
-
: (props.step ?? 1);
|
|
106
|
+
// `steps` contains mapped display values, while pointer movement happens
|
|
107
|
+
// in the slider's index/value space. Use the Radix step for snapping.
|
|
108
|
+
const step = props.step ?? 1;
|
|
102
109
|
const snappedDelta = Math.round(delta / step) * step;
|
|
103
110
|
|
|
104
111
|
const clampedDelta = Math.max(
|
|
@@ -177,7 +184,10 @@ const RangeSlider = React.forwardRef<
|
|
|
177
184
|
<TooltipPortal>
|
|
178
185
|
{props.value != null && props.value.length === 2 && (
|
|
179
186
|
<TooltipContent key={props.value[0]}>
|
|
180
|
-
{prettyScientificNumber(valueMap(props.value[0]), {
|
|
187
|
+
{prettyScientificNumber(valueMap(props.value[0]), {
|
|
188
|
+
maximumFractionDigits: fractionDigits,
|
|
189
|
+
locale,
|
|
190
|
+
})}
|
|
181
191
|
</TooltipContent>
|
|
182
192
|
)}
|
|
183
193
|
</TooltipPortal>
|
|
@@ -198,7 +208,10 @@ const RangeSlider = React.forwardRef<
|
|
|
198
208
|
<TooltipPortal>
|
|
199
209
|
{props.value != null && props.value.length === 2 && (
|
|
200
210
|
<TooltipContent key={props.value[1]}>
|
|
201
|
-
{prettyScientificNumber(valueMap(props.value[1]), {
|
|
211
|
+
{prettyScientificNumber(valueMap(props.value[1]), {
|
|
212
|
+
maximumFractionDigits: fractionDigits,
|
|
213
|
+
locale,
|
|
214
|
+
})}
|
|
202
215
|
</TooltipContent>
|
|
203
216
|
)}
|
|
204
217
|
</TooltipPortal>
|
|
@@ -5,7 +5,10 @@ import { type JSX, useEffect, useId, useState } from "react";
|
|
|
5
5
|
import { useLocale } from "react-aria";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { cn } from "@/utils/cn";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
fractionDigitsForSlider,
|
|
10
|
+
prettyScientificNumber,
|
|
11
|
+
} from "@/utils/numbers";
|
|
9
12
|
import { RangeSlider } from "../../components/ui/range-slider";
|
|
10
13
|
import type { IPlugin, IPluginProps, Setter } from "../types";
|
|
11
14
|
import { Labeled } from "./common/labeled";
|
|
@@ -74,6 +77,7 @@ const RangeSliderComponent = ({
|
|
|
74
77
|
start,
|
|
75
78
|
stop,
|
|
76
79
|
step,
|
|
80
|
+
steps,
|
|
77
81
|
debounce,
|
|
78
82
|
orientation,
|
|
79
83
|
showValue,
|
|
@@ -86,6 +90,12 @@ const RangeSliderComponent = ({
|
|
|
86
90
|
|
|
87
91
|
// Hold internal value
|
|
88
92
|
const [internalValue, setInternalValue] = useState(value);
|
|
93
|
+
const fractionDigits = fractionDigitsForSlider(
|
|
94
|
+
step,
|
|
95
|
+
steps,
|
|
96
|
+
start,
|
|
97
|
+
internalValue.map(valueMap),
|
|
98
|
+
);
|
|
89
99
|
|
|
90
100
|
// Update internal value on prop change
|
|
91
101
|
useEffect(() => {
|
|
@@ -121,6 +131,7 @@ const RangeSliderComponent = ({
|
|
|
121
131
|
min={start}
|
|
122
132
|
max={stop}
|
|
123
133
|
step={step}
|
|
134
|
+
steps={steps ?? undefined}
|
|
124
135
|
orientation={orientation}
|
|
125
136
|
disabled={disabled}
|
|
126
137
|
// Triggered on all value changes
|
|
@@ -154,8 +165,12 @@ const RangeSliderComponent = ({
|
|
|
154
165
|
{showValue && (
|
|
155
166
|
<div className="text-xs text-muted-foreground min-w-[16px]">
|
|
156
167
|
{`${prettyScientificNumber(valueMap(internalValue[0]), {
|
|
168
|
+
maximumFractionDigits: fractionDigits,
|
|
169
|
+
locale,
|
|
170
|
+
})}, ${prettyScientificNumber(valueMap(internalValue[1]), {
|
|
171
|
+
maximumFractionDigits: fractionDigits,
|
|
157
172
|
locale,
|
|
158
|
-
})}
|
|
173
|
+
})}`}
|
|
159
174
|
</div>
|
|
160
175
|
)}
|
|
161
176
|
</div>
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { render } from "@testing-library/react";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
import type { z } from "zod";
|
|
6
|
+
import type { IPluginProps } from "../../types";
|
|
7
|
+
import { RangeSliderPlugin } from "../RangeSliderPlugin";
|
|
8
|
+
|
|
9
|
+
vi.mock("@/components/ui/range-slider", () => ({
|
|
10
|
+
RangeSlider: () => <div data-testid="range-slider" />,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock("react-aria", () => ({
|
|
14
|
+
useLocale: () => ({ locale: "en-US" }),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
describe("RangeSliderPlugin", () => {
|
|
18
|
+
type PluginProps = IPluginProps<
|
|
19
|
+
number[],
|
|
20
|
+
z.infer<typeof RangeSliderPlugin.prototype.validator>
|
|
21
|
+
>;
|
|
22
|
+
|
|
23
|
+
const createProps = (
|
|
24
|
+
value: number[],
|
|
25
|
+
data: Partial<PluginProps["data"]> = {},
|
|
26
|
+
): PluginProps => ({
|
|
27
|
+
host: document.createElement("div"),
|
|
28
|
+
value,
|
|
29
|
+
setValue: vi.fn(),
|
|
30
|
+
data: {
|
|
31
|
+
initialValue: value,
|
|
32
|
+
label: null,
|
|
33
|
+
start: 0,
|
|
34
|
+
stop: 10,
|
|
35
|
+
step: 0.01,
|
|
36
|
+
steps: null,
|
|
37
|
+
debounce: false,
|
|
38
|
+
orientation: "horizontal",
|
|
39
|
+
showValue: true,
|
|
40
|
+
fullWidth: true,
|
|
41
|
+
disabled: false,
|
|
42
|
+
...data,
|
|
43
|
+
},
|
|
44
|
+
functions: {},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("rounds floating-point artifacts in the displayed range", () => {
|
|
48
|
+
const plugin = new RangeSliderPlugin();
|
|
49
|
+
const props = createProps([1.2, 1.500000000000001]);
|
|
50
|
+
|
|
51
|
+
const { getByText } = render(plugin.render(props));
|
|
52
|
+
|
|
53
|
+
expect(getByText("1.2, 1.5")).toBeDefined();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("preserves precision from finer-grained steps", () => {
|
|
57
|
+
const plugin = new RangeSliderPlugin();
|
|
58
|
+
const props = createProps([1.234, 1.235000000000001], { step: 0.001 });
|
|
59
|
+
|
|
60
|
+
const { getByText } = render(plugin.render(props));
|
|
61
|
+
|
|
62
|
+
expect(getByText("1.234, 1.235")).toBeDefined();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("retains value precision when no step is supplied", () => {
|
|
66
|
+
const plugin = new RangeSliderPlugin();
|
|
67
|
+
const props = createProps([1.2345, 1.500000000000001], {
|
|
68
|
+
step: undefined,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const { getByText } = render(plugin.render(props));
|
|
72
|
+
|
|
73
|
+
expect(getByText("1.2345, 1.5")).toBeDefined();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("preserves precision for small displayed values", () => {
|
|
77
|
+
const plugin = new RangeSliderPlugin();
|
|
78
|
+
const props = createProps([0.00123, 0.0012400000000000002], {
|
|
79
|
+
stop: 0.01,
|
|
80
|
+
step: 0.00001,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const { getByText } = render(plugin.render(props));
|
|
84
|
+
|
|
85
|
+
expect(getByText("1.23e-3, 1.24e-3")).toBeDefined();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("infers precision from custom steps", () => {
|
|
89
|
+
const plugin = new RangeSliderPlugin();
|
|
90
|
+
const props = createProps([0, 1], {
|
|
91
|
+
stop: 2,
|
|
92
|
+
step: undefined,
|
|
93
|
+
steps: [1, 1.234, 2],
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const { getByText } = render(plugin.render(props));
|
|
97
|
+
|
|
98
|
+
expect(getByText("1, 1.234")).toBeDefined();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("preserves decimals from the slider origin", () => {
|
|
102
|
+
const plugin = new RangeSliderPlugin();
|
|
103
|
+
const props = createProps([0.5, 1.5], { start: 0.5, stop: 10, step: 1 });
|
|
104
|
+
|
|
105
|
+
const { getByText } = render(plugin.render(props));
|
|
106
|
+
|
|
107
|
+
expect(getByText("0.5, 1.5")).toBeDefined();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { describe, expect, it, vi } from "vitest";
|
|
3
3
|
import {
|
|
4
4
|
countFractionDigits,
|
|
5
|
+
fractionDigitsForSlider,
|
|
5
6
|
maxFractionDigitsForSteps,
|
|
6
7
|
prettyEngineeringNumber,
|
|
7
8
|
prettyNumber,
|
|
@@ -104,6 +105,42 @@ describe("prettyScientificNumber", () => {
|
|
|
104
105
|
"1.23456789101112",
|
|
105
106
|
);
|
|
106
107
|
});
|
|
108
|
+
|
|
109
|
+
it("supports caller-provided display precision", () => {
|
|
110
|
+
expect(
|
|
111
|
+
prettyScientificNumber(1.2340000000000002, {
|
|
112
|
+
maximumFractionDigits: 2,
|
|
113
|
+
locale,
|
|
114
|
+
}),
|
|
115
|
+
).toBe("1.23");
|
|
116
|
+
expect(
|
|
117
|
+
prettyScientificNumber(1.2340000000000002, {
|
|
118
|
+
maximumFractionDigits: 3,
|
|
119
|
+
locale,
|
|
120
|
+
}),
|
|
121
|
+
).toBe("1.234");
|
|
122
|
+
expect(
|
|
123
|
+
prettyScientificNumber(1.234, {
|
|
124
|
+
maximumFractionDigits: 3,
|
|
125
|
+
locale,
|
|
126
|
+
}),
|
|
127
|
+
).toBe("1.234");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("preserves caller-provided precision in scientific notation", () => {
|
|
131
|
+
expect(
|
|
132
|
+
prettyScientificNumber(0.0012300000000000002, {
|
|
133
|
+
maximumFractionDigits: 5,
|
|
134
|
+
locale,
|
|
135
|
+
}),
|
|
136
|
+
).toBe("1.23e-3");
|
|
137
|
+
expect(
|
|
138
|
+
prettyScientificNumber(1_000_001.2300000001, {
|
|
139
|
+
maximumFractionDigits: 2,
|
|
140
|
+
locale,
|
|
141
|
+
}),
|
|
142
|
+
).toBe("1.00000123e6");
|
|
143
|
+
});
|
|
107
144
|
});
|
|
108
145
|
|
|
109
146
|
describe("prettyEngineeringNumber", () => {
|
|
@@ -153,3 +190,23 @@ describe("maxFractionDigitsForSteps", () => {
|
|
|
153
190
|
expect(maxFractionDigitsForSteps([1, 2, 3, 4], 1)).toBe(0);
|
|
154
191
|
});
|
|
155
192
|
});
|
|
193
|
+
|
|
194
|
+
describe("fractionDigitsForSlider", () => {
|
|
195
|
+
it("preserves precision for regular slider steps", () => {
|
|
196
|
+
expect(fractionDigitsForSlider(0.001)).toBe(3);
|
|
197
|
+
expect(fractionDigitsForSlider(0.01)).toBe(2);
|
|
198
|
+
expect(fractionDigitsForSlider(undefined)).toBe(2);
|
|
199
|
+
expect(fractionDigitsForSlider(undefined, null, 0, [0.1234, 0.5])).toBe(4);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("infers precision from custom step values", () => {
|
|
203
|
+
expect(fractionDigitsForSlider(undefined, [1, 1.234, 2])).toBe(3);
|
|
204
|
+
expect(
|
|
205
|
+
fractionDigitsForSlider(undefined, [0.1, 0.2, 0.30000000000000004]),
|
|
206
|
+
).toBe(1);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("includes the slider origin in regular step precision", () => {
|
|
210
|
+
expect(fractionDigitsForSlider(1, null, 0.5)).toBe(1);
|
|
211
|
+
});
|
|
212
|
+
});
|
package/src/utils/numbers.ts
CHANGED
|
@@ -24,19 +24,19 @@ export const maxFractionalDigits = memoizeLastValue((locale: string) => {
|
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
const getScientificFormatter = memoizeLastValue(
|
|
27
|
-
(locale: string) =>
|
|
27
|
+
(locale: string, maximumFractionDigits = 1) =>
|
|
28
28
|
new Intl.NumberFormat(locale, {
|
|
29
29
|
minimumFractionDigits: 1,
|
|
30
|
-
maximumFractionDigits
|
|
30
|
+
maximumFractionDigits,
|
|
31
31
|
notation: "scientific",
|
|
32
32
|
}),
|
|
33
33
|
);
|
|
34
34
|
|
|
35
35
|
const getRoundedFormatter = memoizeLastValue(
|
|
36
|
-
(locale: string) =>
|
|
36
|
+
(locale: string, maximumFractionDigits = 2) =>
|
|
37
37
|
new Intl.NumberFormat(locale, {
|
|
38
38
|
minimumFractionDigits: 0,
|
|
39
|
-
maximumFractionDigits
|
|
39
|
+
maximumFractionDigits,
|
|
40
40
|
}),
|
|
41
41
|
);
|
|
42
42
|
|
|
@@ -60,7 +60,7 @@ export function countFractionDigits(n: number): number {
|
|
|
60
60
|
|
|
61
61
|
/** Max fraction digits to display a set of step values cleanly. */
|
|
62
62
|
export function maxFractionDigitsForSteps(
|
|
63
|
-
steps: number[],
|
|
63
|
+
steps: readonly number[],
|
|
64
64
|
minGap: number,
|
|
65
65
|
): number {
|
|
66
66
|
let max = countFractionDigits(minGap);
|
|
@@ -70,6 +70,41 @@ export function maxFractionDigitsForSteps(
|
|
|
70
70
|
return max;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/** Infer display precision from a slider's regular or custom steps. */
|
|
74
|
+
export function fractionDigitsForSlider(
|
|
75
|
+
step?: number,
|
|
76
|
+
steps?: readonly number[] | null,
|
|
77
|
+
origin?: number,
|
|
78
|
+
values?: readonly number[],
|
|
79
|
+
): number {
|
|
80
|
+
const originDigits = origin == null ? 0 : countFractionDigits(origin);
|
|
81
|
+
|
|
82
|
+
if (steps && steps.length > 0) {
|
|
83
|
+
let minGap = Number.POSITIVE_INFINITY;
|
|
84
|
+
for (let i = 1; i < steps.length; i++) {
|
|
85
|
+
const gap = Math.abs(steps[i] - steps[i - 1]);
|
|
86
|
+
if (gap > 0 && gap < minGap) {
|
|
87
|
+
minGap = gap;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return Math.max(
|
|
92
|
+
originDigits,
|
|
93
|
+
maxFractionDigitsForSteps(steps, Number.isFinite(minGap) ? minGap : 0),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (step == null || !Number.isFinite(step) || step <= 0) {
|
|
98
|
+
return Math.max(
|
|
99
|
+
2,
|
|
100
|
+
originDigits,
|
|
101
|
+
...(values ?? []).map(countFractionDigits),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return Math.max(originDigits, countFractionDigits(step));
|
|
106
|
+
}
|
|
107
|
+
|
|
73
108
|
/** Round away float noise so step-based inputs stay on a clean decimal grid. */
|
|
74
109
|
export function roundToFractionDigits(n: number, digits: number): number {
|
|
75
110
|
return Number(n.toFixed(digits));
|
|
@@ -121,6 +156,7 @@ export function prettyScientificNumber(
|
|
|
121
156
|
value: number,
|
|
122
157
|
opts: {
|
|
123
158
|
shouldRound?: boolean; // Default to false
|
|
159
|
+
maximumFractionDigits?: number;
|
|
124
160
|
locale: string;
|
|
125
161
|
},
|
|
126
162
|
): string {
|
|
@@ -133,14 +169,28 @@ export function prettyScientificNumber(
|
|
|
133
169
|
// Determine if the number should be in scientific notation
|
|
134
170
|
const absValue = Math.abs(value);
|
|
135
171
|
if (absValue < 1e-2 || absValue >= 1e6) {
|
|
136
|
-
|
|
172
|
+
// Scientific notation counts digits after the decimal in the mantissa.
|
|
173
|
+
const digits =
|
|
174
|
+
opts.maximumFractionDigits === undefined
|
|
175
|
+
? 1
|
|
176
|
+
: Math.max(
|
|
177
|
+
1,
|
|
178
|
+
Math.min(
|
|
179
|
+
maxFractionalDigits(opts.locale),
|
|
180
|
+
opts.maximumFractionDigits + Math.floor(Math.log10(absValue)),
|
|
181
|
+
),
|
|
182
|
+
);
|
|
183
|
+
return getScientificFormatter(opts.locale, digits)
|
|
184
|
+
.format(value)
|
|
185
|
+
.toLowerCase();
|
|
137
186
|
}
|
|
138
187
|
|
|
139
188
|
const { shouldRound, locale } = opts;
|
|
140
189
|
|
|
141
|
-
if (shouldRound) {
|
|
142
|
-
|
|
143
|
-
|
|
190
|
+
if (shouldRound || opts.maximumFractionDigits !== undefined) {
|
|
191
|
+
return getRoundedFormatter(locale, opts.maximumFractionDigits ?? 2).format(
|
|
192
|
+
value,
|
|
193
|
+
);
|
|
144
194
|
}
|
|
145
195
|
|
|
146
196
|
// Don't round
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{n as e}from"./feedback-button-C2XmZw_y.js";export{e as FeedbackModal};
|