@react-fabric/form 1.0.0-beta.8 → 1.0.1

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 (215) hide show
  1. package/{src/input/ArrayInput.module.css → css/_arrayInput.css} +8 -8
  2. package/css/_colorPicker.css +95 -0
  3. package/{src/internal/Field.module.css → css/_field.css} +94 -33
  4. package/{src/input/File.module.css → css/_file.css} +21 -6
  5. package/css/styles.css +27 -0
  6. package/dist/cjs/builder/FileField.js +107 -78
  7. package/dist/cjs/builder/FormField.js +60 -39
  8. package/dist/cjs/builder/SchemaEditor.js +72 -0
  9. package/dist/cjs/builder/components/AddSchemaFields.js +64 -0
  10. package/dist/cjs/builder/components/SchemaArrayForm.js +146 -0
  11. package/dist/cjs/builder/components/SchemaField.js +423 -0
  12. package/dist/cjs/form/Controller.js +66 -19
  13. package/dist/cjs/form/Form.js +97 -39
  14. package/dist/cjs/form/useForm.js +32 -0
  15. package/dist/cjs/hooks/useFileUploader.js +74 -45
  16. package/dist/cjs/hooks/useFormBuilder.js +85 -31
  17. package/dist/cjs/i18n/{ar.json.js → ar/form.json.js} +5 -4
  18. package/dist/cjs/i18n/{en.json.js → en/form.json.js} +5 -5
  19. package/dist/cjs/i18n/index.js +4 -4
  20. package/dist/cjs/index.js +41 -2
  21. package/dist/cjs/input/ArrayInput.js +259 -135
  22. package/dist/cjs/input/AvatarInput.js +99 -63
  23. package/dist/cjs/input/Checkbox.js +24 -25
  24. package/dist/cjs/input/ColorInput.js +138 -81
  25. package/dist/cjs/input/ColorPicker.js +78 -95
  26. package/dist/cjs/input/DateInput.js +192 -0
  27. package/dist/cjs/input/Field.js +21 -20
  28. package/dist/cjs/input/File.js +85 -80
  29. package/dist/cjs/input/Hidden.js +27 -13
  30. package/dist/cjs/input/Input.js +49 -48
  31. package/dist/cjs/input/Number.js +46 -44
  32. package/dist/cjs/input/Password.js +51 -49
  33. package/dist/cjs/input/Radio.js +26 -16
  34. package/dist/cjs/input/RadioGroup.js +26 -11
  35. package/dist/cjs/input/{Range.js → RangeSlider.js} +61 -66
  36. package/dist/cjs/input/Search.js +58 -64
  37. package/dist/cjs/input/Slider.js +57 -66
  38. package/dist/cjs/input/Switch.js +29 -27
  39. package/dist/cjs/input/Textarea.js +65 -53
  40. package/dist/cjs/internal/ErrorIcon.js +9 -9
  41. package/dist/cjs/internal/FieldWrapper.js +23 -13
  42. package/dist/cjs/internal/InputWrapper.js +85 -87
  43. package/dist/cjs/ranger/index.js +7 -7
  44. package/dist/cjs/ranger/utils.js +9 -4
  45. package/dist/cjs/select/AutoComplete.js +93 -76
  46. package/dist/cjs/select/DualList.js +235 -154
  47. package/dist/cjs/select/List.js +122 -89
  48. package/dist/cjs/select/Options.js +58 -54
  49. package/dist/cjs/select/Select.js +119 -89
  50. package/dist/cjs/select/helpers.js +5 -5
  51. package/dist/cjs/select/useSelect.js +83 -58
  52. package/dist/cjs/types/schema.js +43 -0
  53. package/dist/cjs/utils/eventHandlers.js +16 -6
  54. package/dist/cjs/utils/getColor.js +6 -6
  55. package/dist/cjs/utils/getProps.js +49 -0
  56. package/dist/esm/builder/FileField.js +109 -80
  57. package/dist/esm/builder/FormField.js +60 -39
  58. package/dist/esm/builder/SchemaEditor.js +72 -0
  59. package/dist/esm/builder/components/AddSchemaFields.js +64 -0
  60. package/dist/esm/builder/components/SchemaArrayForm.js +146 -0
  61. package/dist/esm/builder/components/SchemaField.js +423 -0
  62. package/dist/esm/form/Controller.js +66 -19
  63. package/dist/esm/form/Form.js +97 -39
  64. package/dist/esm/form/useForm.js +32 -0
  65. package/dist/esm/hooks/useFileUploader.js +75 -46
  66. package/dist/esm/hooks/useFormBuilder.js +84 -30
  67. package/dist/esm/i18n/{ar.json.js → ar/form.json.js} +5 -4
  68. package/dist/esm/i18n/{en.json.js → en/form.json.js} +5 -5
  69. package/dist/esm/i18n/index.js +2 -2
  70. package/dist/esm/index.js +36 -1
  71. package/dist/esm/input/ArrayInput.js +260 -136
  72. package/dist/esm/input/AvatarInput.js +99 -63
  73. package/dist/esm/input/Checkbox.js +26 -27
  74. package/dist/esm/input/ColorInput.js +141 -84
  75. package/dist/esm/input/ColorPicker.js +80 -97
  76. package/dist/esm/input/DateInput.js +192 -0
  77. package/dist/esm/input/Field.js +21 -20
  78. package/dist/esm/input/File.js +89 -84
  79. package/dist/esm/input/Hidden.js +28 -14
  80. package/dist/esm/input/Input.js +51 -50
  81. package/dist/esm/input/Number.js +48 -46
  82. package/dist/esm/input/Password.js +54 -52
  83. package/dist/esm/input/Radio.js +27 -17
  84. package/dist/esm/input/RadioGroup.js +27 -12
  85. package/dist/esm/input/{Range.js → RangeSlider.js} +62 -67
  86. package/dist/esm/input/Search.js +61 -67
  87. package/dist/esm/input/Slider.js +58 -67
  88. package/dist/esm/input/Switch.js +31 -29
  89. package/dist/esm/input/Textarea.js +67 -55
  90. package/dist/esm/internal/ErrorIcon.js +11 -11
  91. package/dist/esm/internal/FieldWrapper.js +23 -13
  92. package/dist/esm/internal/InputWrapper.js +88 -90
  93. package/dist/esm/ranger/index.js +7 -7
  94. package/dist/esm/ranger/utils.js +9 -4
  95. package/dist/esm/select/AutoComplete.js +95 -78
  96. package/dist/esm/select/DualList.js +233 -152
  97. package/dist/esm/select/List.js +125 -92
  98. package/dist/esm/select/Options.js +58 -54
  99. package/dist/esm/select/Select.js +122 -92
  100. package/dist/esm/select/helpers.js +5 -5
  101. package/dist/esm/select/useSelect.js +84 -59
  102. package/{src/input/ColorPicker.module.css → dist/esm/types/schema.js} +18 -27
  103. package/dist/esm/utils/eventHandlers.js +16 -6
  104. package/dist/esm/utils/getColor.js +6 -6
  105. package/dist/esm/utils/getProps.js +49 -0
  106. package/dist/types/builder/FileField.d.ts +7 -4
  107. package/dist/types/builder/FormField.d.ts +4 -2
  108. package/dist/types/builder/SchemaEditor.d.ts +6 -0
  109. package/dist/types/builder/components/AddSchemaFields.d.ts +2 -0
  110. package/dist/types/builder/components/SchemaArrayForm.d.ts +2 -0
  111. package/dist/types/builder/components/SchemaField.d.ts +12 -0
  112. package/dist/types/form/Controller.d.ts +45 -3
  113. package/dist/types/form/Form.d.ts +38 -7
  114. package/dist/types/form/useForm.d.ts +6 -0
  115. package/dist/types/hooks/useFileUploader.d.ts +24 -3
  116. package/dist/types/hooks/useFormBuilder.d.ts +41 -3
  117. package/dist/types/index.d.ts +32 -2
  118. package/dist/types/input/ArrayInput.d.ts +68 -13
  119. package/dist/types/input/AvatarInput.d.ts +23 -6
  120. package/dist/types/input/Checkbox.d.ts +12 -10
  121. package/dist/types/input/ColorInput.d.ts +15 -5
  122. package/dist/types/input/ColorPicker.d.ts +10 -2
  123. package/dist/types/input/DateInput.d.ts +21 -0
  124. package/dist/types/input/Field.d.ts +9 -36
  125. package/dist/types/input/File.d.ts +16 -4
  126. package/dist/types/input/Hidden.d.ts +7 -6
  127. package/dist/types/input/Input.d.ts +11 -4
  128. package/dist/types/input/Number.d.ts +9 -4
  129. package/dist/types/input/Password.d.ts +9 -4
  130. package/dist/types/input/Radio.d.ts +9 -4
  131. package/dist/types/input/RadioGroup.d.ts +14 -6
  132. package/dist/types/input/{Range.d.ts → RangeSlider.d.ts} +11 -6
  133. package/dist/types/input/Search.d.ts +14 -4
  134. package/dist/types/input/Slider.d.ts +9 -6
  135. package/dist/types/input/Switch.d.ts +13 -8
  136. package/dist/types/input/Textarea.d.ts +15 -4
  137. package/dist/types/internal/ErrorIcon.d.ts +4 -1
  138. package/dist/types/internal/FieldWrapper.d.ts +10 -1
  139. package/dist/types/internal/InputWrapper.d.ts +19 -1
  140. package/dist/types/ranger/index.d.ts +7 -3
  141. package/dist/types/ranger/utils.d.ts +2 -2
  142. package/dist/types/select/AutoComplete.d.ts +7 -2
  143. package/dist/types/select/DualList.d.ts +11 -37
  144. package/dist/types/select/List.d.ts +7 -2
  145. package/dist/types/select/Options.d.ts +8 -7
  146. package/dist/types/select/Select.d.ts +7 -2
  147. package/dist/types/select/helpers.d.ts +2 -2
  148. package/dist/types/select/useSelect.d.ts +10 -28
  149. package/dist/types/types/index.d.ts +73 -39
  150. package/dist/types/types/schema.d.ts +78 -32
  151. package/dist/types/utils/eventHandlers.d.ts +4 -4
  152. package/dist/types/utils/getColor.d.ts +4 -4
  153. package/dist/types/utils/getProps.d.ts +26 -0
  154. package/dist/types/utils/index.d.ts +1 -0
  155. package/package.json +25 -25
  156. package/src/builder/FileField.tsx +73 -56
  157. package/src/builder/FormField.tsx +66 -58
  158. package/src/builder/SchemaEditor.tsx +78 -0
  159. package/src/builder/components/AddSchemaFields.tsx +64 -0
  160. package/src/builder/components/SchemaArrayForm.tsx +146 -0
  161. package/src/builder/components/SchemaField.tsx +405 -0
  162. package/src/form/Controller.tsx +61 -24
  163. package/src/form/Form.tsx +74 -50
  164. package/src/form/useForm.ts +53 -0
  165. package/src/hooks/useFileUploader.ts +115 -84
  166. package/src/hooks/useFormBuilder.tsx +80 -41
  167. package/src/i18n/{ar.json → ar/form.json} +5 -4
  168. package/src/i18n/{en.json → en/form.json} +5 -5
  169. package/src/i18n/index.ts +2 -2
  170. package/src/index.ts +34 -20
  171. package/src/input/ArrayInput.tsx +255 -173
  172. package/src/input/AvatarInput.tsx +82 -39
  173. package/src/input/Checkbox.tsx +30 -54
  174. package/src/input/ColorInput.tsx +146 -105
  175. package/src/input/ColorPicker.tsx +76 -102
  176. package/src/input/DateInput.tsx +225 -0
  177. package/src/input/Field.tsx +23 -65
  178. package/src/input/File.tsx +76 -98
  179. package/src/input/Hidden.tsx +36 -24
  180. package/src/input/Input.tsx +55 -53
  181. package/src/input/Number.tsx +49 -54
  182. package/src/input/Password.tsx +61 -66
  183. package/src/input/Radio.tsx +28 -37
  184. package/src/input/RadioGroup.tsx +30 -25
  185. package/src/input/{Range.tsx → RangeSlider.tsx} +81 -109
  186. package/src/input/Search.tsx +64 -77
  187. package/src/input/Slider.tsx +80 -109
  188. package/src/input/Switch.tsx +39 -53
  189. package/src/input/Textarea.tsx +72 -59
  190. package/src/internal/ErrorIcon.tsx +22 -21
  191. package/src/internal/FieldWrapper.tsx +33 -23
  192. package/src/internal/InputWrapper.tsx +132 -143
  193. package/src/ranger/index.tsx +37 -117
  194. package/src/ranger/utils.ts +12 -11
  195. package/src/select/AutoComplete.tsx +113 -123
  196. package/src/select/DualList.tsx +218 -204
  197. package/src/select/List.tsx +130 -97
  198. package/src/select/Options.tsx +59 -57
  199. package/src/select/Select.tsx +148 -122
  200. package/src/select/helpers.ts +13 -8
  201. package/src/select/useSelect.tsx +110 -127
  202. package/src/types/index.ts +76 -45
  203. package/src/types/schema.ts +97 -40
  204. package/src/utils/eventHandlers.ts +24 -26
  205. package/src/utils/getColor.ts +9 -8
  206. package/src/utils/getProps.ts +47 -0
  207. package/src/utils/index.ts +1 -0
  208. package/dist/cjs/input/ArrayInput.module.css +0 -1
  209. package/dist/cjs/input/ColorPicker.module.css +0 -1
  210. package/dist/cjs/input/File.module.css +0 -1
  211. package/dist/cjs/internal/Field.module.css +0 -1
  212. package/dist/esm/input/ArrayInput.module.css +0 -1
  213. package/dist/esm/input/ColorPicker.module.css +0 -1
  214. package/dist/esm/input/File.module.css +0 -1
  215. package/dist/esm/internal/Field.module.css +0 -1
