@rovula/ui 0.0.76 → 0.0.78
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/cjs/bundle.css +40 -0
- package/dist/cjs/bundle.js +3 -3
- package/dist/cjs/bundle.js.map +1 -1
- package/dist/cjs/types/components/Dropdown/Dropdown.stories.d.ts +7 -0
- package/dist/cjs/types/components/InputFilter/InputFilter.stories.d.ts +7 -0
- package/dist/cjs/types/components/MaskedTextInput/MaskedTextInput.d.ts +75 -0
- package/dist/cjs/types/components/MaskedTextInput/MaskedTextInput.stories.d.ts +491 -0
- package/dist/cjs/types/components/MaskedTextInput/index.d.ts +3 -0
- package/dist/cjs/types/components/NumberInput/NumberInput.d.ts +39 -0
- package/dist/cjs/types/components/NumberInput/NumberInput.stories.d.ts +18 -0
- package/dist/cjs/types/components/NumberInput/index.d.ts +2 -0
- package/dist/cjs/types/components/RadioGroup/RadioGroup.stories.d.ts +1 -1
- package/dist/cjs/types/components/Search/Search.stories.d.ts +7 -0
- package/dist/cjs/types/components/Slider/Slider.stories.d.ts +1 -1
- package/dist/cjs/types/components/TextInput/TextInput.d.ts +14 -0
- package/dist/cjs/types/components/TextInput/TextInput.stories.d.ts +14 -0
- package/dist/cjs/types/index.d.ts +4 -0
- package/dist/components/MaskedTextInput/MaskedTextInput.js +267 -0
- package/dist/components/MaskedTextInput/MaskedTextInput.stories.js +167 -0
- package/dist/components/MaskedTextInput/index.js +2 -0
- package/dist/components/NumberInput/NumberInput.js +254 -0
- package/dist/components/NumberInput/NumberInput.stories.js +212 -0
- package/dist/components/NumberInput/index.js +1 -0
- package/dist/components/TextInput/TextInput.js +13 -11
- package/dist/components/Toast/Toast.styles.js +1 -1
- package/dist/esm/bundle.css +40 -0
- package/dist/esm/bundle.js +3 -3
- package/dist/esm/bundle.js.map +1 -1
- package/dist/esm/types/components/Dropdown/Dropdown.stories.d.ts +7 -0
- package/dist/esm/types/components/InputFilter/InputFilter.stories.d.ts +7 -0
- package/dist/esm/types/components/MaskedTextInput/MaskedTextInput.d.ts +75 -0
- package/dist/esm/types/components/MaskedTextInput/MaskedTextInput.stories.d.ts +491 -0
- package/dist/esm/types/components/MaskedTextInput/index.d.ts +3 -0
- package/dist/esm/types/components/NumberInput/NumberInput.d.ts +39 -0
- package/dist/esm/types/components/NumberInput/NumberInput.stories.d.ts +18 -0
- package/dist/esm/types/components/NumberInput/index.d.ts +2 -0
- package/dist/esm/types/components/RadioGroup/RadioGroup.stories.d.ts +1 -1
- package/dist/esm/types/components/Search/Search.stories.d.ts +7 -0
- package/dist/esm/types/components/Slider/Slider.stories.d.ts +1 -1
- package/dist/esm/types/components/TextInput/TextInput.d.ts +14 -0
- package/dist/esm/types/components/TextInput/TextInput.stories.d.ts +14 -0
- package/dist/esm/types/index.d.ts +4 -0
- package/dist/index.d.ts +110 -1
- package/dist/index.js +2 -0
- package/dist/src/theme/global.css +51 -0
- package/package.json +1 -1
- package/src/components/MaskedTextInput/MaskedTextInput.stories.tsx +414 -0
- package/src/components/MaskedTextInput/MaskedTextInput.tsx +391 -0
- package/src/components/MaskedTextInput/README.md +202 -0
- package/src/components/MaskedTextInput/index.ts +3 -0
- package/src/components/NumberInput/NumberInput.stories.tsx +350 -0
- package/src/components/NumberInput/NumberInput.tsx +428 -0
- package/src/components/NumberInput/index.ts +2 -0
- package/src/components/TextInput/TextInput.tsx +54 -12
- package/src/components/Toast/Toast.styles.tsx +1 -1
- package/src/index.ts +7 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
FC,
|
|
3
|
+
forwardRef,
|
|
4
|
+
useCallback,
|
|
5
|
+
useImperativeHandle,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
useEffect,
|
|
9
|
+
} from "react";
|
|
10
|
+
import TextInput, { InputProps } from "../TextInput/TextInput";
|
|
11
|
+
import { cn } from "@/utils/cn";
|
|
12
|
+
|
|
13
|
+
export type MaskRule = {
|
|
14
|
+
pattern: RegExp;
|
|
15
|
+
placeholder: string;
|
|
16
|
+
isLiteral?: boolean;
|
|
17
|
+
validator?: (char: string) => boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type MaskedTextInputProps = InputProps & {
|
|
21
|
+
mask?: string;
|
|
22
|
+
maskChar?: string;
|
|
23
|
+
showMask?: boolean;
|
|
24
|
+
guide?: boolean;
|
|
25
|
+
keepCharPositions?: boolean;
|
|
26
|
+
rules?: Record<string, RegExp | ((char: string) => boolean)>;
|
|
27
|
+
onMaskedChange?: (value: string, rawValue: string) => void;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Kendo UI style mask patterns
|
|
31
|
+
export const MASK_PATTERNS = {
|
|
32
|
+
PHONE: "(000) 000-0000",
|
|
33
|
+
PHONE_INTL: "+000 000 000 0000",
|
|
34
|
+
CREDIT_CARD: "0000 0000 0000 0000",
|
|
35
|
+
DATE: "00/00/0000",
|
|
36
|
+
TIME: "00:00",
|
|
37
|
+
SSN: "000-00-0000",
|
|
38
|
+
ZIP_CODE: "00000",
|
|
39
|
+
ZIP_CODE_EXT: "00000-0000",
|
|
40
|
+
CURRENCY: "$000,000.00",
|
|
41
|
+
PERCENTAGE: "000%",
|
|
42
|
+
LICENSE_PLATE: "AAA-0000",
|
|
43
|
+
PRODUCT_CODE: "AA-0000-AA",
|
|
44
|
+
ALPHANUMERIC: "AAAA-0000",
|
|
45
|
+
} as const;
|
|
46
|
+
|
|
47
|
+
// Kendo UI mask rules
|
|
48
|
+
const KENDO_RULES: Record<string, { pattern: RegExp; placeholder: string }> = {
|
|
49
|
+
"0": { pattern: /[0-9]/, placeholder: "0" }, // Any digit 0-9
|
|
50
|
+
"9": { pattern: /[0-9\s]/, placeholder: "9" }, // Any digit 0-9 or space
|
|
51
|
+
"#": { pattern: /[0-9\s+\-]/, placeholder: "#" }, // Any digit, space, +, or -
|
|
52
|
+
L: { pattern: /[a-zA-Z]/, placeholder: "L" }, // Any letter
|
|
53
|
+
"?": { pattern: /[a-zA-Z\s]/, placeholder: "?" }, // Any letter or space
|
|
54
|
+
"&": { pattern: /[^\s]/, placeholder: "&" }, // Any character except space
|
|
55
|
+
C: { pattern: /./, placeholder: "C" }, // Any character including space
|
|
56
|
+
A: { pattern: /[a-zA-Z0-9]/, placeholder: "A" }, // Any alphanumeric
|
|
57
|
+
a: { pattern: /[a-zA-Z0-9\s]/, placeholder: "a" }, // Any alphanumeric or space
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Helper function to create mask pattern from string using Kendo UI rules
|
|
61
|
+
const createMaskPattern = (
|
|
62
|
+
mask: string,
|
|
63
|
+
customRules?: Record<string, RegExp | ((char: string) => boolean)>
|
|
64
|
+
): MaskRule[] => {
|
|
65
|
+
const rules = { ...KENDO_RULES, ...customRules };
|
|
66
|
+
const pattern: MaskRule[] = [];
|
|
67
|
+
let i = 0;
|
|
68
|
+
|
|
69
|
+
while (i < mask.length) {
|
|
70
|
+
const char = mask[i];
|
|
71
|
+
|
|
72
|
+
if (char === "\\" && i + 1 < mask.length) {
|
|
73
|
+
// Escaped character - treat as literal
|
|
74
|
+
const nextChar = mask[i + 1];
|
|
75
|
+
pattern.push({
|
|
76
|
+
pattern: new RegExp(`^${nextChar}$`),
|
|
77
|
+
placeholder: nextChar,
|
|
78
|
+
isLiteral: true,
|
|
79
|
+
});
|
|
80
|
+
i += 2;
|
|
81
|
+
} else if (rules[char]) {
|
|
82
|
+
// Apply Kendo rule
|
|
83
|
+
const rule = rules[char];
|
|
84
|
+
if (typeof rule === "function") {
|
|
85
|
+
pattern.push({
|
|
86
|
+
pattern: /./, // Accept any character, validate with function
|
|
87
|
+
placeholder: char,
|
|
88
|
+
validator: rule,
|
|
89
|
+
});
|
|
90
|
+
} else if (rule instanceof RegExp) {
|
|
91
|
+
pattern.push({
|
|
92
|
+
pattern: rule,
|
|
93
|
+
placeholder: char,
|
|
94
|
+
});
|
|
95
|
+
} else {
|
|
96
|
+
pattern.push({
|
|
97
|
+
pattern: rule.pattern,
|
|
98
|
+
placeholder: rule.placeholder,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
i++;
|
|
102
|
+
} else {
|
|
103
|
+
// Literal character
|
|
104
|
+
pattern.push({
|
|
105
|
+
pattern: new RegExp(`^${char.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`),
|
|
106
|
+
placeholder: char,
|
|
107
|
+
isLiteral: true,
|
|
108
|
+
});
|
|
109
|
+
i++;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return pattern;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Helper function to apply mask to value using Kendo UI rules
|
|
117
|
+
const applyMask = (
|
|
118
|
+
value: string,
|
|
119
|
+
maskPattern: MaskRule[],
|
|
120
|
+
maskChar: string = "_",
|
|
121
|
+
showMask: boolean = true,
|
|
122
|
+
guide: boolean = true
|
|
123
|
+
): string => {
|
|
124
|
+
let maskedValue = "";
|
|
125
|
+
let valueIndex = 0;
|
|
126
|
+
|
|
127
|
+
for (let i = 0; i < maskPattern.length; i++) {
|
|
128
|
+
const rule = maskPattern[i];
|
|
129
|
+
|
|
130
|
+
if (rule.isLiteral) {
|
|
131
|
+
// Literal character - always add
|
|
132
|
+
maskedValue += rule.placeholder;
|
|
133
|
+
} else {
|
|
134
|
+
// Input character - find next valid character
|
|
135
|
+
let foundValid = false;
|
|
136
|
+
while (valueIndex < value.length) {
|
|
137
|
+
const char = value[valueIndex];
|
|
138
|
+
valueIndex++;
|
|
139
|
+
|
|
140
|
+
// Check with validator function first if exists
|
|
141
|
+
const isValid = rule.validator
|
|
142
|
+
? rule.validator(char)
|
|
143
|
+
: rule.pattern.test(char);
|
|
144
|
+
|
|
145
|
+
if (isValid) {
|
|
146
|
+
maskedValue += char;
|
|
147
|
+
foundValid = true;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
// Skip invalid characters and continue searching
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!foundValid && guide && showMask) {
|
|
154
|
+
// No valid character found, fill with placeholder
|
|
155
|
+
maskedValue += maskChar;
|
|
156
|
+
} else if (!foundValid) {
|
|
157
|
+
// No placeholder needed, stop building the mask
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return maskedValue;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Helper function to extract raw value from masked value
|
|
167
|
+
const extractRawValue = (
|
|
168
|
+
maskedValue: string,
|
|
169
|
+
maskPattern: MaskRule[]
|
|
170
|
+
): string => {
|
|
171
|
+
let rawValue = "";
|
|
172
|
+
|
|
173
|
+
for (let i = 0; i < maskedValue.length && i < maskPattern.length; i++) {
|
|
174
|
+
const rule = maskPattern[i];
|
|
175
|
+
const char = maskedValue[i];
|
|
176
|
+
|
|
177
|
+
if (!rule.isLiteral) {
|
|
178
|
+
// Only include non-literal characters in raw value
|
|
179
|
+
rawValue += char;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return rawValue;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// Helper function to get cursor position after masking
|
|
187
|
+
const getCursorPosition = (
|
|
188
|
+
maskedValue: string,
|
|
189
|
+
rawInputLength: number,
|
|
190
|
+
maskPattern: MaskRule[]
|
|
191
|
+
): number => {
|
|
192
|
+
let inputCount = 0;
|
|
193
|
+
let cursorPos = 0;
|
|
194
|
+
|
|
195
|
+
for (let i = 0; i < maskPattern.length && i < maskedValue.length; i++) {
|
|
196
|
+
const rule = maskPattern[i];
|
|
197
|
+
const char = maskedValue[i];
|
|
198
|
+
|
|
199
|
+
if (rule.isLiteral) {
|
|
200
|
+
cursorPos = i + 1;
|
|
201
|
+
} else {
|
|
202
|
+
inputCount++;
|
|
203
|
+
cursorPos = i + 1;
|
|
204
|
+
|
|
205
|
+
if (inputCount >= rawInputLength) {
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return cursorPos;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
export const MaskedTextInput = forwardRef<
|
|
215
|
+
HTMLInputElement,
|
|
216
|
+
MaskedTextInputProps
|
|
217
|
+
>(
|
|
218
|
+
(
|
|
219
|
+
{
|
|
220
|
+
mask,
|
|
221
|
+
maskChar = "_",
|
|
222
|
+
showMask = true,
|
|
223
|
+
guide = true,
|
|
224
|
+
keepCharPositions = false,
|
|
225
|
+
rules,
|
|
226
|
+
onMaskedChange,
|
|
227
|
+
onChange,
|
|
228
|
+
value,
|
|
229
|
+
defaultValue,
|
|
230
|
+
...props
|
|
231
|
+
},
|
|
232
|
+
ref
|
|
233
|
+
) => {
|
|
234
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
235
|
+
const [maskedValue, setMaskedValue] = useState<string>("");
|
|
236
|
+
const [rawValue, setRawValue] = useState<string>("");
|
|
237
|
+
|
|
238
|
+
// Parse mask pattern using Kendo UI rules
|
|
239
|
+
const maskPattern = React.useMemo(() => {
|
|
240
|
+
if (!mask) return null;
|
|
241
|
+
|
|
242
|
+
return createMaskPattern(mask, rules);
|
|
243
|
+
}, [mask, rules]);
|
|
244
|
+
|
|
245
|
+
// Initialize values
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
const initialValue = value || defaultValue || "";
|
|
248
|
+
if (maskPattern && initialValue) {
|
|
249
|
+
const masked = applyMask(
|
|
250
|
+
initialValue as string,
|
|
251
|
+
maskPattern,
|
|
252
|
+
maskChar,
|
|
253
|
+
showMask,
|
|
254
|
+
guide
|
|
255
|
+
);
|
|
256
|
+
const raw = extractRawValue(masked, maskPattern);
|
|
257
|
+
setMaskedValue(masked);
|
|
258
|
+
setRawValue(raw);
|
|
259
|
+
} else {
|
|
260
|
+
setMaskedValue(initialValue as string);
|
|
261
|
+
setRawValue(initialValue as string);
|
|
262
|
+
}
|
|
263
|
+
}, [maskPattern, maskChar, showMask, guide, value, defaultValue]);
|
|
264
|
+
|
|
265
|
+
useImperativeHandle(ref, () => inputRef?.current as HTMLInputElement);
|
|
266
|
+
|
|
267
|
+
const handleChange = useCallback(
|
|
268
|
+
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
269
|
+
const inputValue = event.target.value;
|
|
270
|
+
|
|
271
|
+
if (!maskPattern) {
|
|
272
|
+
setMaskedValue(inputValue);
|
|
273
|
+
setRawValue(inputValue);
|
|
274
|
+
onChange?.(event);
|
|
275
|
+
onMaskedChange?.(inputValue, inputValue);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const newMaskedValue = applyMask(
|
|
280
|
+
inputValue,
|
|
281
|
+
maskPattern,
|
|
282
|
+
maskChar,
|
|
283
|
+
showMask,
|
|
284
|
+
guide
|
|
285
|
+
);
|
|
286
|
+
const newRawValue = extractRawValue(newMaskedValue, maskPattern);
|
|
287
|
+
|
|
288
|
+
setMaskedValue(newMaskedValue);
|
|
289
|
+
setRawValue(newRawValue);
|
|
290
|
+
|
|
291
|
+
// Create synthetic event with masked value
|
|
292
|
+
const syntheticEvent = {
|
|
293
|
+
...event,
|
|
294
|
+
target: {
|
|
295
|
+
...event.target,
|
|
296
|
+
value: newMaskedValue,
|
|
297
|
+
},
|
|
298
|
+
} as React.ChangeEvent<HTMLInputElement>;
|
|
299
|
+
|
|
300
|
+
onChange?.(syntheticEvent);
|
|
301
|
+
onMaskedChange?.(newMaskedValue, newRawValue);
|
|
302
|
+
|
|
303
|
+
// Set cursor position after state update
|
|
304
|
+
setTimeout(() => {
|
|
305
|
+
if (inputRef.current) {
|
|
306
|
+
const rawLength = newRawValue.replace(/[_\s]/g, "").length;
|
|
307
|
+
const newCursorPos = getCursorPosition(
|
|
308
|
+
newMaskedValue,
|
|
309
|
+
rawLength,
|
|
310
|
+
maskPattern
|
|
311
|
+
);
|
|
312
|
+
inputRef.current.setSelectionRange(newCursorPos, newCursorPos);
|
|
313
|
+
}
|
|
314
|
+
}, 0);
|
|
315
|
+
},
|
|
316
|
+
[maskPattern, maskChar, showMask, guide, onChange, onMaskedChange]
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const handleKeyDown = useCallback(
|
|
320
|
+
(event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
321
|
+
if (!maskPattern) {
|
|
322
|
+
props.onKeyDown?.(event);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const { key, ctrlKey, metaKey } = event;
|
|
327
|
+
const input = event.target as HTMLInputElement;
|
|
328
|
+
const cursorPos = input.selectionStart || 0;
|
|
329
|
+
|
|
330
|
+
// Allow navigation and editing keys
|
|
331
|
+
if (
|
|
332
|
+
key === "Backspace" ||
|
|
333
|
+
key === "Delete" ||
|
|
334
|
+
key === "ArrowLeft" ||
|
|
335
|
+
key === "ArrowRight" ||
|
|
336
|
+
key === "Home" ||
|
|
337
|
+
key === "End" ||
|
|
338
|
+
key === "Tab" ||
|
|
339
|
+
key.length > 1 || // Allow other special keys
|
|
340
|
+
ctrlKey ||
|
|
341
|
+
metaKey
|
|
342
|
+
) {
|
|
343
|
+
props.onKeyDown?.(event);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Find the next non-literal position from cursor
|
|
348
|
+
let targetPos = cursorPos;
|
|
349
|
+
while (
|
|
350
|
+
targetPos < maskPattern.length &&
|
|
351
|
+
maskPattern[targetPos].isLiteral
|
|
352
|
+
) {
|
|
353
|
+
targetPos++;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Check if we have a valid position and the key matches
|
|
357
|
+
if (targetPos < maskPattern.length) {
|
|
358
|
+
const targetRule = maskPattern[targetPos];
|
|
359
|
+
|
|
360
|
+
// Check with validator function first if exists
|
|
361
|
+
const isValid = targetRule.validator
|
|
362
|
+
? targetRule.validator(key)
|
|
363
|
+
: targetRule.pattern.test(key);
|
|
364
|
+
|
|
365
|
+
if (!isValid) {
|
|
366
|
+
event.preventDefault();
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
props.onKeyDown?.(event);
|
|
372
|
+
},
|
|
373
|
+
[maskPattern, props]
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
return (
|
|
377
|
+
<TextInput
|
|
378
|
+
{...props}
|
|
379
|
+
ref={inputRef}
|
|
380
|
+
value={maskedValue}
|
|
381
|
+
onChange={handleChange}
|
|
382
|
+
onKeyDown={handleKeyDown}
|
|
383
|
+
className={cn(props.className)}
|
|
384
|
+
/>
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
MaskedTextInput.displayName = "MaskedTextInput";
|
|
390
|
+
|
|
391
|
+
export default MaskedTextInput;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# MaskedTextInput Component
|
|
2
|
+
|
|
3
|
+
A powerful masked input component following Kendo UI masking rules with full TypeScript support.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ Kendo UI compatible mask rules (0, 9, #, L, ?, &, C, A, a)
|
|
8
|
+
- ✅ Custom mask rules support (RegExp or function-based)
|
|
9
|
+
- ✅ Proper cursor positioning
|
|
10
|
+
- ✅ Automatic literal character insertion
|
|
11
|
+
- ✅ Smart character validation
|
|
12
|
+
- ✅ Raw value extraction
|
|
13
|
+
- ✅ Full TypeScript support
|
|
14
|
+
|
|
15
|
+
## Kendo UI Mask Rules
|
|
16
|
+
|
|
17
|
+
| Rule | Description | Example |
|
|
18
|
+
|------|-------------|---------|
|
|
19
|
+
| `0` | Any digit 0-9 (required) | `000-000-0000` |
|
|
20
|
+
| `L` | Any letter (a-z, A-Z) (required) | `LLL-LLLL` |
|
|
21
|
+
| `A` | Any alphanumeric (required) | `AAAA-0000` |
|
|
22
|
+
| `#` | Digit, space, +, or - | `#00-000-0000` |
|
|
23
|
+
| `9` | Any digit or space (optional) ⚠️ | `999-999-9999` |
|
|
24
|
+
| `?` | Any letter or space (optional) ⚠️ | `???-????` |
|
|
25
|
+
| `a` | Any alphanumeric or space (optional) ⚠️ | `aaaa-aaaa` |
|
|
26
|
+
| `&` | Any character except space | `&&&-&&&` |
|
|
27
|
+
| `C` | Any character including space | `CCC CCC` |
|
|
28
|
+
|
|
29
|
+
> ⚠️ **Note on Optional Rules (9, ?, a):** These rules accept space, making the position optional. Pressing space will skip to the next position. Use `guide={false}` for better UX with optional rules.
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
### Basic Usage
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
import { MaskedTextInput, MASK_PATTERNS } from '@rovula/ui';
|
|
37
|
+
|
|
38
|
+
// Phone number
|
|
39
|
+
<MaskedTextInput
|
|
40
|
+
label="Phone Number"
|
|
41
|
+
mask={MASK_PATTERNS.PHONE}
|
|
42
|
+
/>
|
|
43
|
+
|
|
44
|
+
// Custom mask
|
|
45
|
+
<MaskedTextInput
|
|
46
|
+
label="License Plate"
|
|
47
|
+
mask="AAA-0000"
|
|
48
|
+
/>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### With Callbacks
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const [maskedValue, setMaskedValue] = useState("");
|
|
55
|
+
const [rawValue, setRawValue] = useState("");
|
|
56
|
+
|
|
57
|
+
<MaskedTextInput
|
|
58
|
+
label="Phone"
|
|
59
|
+
mask="(000) 000-0000"
|
|
60
|
+
onMaskedChange={(masked, raw) => {
|
|
61
|
+
setMaskedValue(masked); // "(123) 456-7890"
|
|
62
|
+
setRawValue(raw); // "1234567890"
|
|
63
|
+
}}
|
|
64
|
+
/>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Custom Rules
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
// Using RegExp - only digits 3-9
|
|
71
|
+
<MaskedTextInput
|
|
72
|
+
mask="~-~-~"
|
|
73
|
+
rules={{
|
|
74
|
+
"~": /[3-9]/
|
|
75
|
+
}}
|
|
76
|
+
/>
|
|
77
|
+
|
|
78
|
+
// Using function - only uppercase letters
|
|
79
|
+
<MaskedTextInput
|
|
80
|
+
mask="*-*-*"
|
|
81
|
+
rules={{
|
|
82
|
+
"*": (char: string) => char === char.toUpperCase() && /[A-Z]/.test(char)
|
|
83
|
+
}}
|
|
84
|
+
/>
|
|
85
|
+
|
|
86
|
+
// Multiple custom rules
|
|
87
|
+
<MaskedTextInput
|
|
88
|
+
mask="~*-~*-~*"
|
|
89
|
+
rules={{
|
|
90
|
+
"~": /[0-9]/, // Digit
|
|
91
|
+
"*": /[A-Z]/ // Uppercase letter
|
|
92
|
+
}}
|
|
93
|
+
/>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Escaping Characters
|
|
97
|
+
|
|
98
|
+
Use backslash `\` to escape mask characters and treat them as literals:
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
<MaskedTextInput mask="\0\0\0-0000" />
|
|
102
|
+
// Result: "000-1234" (first 000 is literal)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Props
|
|
106
|
+
|
|
107
|
+
| Prop | Type | Default | Description |
|
|
108
|
+
|------|------|---------|-------------|
|
|
109
|
+
| `mask` | `string` | - | Mask pattern using Kendo UI rules |
|
|
110
|
+
| `maskChar` | `string` | `"_"` | Placeholder character for empty positions |
|
|
111
|
+
| `showMask` | `boolean` | `true` | Show mask placeholders |
|
|
112
|
+
| `guide` | `boolean` | `true` | Show guide placeholders |
|
|
113
|
+
| `rules` | `Record<string, RegExp \| Function>` | - | Custom mask rules |
|
|
114
|
+
| `onMaskedChange` | `(masked: string, raw: string) => void` | - | Callback with masked and raw values |
|
|
115
|
+
|
|
116
|
+
## Pre-defined Patterns
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
export const MASK_PATTERNS = {
|
|
120
|
+
PHONE: "(000) 000-0000",
|
|
121
|
+
PHONE_INTL: "+000 000 000 0000",
|
|
122
|
+
CREDIT_CARD: "0000 0000 0000 0000",
|
|
123
|
+
DATE: "00/00/0000",
|
|
124
|
+
TIME: "00:00",
|
|
125
|
+
SSN: "000-00-0000",
|
|
126
|
+
ZIP_CODE: "00000",
|
|
127
|
+
ZIP_CODE_EXT: "00000-0000",
|
|
128
|
+
CURRENCY: "$000,000.00",
|
|
129
|
+
PERCENTAGE: "000%",
|
|
130
|
+
LICENSE_PLATE: "AAA-0000",
|
|
131
|
+
PRODUCT_CODE: "AA-0000-AA",
|
|
132
|
+
ALPHANUMERIC: "AAAA-0000",
|
|
133
|
+
};
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Examples
|
|
137
|
+
|
|
138
|
+
### Phone Number
|
|
139
|
+
```tsx
|
|
140
|
+
<MaskedTextInput mask="(000) 000-0000" label="Phone" />
|
|
141
|
+
// Input: 1234567890
|
|
142
|
+
// Display: (123) 456-7890
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### License Plate
|
|
146
|
+
```tsx
|
|
147
|
+
<MaskedTextInput mask="LLL-0000" label="License Plate" />
|
|
148
|
+
// Input: ABC1234
|
|
149
|
+
// Display: ABC-1234
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Mixed Pattern
|
|
153
|
+
```tsx
|
|
154
|
+
<MaskedTextInput mask="AA-0000-AA" label="Product Code" />
|
|
155
|
+
// Input: AB1234CD
|
|
156
|
+
// Display: AB-1234-CD
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Optional Input (with space)
|
|
160
|
+
```tsx
|
|
161
|
+
<MaskedTextInput
|
|
162
|
+
mask="999-999-9999"
|
|
163
|
+
label="Optional Phone"
|
|
164
|
+
guide={false}
|
|
165
|
+
/>
|
|
166
|
+
// Input: 123 (then press space to skip)
|
|
167
|
+
// Display: 123- - (spaces are allowed)
|
|
168
|
+
// Good for: Optional/partial input
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Technical Details
|
|
172
|
+
|
|
173
|
+
### How It Works
|
|
174
|
+
|
|
175
|
+
1. **Input Processing**: Characters are validated against mask rules
|
|
176
|
+
2. **Literal Insertion**: Static characters are automatically added
|
|
177
|
+
3. **Cursor Management**: Cursor is positioned correctly after input
|
|
178
|
+
4. **Value Extraction**: Raw value (without literals) is maintained
|
|
179
|
+
|
|
180
|
+
### Key Improvements
|
|
181
|
+
|
|
182
|
+
- ✅ Fixed typing issues - can now type in all mask patterns
|
|
183
|
+
- ✅ Fixed cursor positioning - cursor stays in correct position
|
|
184
|
+
- ✅ Improved character validation - better handling of invalid input
|
|
185
|
+
- ✅ Smart literal skipping - automatically moves past literal characters
|
|
186
|
+
- ✅ Better performance - optimized mask application logic
|
|
187
|
+
|
|
188
|
+
## Browser Support
|
|
189
|
+
|
|
190
|
+
Works in all modern browsers that support ES6+ and React 16.8+.
|
|
191
|
+
|
|
192
|
+
## TypeScript
|
|
193
|
+
|
|
194
|
+
Fully typed with TypeScript. Export types:
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
import type {
|
|
198
|
+
MaskedTextInputProps,
|
|
199
|
+
MaskRule
|
|
200
|
+
} from '@rovula/ui';
|
|
201
|
+
```
|
|
202
|
+
|