@rovula/ui 0.1.8 → 0.1.10
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 +0 -10
- 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 +4 -0
- package/dist/cjs/types/components/Form/ValidationHintList.d.ts +4 -1
- package/dist/cjs/types/components/InputFilter/InputFilter.stories.d.ts +4 -0
- package/dist/cjs/types/components/MaskedTextInput/MaskedTextInput.d.ts +4 -0
- package/dist/cjs/types/components/MaskedTextInput/MaskedTextInput.stories.d.ts +8 -0
- package/dist/cjs/types/components/PasswordInput/PasswordInput.stories.d.ts +4 -0
- package/dist/cjs/types/components/Search/Search.stories.d.ts +4 -0
- package/dist/cjs/types/components/TextArea/TextArea.d.ts +8 -0
- package/dist/cjs/types/components/TextArea/TextArea.stories.d.ts +4 -0
- package/dist/cjs/types/components/TextInput/TextInput.d.ts +8 -0
- package/dist/cjs/types/components/TextInput/TextInput.stories.d.ts +20 -0
- package/dist/components/Form/ValidationHintList.js +9 -9
- package/dist/components/OtpInput/OtpInput.js +1 -1
- package/dist/components/TextArea/TextArea.js +32 -3
- package/dist/components/TextArea/TextArea.stories.js +29 -0
- package/dist/components/TextInput/TextInput.js +38 -2
- package/dist/components/TextInput/TextInput.stories.js +28 -0
- package/dist/esm/bundle.css +0 -10
- package/dist/esm/bundle.js +2 -2
- package/dist/esm/bundle.js.map +1 -1
- package/dist/esm/types/components/Dropdown/Dropdown.stories.d.ts +4 -0
- package/dist/esm/types/components/Form/ValidationHintList.d.ts +4 -1
- package/dist/esm/types/components/InputFilter/InputFilter.stories.d.ts +4 -0
- package/dist/esm/types/components/MaskedTextInput/MaskedTextInput.d.ts +4 -0
- package/dist/esm/types/components/MaskedTextInput/MaskedTextInput.stories.d.ts +8 -0
- package/dist/esm/types/components/PasswordInput/PasswordInput.stories.d.ts +4 -0
- package/dist/esm/types/components/Search/Search.stories.d.ts +4 -0
- package/dist/esm/types/components/TextArea/TextArea.d.ts +8 -0
- package/dist/esm/types/components/TextArea/TextArea.stories.d.ts +4 -0
- package/dist/esm/types/components/TextInput/TextInput.d.ts +8 -0
- package/dist/esm/types/components/TextInput/TextInput.stories.d.ts +20 -0
- package/dist/index.d.ts +24 -1
- package/dist/src/theme/global.css +0 -13
- package/package.json +1 -1
- package/src/components/Form/ValidationHintList.tsx +24 -11
- package/src/components/OtpInput/OtpInput.tsx +22 -9
- package/src/components/TextArea/TextArea.stories.tsx +108 -0
- package/src/components/TextArea/TextArea.tsx +52 -1
- package/src/components/TextInput/TextInput.stories.tsx +120 -5
- package/src/components/TextInput/TextInput.tsx +65 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { forwardRef, useImperativeHandle, useMemo, useRef } from "react";
|
|
1
|
+
import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useRef, FocusEvent, ChangeEvent } from "react";
|
|
2
2
|
import { cn } from "@/utils/cn";
|
|
3
3
|
import {
|
|
4
4
|
textareaVariant,
|
|
@@ -26,6 +26,10 @@ export type TextAreaProps = {
|
|
|
26
26
|
hasClearIcon?: boolean;
|
|
27
27
|
labelClassName?: string;
|
|
28
28
|
className?: string;
|
|
29
|
+
normalize?: (value: string) => string;
|
|
30
|
+
format?: (value: string) => string;
|
|
31
|
+
trimOnCommit?: boolean;
|
|
32
|
+
normalizeOnCommit?: (value: string) => string;
|
|
29
33
|
} & Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "size">;
|
|
30
34
|
|
|
31
35
|
export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
|
@@ -47,6 +51,10 @@ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
|
|
47
51
|
hasClearIcon = true,
|
|
48
52
|
labelClassName,
|
|
49
53
|
className,
|
|
54
|
+
normalize,
|
|
55
|
+
format,
|
|
56
|
+
trimOnCommit,
|
|
57
|
+
normalizeOnCommit,
|
|
50
58
|
...props
|
|
51
59
|
},
|
|
52
60
|
ref
|
|
@@ -56,6 +64,46 @@ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
|
|
56
64
|
|
|
57
65
|
useImperativeHandle(ref, () => textareaRef?.current as HTMLTextAreaElement);
|
|
58
66
|
|
|
67
|
+
const handleChange = useCallback(
|
|
68
|
+
(e: ChangeEvent<HTMLTextAreaElement>) => {
|
|
69
|
+
if (normalize) {
|
|
70
|
+
e.target.value = normalize(e.target.value);
|
|
71
|
+
}
|
|
72
|
+
props.onChange?.(e);
|
|
73
|
+
},
|
|
74
|
+
[normalize, props.onChange]
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const commitValue = useCallback(
|
|
78
|
+
(e: FocusEvent<HTMLTextAreaElement>) => {
|
|
79
|
+
const textarea = e.currentTarget;
|
|
80
|
+
let committed = textarea.value;
|
|
81
|
+
if (trimOnCommit) committed = committed.trim();
|
|
82
|
+
if (normalizeOnCommit) committed = normalizeOnCommit(committed);
|
|
83
|
+
if (committed !== textarea.value) {
|
|
84
|
+
textarea.value = committed;
|
|
85
|
+
props.onChange?.({
|
|
86
|
+
...e,
|
|
87
|
+
target: textarea,
|
|
88
|
+
} as unknown as ChangeEvent<HTMLTextAreaElement>);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
[trimOnCommit, normalizeOnCommit, props.onChange]
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const handleBlur = useCallback(
|
|
95
|
+
(e: FocusEvent<HTMLTextAreaElement>) => {
|
|
96
|
+
if (trimOnCommit || normalizeOnCommit) commitValue(e);
|
|
97
|
+
props.onBlur?.(e);
|
|
98
|
+
},
|
|
99
|
+
[trimOnCommit, normalizeOnCommit, commitValue, props.onBlur]
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const displayValue =
|
|
103
|
+
format && typeof props.value === "string"
|
|
104
|
+
? format(props.value)
|
|
105
|
+
: props.value;
|
|
106
|
+
|
|
59
107
|
// Reuse TextInput visual language via utility classes to stay consistent
|
|
60
108
|
const containerClassName = useMemo(
|
|
61
109
|
() => `inline-flex flex-col ${fullwidth ? "w-full" : ""}`,
|
|
@@ -83,6 +131,9 @@ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
|
|
83
131
|
disabled={disabled}
|
|
84
132
|
placeholder={isFloatingLabel ? " " : props.placeholder}
|
|
85
133
|
className={cn(textareaClassName, className)}
|
|
134
|
+
value={displayValue}
|
|
135
|
+
onChange={normalize ? handleChange : props.onChange}
|
|
136
|
+
onBlur={trimOnCommit || normalizeOnCommit ? handleBlur : props.onBlur}
|
|
86
137
|
/>
|
|
87
138
|
{hasClearIcon && (
|
|
88
139
|
<div
|
|
@@ -237,7 +237,9 @@ const KeepFooterSpaceDemo = () => {
|
|
|
237
237
|
keepFooterSpace
|
|
238
238
|
size="lg"
|
|
239
239
|
error={hasError}
|
|
240
|
-
errorMessage={
|
|
240
|
+
errorMessage={
|
|
241
|
+
hasError ? "Please enter a valid email address" : undefined
|
|
242
|
+
}
|
|
241
243
|
/>
|
|
242
244
|
</div>
|
|
243
245
|
<div>
|
|
@@ -249,7 +251,9 @@ const KeepFooterSpaceDemo = () => {
|
|
|
249
251
|
label="Email"
|
|
250
252
|
size="lg"
|
|
251
253
|
error={hasError}
|
|
252
|
-
errorMessage={
|
|
254
|
+
errorMessage={
|
|
255
|
+
hasError ? "Please enter a valid email address" : undefined
|
|
256
|
+
}
|
|
253
257
|
/>
|
|
254
258
|
</div>
|
|
255
259
|
<div></div>
|
|
@@ -315,7 +319,9 @@ const FeedbackApiDemo = () => {
|
|
|
315
319
|
error={legacyError}
|
|
316
320
|
errorMessage={legacyError ? "Invalid email format." : undefined}
|
|
317
321
|
warning={legacyWarning}
|
|
318
|
-
warningMessage={
|
|
322
|
+
warningMessage={
|
|
323
|
+
legacyWarning ? "Please verify this email." : undefined
|
|
324
|
+
}
|
|
319
325
|
/>
|
|
320
326
|
</div>
|
|
321
327
|
|
|
@@ -336,8 +342,8 @@ const FeedbackApiDemo = () => {
|
|
|
336
342
|
useStatusOverride
|
|
337
343
|
? "Status explicitly sets warning."
|
|
338
344
|
: legacyWarning
|
|
339
|
-
|
|
340
|
-
|
|
345
|
+
? "Please verify this email."
|
|
346
|
+
: undefined
|
|
341
347
|
}
|
|
342
348
|
/>
|
|
343
349
|
</div>
|
|
@@ -349,3 +355,112 @@ const FeedbackApiDemo = () => {
|
|
|
349
355
|
export const FeedbackApiCompatibility = {
|
|
350
356
|
render: () => <FeedbackApiDemo />,
|
|
351
357
|
} satisfies StoryObj;
|
|
358
|
+
|
|
359
|
+
const NormalizeDemo = () => {
|
|
360
|
+
const [value, setValue] = useState("");
|
|
361
|
+
return (
|
|
362
|
+
<div className="flex flex-col gap-6 w-full max-w-md">
|
|
363
|
+
<p className="text-sm text-text-g-contrast-low">
|
|
364
|
+
<code>normalize</code> strips disallowed characters on every keystroke.
|
|
365
|
+
This example removes backslash and special path characters in real-time.
|
|
366
|
+
</p>
|
|
367
|
+
<TextInput
|
|
368
|
+
id="normalize-demo"
|
|
369
|
+
label="Device Name"
|
|
370
|
+
size="lg"
|
|
371
|
+
keepFooterSpace
|
|
372
|
+
helperText={`Stored value: "${value}"`}
|
|
373
|
+
value={value}
|
|
374
|
+
normalize={(v) => v.trimStart().replace(/[\\/:*?"'<>|]/g, "")}
|
|
375
|
+
onChange={(e) => setValue(e.target.value)}
|
|
376
|
+
/>
|
|
377
|
+
</div>
|
|
378
|
+
);
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
export const Normalize = {
|
|
382
|
+
render: () => <NormalizeDemo />,
|
|
383
|
+
} satisfies StoryObj;
|
|
384
|
+
|
|
385
|
+
const FormatDemo = () => {
|
|
386
|
+
const [value, setValue] = useState("1000000");
|
|
387
|
+
return (
|
|
388
|
+
<div className="flex flex-col gap-6 w-full max-w-md">
|
|
389
|
+
<p className="text-sm text-text-g-contrast-low">
|
|
390
|
+
<code>format</code> transforms the displayed value without changing the
|
|
391
|
+
stored value. This example displays numbers with comma separators.
|
|
392
|
+
</p>
|
|
393
|
+
<TextInput
|
|
394
|
+
id="format-demo"
|
|
395
|
+
label="Amount"
|
|
396
|
+
size="lg"
|
|
397
|
+
keepFooterSpace
|
|
398
|
+
helperText={`Stored value: "${value}"`}
|
|
399
|
+
value={value}
|
|
400
|
+
format={(v) => Number(v.replace(/,/g, "") || 0).toLocaleString()}
|
|
401
|
+
normalize={(v) => v.replace(/[^0-9]/g, "")}
|
|
402
|
+
onChange={(e) => setValue(e.target.value)}
|
|
403
|
+
/>
|
|
404
|
+
</div>
|
|
405
|
+
);
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
export const Format = {
|
|
409
|
+
render: () => <FormatDemo />,
|
|
410
|
+
} satisfies StoryObj;
|
|
411
|
+
|
|
412
|
+
const TrimOnCommitDemo = () => {
|
|
413
|
+
const [value, setValue] = useState("");
|
|
414
|
+
return (
|
|
415
|
+
<div className="flex flex-col gap-6 w-full max-w-md">
|
|
416
|
+
<p className="text-sm text-text-g-contrast-low">
|
|
417
|
+
<code>trimOnCommit</code> trims leading and trailing whitespace when the
|
|
418
|
+
user blurs the input or presses Enter. Try typing{" "}
|
|
419
|
+
<code>"hello "</code> then click outside or press Enter.
|
|
420
|
+
</p>
|
|
421
|
+
<TextInput
|
|
422
|
+
id="trim-commit-demo"
|
|
423
|
+
label="Name"
|
|
424
|
+
size="lg"
|
|
425
|
+
keepFooterSpace
|
|
426
|
+
helperText={`Stored value: "${value}"`}
|
|
427
|
+
value={value}
|
|
428
|
+
trimOnCommit
|
|
429
|
+
onChange={(e) => setValue(e.target.value)}
|
|
430
|
+
/>
|
|
431
|
+
</div>
|
|
432
|
+
);
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
export const TrimOnCommit = {
|
|
436
|
+
render: () => <TrimOnCommitDemo />,
|
|
437
|
+
} satisfies StoryObj;
|
|
438
|
+
|
|
439
|
+
const NormalizeOnCommitDemo = () => {
|
|
440
|
+
const [value, setValue] = useState("");
|
|
441
|
+
return (
|
|
442
|
+
<div className="flex flex-col gap-6 w-full max-w-md">
|
|
443
|
+
<p className="text-sm text-text-g-contrast-low">
|
|
444
|
+
<code>normalizeOnCommit</code> applies a custom transform when the user
|
|
445
|
+
blurs or presses Enter. Use with <code>trimOnCommit</code> to compose —
|
|
446
|
+
trim runs first, then <code>normalizeOnCommit</code>. This example trims
|
|
447
|
+
and uppercases on commit.
|
|
448
|
+
</p>
|
|
449
|
+
<TextInput
|
|
450
|
+
id="normalize-on-commit-demo"
|
|
451
|
+
label="Code"
|
|
452
|
+
size="lg"
|
|
453
|
+
keepFooterSpace
|
|
454
|
+
helperText={`Stored value: "${value}"`}
|
|
455
|
+
value={value}
|
|
456
|
+
trimOnCommit
|
|
457
|
+
normalizeOnCommit={(v) => v.toUpperCase()}
|
|
458
|
+
onChange={(e) => setValue(e.target.value)}
|
|
459
|
+
/>
|
|
460
|
+
</div>
|
|
461
|
+
);
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
export const NormalizeOnCommit = {
|
|
465
|
+
render: () => <NormalizeOnCommitDemo />,
|
|
466
|
+
} satisfies StoryObj;
|
|
@@ -5,6 +5,9 @@ import React, {
|
|
|
5
5
|
useImperativeHandle,
|
|
6
6
|
useMemo,
|
|
7
7
|
useRef,
|
|
8
|
+
FocusEvent,
|
|
9
|
+
KeyboardEvent,
|
|
10
|
+
ChangeEvent,
|
|
8
11
|
} from "react";
|
|
9
12
|
import {
|
|
10
13
|
helperTextVariant,
|
|
@@ -60,6 +63,10 @@ export type InputProps = {
|
|
|
60
63
|
onClickEndIcon?: () => void;
|
|
61
64
|
renderStartIcon?: () => ReactNode;
|
|
62
65
|
renderEndIcon?: () => ReactNode;
|
|
66
|
+
normalize?: (value: string) => string;
|
|
67
|
+
format?: (value: string) => string;
|
|
68
|
+
trimOnCommit?: boolean;
|
|
69
|
+
normalizeOnCommit?: (value: string) => string;
|
|
63
70
|
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, "size">;
|
|
64
71
|
|
|
65
72
|
export const TextInput = forwardRef<HTMLInputElement, InputProps>(
|
|
@@ -94,6 +101,10 @@ export const TextInput = forwardRef<HTMLInputElement, InputProps>(
|
|
|
94
101
|
renderStartIcon,
|
|
95
102
|
renderEndIcon,
|
|
96
103
|
classes,
|
|
104
|
+
normalize,
|
|
105
|
+
format,
|
|
106
|
+
trimOnCommit,
|
|
107
|
+
normalizeOnCommit,
|
|
97
108
|
...props
|
|
98
109
|
},
|
|
99
110
|
ref
|
|
@@ -174,6 +185,56 @@ export const TextInput = forwardRef<HTMLInputElement, InputProps>(
|
|
|
174
185
|
|
|
175
186
|
useImperativeHandle(ref, () => inputRef?.current as HTMLInputElement);
|
|
176
187
|
|
|
188
|
+
const handleChange = useCallback(
|
|
189
|
+
(e: ChangeEvent<HTMLInputElement>) => {
|
|
190
|
+
if (normalize) {
|
|
191
|
+
e.target.value = normalize(e.target.value);
|
|
192
|
+
}
|
|
193
|
+
props.onChange?.(e);
|
|
194
|
+
},
|
|
195
|
+
[normalize, props.onChange]
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const commitValue = useCallback(
|
|
199
|
+
(e: FocusEvent<HTMLInputElement> | KeyboardEvent<HTMLInputElement>) => {
|
|
200
|
+
const input = e.currentTarget;
|
|
201
|
+
let committed = input.value;
|
|
202
|
+
if (trimOnCommit) committed = committed.trim();
|
|
203
|
+
if (normalizeOnCommit) committed = normalizeOnCommit(committed);
|
|
204
|
+
if (committed !== input.value) {
|
|
205
|
+
input.value = committed;
|
|
206
|
+
props.onChange?.({
|
|
207
|
+
...e,
|
|
208
|
+
target: input,
|
|
209
|
+
} as unknown as ChangeEvent<HTMLInputElement>);
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
[trimOnCommit, normalizeOnCommit, props.onChange]
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const handleBlur = useCallback(
|
|
216
|
+
(e: FocusEvent<HTMLInputElement>) => {
|
|
217
|
+
if (trimOnCommit || normalizeOnCommit) commitValue(e);
|
|
218
|
+
props.onBlur?.(e);
|
|
219
|
+
},
|
|
220
|
+
[trimOnCommit, normalizeOnCommit, commitValue, props.onBlur]
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
const handleKeyDown = useCallback(
|
|
224
|
+
(e: KeyboardEvent<HTMLInputElement>) => {
|
|
225
|
+
if ((trimOnCommit || normalizeOnCommit) && e.key === "Enter") {
|
|
226
|
+
commitValue(e);
|
|
227
|
+
}
|
|
228
|
+
props.onKeyDown?.(e);
|
|
229
|
+
},
|
|
230
|
+
[trimOnCommit, normalizeOnCommit, commitValue, props.onKeyDown]
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const displayValue =
|
|
234
|
+
format && typeof props.value === "string"
|
|
235
|
+
? format(props.value)
|
|
236
|
+
: props.value;
|
|
237
|
+
|
|
177
238
|
const handleClearInput = useCallback(() => {
|
|
178
239
|
if (inputRef.current) {
|
|
179
240
|
inputRef.current.value = "";
|
|
@@ -325,7 +386,11 @@ export const TextInput = forwardRef<HTMLInputElement, InputProps>(
|
|
|
325
386
|
type={type}
|
|
326
387
|
id={_id}
|
|
327
388
|
disabled={disabled}
|
|
389
|
+
value={displayValue}
|
|
328
390
|
className={cn(inputClassname, props.className)}
|
|
391
|
+
onChange={normalize ? handleChange : props.onChange}
|
|
392
|
+
onBlur={trimOnCommit || normalizeOnCommit ? handleBlur : props.onBlur}
|
|
393
|
+
onKeyDown={trimOnCommit || normalizeOnCommit ? handleKeyDown : props.onKeyDown}
|
|
329
394
|
/>
|
|
330
395
|
{hasSearchIcon && !hasLeftSectionIcon && (
|
|
331
396
|
<div
|