@@ -21,34 +21,21 @@
21
21
  * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
22
  */
23
23
 
24
- import { CoreIcons, Icon } from "@react-fabric/core";
25
- import { type RefProp } from "@react-fabric/core/dist/types/types";
26
- import {
27
- FileUtil,
28
- Format,
29
- isArray,
30
- isEmpty,
31
- mergeRefs,
32
- } from "@react-fabric/utilities";
33
- import classNames from "classnames";
34
- import {
35
- Fragment,
36
- useCallback,
37
- useDeferredValue,
38
- useEffect,
39
- useMemo,
40
- useRef,
41
- useState,
42
- type ChangeEvent,
43
- } from "react";
24
+ import { Icon, useControlledValue } from "@react-fabric/core";
25
+ import type { RefProp } from "@react-fabric/core/dist/types/types";
26
+ import { cn, FileUtil, Format, isArray, isEmpty, mergeRefs } from "@react-fabric/utilities";
27
+ import { Fragment, useCallback, useEffect, useId, useRef, useState } from "react";
44
28
  import { InputWrapper } from "../internal/InputWrapper";
45
- import { type InputProps } from "../types";
46
- import { handleEnter } from "../utils";
47
- import classes from "./File.module.css";
29
+ import type { InputProps, WrapperProps } from "../types";
30
+ import { getWrapperProps, useHandleEnter } from "../utils";
48
31
 
