@ledgerhq/lumen-utils-shared 0.1.6 → 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,143 +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
|
-
s > 0 && n.length > s && (n = n.slice(0, s));
|
|
35
|
-
else {
|
|
36
|
-
let a = n.slice(0, u), f = n.slice(u + 1).replace(/\./g, "");
|
|
37
|
-
s > 0 && a.length > s && (a = a.slice(0, s)), f = f.slice(0, l), a === "" && (a = "0");
|
|
38
|
-
const h = i.endsWith(".") || n.endsWith(".");
|
|
39
|
-
n = f.length > 0 ? `${a}.${f}` : h ? `${a}.` : a;
|
|
40
|
-
}
|
|
41
|
-
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);
|
|
42
37
|
}
|
|
43
|
-
function
|
|
38
|
+
function H(t, e) {
|
|
44
39
|
if (Object.is(t, e)) return !0;
|
|
45
|
-
const r = t,
|
|
46
|
-
return
|
|
47
|
-
(
|
|
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])
|
|
48
43
|
);
|
|
49
44
|
}
|
|
50
|
-
function
|
|
51
|
-
const r = W(e),
|
|
52
|
-
const
|
|
53
|
-
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 });
|
|
54
49
|
};
|
|
55
|
-
|
|
56
|
-
function
|
|
57
|
-
consumerName:
|
|
58
|
-
contextRequired:
|
|
50
|
+
n.displayName = t + "Provider";
|
|
51
|
+
function i({
|
|
52
|
+
consumerName: s,
|
|
53
|
+
contextRequired: o
|
|
59
54
|
}) {
|
|
60
|
-
const
|
|
61
|
-
if (
|
|
62
|
-
return
|
|
63
|
-
if (
|
|
55
|
+
const c = w(r);
|
|
56
|
+
if (c)
|
|
57
|
+
return c;
|
|
58
|
+
if (o)
|
|
64
59
|
throw new Error(
|
|
65
|
-
`${
|
|
60
|
+
`${s} must be used within ${t}`
|
|
66
61
|
);
|
|
67
62
|
return e || {};
|
|
68
63
|
}
|
|
69
|
-
return [
|
|
64
|
+
return [n, i];
|
|
70
65
|
}
|
|
71
|
-
const [
|
|
66
|
+
const [ot, Y] = U("Disabled", { disabled: !1 }), it = ({
|
|
72
67
|
consumerName: t,
|
|
73
68
|
contextRequired: e,
|
|
74
69
|
mergeWith: r
|
|
75
70
|
}) => {
|
|
76
|
-
const
|
|
71
|
+
const n = Y({
|
|
77
72
|
consumerName: t,
|
|
78
73
|
contextRequired: e ?? !1
|
|
79
74
|
});
|
|
80
|
-
return !!(r?.disabled ||
|
|
75
|
+
return !!(r?.disabled || n.disabled);
|
|
81
76
|
};
|
|
82
|
-
function
|
|
77
|
+
function st(...t) {
|
|
83
78
|
return R(B(t));
|
|
84
79
|
}
|
|
85
|
-
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);
|
|
86
81
|
function S(t, e) {
|
|
87
82
|
if (typeof t == "function")
|
|
88
83
|
return t(e);
|
|
89
84
|
typeof t == "object" && t !== null && "current" in t && (t.current = e);
|
|
90
85
|
}
|
|
91
|
-
function
|
|
86
|
+
function G(...t) {
|
|
92
87
|
const e = /* @__PURE__ */ new Map();
|
|
93
88
|
return (r) => {
|
|
94
89
|
if (r === null) {
|
|
95
|
-
t.forEach((
|
|
96
|
-
const
|
|
97
|
-
typeof
|
|
90
|
+
t.forEach((n) => {
|
|
91
|
+
const i = e.get(n);
|
|
92
|
+
typeof i == "function" && i(), S(n, null);
|
|
98
93
|
}), e.clear();
|
|
99
94
|
return;
|
|
100
95
|
}
|
|
101
|
-
t.forEach((
|
|
102
|
-
const
|
|
103
|
-
typeof
|
|
96
|
+
t.forEach((n) => {
|
|
97
|
+
const i = S(n, r);
|
|
98
|
+
typeof i == "function" && e.set(n, i);
|
|
104
99
|
});
|
|
105
100
|
};
|
|
106
101
|
}
|
|
107
|
-
function
|
|
108
|
-
return
|
|
102
|
+
function lt(...t) {
|
|
103
|
+
return C(() => G(...t), t);
|
|
109
104
|
}
|
|
110
|
-
const
|
|
105
|
+
const at = (t, e) => {
|
|
111
106
|
let r = t;
|
|
112
|
-
for (const
|
|
107
|
+
for (const n of e) {
|
|
113
108
|
if (r == null)
|
|
114
109
|
return;
|
|
115
|
-
r = r[
|
|
110
|
+
r = r[n];
|
|
116
111
|
}
|
|
117
112
|
return r;
|
|
118
|
-
},
|
|
113
|
+
}, ut = ({
|
|
119
114
|
currentStep: t,
|
|
120
115
|
totalSteps: e,
|
|
121
116
|
size: r,
|
|
122
|
-
label:
|
|
123
|
-
strokeWidth:
|
|
124
|
-
arcPercentage:
|
|
117
|
+
label: n,
|
|
118
|
+
strokeWidth: i = 4,
|
|
119
|
+
arcPercentage: s = 0.75
|
|
125
120
|
}) => {
|
|
126
|
-
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;
|
|
127
122
|
return {
|
|
128
|
-
displayLabel:
|
|
129
|
-
progress:
|
|
130
|
-
r:
|
|
131
|
-
cx:
|
|
132
|
-
cy:
|
|
133
|
-
circumference:
|
|
134
|
-
trackArcLength:
|
|
135
|
-
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}`,
|
|
136
131
|
progressDashArray: D,
|
|
137
132
|
progressDashOffset: P,
|
|
138
|
-
showMinimalDot:
|
|
133
|
+
showMinimalDot: h
|
|
139
134
|
};
|
|
140
|
-
},
|
|
135
|
+
}, ft = (t) => C(
|
|
141
136
|
() => ({
|
|
142
137
|
integerPart: Array.from(t.integerPart, (e) => ({
|
|
143
138
|
value: e,
|
|
@@ -149,57 +144,57 @@ const ot = (t, e) => {
|
|
|
149
144
|
})) : []
|
|
150
145
|
}),
|
|
151
146
|
[t.integerPart, t.decimalPart]
|
|
152
|
-
),
|
|
147
|
+
), dt = (t, e, r) => {
|
|
153
148
|
if (e) return r;
|
|
154
|
-
const
|
|
155
|
-
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}`;
|
|
156
151
|
};
|
|
157
|
-
function
|
|
158
|
-
const { leading:
|
|
159
|
-
let
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
return
|
|
164
|
-
}, D = (
|
|
165
|
-
const
|
|
166
|
-
return e -
|
|
167
|
-
}, P = (
|
|
168
|
-
const
|
|
169
|
-
return
|
|
170
|
-
}, $ = (
|
|
171
|
-
const
|
|
172
|
-
if (P(
|
|
173
|
-
$(
|
|
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);
|
|
174
169
|
return;
|
|
175
170
|
}
|
|
176
|
-
|
|
171
|
+
o = setTimeout(y, D(l));
|
|
177
172
|
}, M = () => {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
},
|
|
181
|
-
|
|
182
|
-
},
|
|
183
|
-
const
|
|
184
|
-
if (
|
|
185
|
-
if (
|
|
186
|
-
return
|
|
187
|
-
if (
|
|
188
|
-
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);
|
|
189
184
|
}
|
|
190
|
-
return
|
|
185
|
+
return o === null && (o = setTimeout(y, e)), u;
|
|
191
186
|
};
|
|
192
|
-
return
|
|
187
|
+
return A.cancel = L, A.flush = j, A.pending = k, A;
|
|
193
188
|
}
|
|
194
|
-
function
|
|
195
|
-
const { leading:
|
|
196
|
-
return
|
|
197
|
-
leading:
|
|
198
|
-
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,
|
|
199
194
|
maxWait: e
|
|
200
195
|
});
|
|
201
196
|
}
|
|
202
|
-
const
|
|
197
|
+
const gt = ({
|
|
203
198
|
onClick: t,
|
|
204
199
|
disabled: e
|
|
205
200
|
}) => t ? e ? {
|
|
@@ -209,43 +204,43 @@ const at = ({
|
|
|
209
204
|
} : {
|
|
210
205
|
role: "button",
|
|
211
206
|
tabIndex: 0,
|
|
212
|
-
onKeyDown: (
|
|
213
|
-
(
|
|
207
|
+
onKeyDown: (n) => {
|
|
208
|
+
(n.key === "Enter" || n.key === " ") && (n.preventDefault(), t?.(n));
|
|
214
209
|
},
|
|
215
210
|
onClick: t
|
|
216
|
-
} : void 0,
|
|
211
|
+
} : void 0, I = " ", pt = ({
|
|
217
212
|
label: t,
|
|
218
213
|
placeholder: e
|
|
219
214
|
}) => {
|
|
220
215
|
const r = typeof e == "string" && e.trim().length > 0 ? e : void 0;
|
|
221
216
|
return t ? {
|
|
222
|
-
inputPlaceholder: r ??
|
|
217
|
+
inputPlaceholder: r ?? I,
|
|
223
218
|
labelStaysFloatedWithPlaceholder: r !== void 0
|
|
224
219
|
} : {
|
|
225
|
-
inputPlaceholder: e ??
|
|
220
|
+
inputPlaceholder: e ?? I,
|
|
226
221
|
labelStaysFloatedWithPlaceholder: !1
|
|
227
222
|
};
|
|
228
223
|
};
|
|
229
224
|
export {
|
|
230
|
-
|
|
225
|
+
ot as DisabledProvider,
|
|
231
226
|
S as assignRef,
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
|
251
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
|
-
// No decimal point, just apply integer length limit
|
|
66
|
-
if (maxIntegerLength > 0 && cleaned.length > maxIntegerLength) {
|
|
67
|
-
cleaned = cleaned.slice(0, maxIntegerLength);
|
|
68
|
-
}
|
|
69
|
-
} else {
|
|
70
|
-
// Split integer and decimal parts
|
|
71
|
-
let integerPart = cleaned.slice(0, firstDot);
|
|
72
|
-
let decimalPart = cleaned.slice(firstDot + 1).replace(/\./g, '');
|
|
73
93
|
|
|
74
|
-
|
|
75
|
-
if (maxIntegerLength > 0 && integerPart.length > maxIntegerLength) {
|
|
76
|
-
integerPart = integerPart.slice(0, maxIntegerLength);
|
|
77
|
-
}
|
|
78
|
-
decimalPart = decimalPart.slice(0, maxDecimalLength);
|
|
94
|
+
const sanitized = stripLeadingZeros(sanitize(value));
|
|
79
95
|
|
|
80
|
-
|
|
81
|
-
|
|
96
|
+
const cleaned = allowDecimals
|
|
97
|
+
? formatDecimal(sanitized, maxIntegerLength, maxDecimalLength)
|
|
98
|
+
: formatIntegerOnly(sanitized, maxIntegerLength);
|
|
82
99
|
|
|
83
|
-
|
|
84
|
-
const hasTrailingDot =
|
|
85
|
-
normalizedValue.endsWith('.') || cleaned.endsWith('.');
|
|
86
|
-
cleaned =
|
|
87
|
-
decimalPart.length > 0
|
|
88
|
-
? `${integerPart}.${decimalPart}`
|
|
89
|
-
: hasTrailingDot
|
|
90
|
-
? `${integerPart}.`
|
|
91
|
-
: integerPart;
|
|
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
|
}
|