@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.
Files changed (43) hide show
  1. package/dist/cjs/bundle.css +0 -10
  2. package/dist/cjs/bundle.js +3 -3
  3. package/dist/cjs/bundle.js.map +1 -1
  4. package/dist/cjs/types/components/Dropdown/Dropdown.stories.d.ts +4 -0
  5. package/dist/cjs/types/components/Form/ValidationHintList.d.ts +4 -1
  6. package/dist/cjs/types/components/InputFilter/InputFilter.stories.d.ts +4 -0
  7. package/dist/cjs/types/components/MaskedTextInput/MaskedTextInput.d.ts +4 -0
  8. package/dist/cjs/types/components/MaskedTextInput/MaskedTextInput.stories.d.ts +8 -0
  9. package/dist/cjs/types/components/PasswordInput/PasswordInput.stories.d.ts +4 -0
  10. package/dist/cjs/types/components/Search/Search.stories.d.ts +4 -0
  11. package/dist/cjs/types/components/TextArea/TextArea.d.ts +8 -0
  12. package/dist/cjs/types/components/TextArea/TextArea.stories.d.ts +4 -0
  13. package/dist/cjs/types/components/TextInput/TextInput.d.ts +8 -0
  14. package/dist/cjs/types/components/TextInput/TextInput.stories.d.ts +20 -0
  15. package/dist/components/Form/ValidationHintList.js +9 -9
  16. package/dist/components/OtpInput/OtpInput.js +1 -1
  17. package/dist/components/TextArea/TextArea.js +32 -3
  18. package/dist/components/TextArea/TextArea.stories.js +29 -0
  19. package/dist/components/TextInput/TextInput.js +38 -2
  20. package/dist/components/TextInput/TextInput.stories.js +28 -0
  21. package/dist/esm/bundle.css +0 -10
  22. package/dist/esm/bundle.js +2 -2
  23. package/dist/esm/bundle.js.map +1 -1
  24. package/dist/esm/types/components/Dropdown/Dropdown.stories.d.ts +4 -0
  25. package/dist/esm/types/components/Form/ValidationHintList.d.ts +4 -1
  26. package/dist/esm/types/components/InputFilter/InputFilter.stories.d.ts +4 -0
  27. package/dist/esm/types/components/MaskedTextInput/MaskedTextInput.d.ts +4 -0
  28. package/dist/esm/types/components/MaskedTextInput/MaskedTextInput.stories.d.ts +8 -0
  29. package/dist/esm/types/components/PasswordInput/PasswordInput.stories.d.ts +4 -0
  30. package/dist/esm/types/components/Search/Search.stories.d.ts +4 -0
  31. package/dist/esm/types/components/TextArea/TextArea.d.ts +8 -0
  32. package/dist/esm/types/components/TextArea/TextArea.stories.d.ts +4 -0
  33. package/dist/esm/types/components/TextInput/TextInput.d.ts +8 -0
  34. package/dist/esm/types/components/TextInput/TextInput.stories.d.ts +20 -0
  35. package/dist/index.d.ts +24 -1
  36. package/dist/src/theme/global.css +0 -13
  37. package/package.json +1 -1
  38. package/src/components/Form/ValidationHintList.tsx +24 -11
  39. package/src/components/OtpInput/OtpInput.tsx +22 -9
  40. package/src/components/TextArea/TextArea.stories.tsx +108 -0
  41. package/src/components/TextArea/TextArea.tsx +52 -1
  42. package/src/components/TextInput/TextInput.stories.tsx +120 -5
  43. 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={hasError ? "Please enter a valid email address" : undefined}
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={hasError ? "Please enter a valid email address" : undefined}
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={legacyWarning ? "Please verify this email." : undefined}
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
- ? "Please verify this email."
340
- : undefined
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