49
- export interface FileInputProps
50
- extends InputProps<AnyObject>,
51
- RefProp<HTMLInputElement> {
32
+ interface FileType {
33
+ name: string;
34
+ type?: string;
35
+ size?: number;
36
+ }
37
+
38
+ export interface FileInputProps extends InputProps<FileType | FileType[]>, WrapperProps, RefProp<HTMLInputElement> {
52
39
  /**
53
40
  * allow selecting mutiple files
54
41
  */
@@ -63,125 +50,119 @@ export interface FileInputProps
63
50
  accept?: string;
64
51
  }
65
52
 
66
- const FileExists = (prop: AnyObject) => {
53
+ function FileExists(prop: { name: string }) {
67
54
  const [kill, setKill] = useState(false);
68
55
  useEffect(() => {
69
- setTimeout(() => {
56
+ const tmr = setTimeout(() => {
70
57
  setKill(true);
71
58
  }, 2000);
72
- });
59
+ return () => {
60
+ clearTimeout(tmr);
61
+ };
62
+ }, []);
73
63
  return kill ? null : (
74
- <div className="bg-danger-500 text-white px-2 rounded text-xs">
75
- File [{prop.name}] already added to list
76
- </div>
64
+ <div className="bg-danger-500 text-white px-2 rounded text-xs">File [{prop.name}] already added to list</div>
77
65
  );
78
- };
66
+ }
79
67
 
80
- export const FileInput = ({
81
- ref,
82
- value,
83
- invalid,
84
- readOnly,
85
- disabled,
86
- required,
87
- placeholder,
88
- autoFocus,
89
- error,
90
- name,
91
- accept,
92
- showList = true,
93
- multiple,
94
- onChange,
95
- onEnterPressed,
96
- ...rest
97
- }: FileInputProps) => {
68
+ /**
69
+ * File input component that allows users to select files from their system.
70
+ * It supports multiple file selection and displays a list of selected files.
71
+ * The component also provides functionality to remove files from the list.
72
+ * It handles file changes, invalid states, and read-only or disabled states.
73
+ */
74
+ export function FileInput(props: FileInputProps) {
75
+ const {
76
+ ref,
77
+ value,
78
+ invalid,
79
+ readOnly,
80
+ disabled,
81
+ placeholder,
82
+ autoFocus,
83
+ error,
84
+ name,
85
+ accept,
86
+ showList = true,
87
+ multiple,
88
+ onChange,
89
+ onEnterPressed,
90
+ } = props;
98
91
  const listRef = useRef<File[]>([]);
99
92
  const inputRef = useRef<HTMLInputElement>(null);
100
- const [exists, setExists] = useState<string[]>([]);
101
- const [actualValue, setActualValue] = useState<File | File[] | null>();
102
- const deferred = useDeferredValue(value);
93
+ const [exists, setExists] = useState<string[]>(() => []);
103
94
 
104
- const handleEnterPressed = useMemo(
105
- () => handleEnter(onEnterPressed),
106
- [onEnterPressed],
107
- );
95
+ const id = useId();
96
+ const { currentValue, updateValue } = useControlledValue(value, null);
108
97
 
109
- useEffect(() => {
110
- setActualValue(deferred);
111
- }, [deferred]);
98
+ const handleEnterPressed = useHandleEnter(onEnterPressed);
112
99
 
113
100
  const handleChange = useCallback(
114
- (e?: ChangeEvent<HTMLInputElement>) => {
101
+ (e?: React.ChangeEvent<HTMLInputElement>) => {
115
102
  setExists([]);
116
103
  if (multiple) {
117
104
  if (e?.target.files) {
118
105
  if (showList) {
119
106
  const exists: string[] = [];
120
107
  Array.from(e.target.files).forEach((file) => {
121
- listRef.current.find((ef) => ef.name === file.name)
122
- ? exists.push(file.name)
123
- : listRef.current.push(file);
108
+ if (listRef.current.find((ef) => ef.name === file.name)) exists.push(file.name);
109
+ else listRef.current.push(file);
124
110
  });
125
- setActualValue(listRef.current.slice());
111
+ updateValue(listRef.current.slice());
126
112
  setExists(exists);
127
113
  }
128
114
  e.target.value = "";
129
115
  onChange?.(listRef.current);
130
116
  } else {
131
117
  listRef.current = [];
132
- setActualValue(listRef.current);
118
+ updateValue(listRef.current);
133
119
  onChange?.(listRef.current);
134
120
  }
135
121
  } else {
136
- setActualValue(e?.target.files?.item(0));
122
+ updateValue(e?.target.files?.item(0));
137
123
  onChange?.(e?.target.files?.item(0) ?? undefined);
138
124
  if (!e && inputRef.current) inputRef.current.value = "";
139
125
  }
140
126
  },
141
- [onChange, multiple, showList],
127
+ [multiple, showList, onChange, updateValue],
142
128
  );
143
129
 
144
130
  const removeFile = useCallback(
145
131
  (idx: number) => {
146
132
  listRef.current.splice(idx, 1);
147
- setActualValue(listRef.current);
133
+ updateValue(listRef.current);
148
134
  onChange?.(listRef.current);
149
135
  },
150
- [onChange],
136
+ [onChange, updateValue],
151
137
  );
152
138
 
153
139
  return (
154
140
  <InputWrapper
155
- showClear={!isEmpty(actualValue)}
141
+ id={id}
142
+ showClear={!isEmpty(currentValue) && !disabled && !readOnly}
156
143
  onClear={handleChange}
157
- invalid={invalid}
158
- readOnly={readOnly}
159
- disabled={disabled}
160
- required={required}
161
- error={error}
144
+ {...getWrapperProps(props)}
162
145
  listing={
163
146
  showList && (
164
147
  <Fragment>
165
- {isArray(actualValue) &&
166
- actualValue?.map?.((file: AnyObject, idx: number) => (
167
- <div
168
- key={idx}
169
- className="flex gap-1 items-center text-sm text-dimmed py-1 px-2"
170
- >
148
+ {isArray(currentValue) &&
149
+ currentValue?.map?.((file, idx: number) => (
150
+ // eslint-disable-next-line @eslint-react/no-array-index-key
151
+ <div key={idx} className="flex gap-1 items-center text-sm text-dimmed py-1 px-2">
171
152
  <div className="flex-1 truncate">
172
153
  <div className="font-medium">{file.name}</div>
173
154
  <span className="text-xs">{FileUtil.mime(file.type)}</span>
174
155
  </div>
175
- <span className="flex-content text-end w-24">
176
- {Format.bytes(file.size)}
177
- </span>
178
- <Icon
179
- className="cursor-pointer opacity-30 hover:opacity-80"
180
- icon={CoreIcons.close}
156
+ <span className="flex-content text-end w-24">{Format.bytes(file.size)}</span>
157
+ <button
158
+ type="button"
159
+ className={cn("size-fit leading-0 cursor-pointer")}
181
160
  onClick={() => {
182
161
  removeFile(idx);
183
162
  }}
184
- />
163
+ >
164
+ <Icon className="cursor-pointer opacity-30 hover:opacity-80" icon="icon-[mdi--close]" />
165
+ </button>
185
166
  </div>
186
167
  ))}
187
168
  {exists.map((name) => (
@@ -190,15 +171,12 @@ export const FileInput = ({
190
171
  </Fragment>
191
172
  )
192
173
  }
193
- {...rest}
194
174
  >
195
175
  <input
196
- className={classNames(
197
- "appearance-none bg-transparent py-1 px-2 flex-1 border-none outline-none ring-0",
198
- classes.fileInput,
199
- isArray(actualValue) &&
200
- actualValue?.length &&
201
- "text-[var(--fabric-bg)]",
176
+ className={cn(
177
+ "fabric-fileInput",
178
+ "appearance-none bg-transparent py-1 px-2 flex-1 border-none outline-none ring-0 peer",
179
+ isArray(currentValue) && currentValue?.length && "text-(--fabric-bg)",
202
180
  )}
203
181
  aria-invalid={invalid}
204
182
  aria-disabled={disabled}
@@ -221,4 +199,4 @@ export const FileInput = ({
221
199
  />
222
200
  </InputWrapper>
223
201
  );
224
- };
202
+ }
@@ -21,7 +21,10 @@
21
21
  * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
22
  */
23
23
 
24
- import { Fragment, useEffect } from "react";
24
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
25
+
26
+ import { isEqual } from "@react-fabric/utilities";
27
+ import { Fragment, useDeferredValue, useEffect, useRef } from "react";
25
28
  import { ErrorIcon } from "../internal/ErrorIcon";
26
29
 
27
30
  interface Props {
@@ -30,36 +33,45 @@ interface Props {
30
33
  */
31
34
  name?: string;
32
35
  /**
33
- * hidden value will fire change for form update
36
+ * hidden input value
34
37
  */
35
38
  hiddenValue?: AnyObject;
36
- /**
37
- * form onChange handler
38
- */
39
- onChange?: (value: AnyObject) => void;
40
39
  }
41
40
 
42
- export const HiddenInput = ({
43
- hiddenValue,
44
- onChange,
45
- // @ts-expect-error ignore
46
- // eslint-disable-next-line react/prop-types
47
- invalid,
48
- // @ts-expect-error ignore
49
- // eslint-disable-next-line react/prop-types
50
- error,
51
- // @ts-expect-error ignore
52
- // eslint-disable-next-line react/prop-types
53
- value = "",
54
- name,
55
- }: Props) => {
41
+ /**
42
+ * Hidden input component to handle hidden values in forms.
43
+ * It will trigger the onChange handler with the hidden value after a short delay.
44
+ * This is useful for cases where you need to update the form state without user interaction.
45
+ */
46
+ export function HiddenInput(props: Props) {
47
+ const {
48
+ // @ts-expect-error ignore
49
+ invalid = false,
50
+ // @ts-expect-error ignore
51
+ error = "",
52
+ // @ts-expect-error ignore
53
+ onChange,
54
+ // @ts-expect-error ignore
55
+ value,
56
+ hiddenValue,
57
+ name,
58
+ } = props;
59
+
60
+ const valueRef = useRef(value);
61
+ const deferred = useDeferredValue(hiddenValue);
62
+ useEffect(() => {
63
+ valueRef.current = value;
64
+ }, [value]);
56
65
  useEffect(() => {
57
- onChange?.(hiddenValue);
58
- }, [hiddenValue]);
66
+ if (!isEqual(valueRef.current, deferred)) {
67
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
68
+ onChange?.(deferred);
69
+ }
70
+ }, [deferred, onChange]);
59
71
  return (
60
72
  <Fragment>
61
- <input type="hidden" onChange={onChange} value={value} name={name} />
73
+ <input type="hidden" value={value ?? ""} name={name} />
62
74
  <ErrorIcon invalid={invalid} error={error} />
63
75
  </Fragment>
64
76
  );
65
- };
77
+ }
@@ -21,76 +21,76 @@
21
21
  * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
22
  */
23
23
 
24
- import { type RefProp } from "@react-fabric/core/dist/types/types";
25
- import {
26
- useCallback,
27
- useDeferredValue,
28
- useEffect,
29
- useMemo,
30
- useState,
31
- type ChangeEvent,
32
- } from "react";
24
+ import { useControlledValue } from "@react-fabric/core";
25
+ import type { RefProp } from "@react-fabric/core/dist/types/types";
26
+ import { cn } from "@react-fabric/utilities";
27
+ import type { InputHTMLAttributes } from "react";
28
+ import { useCallback, useId } from "react";
33
29
  import { InputWrapper } from "../internal/InputWrapper";
34
- import { type InputProps } from "../types";
35
- import { handleEnter } from "../utils";
30
+ import type { InputProps, WrapperProps } from "../types";
31
+ import { getWrapperProps, useHandleEnter } from "../utils";
36
32
 
37
- export interface TextInputProps extends InputProps, RefProp<HTMLInputElement> {
33
+ export interface TextInputProps extends InputProps, WrapperProps, RefProp<HTMLInputElement> {
38
34
  /**
39
35
  * input type
40
36
  */
41
37
  type?: "email" | "file" | "search" | "tel" | "text" | "url";
38
+
39
+ autoComplete?: InputHTMLAttributes<HTMLInputElement>["autoComplete"];
42
40
  }
43
41
 
44
- export const Input = ({
45
- ref,
46
- name,
47
- value,
48
- invalid,
49
- readOnly,
50
- disabled,
51
- required,
52
- placeholder,
53
- autoFocus,
54
- error,
55
- type = "text",
56
- onChange,
57
- onEnterPressed,
58
- ...rest
59
- }: TextInputProps) => {
60
- const [actualValue, setActualValue] = useState("");
61
- const deferred = useDeferredValue(value);
42
+ /**
43
+ * This component is used to render a text input field with various properties such as name, value, invalid state, read-only, disabled, required, placeholder, autoFocus, error message, monospace font option, and event handlers for blur, focus, change, and enter key press.
44
+ * It also includes a clear button that appears when there is a value in the input field, allowing users to clear the input easily.
45
+ * The component uses a wrapper to handle the clear button and styling, and it supports deferred value updates to optimize performance.
46
+ */
47
+ export function Input(props: TextInputProps) {
48
+ const {
49
+ value,
50
+ defaultValue,
51
+ onEnterPressed,
52
+ onChange,
53
+ onBlur,
54
+ onFocus,
55
+ disabled,
56
+ readOnly,
57
+ invalid,
58
+ error,
59
+ placeholder,
60
+ name,
61
+ type,
62
+ autoFocus,
63
+ allowClear,
64
+ autoComplete = "off",
65
+ ref,
66
+ } = props;
62
67
 
63
- const handleEnterPressed = useMemo(
64
- () => handleEnter(onEnterPressed),
65
- [onEnterPressed],
66
- );
68
+ const id = useId();
69
+ const { currentValue, updateValue } = useControlledValue(value, defaultValue);
67
70
 
68
- useEffect(() => {
69
- setActualValue(deferred ?? "");
70
- }, [deferred]);
71
+ const handleEnterPressed = useHandleEnter(onEnterPressed);
71
72
 
72
73
  const handleChange = useCallback(
73
- (e?: ChangeEvent<HTMLInputElement>) => {
74
- const value = e?.target.value;
75
- setActualValue(value ?? "");
76
- onChange?.(value ?? undefined);
74
+ (e?: React.ChangeEvent<HTMLInputElement>) => {
75
+ const value = e?.target.value ?? defaultValue;
76
+ updateValue(value);
77
+ onChange?.(value);
77
78
  },
78
- [onchange],
79
+ [defaultValue, updateValue, onChange],
79
80
  );
80
81
 
81
82
  return (
82
83
  <InputWrapper
83
- showClear={!!actualValue}
84
+ id={id}
85
+ showClear={allowClear && (currentValue ?? "") !== (defaultValue ?? "") && !disabled && !readOnly}
84
86
  onClear={handleChange}
85
- invalid={invalid}
86
- readOnly={readOnly}
87
- disabled={disabled}
88
- required={required}
89
- error={error}
90
- {...rest}
87
+ {...getWrapperProps(props)}
91
88
  >
92
89
  <input
93
- className="appearance-none bg-transparent py-1 px-2 flex-1 border-none outline-none ring-0 peer"
90
+ id={id}
91
+ className={cn(
92
+ "appearance-none bg-transparent py-1 px-4 not-first:ps-2 not-last:pe-2 flex-1 border-none outline-none ring-0 peer",
93
+ )}
94
94
  aria-invalid={invalid}
95
95
  aria-disabled={disabled}
96
96
  aria-readonly={readOnly}
@@ -103,12 +103,14 @@ export const Input = ({
103
103
  size={1}
104
104
  type={type}
105
105
  ref={ref}
106
- autoComplete="off"
106
+ autoComplete={autoComplete}
107
107
  {...{ autoFocus }}
108
- value={actualValue}
108
+ value={currentValue ?? ""}
109
+ onBlur={onBlur}
110
+ onFocus={onFocus}
109
111
  onChange={handleChange}
110
112
  onKeyDown={handleEnterPressed}
111
113
  />
112
114
  </InputWrapper>
113
115
  );
114
- };
116
+ }
@@ -21,22 +21,14 @@
21
21
  * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
22
  */
23
23
 
24
- import { type RefProp } from "@react-fabric/core/dist/types/types";
25
- import {
26
- useCallback,
27
- useDeferredValue,
28
- useEffect,
29
- useMemo,
30
- useState,
31
- type ChangeEvent,
32
- } from "react";
24
+ import { useControlledValue } from "@react-fabric/core";
25
+ import type { RefProp } from "@react-fabric/core/dist/types/types";
26
+ import { useCallback, useId } from "react";
33
27
  import { InputWrapper } from "../internal/InputWrapper";
34
- import { type InputProps } from "../types";
35
- import { handleEnter } from "../utils";
28
+ import type { InputProps, WrapperProps } from "../types";
29
+ import { getWrapperProps, useHandleEnter } from "../utils";
36
30
 
37
- export interface NumberProps
38
- extends InputProps<number>,
39
- RefProp<HTMLInputElement> {
31
+ export interface NumberProps extends InputProps<number>, WrapperProps, RefProp<HTMLInputElement> {
40
32
  /**
41
33
  * minimum value
42
34
  */
@@ -51,56 +43,57 @@ export interface NumberProps
51
43
  step?: number;
52
44
  }
53
45
 
54
- export const Number = ({
55
- ref,
56
- value,
57
- invalid,
58
- readOnly,
59
- disabled,
60
- required,
61
- placeholder,
62
- autoFocus,
63
- name,
64
- min,
65
- max,
66
- step,
67
- onChange,
68
- onEnterPressed,
69
- ...rest
70
- }: NumberProps) => {
71
- const [actualValue, setActualValue] = useState<"" | number>("");
72
- const deferred = useDeferredValue(value);
46
+ /**
47
+ * This component allows users to input numeric values with optional minimum, maximum, and step constraints.
48
+ * It provides a clear interface for entering numbers, with support for validation and accessibility features.
49
+ * It also includes a clear button to reset the input value.
50
+ */
51
+ export function Number(props: NumberProps) {
52
+ const {
53
+ value,
54
+ defaultValue,
55
+ onEnterPressed,
56
+ onChange,
57
+ onBlur,
58
+ onFocus,
59
+ disabled,
60
+ readOnly,
61
+ invalid,
62
+ min,
63
+ max,
64
+ step,
65
+ placeholder,
66
+ name,
67
+ autoFocus,
68
+ allowClear,
69
+ ref,
70
+ } = props;
73
71
 
74
- const handleEnterPressed = useMemo(
75
- () => handleEnter(onEnterPressed),
76
- [onEnterPressed],
77
- );
72
+ const id = useId();
73
+ const { currentValue, updateValue } = useControlledValue(value, defaultValue);
78
74
 
79
- useEffect(() => {
80
- setActualValue(deferred ?? "");
81
- }, [deferred]);
75
+ const handleEnterPressed = useHandleEnter(onEnterPressed);
82
76
 
83
77
  const handleChange = useCallback(
84
- (e?: ChangeEvent<HTMLInputElement>) => {
85
- const number = e?.target.valueAsNumber;
86
- setActualValue(number ?? "");
87
- onChange?.(number ?? undefined);
78
+ (e?: React.ChangeEvent<HTMLInputElement>) => {
79
+ const number = e?.target.valueAsNumber ?? defaultValue;
80
+ updateValue(number);
81
+ onChange?.(number);
88
82
  },
89
- [onChange],
83
+ [defaultValue, updateValue, onChange],
90
84
  );
91
85
 
92
86
  return (
93
87
  <InputWrapper
94
- showClear={!!actualValue}
88
+ id={id}
89
+ showClear={allowClear && (currentValue ?? "") !== (defaultValue ?? "") && !disabled && !readOnly}
95
90
  onClear={handleChange}
96
- invalid={invalid}
97
- readOnly={readOnly}
98
- disabled={disabled}
99
- required={required}
100
- {...rest}
91
+ hasStepper
92
+ {...getWrapperProps(props)}
101
93
  >
102
94
  <input
103
- className="appearance-none bg-transparent py-1 px-2 flex-1 border-none outline-none ring-0"
95
+ id={id}
96
+ className="appearance-none bg-transparent py-1 ps-4 pe-2 not-first:ps-2 flex-1 border-none outline-none ring-0 peer"
104
97
  aria-invalid={invalid}
105
98
  aria-disabled={disabled}
106
99
  aria-readonly={readOnly}
@@ -117,10 +110,12 @@ export const Number = ({
117
110
  ref={ref}
118
111
  autoComplete="off"
119
112
  {...{ autoFocus }}
120
- value={actualValue}
113
+ value={currentValue ?? ""}
114
+ onBlur={onBlur}
115
+ onFocus={onFocus}
121
116
  onChange={handleChange}
122
117
  onKeyDown={handleEnterPressed}
123
118
  />
124
119
  </InputWrapper>
125
120
  );
126
- };
121
+ }