From 1204a7c04667af0c0b527650f962e54cd70c0d80 Mon Sep 17 00:00:00 2001 From: k88hudson-cfa Date: Mon, 11 May 2026 23:29:05 -0400 Subject: [PATCH] feat(components): add sliderDisplay prop to NumberInput Custom (value: number) => string formatter for slider thumb labels and min/max labels. Applies to single sliders and ranges; the regular text input is unaffected. --- .../components/src/NumberInput/NumberInput.md | 51 +++++++++++++++++++ .../src/NumberInput/NumberInput.test.ts | 48 +++++++++++++++++ .../src/NumberInput/NumberInput.vue | 5 ++ 3 files changed, 104 insertions(+) diff --git a/cfasim-ui/components/src/NumberInput/NumberInput.md b/cfasim-ui/components/src/NumberInput/NumberInput.md index 0718672..b48ba29 100644 --- a/cfasim-ui/components/src/NumberInput/NumberInput.md +++ b/cfasim-ui/components/src/NumberInput/NumberInput.md @@ -16,6 +16,12 @@ const ageRange = ref([18, 65]) const coverageRange = ref([0.2, 0.8]) const minAge = ref(18) const maxAge = ref(65) +const dayMs = 24 * 60 * 60 * 1000 +const dateStart = Date.UTC(2024, 0, 1) +const dateEnd = Date.UTC(2024, 11, 31) +const dateRange = ref([Date.UTC(2024, 2, 1), Date.UTC(2024, 8, 30)]) +const formatDate = (ms) => + new Date(ms).toLocaleDateString("en-US", { month: "short", day: "numeric" }) @@ -226,6 +232,51 @@ Range mode works with `percent` and `live` as well: +### Custom slider display + +Pass `slider-display` (a `(value: number) => string` function) to format the +thumb labels and the min/max labels however you like. The internal model is +still a number — only the displayed text changes. This applies to single +sliders and ranges; the regular text input is unaffected. + + +
+ +
+ + +
+ ### Live slider With `live`, the model updates while dragging the slider thumb rather than only on release. diff --git a/cfasim-ui/components/src/NumberInput/NumberInput.test.ts b/cfasim-ui/components/src/NumberInput/NumberInput.test.ts index 1146fc0..63f711d 100644 --- a/cfasim-ui/components/src/NumberInput/NumberInput.test.ts +++ b/cfasim-ui/components/src/NumberInput/NumberInput.test.ts @@ -801,6 +801,33 @@ describe("NumberInput", () => { expect((input.element as HTMLInputElement).value).toBe("2.500"); }); + it("applies sliderDisplay to single-slider thumb and labels", () => { + const wrapper = mount(NumberInput, { + props: { + modelValue: 50, + slider: true, + min: 0, + max: 100, + sliderDisplay: (v: number) => `${v} days`, + }, + }); + expect(wrapper.find(".slider-current").text()).toBe("50 days"); + const labels = wrapper.findAll(".slider-labels span"); + expect(labels[0].text()).toBe("0 days"); + expect(labels[1].text()).toBe("100 days"); + }); + + it("ignores sliderDisplay for regular (non-slider) inputs", () => { + const wrapper = mount(NumberInput, { + props: { + modelValue: 50, + sliderDisplay: (v: number) => `${v} days`, + }, + }); + const input = wrapper.find("input"); + expect((input.element as HTMLInputElement).value).toBe("50"); + }); + it("syncs local value when model changes externally", async () => { const wrapper = mount(NumberInput, { props: { @@ -946,6 +973,27 @@ describe("NumberInput", () => { expect(slider.props("modelValue")).toEqual([10, 90]); }); + it("uses sliderDisplay to format thumb labels and min/max labels", () => { + const fmt = (v: number) => new Date(v).toISOString().slice(0, 10); + const start = Date.UTC(2024, 0, 1); + const mid = Date.UTC(2024, 5, 1); + const end = Date.UTC(2024, 11, 31); + const wrapper = mount(NumberInput, { + props: { + range: [mid, end] as NumberRange, + min: start, + max: end, + sliderDisplay: fmt, + }, + }); + const thumbs = wrapper.findAll(".slider-thumb"); + expect(thumbs[0].text()).toBe("2024-06-01"); + expect(thumbs[1].text()).toBe("2024-12-31"); + const labels = wrapper.findAll(".slider-labels span"); + expect(labels[0].text()).toBe("2024-01-01"); + expect(labels[1].text()).toBe("2024-12-31"); + }); + it("syncs slider when range model changes externally", async () => { const wrapper = mount(NumberInput, { props: { diff --git a/cfasim-ui/components/src/NumberInput/NumberInput.vue b/cfasim-ui/components/src/NumberInput/NumberInput.vue index 68a3897..65aedcb 100644 --- a/cfasim-ui/components/src/NumberInput/NumberInput.vue +++ b/cfasim-ui/components/src/NumberInput/NumberInput.vue @@ -29,6 +29,10 @@ const props = defineProps<{ numberType?: "integer" | "float"; required?: boolean; decimals?: number; + // Custom formatter for slider thumb labels and min/max labels. Overrides + // the default percent/decimal formatting when provided. Only consulted in + // slider/range mode — the text input keeps its own number-shaped formatting. + sliderDisplay?: (value: number) => string; }>(); function isRangeValue(v: unknown): v is NumberRange { @@ -109,6 +113,7 @@ function roundToDecimals(v: number, d: number): number { function formatSliderValue(v: number | undefined) { if (v == null) return ""; + if (props.sliderDisplay) return props.sliderDisplay(v); const d = displayDecimals.value; if (props.percent) return (v * 100).toFixed(d) + "%"; return v.toLocaleString("en-US", {