@ledgerhq/lumen-utils-shared 0.1.5 → 0.1.7
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/index.js
CHANGED
|
@@ -1,142 +1,138 @@
|
|
|
1
|
-
import { jsx as
|
|
2
|
-
import { createContext as W, useRef as
|
|
1
|
+
import { jsx as F } from "react/jsx-runtime";
|
|
2
|
+
import { createContext as W, useRef as v, useContext as w, useMemo as C } from "react";
|
|
3
3
|
import { clsx as B } from "clsx";
|
|
4
4
|
import { twMerge as R } from "tailwind-merge";
|
|
5
|
-
function
|
|
5
|
+
function et(t) {
|
|
6
6
|
return t ? t.split(/[-_ ]+/).map((e) => e.charAt(0).toUpperCase() + e.slice(1).toLowerCase()).join("") : "";
|
|
7
7
|
}
|
|
8
8
|
const _ = {
|
|
9
9
|
md: { max: 48, min: 17, scale: 2, startAt: 0 },
|
|
10
10
|
sm: { max: 28, min: 12, scale: 2, startAt: 6 }
|
|
11
11
|
};
|
|
12
|
-
function
|
|
13
|
-
const { max: r, min:
|
|
14
|
-
return Math.max(
|
|
12
|
+
function nt(t, e = "md") {
|
|
13
|
+
const { max: r, min: n, scale: i, startAt: s } = _[e], o = t.replace(/\D/g, "").length;
|
|
14
|
+
return Math.max(n, r - Math.max(0, o - s) * i);
|
|
15
15
|
}
|
|
16
|
-
function
|
|
16
|
+
function z(t, e = !0) {
|
|
17
17
|
if (!t) return "";
|
|
18
|
-
const r = t.includes("."), [
|
|
19
|
-
return r && e ?
|
|
18
|
+
const r = t.includes("."), [n, i] = t.split("."), s = n.replace(/\B(?=(\d{3})+(?!\d))/g, " ");
|
|
19
|
+
return r && e ? i ? `${s}.${i}` : `${s}.` : s;
|
|
20
20
|
}
|
|
21
|
-
|
|
21
|
+
const Z = (t) => t.replaceAll(",", ".").replaceAll(/[^\d.]/g, ""), q = (t) => t.replace(/^0+(?=\d)/, ""), T = (t, e) => e > 0 ? t.slice(0, e) : t, K = (t, e) => T(t.replaceAll(/\D/g, ""), e), N = (t, e, r) => {
|
|
22
|
+
if (t === ".") return "0.";
|
|
23
|
+
const n = t.indexOf(".");
|
|
24
|
+
if (n === -1) return T(t, e);
|
|
25
|
+
const i = T(t.slice(0, n), e) || "0", s = t.slice(n + 1).replaceAll(".", "").slice(0, r);
|
|
26
|
+
return s.length > 0 ? `${i}.${s}` : t.endsWith(".") ? `${i}.` : i;
|
|
27
|
+
};
|
|
28
|
+
function rt(t, e = {}) {
|
|
22
29
|
const {
|
|
23
30
|
allowDecimals: r = !0,
|
|
24
|
-
thousandsSeparator:
|
|
25
|
-
maxIntegerLength:
|
|
26
|
-
maxDecimalLength:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return n = n.replace(/\D/g, ""), s > 0 && n.length > s && (n = n.slice(0, s)), o ? A(n) : n;
|
|
31
|
-
n === "." && (n = "0.");
|
|
32
|
-
const u = n.indexOf(".");
|
|
33
|
-
if (u !== -1) {
|
|
34
|
-
let a = n.slice(0, u), f = n.slice(u + 1).replace(/\./g, "");
|
|
35
|
-
s > 0 && a.length > s && (a = a.slice(0, s)), f = f.slice(0, l), a === "" && (a = "0");
|
|
36
|
-
const h = i.endsWith(".") || n.endsWith(".");
|
|
37
|
-
n = f.length > 0 ? `${a}.${f}` : h ? `${a}.` : a;
|
|
38
|
-
} else
|
|
39
|
-
s > 0 && n.length > s && (n = n.slice(0, s));
|
|
40
|
-
return o ? A(n) : n;
|
|
31
|
+
thousandsSeparator: n = !0,
|
|
32
|
+
maxIntegerLength: i = 9,
|
|
33
|
+
maxDecimalLength: s = 9,
|
|
34
|
+
decimalSeparator: o = "."
|
|
35
|
+
} = e, c = q(Z(t)), a = r ? N(c, i, s) : K(c, i), d = n ? z(a) : a;
|
|
36
|
+
return o === "." ? d : d.replaceAll(".", o);
|
|
41
37
|
}
|
|
42
|
-
function
|
|
38
|
+
function H(t, e) {
|
|
43
39
|
if (Object.is(t, e)) return !0;
|
|
44
|
-
const r = t,
|
|
45
|
-
return
|
|
46
|
-
(
|
|
40
|
+
const r = t, n = e, i = Object.keys(r), s = Object.keys(n);
|
|
41
|
+
return i.length !== s.length ? !1 : i.every(
|
|
42
|
+
(o) => Object.prototype.hasOwnProperty.call(n, o) && Object.is(r[o], n[o])
|
|
47
43
|
);
|
|
48
44
|
}
|
|
49
|
-
function
|
|
50
|
-
const r = W(e),
|
|
51
|
-
const
|
|
52
|
-
return
|
|
45
|
+
function U(t, e) {
|
|
46
|
+
const r = W(e), n = ({ children: s, value: o }) => {
|
|
47
|
+
const c = v(o);
|
|
48
|
+
return H(c.current, o) || (c.current = o), /* @__PURE__ */ F(r.Provider, { value: c.current, children: s });
|
|
53
49
|
};
|
|
54
|
-
|
|
55
|
-
function
|
|
56
|
-
consumerName:
|
|
57
|
-
contextRequired:
|
|
50
|
+
n.displayName = t + "Provider";
|
|
51
|
+
function i({
|
|
52
|
+
consumerName: s,
|
|
53
|
+
contextRequired: o
|
|
58
54
|
}) {
|
|
59
|
-
const
|
|
60
|
-
if (
|
|
61
|
-
return
|
|
62
|
-
if (
|
|
55
|
+
const c = w(r);
|
|
56
|
+
if (c)
|
|
57
|
+
return c;
|
|
58
|
+
if (o)
|
|
63
59
|
throw new Error(
|
|
64
|
-
`${
|
|
60
|
+
`${s} must be used within ${t}`
|
|
65
61
|
);
|
|
66
62
|
return e || {};
|
|
67
63
|
}
|
|
68
|
-
return [
|
|
64
|
+
return [n, i];
|
|
69
65
|
}
|
|
70
|
-
const [
|
|
66
|
+
const [ot, Y] = U("Disabled", { disabled: !1 }), it = ({
|
|
71
67
|
consumerName: t,
|
|
72
68
|
contextRequired: e,
|
|
73
69
|
mergeWith: r
|
|
74
70
|
}) => {
|
|
75
|
-
const
|
|
71
|
+
const n = Y({
|
|
76
72
|
consumerName: t,
|
|
77
73
|
contextRequired: e ?? !1
|
|
78
74
|
});
|
|
79
|
-
return !!(r?.disabled ||
|
|
75
|
+
return !!(r?.disabled || n.disabled);
|
|
80
76
|
};
|
|
81
|
-
function
|
|
77
|
+
function st(...t) {
|
|
82
78
|
return R(B(t));
|
|
83
79
|
}
|
|
84
|
-
const O = (t) => typeof t == "string" || typeof t == "number",
|
|
80
|
+
const O = (t) => typeof t == "string" || typeof t == "number", ct = (t) => Array.isArray(t) ? t.every(O) : O(t);
|
|
85
81
|
function S(t, e) {
|
|
86
82
|
if (typeof t == "function")
|
|
87
83
|
return t(e);
|
|
88
84
|
typeof t == "object" && t !== null && "current" in t && (t.current = e);
|
|
89
85
|
}
|
|
90
|
-
function
|
|
86
|
+
function G(...t) {
|
|
91
87
|
const e = /* @__PURE__ */ new Map();
|
|
92
88
|
return (r) => {
|
|
93
89
|
if (r === null) {
|
|
94
|
-
t.forEach((
|
|
95
|
-
const
|
|
96
|
-
typeof
|
|
90
|
+
t.forEach((n) => {
|
|
91
|
+
const i = e.get(n);
|
|
92
|
+
typeof i == "function" && i(), S(n, null);
|
|
97
93
|
}), e.clear();
|
|
98
94
|
return;
|
|
99
95
|
}
|
|
100
|
-
t.forEach((
|
|
101
|
-
const
|
|
102
|
-
typeof
|
|
96
|
+
t.forEach((n) => {
|
|
97
|
+
const i = S(n, r);
|
|
98
|
+
typeof i == "function" && e.set(n, i);
|
|
103
99
|
});
|
|
104
100
|
};
|
|
105
101
|
}
|
|
106
|
-
function
|
|
107
|
-
return
|
|
102
|
+
function lt(...t) {
|
|
103
|
+
return C(() => G(...t), t);
|
|
108
104
|
}
|
|
109
|
-
const
|
|
105
|
+
const at = (t, e) => {
|
|
110
106
|
let r = t;
|
|
111
|
-
for (const
|
|
107
|
+
for (const n of e) {
|
|
112
108
|
if (r == null)
|
|
113
109
|
return;
|
|
114
|
-
r = r[
|
|
110
|
+
r = r[n];
|
|
115
111
|
}
|
|
116
112
|
return r;
|
|
117
|
-
},
|
|
113
|
+
}, ut = ({
|
|
118
114
|
currentStep: t,
|
|
119
115
|
totalSteps: e,
|
|
120
116
|
size: r,
|
|
121
|
-
label:
|
|
122
|
-
strokeWidth:
|
|
123
|
-
arcPercentage:
|
|
117
|
+
label: n,
|
|
118
|
+
strokeWidth: i = 4,
|
|
119
|
+
arcPercentage: s = 0.75
|
|
124
120
|
}) => {
|
|
125
|
-
const
|
|
121
|
+
const o = Math.min(Math.max(t, 0), e), c = n ?? `${o}/${e}`, a = e <= 0 ? 0 : o / e, d = (r - i) / 2, g = r / 2, p = r / 2, u = 2 * Math.PI * d, m = u * s, b = m * (1 - a), h = o <= 0, D = `${m} ${u}`, P = h ? m - 2 : b;
|
|
126
122
|
return {
|
|
127
|
-
displayLabel:
|
|
128
|
-
progress:
|
|
129
|
-
r:
|
|
130
|
-
cx:
|
|
131
|
-
cy:
|
|
132
|
-
circumference:
|
|
133
|
-
trackArcLength:
|
|
134
|
-
trackDashArray: `${
|
|
123
|
+
displayLabel: c,
|
|
124
|
+
progress: a,
|
|
125
|
+
r: d,
|
|
126
|
+
cx: g,
|
|
127
|
+
cy: p,
|
|
128
|
+
circumference: u,
|
|
129
|
+
trackArcLength: m,
|
|
130
|
+
trackDashArray: `${m} ${u}`,
|
|
135
131
|
progressDashArray: D,
|
|
136
132
|
progressDashOffset: P,
|
|
137
|
-
showMinimalDot:
|
|
133
|
+
showMinimalDot: h
|
|
138
134
|
};
|
|
139
|
-
},
|
|
135
|
+
}, ft = (t) => C(
|
|
140
136
|
() => ({
|
|
141
137
|
integerPart: Array.from(t.integerPart, (e) => ({
|
|
142
138
|
value: e,
|
|
@@ -148,57 +144,57 @@ const ot = (t, e) => {
|
|
|
148
144
|
})) : []
|
|
149
145
|
}),
|
|
150
146
|
[t.integerPart, t.decimalPart]
|
|
151
|
-
),
|
|
147
|
+
), dt = (t, e, r) => {
|
|
152
148
|
if (e) return r;
|
|
153
|
-
const
|
|
154
|
-
return t.currencyPosition === "end" ? `${
|
|
149
|
+
const n = t.decimalPart ? `${t.decimalSeparator}${t.decimalPart}` : "", i = `${t.integerPart}${n}`;
|
|
150
|
+
return t.currencyPosition === "end" ? `${i} ${t.currencyText}` : `${t.currencyText} ${i}`;
|
|
155
151
|
};
|
|
156
|
-
function
|
|
157
|
-
const { leading:
|
|
158
|
-
let
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
return
|
|
163
|
-
}, D = (
|
|
164
|
-
const
|
|
165
|
-
return e -
|
|
166
|
-
}, P = (
|
|
167
|
-
const
|
|
168
|
-
return
|
|
169
|
-
}, $ = (
|
|
170
|
-
const
|
|
171
|
-
if (P(
|
|
172
|
-
$(
|
|
152
|
+
function J(t, e, r = {}) {
|
|
153
|
+
const { leading: n = !1, trailing: i = !0, maxWait: s } = r;
|
|
154
|
+
let o = null, c = null, a = null, d = 0, g = null, p = null, u;
|
|
155
|
+
const m = s !== void 0, b = m ? Math.max(s, e) : 0, h = (l) => {
|
|
156
|
+
d = l;
|
|
157
|
+
const f = g, x = p;
|
|
158
|
+
return g = null, p = null, u = t.apply(x, f), u;
|
|
159
|
+
}, D = (l) => {
|
|
160
|
+
const f = a ? l - a : 0;
|
|
161
|
+
return e - f;
|
|
162
|
+
}, P = (l) => {
|
|
163
|
+
const f = a ? l - a : e, x = l - d;
|
|
164
|
+
return a === null || f >= e || f < 0 || m && x >= b;
|
|
165
|
+
}, $ = (l) => (o = null, i && g ? h(l) : (g = null, p = null, u)), y = () => {
|
|
166
|
+
const l = Date.now();
|
|
167
|
+
if (P(l)) {
|
|
168
|
+
$(l);
|
|
173
169
|
return;
|
|
174
170
|
}
|
|
175
|
-
|
|
171
|
+
o = setTimeout(y, D(l));
|
|
176
172
|
}, M = () => {
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
},
|
|
180
|
-
|
|
181
|
-
},
|
|
182
|
-
const
|
|
183
|
-
if (
|
|
184
|
-
if (
|
|
185
|
-
return
|
|
186
|
-
if (
|
|
187
|
-
return clearTimeout(
|
|
173
|
+
const l = Date.now();
|
|
174
|
+
c = null, g && (h(l), o !== null && (clearTimeout(o), o = setTimeout(y, e)));
|
|
175
|
+
}, E = (l) => (d = l, o = setTimeout(y, e), m && (c = setTimeout(M, b)), n ? h(l) : u), L = () => {
|
|
176
|
+
o !== null && clearTimeout(o), c !== null && clearTimeout(c), d = 0, g = null, p = null, a = null, o = null, c = null;
|
|
177
|
+
}, j = () => o === null && c === null ? u : $(Date.now()), k = () => o !== null || c !== null, A = function(...l) {
|
|
178
|
+
const f = Date.now(), x = P(f);
|
|
179
|
+
if (g = l, p = this, a = f, x) {
|
|
180
|
+
if (o === null)
|
|
181
|
+
return E(f);
|
|
182
|
+
if (m)
|
|
183
|
+
return clearTimeout(o), o = setTimeout(y, e), h(f);
|
|
188
184
|
}
|
|
189
|
-
return
|
|
185
|
+
return o === null && (o = setTimeout(y, e)), u;
|
|
190
186
|
};
|
|
191
|
-
return
|
|
187
|
+
return A.cancel = L, A.flush = j, A.pending = k, A;
|
|
192
188
|
}
|
|
193
|
-
function
|
|
194
|
-
const { leading:
|
|
195
|
-
return
|
|
196
|
-
leading:
|
|
197
|
-
trailing:
|
|
189
|
+
function mt(t, e, r = {}) {
|
|
190
|
+
const { leading: n = !0, trailing: i = !0 } = r;
|
|
191
|
+
return J(t, e, {
|
|
192
|
+
leading: n,
|
|
193
|
+
trailing: i,
|
|
198
194
|
maxWait: e
|
|
199
195
|
});
|
|
200
196
|
}
|
|
201
|
-
const
|
|
197
|
+
const gt = ({
|
|
202
198
|
onClick: t,
|
|
203
199
|
disabled: e
|
|
204
200
|
}) => t ? e ? {
|
|
@@ -208,43 +204,43 @@ const at = ({
|
|
|
208
204
|
} : {
|
|
209
205
|
role: "button",
|
|
210
206
|
tabIndex: 0,
|
|
211
|
-
onKeyDown: (
|
|
212
|
-
(
|
|
207
|
+
onKeyDown: (n) => {
|
|
208
|
+
(n.key === "Enter" || n.key === " ") && (n.preventDefault(), t?.(n));
|
|
213
209
|
},
|
|
214
210
|
onClick: t
|
|
215
|
-
} : void 0,
|
|
211
|
+
} : void 0, I = " ", pt = ({
|
|
216
212
|
label: t,
|
|
217
213
|
placeholder: e
|
|
218
214
|
}) => {
|
|
219
215
|
const r = typeof e == "string" && e.trim().length > 0 ? e : void 0;
|
|
220
216
|
return t ? {
|
|
221
|
-
inputPlaceholder: r ??
|
|
217
|
+
inputPlaceholder: r ?? I,
|
|
222
218
|
labelStaysFloatedWithPlaceholder: r !== void 0
|
|
223
219
|
} : {
|
|
224
|
-
inputPlaceholder: e ??
|
|
220
|
+
inputPlaceholder: e ?? I,
|
|
225
221
|
labelStaysFloatedWithPlaceholder: !1
|
|
226
222
|
};
|
|
227
223
|
};
|
|
228
224
|
export {
|
|
229
|
-
|
|
225
|
+
ot as DisabledProvider,
|
|
230
226
|
S as assignRef,
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
227
|
+
dt as buildAriaLabel,
|
|
228
|
+
st as cn,
|
|
229
|
+
U as createSafeContext,
|
|
230
|
+
J as debounce,
|
|
231
|
+
z as formatThousands,
|
|
232
|
+
gt as getButtonA11yProps,
|
|
233
|
+
nt as getFontSize,
|
|
234
|
+
at as getObjectPath,
|
|
235
|
+
ut as getStepperCalculations,
|
|
236
|
+
ct as isTextChildren,
|
|
237
|
+
G as mergeRefs,
|
|
238
|
+
pt as resolveBaseInputPlaceholder,
|
|
239
|
+
H as shallowEqual,
|
|
240
|
+
rt as textFormatter,
|
|
241
|
+
mt as throttle,
|
|
242
|
+
et as toPascalCase,
|
|
243
|
+
it as useDisabledContext,
|
|
244
|
+
lt as useMergedRef,
|
|
245
|
+
ft as useSplitText
|
|
250
246
|
};
|
|
@@ -7,11 +7,20 @@ export type TextFormatterOptions = {
|
|
|
7
7
|
maxIntegerLength?: number;
|
|
8
8
|
/** Maximum length for decimal part (after decimal) */
|
|
9
9
|
maxDecimalLength?: number;
|
|
10
|
+
/**
|
|
11
|
+
* Character used to display the decimal separator.
|
|
12
|
+
* Input parsing always accepts both `.` and `,` regardless of this value.
|
|
13
|
+
*/
|
|
14
|
+
decimalSeparator?: '.' | ',';
|
|
10
15
|
};
|
|
11
16
|
/**
|
|
12
17
|
* Formats and validates numeric input text for amount inputs.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
18
|
+
*
|
|
19
|
+
* Parsing is always lenient: both `.` and `,` are accepted as the decimal
|
|
20
|
+
* separator on input, so the result stays correct regardless of the locale
|
|
21
|
+
* keyboard. The `decimalSeparator` option only controls how the result is
|
|
22
|
+
* displayed.
|
|
23
|
+
*
|
|
15
24
|
* @returns Formatted and cleaned numeric string
|
|
16
25
|
*
|
|
17
26
|
* @example
|
|
@@ -19,9 +28,11 @@ export type TextFormatterOptions = {
|
|
|
19
28
|
* textFormatter('.5') // '0.5'
|
|
20
29
|
* textFormatter('1.2.3') // '1.23'
|
|
21
30
|
* textFormatter('1,5') // '1.5'
|
|
31
|
+
* textFormatter('1,5', { decimalSeparator: ',' }) // '1,5'
|
|
22
32
|
* textFormatter('abc123') // '123'
|
|
23
33
|
* textFormatter('1000000', { thousandsSeparator: true }) // '1 000 000'
|
|
24
34
|
* textFormatter('1234.5678', { thousandsSeparator: true }) // '1 234.5678'
|
|
35
|
+
* textFormatter('1234.5', { decimalSeparator: ',' }) // '1 234,5'
|
|
25
36
|
* textFormatter('123456789012', { maxIntegerLength: 9 }) // '123456789'
|
|
26
37
|
* textFormatter('123.123456', { maxDecimalLength: 2 }) // '123.12'
|
|
27
38
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"textFormatter.d.ts","sourceRoot":"","sources":["../../../src/lib/inputFormatter/textFormatter.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,oBAAoB,GAAG;IACjC,sCAAsC;IACtC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,uDAAuD;IACvD,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,uDAAuD;IACvD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,sDAAsD;IACtD,gBAAgB,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"textFormatter.d.ts","sourceRoot":"","sources":["../../../src/lib/inputFormatter/textFormatter.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,oBAAoB,GAAG;IACjC,sCAAsC;IACtC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,uDAAuD;IACvD,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,uDAAuD;IACvD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,sDAAsD;IACtD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,GAAG,GAAG,GAAG,CAAC;CAC9B,CAAC;AAyCF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,aAAa;AAC3B,gCAAgC;AAChC,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,oBAAyB,GACjC,MAAM,CAsBR"}
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { textFormatter } from './textFormatter.js';
|
|
2
|
+
import { textFormatter, type TextFormatterOptions } from './textFormatter.js';
|
|
3
3
|
|
|
4
4
|
describe('textFormatter', () => {
|
|
5
5
|
describe('decimal handling with allowDecimals=true (default)', () => {
|
|
@@ -93,6 +93,64 @@ describe('textFormatter', () => {
|
|
|
93
93
|
});
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
+
describe('decimalSeparator display', () => {
|
|
97
|
+
type Case = {
|
|
98
|
+
name: string;
|
|
99
|
+
input: string;
|
|
100
|
+
expected: string;
|
|
101
|
+
options?: TextFormatterOptions;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const commaSeparatorCases: Case[] = [
|
|
105
|
+
{
|
|
106
|
+
name: 'display dot decimal as comma',
|
|
107
|
+
input: '1.5',
|
|
108
|
+
expected: '1,5',
|
|
109
|
+
options: { decimalSeparator: ',' },
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: 'display comma input as comma',
|
|
113
|
+
input: '1,5',
|
|
114
|
+
expected: '1,5',
|
|
115
|
+
options: { decimalSeparator: ',' },
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'comma decimal with space thousands',
|
|
119
|
+
input: '1234.5',
|
|
120
|
+
expected: '1 234,5',
|
|
121
|
+
options: { decimalSeparator: ',' },
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'preserve trailing separator while typing',
|
|
125
|
+
input: '12.',
|
|
126
|
+
expected: '12,',
|
|
127
|
+
options: { decimalSeparator: ',' },
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'no decimal part is unaffected',
|
|
131
|
+
input: '1234',
|
|
132
|
+
expected: '1 234',
|
|
133
|
+
options: { decimalSeparator: ',' },
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'integer-only mode is unaffected by comma separator',
|
|
137
|
+
input: '1.5',
|
|
138
|
+
expected: '15',
|
|
139
|
+
options: { decimalSeparator: ',', allowDecimals: false },
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'explicit dot separator matches default',
|
|
143
|
+
input: '1234.5',
|
|
144
|
+
expected: '1 234.5',
|
|
145
|
+
options: { decimalSeparator: '.' },
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
it.each(commaSeparatorCases)('$name', ({ input, expected, options }) => {
|
|
150
|
+
expect(textFormatter(input, options)).toBe(expected);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
96
154
|
describe('non-numeric character removal', () => {
|
|
97
155
|
type Case = { name: string; input: string; expected: string };
|
|
98
156
|
|
|
@@ -9,12 +9,60 @@ export type TextFormatterOptions = {
|
|
|
9
9
|
maxIntegerLength?: number;
|
|
10
10
|
/** Maximum length for decimal part (after decimal) */
|
|
11
11
|
maxDecimalLength?: number;
|
|
12
|
+
/**
|
|
13
|
+
* Character used to display the decimal separator.
|
|
14
|
+
* Input parsing always accepts both `.` and `,` regardless of this value.
|
|
15
|
+
*/
|
|
16
|
+
decimalSeparator?: '.' | ',';
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** Keep only digits and dots; commas are treated as decimal separators. */
|
|
20
|
+
const sanitize = (value: string): string =>
|
|
21
|
+
value.replaceAll(',', '.').replaceAll(/[^\d.]/g, '');
|
|
22
|
+
|
|
23
|
+
/** Drop leading zeros, keeping one when it precedes a dot or stands alone (e.g. "0", "0.5"). */
|
|
24
|
+
const stripLeadingZeros = (value: string): string =>
|
|
25
|
+
value.replace(/^0+(?=\d)/, '');
|
|
26
|
+
|
|
27
|
+
/** Truncate to `max` characters. A `max` of 0 disables the limit. */
|
|
28
|
+
const limitIntegerLength = (value: string, max: number): string =>
|
|
29
|
+
max > 0 ? value.slice(0, max) : value;
|
|
30
|
+
|
|
31
|
+
const formatIntegerOnly = (value: string, maxIntegerLength: number): string =>
|
|
32
|
+
limitIntegerLength(value.replaceAll(/\D/g, ''), maxIntegerLength);
|
|
33
|
+
|
|
34
|
+
const formatDecimal = (
|
|
35
|
+
value: string,
|
|
36
|
+
maxIntegerLength: number,
|
|
37
|
+
maxDecimalLength: number,
|
|
38
|
+
): string => {
|
|
39
|
+
// A lone separator becomes "0." so the user can keep typing decimals.
|
|
40
|
+
if (value === '.') return '0.';
|
|
41
|
+
|
|
42
|
+
const firstDot = value.indexOf('.');
|
|
43
|
+
if (firstDot === -1) return limitIntegerLength(value, maxIntegerLength);
|
|
44
|
+
|
|
45
|
+
const integerPart =
|
|
46
|
+
limitIntegerLength(value.slice(0, firstDot), maxIntegerLength) || '0';
|
|
47
|
+
// Extra dots are ignored, e.g. the decimal part of "1.2.3" is "23".
|
|
48
|
+
const decimalPart = value
|
|
49
|
+
.slice(firstDot + 1)
|
|
50
|
+
.replaceAll('.', '')
|
|
51
|
+
.slice(0, maxDecimalLength);
|
|
52
|
+
|
|
53
|
+
if (decimalPart.length > 0) return `${integerPart}.${decimalPart}`;
|
|
54
|
+
// Preserve a trailing dot mid-typing (e.g. "12.").
|
|
55
|
+
return value.endsWith('.') ? `${integerPart}.` : integerPart;
|
|
12
56
|
};
|
|
13
57
|
|
|
14
58
|
/**
|
|
15
59
|
* Formats and validates numeric input text for amount inputs.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
60
|
+
*
|
|
61
|
+
* Parsing is always lenient: both `.` and `,` are accepted as the decimal
|
|
62
|
+
* separator on input, so the result stays correct regardless of the locale
|
|
63
|
+
* keyboard. The `decimalSeparator` option only controls how the result is
|
|
64
|
+
* displayed.
|
|
65
|
+
*
|
|
18
66
|
* @returns Formatted and cleaned numeric string
|
|
19
67
|
*
|
|
20
68
|
* @example
|
|
@@ -22,9 +70,11 @@ export type TextFormatterOptions = {
|
|
|
22
70
|
* textFormatter('.5') // '0.5'
|
|
23
71
|
* textFormatter('1.2.3') // '1.23'
|
|
24
72
|
* textFormatter('1,5') // '1.5'
|
|
73
|
+
* textFormatter('1,5', { decimalSeparator: ',' }) // '1,5'
|
|
25
74
|
* textFormatter('abc123') // '123'
|
|
26
75
|
* textFormatter('1000000', { thousandsSeparator: true }) // '1 000 000'
|
|
27
76
|
* textFormatter('1234.5678', { thousandsSeparator: true }) // '1 234.5678'
|
|
77
|
+
* textFormatter('1234.5', { decimalSeparator: ',' }) // '1 234,5'
|
|
28
78
|
* textFormatter('123456789012', { maxIntegerLength: 9 }) // '123456789'
|
|
29
79
|
* textFormatter('123.123456', { maxDecimalLength: 2 }) // '123.12'
|
|
30
80
|
*/
|
|
@@ -38,58 +88,20 @@ export function textFormatter(
|
|
|
38
88
|
thousandsSeparator = true,
|
|
39
89
|
maxIntegerLength = 9,
|
|
40
90
|
maxDecimalLength = 9,
|
|
91
|
+
decimalSeparator = '.',
|
|
41
92
|
} = options;
|
|
42
|
-
// Normalize input: convert comma to dot, remove non-digit/non-dot characters
|
|
43
|
-
const normalizedValue = value.replace(',', '.').replace(/[^\d.]/g, '');
|
|
44
|
-
let cleaned = normalizedValue;
|
|
45
|
-
|
|
46
|
-
// Remove leading zeros (except when followed by dot)
|
|
47
|
-
cleaned = cleaned.replace(/^0+(?=\d)/, '');
|
|
48
|
-
|
|
49
|
-
if (!allowDecimals) {
|
|
50
|
-
// Integer-only: remove any non-digit and apply max length
|
|
51
|
-
cleaned = cleaned.replace(/\D/g, '');
|
|
52
|
-
if (maxIntegerLength > 0 && cleaned.length > maxIntegerLength) {
|
|
53
|
-
cleaned = cleaned.slice(0, maxIntegerLength);
|
|
54
|
-
}
|
|
55
|
-
return thousandsSeparator ? formatThousands(cleaned) : cleaned;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Handle single dot input as "0."
|
|
59
|
-
if (cleaned === '.') {
|
|
60
|
-
cleaned = '0.';
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const firstDot = cleaned.indexOf('.');
|
|
64
|
-
if (firstDot !== -1) {
|
|
65
|
-
// Split integer and decimal parts
|
|
66
|
-
let integerPart = cleaned.slice(0, firstDot);
|
|
67
|
-
let decimalPart = cleaned.slice(firstDot + 1).replace(/\./g, '');
|
|
68
93
|
|
|
69
|
-
|
|
70
|
-
if (maxIntegerLength > 0 && integerPart.length > maxIntegerLength) {
|
|
71
|
-
integerPart = integerPart.slice(0, maxIntegerLength);
|
|
72
|
-
}
|
|
73
|
-
decimalPart = decimalPart.slice(0, maxDecimalLength);
|
|
94
|
+
const sanitized = stripLeadingZeros(sanitize(value));
|
|
74
95
|
|
|
75
|
-
|
|
76
|
-
|
|
96
|
+
const cleaned = allowDecimals
|
|
97
|
+
? formatDecimal(sanitized, maxIntegerLength, maxDecimalLength)
|
|
98
|
+
: formatIntegerOnly(sanitized, maxIntegerLength);
|
|
77
99
|
|
|
78
|
-
|
|
79
|
-
const hasTrailingDot =
|
|
80
|
-
normalizedValue.endsWith('.') || cleaned.endsWith('.');
|
|
81
|
-
cleaned =
|
|
82
|
-
decimalPart.length > 0
|
|
83
|
-
? `${integerPart}.${decimalPart}`
|
|
84
|
-
: hasTrailingDot
|
|
85
|
-
? `${integerPart}.`
|
|
86
|
-
: integerPart;
|
|
87
|
-
} else {
|
|
88
|
-
// No decimal point, just apply integer length limit
|
|
89
|
-
if (maxIntegerLength > 0 && cleaned.length > maxIntegerLength) {
|
|
90
|
-
cleaned = cleaned.slice(0, maxIntegerLength);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
100
|
+
const grouped = thousandsSeparator ? formatThousands(cleaned) : cleaned;
|
|
93
101
|
|
|
94
|
-
|
|
102
|
+
// formatThousands groups with spaces and keeps a single dot for the decimal,
|
|
103
|
+
// so localizing the separator only ever touches that one dot.
|
|
104
|
+
return decimalSeparator === '.'
|
|
105
|
+
? grouped
|
|
106
|
+
: grouped.replaceAll('.', decimalSeparator);
|
|
95
107
|
}
|