@react-fabric/form 1.0.0-beta.9 → 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
@@ -22,9 +22,12 @@
22
22
  */
23
23
 
24
24
  import { Avatar, Icon, ProgressBar } from "@react-fabric/core";
25
- import { type RefProp } from "@react-fabric/core/dist/types/types";
25
+ import type { AvatarProps } from "@react-fabric/core/dist/types/components/avatar/Avatar";
26
+ import type { RefProp } from "@react-fabric/core/dist/types/types";
27
+ import { cn } from "@react-fabric/utilities";
26
28
  import { useEffect, useState } from "react";
27
- import { useFileUploader, type UploadHandler } from "../hooks/useFileUploader";
29
+ import type { UploadHandler } from "../hooks/useFileUploader";
30
+ import { useFileUploader } from "../hooks/useFileUploader";
28
31
  import { HiddenInput } from "./Hidden";
29
32
 
30
33
  export interface Props extends RefProp {
@@ -37,14 +40,25 @@ export interface Props extends RefProp {
37
40
  */
38
41
  value?: string;
39
42
  /**
40
- * avatar stream src
43
+ * default avatar image
41
44
  */
42
- src?: string;
45
+ defaultValue?: string;
46
+ /**
47
+ * avatar name
48
+ */
49
+ avatarName?: string;
50
+ rounded?: boolean;
51
+ variant?: AvatarProps["variant"];
52
+ fallbackIcon?: AvatarProps["fallbackIcon"];
53
+ avatarClassName?: AvatarProps["className"];
43
54
  size?: string | number;
44
55
  /**
45
56
  * upload handler to store file in temp storage
46
57
  */
47
58
  uploadHandler: UploadHandler;
59
+ /**
60
+ * file stream path
61
+ */
48
62
  fileUrl?: (path: string) => string;
49
63
  /**
50
64
  * change handler
@@ -53,25 +67,40 @@ export interface Props extends RefProp {
53
67
  * @param base64
54
68
  * @returns storage path
55
69
  */
56
- onChange?: (path: string) => void;
70
+ onChange?: (path: string | null) => void;
57
71
  }
58
72
 
73
+ /**
74
+ * Avatar input component for uploading and displaying user avatars.
75
+ * It allows users to select an image file, upload it, and display the avatar.
76
+ * It also provides a way to remove the uploaded file and shows upload progress.
77
+ */
59
78
  export const AvatarInput = ({
60
- src,
61
79
  size = "6rem",
80
+ avatarName,
81
+ avatarClassName,
82
+ value,
83
+ rounded,
84
+ defaultValue,
85
+ fallbackIcon,
86
+ variant,
62
87
  fileUrl,
63
88
  uploadHandler,
89
+ onChange,
64
90
  ...rest
65
- }: Props) => {
66
- const { files, list, upload, remove } = useFileUploader(
91
+ }: Props): React.ReactElement => {
92
+ const { files, upload, remove } = useFileUploader(
67
93
  async (data, config) => await uploadHandler?.(data, config),
68
- rest.value,
69
- false,
94
+ value ?? defaultValue,
95
+ {
96
+ multiple: false,
97
+ onChange: (files) => onChange?.(files?.path ?? null),
98
+ },
70
99
  );
71
100
 
72
101
  const [base64, setBase64] = useState("");
73
102
  useEffect(() => {
74
- if (list?.length) {
103
+ if (files?.length) {
75
104
  const file = files?.[0]?.file;
76
105
  void (
77
106
  file &&
@@ -87,19 +116,37 @@ export const AvatarInput = ({
87
116
  })
88
117
  );
89
118
  } else {
90
- setBase64("");
119
+ // eslint-disable-next-line @eslint-react/set-state-in-effect
120
+ setBase64(fileUrl?.(defaultValue ?? "") ?? defaultValue ?? "");
91
121
  }
92
- }, [files, list]);
122
+ }, [files, defaultValue, fileUrl]);
93
123
 
94
124
  useEffect(() => {
95
- src && setBase64(fileUrl?.(src) ?? src);
96
- }, [src]);
125
+ // eslint-disable-next-line @eslint-react/set-state-in-effect
126
+ if (!value && defaultValue) setBase64(fileUrl?.(defaultValue) ?? defaultValue);
127
+ // eslint-disable-next-line @eslint-react/set-state-in-effect
128
+ if (value) setBase64(fileUrl?.(value) ?? value);
129
+ }, [value, defaultValue, fileUrl]);
97
130
 
98
131
  return (
99
- <div className="flex flex-nowrap items-end">
100
- <div className="inline-block leading-none rounded-full relative outline overflow-hidden">
101
- <Avatar name="temp" size={size} avatar={base64} />
102
- <div className="absolute inset-x-0 bottom-0 cursor-pointer bg-opacity-20 hover:bg-opacity-50 bg-black text-white text-xs text-center py-1">
132
+ <div className="flex flex-nowrap items-end overflow-hidden p-px">
133
+ <div
134
+ className={cn(
135
+ "inline-block leading-none relative outline overflow-hidden",
136
+ rounded && "rounded-full",
137
+ !rounded && "rounded",
138
+ )}
139
+ >
140
+ <Avatar
141
+ name={avatarName ?? "temp"}
142
+ size={size}
143
+ avatar={base64}
144
+ rounded={rounded}
145
+ variant={variant}
146
+ fallbackIcon={fallbackIcon}
147
+ className={avatarClassName}
148
+ />
149
+ <div className="absolute inset-x-0 bottom-0 cursor-pointer opacity-50 bg-black/50 hover:opacity-90 text-white text-xs text-center py-1">
103
150
  <span>Edit</span>
104
151
  <input
105
152
  type="file"
@@ -109,40 +156,36 @@ export const AvatarInput = ({
109
156
  />
110
157
  </div>
111
158
  </div>
112
- <HiddenInput hiddenValue={list?.[0]?.path} {...rest} />
159
+ <HiddenInput hiddenValue={files?.[0]?.path} {...rest} />
113
160
  {files.map((file, idx) => (
114
- <div key={idx} className="px-2 py-1 flex-1">
161
+ // eslint-disable-next-line @eslint-react/no-array-index-key
162
+ <div key={idx} className="px-2 py-1 flex-1 overflow-hidden">
115
163
  <div className="flex flex-nowrap gap-1 items-center">
116
- <div className="flex-initial truncate pe-2">{file.filename}</div>
117
164
  {file.error && (
118
165
  <div className="flex-1 truncate px-2">
119
166
  <span className="text-danger-600">{file.error}</span>
120
167
  </div>
121
168
  )}
122
169
  {file.progress && (
123
- <Icon
124
- icon="mdi mdi-stop-circle"
125
- bg="danger"
126
- color="white"
127
- size="sm"
128
- className="p-[2px]"
129
- aria-label="abort"
170
+ <button
171
+ type="button"
172
+ className={cn("size-fit flex-content leading-0 cursor-pointer")}
130
173
  onClick={() => file.abort?.()}
131
- />
174
+ >
175
+ <Icon icon="icon-[mdi--stop]" bg="danger" color="white" size="sm" className="p-0.5" aria-label="abort" />
176
+ </button>
132
177
  )}
133
178
  {!file.progress && (
134
- <Icon
135
- icon="mdi mdi-trash-can"
136
- color="danger"
137
- size="sm"
138
- aria-label="remove"
179
+ <button
180
+ type="button"
181
+ className={cn("size-fit flex-content leading-0 cursor-pointer")}
139
182
  onClick={() => remove(file.key)}
140
- />
183
+ >
184
+ <Icon icon="icon-[mdi--trash-can-outline]" color="danger" size="sm" aria-label="remove" />
185
+ </button>
141
186
  )}
142
187
  </div>
143
- {file.progress && (
144
- <ProgressBar animate value={file.progress * 100} size="xxs" />
145
- )}
188
+ {file.progress && <ProgressBar animate value={file.progress * 100} size="xxs" />}
146
189
  </div>
147
190
  ))}
148
191
  </div>
@@ -21,37 +21,25 @@
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 classNames from "classnames";
27
- import {
28
- useCallback,
29
- useDeferredValue,
30
- useEffect,
31
- useMemo,
32
- useState,
33
- type ChangeEvent,
34
- } from "react";
24
+ import { Icon, 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 { useCallback, useMemo } from "react";
35
28
  import { ErrorIcon } from "../internal/ErrorIcon";
36
29
 
37
- export interface CheckboxProps extends RefProp<HTMLInputElement> {
30
+ interface CheckboxProps extends RefProp<HTMLInputElement> {
38
31
  /**
39
32
  * checkbox label
40
33
  */
41
- label?: JSX.Element | string;
34
+ label?: React.ReactElement | string;
42
35
  /**
43
36
  * checked
44
37
  */
45
38
  checked?: boolean;
46
- /**
47
- * value
48
- */
49
- value?: string | number;
50
39
  /**
51
40
  * on checked change
52
41
  */
53
- onChange?: (checked: boolean | string | number) => void;
54
-
42
+ onChange?: (checked: boolean) => void;
55
43
  /**
56
44
  * disabled input
57
45
  */
@@ -82,10 +70,15 @@ export interface CheckboxProps extends RefProp<HTMLInputElement> {
82
70
  name?: string;
83
71
  }
84
72
 
85
- export const Checkbox = ({
73
+ /**
74
+ * Checkbox component for rendering a checkbox input with label and error handling.
75
+ * It supports custom icons for checked and unchecked states, and handles disabled and invalid states.
76
+ * The component uses a deferred value to manage the checked state, ensuring smooth updates.
77
+ * It also provides an error icon to indicate validation errors.
78
+ */
79
+ export function Checkbox({
86
80
  ref,
87
81
  label,
88
- value,
89
82
  name,
90
83
  checked,
91
84
  invalid,
@@ -96,43 +89,31 @@ export const Checkbox = ({
96
89
  iconChecked,
97
90
  onChange,
98
91
  ...rest
99
- }: CheckboxProps) => {
100
- const [actualValue, setActualValue] = useState(false);
101
- const deferred = useDeferredValue(!!checked);
92
+ }: CheckboxProps) {
93
+ const { currentValue, updateValue } = useControlledValue(!!checked, false);
102
94
 
103
- const iconOn = useMemo(
104
- () => iconChecked ?? icon ?? CoreIcons.checkboxOn,
105
- [icon, iconChecked],
106
- );
107
- const iconOff = useMemo(() => icon ?? CoreIcons.checkboxOff, [icon]);
108
-
109
- useEffect(() => {
110
- setActualValue(deferred);
111
- }, [deferred]);
95
+ const iconOn = useMemo(() => iconChecked ?? icon ?? "icon-[mdi--checkbox-marked]", [icon, iconChecked]);
96
+ const iconOff = useMemo(() => icon ?? "icon-[mdi--checkbox-blank-outline]", [icon]);
112
97
 
113
98
  const handleChange = useCallback(
114
- (e?: ChangeEvent<HTMLInputElement>) => {
115
- setActualValue(e?.target.checked ?? false);
99
+ (e?: React.ChangeEvent<HTMLInputElement>) => {
100
+ updateValue(e?.target.checked ?? false);
116
101
  onChange?.(e?.target.checked ?? false);
117
102
  },
118
- [onChange, value],
103
+ [onChange, updateValue],
119
104
  );
120
105
 
121
106
  return (
122
107
  <label
123
- className={classNames(
108
+ className={cn(
124
109
  invalid ? "ring-danger-500" : "ring-primary-500",
125
- disabled
126
- ? "opacity-50 cursor-not-allowed"
127
- : " opacity-85 hover:opacity-100 cursor-pointer",
128
- "inline-flex gap-1 py-1 items-center relative rounded ring-offset-2 has-[:focus-visible]:ring-1",
110
+ disabled ? "opacity-50 cursor-not-allowed" : " opacity-85 hover:opacity-100 cursor-pointer",
111
+ "inline-flex gap-1 py-1 items-center relative rounded ring-offset-2 has-focus-visible:ring-1",
129
112
  )}
130
113
  style={{ width }}
131
114
  >
132
115
  <input
133
- className={classNames(
134
- "appearance-none absolute opacity-0 inset-0 bg-transparent border-none outline-none ring-0",
135
- )}
116
+ className={cn("appearance-none absolute opacity-0 inset-0 bg-transparent border-none outline-none ring-0")}
136
117
  aria-invalid={invalid}
137
118
  aria-disabled={disabled}
138
119
  disabled={disabled}
@@ -143,23 +124,18 @@ export const Checkbox = ({
143
124
  tabIndex={0}
144
125
  name={name}
145
126
  autoComplete="off"
146
- checked={actualValue}
127
+ checked={!!currentValue}
147
128
  onChange={handleChange}
148
129
  {...rest}
149
130
  />
150
131
  <Icon
151
- className={classNames(
152
- "p-0.5",
153
- actualValue && icon && !iconChecked && "bg-primary-500 text-white",
154
- )}
132
+ className={cn("p-0.5", currentValue && icon && !iconChecked && "bg-primary-500 text-white!")}
155
133
  size="1.25em"
156
134
  color={invalid ? "danger" : "primary"}
157
- icon={actualValue ? iconOn : iconOff}
135
+ icon={currentValue ? iconOn : iconOff}
158
136
  />
159
137
  <ErrorIcon invalid={invalid} error={error} />
160
- {label && (
161
- <span className="flex-initial leading-none truncate">{label}</span>
162
- )}
138
+ {label && <span className="flex-initial leading-tight truncate">{label}</span>}
163
139
  </label>
164
140
  );
165
- };
141
+ }
@@ -21,137 +21,178 @@
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, Dropdown, Icon } from "@react-fabric/core";
25
- import { type RefProp } from "@react-fabric/core/dist/types/types";
26
- import { isString } from "@react-fabric/utilities";
27
- import classNames from "classnames";
28
24
  import {
29
- startTransition,
30
- useCallback,
31
- useDeferredValue,
32
- useEffect,
33
- useState,
34
- } from "react";
35
- import { type InputProps } from "../types";
36
- import { ColorPicker, type ColorProps } from "./ColorPicker";
25
+ arrow,
26
+ autoUpdate,
27
+ flip,
28
+ FloatingArrow,
29
+ FloatingFocusManager,
30
+ offset,
31
+ shift,
32
+ useClick,
33
+ useDismiss,
34
+ useFloating,
35
+ useInteractions,
36
+ } from "@floating-ui/react";
37
+ import { Icon, useControlledValue } from "@react-fabric/core";
38
+ import type { RefProp } from "@react-fabric/core/dist/types/types";
39
+ import { mergeRefs } from "@react-fabric/utilities";
40
+ import { startTransition, useCallback, useRef, useState } from "react";
41
+ import { InputWrapper } from "../internal/InputWrapper";
42
+ import type { InputProps, WrapperProps } from "../types";
43
+ import { getWrapperProps } from "../utils";
44
+ import type { ColorProps } from "./ColorPicker";
45
+ import { ColorPicker } from "./ColorPicker";
37
46
 
38
- export interface ColorInputProps extends InputProps, ColorProps, RefProp {
47
+ export interface ColorInputProps extends InputProps, WrapperProps, Omit<ColorProps, "onChange">, RefProp<HTMLButtonElement> {
39
48
  /**
40
49
  * show eydrop picker
41
50
  */
42
51
  showPicker?: boolean;
43
52
  }
44
53
 
45
- export const ColorInput = ({
46
- ref,
47
- required,
48
- disabled,
49
- allowClear,
50
- invalid,
51
- error,
52
- value,
53
- showPicker,
54
- defaultColor = "",
55
- decorateEnd,
56
- decorateStart,
57
- onChange,
58
- // @ts-expect-error ignore
59
- noOutline,
60
- ...rest
61
- }: ColorInputProps) => {
62
- const [actualValue, setActualValue] = useState("");
63
- const deferred = useDeferredValue(value ?? defaultColor);
54
+ interface EyeDropper {
55
+ open(): Promise<{ sRGBHex: string }>;
56
+ }
57
+
58
+ interface Window {
59
+ EyeDropper?: {
60
+ new (): EyeDropper;
61
+ };
62
+ }
64
63
 
65
- useEffect(() => {
66
- setActualValue(deferred ?? "");
67
- }, [deferred]);
64
+ /**
65
+ * Color input component that allows users to select a color
66
+ * and optionally use an eye dropper to pick a color from the screen.
67
+ * It uses the Floating UI library for positioning the color picker.
68
+ * It also provides a button to clear the selected color.
69
+ *
70
+ * This component is a wrapper around the `ColorPicker` component
71
+ * and provides additional functionality such as showing an eye dropper
72
+ * and handling the color selection state.
73
+ */
74
+ export function ColorInput(props: ColorInputProps) {
75
+ const {
76
+ ref,
77
+ disabled,
78
+ readOnly,
79
+ value,
80
+ invalid,
81
+ allowClear,
82
+ showPicker,
83
+ defaultColor = "",
84
+ onBlur,
85
+ onFocus,
86
+ onChange,
87
+ } = props;
88
+ const { currentValue, updateValue } = useControlledValue(value, defaultColor);
68
89
 
69
90
  const handleChange = useCallback(
70
91
  (e?: string) => {
71
- setActualValue(e ?? defaultColor);
72
- onChange != null && startTransition(() => onChange(e));
92
+ const value = e ?? defaultColor;
93
+ updateValue(value);
94
+ if (onChange) startTransition(() => onChange(value ?? null));
73
95
  },
74
- [onChange],
96
+ [defaultColor, updateValue, onChange],
75
97
  );
76
98
 
99
+ const arrowRef = useRef(null);
100
+ const [isOpen, setIsOpen] = useState(false);
101
+ const { refs, floatingStyles, context } = useFloating({
102
+ open: isOpen,
103
+ onOpenChange: (open, _, reason) => {
104
+ if (reason === "reference-press") return;
105
+ setIsOpen(open);
106
+ },
107
+ strategy: "fixed",
108
+ placement: "bottom",
109
+ whileElementsMounted: autoUpdate,
110
+ middleware: [
111
+ shift({ padding: 8 }),
112
+ flip(),
113
+ offset(9),
114
+ arrow({
115
+ element: arrowRef,
116
+ }),
117
+ ],
118
+ });
119
+ const click = useClick(context, {
120
+ enabled: !disabled && !readOnly,
121
+ });
122
+ const dismiss = useDismiss(context, {
123
+ referencePress: true,
124
+ });
125
+
126
+ const { getReferenceProps, getFloatingProps } = useInteractions([dismiss, click]);
127
+
77
128
  return (
78
- <div
79
- className={classNames(
80
- "flex-content min-h-[1.5em] min-w-[3.5em] self-stretch flex items-center outline-tint-200 rounded",
81
- !disabled && "bg-base",
82
- !noOutline && "bg-[var(--fabric-input)] outline overflow-hidden",
83
- )}
129
+ <InputWrapper
130
+ showClear={allowClear && currentValue !== defaultColor && !disabled && !readOnly}
131
+ onClear={handleChange}
132
+ {...getWrapperProps(props)}
84
133
  >
85
- {decorateStart && (
134
+ <button
135
+ data-ref="button"
136
+ type="button"
137
+ className="flex-1 flex items-center"
138
+ ref={mergeRefs(ref, (el: HTMLButtonElement) => refs.setReference(el))}
139
+ {...getReferenceProps({
140
+ onBlur,
141
+ onFocus,
142
+ })}
143
+ data-inner-clickable="true"
144
+ data-dropdown-open={isOpen ? true : undefined}
145
+ >
86
146
  <div
87
- className={classNames(
88
- "text-muted leading-none flex-content",
89
- isString(decorateStart) && "px-2",
90
- )}
147
+ className="h-[1.5em] min-w-[1.5em] flex-1 flex items-center justify-center self-stretch cursor-pointer m-[0.25em] rounded outline outline-tint-100"
148
+ aria-invalid={invalid}
149
+ style={{
150
+ backgroundColor: currentValue ?? "",
151
+ opacity: disabled ? 0.65 : 1,
152
+ pointerEvents: disabled ? "none" : undefined,
153
+ }}
91
154
  >
92
- {decorateStart}
155
+ {!currentValue && <Icon size="md" className="text-muted" icon="icon-[mdi--palette]" />}
93
156
  </div>
94
- )}
95
- <Dropdown showArrow disabled={disabled}>
96
- <button data-ref="button" className="flex-1 flex items-center">
157
+ </button>
158
+ {isOpen && (
159
+ <FloatingFocusManager context={context} closeOnFocusOut>
97
160
  <div
98
- role="none"
99
- className="h-[1.5em] min-w-[1.5em] flex-1 flex items-center justify-center self-stretch cursor-pointer m-[0.25em] rounded outline outline-tint-100"
161
+ ref={(el) => refs.setFloating(el)}
100
162
  style={{
101
- backgroundColor: actualValue,
102
- opacity: disabled ? 0.65 : 1,
103
- pointerEvents: disabled ? "none" : undefined,
163
+ zIndex: "var(--z-popover)",
164
+ ...floatingStyles,
104
165
  }}
105
- onClick={(e) =>
106
- (e.currentTarget.nextElementSibling as HTMLElement)?.focus()
107
- }
166
+ data-ref="dropdownBody"
167
+ {...getFloatingProps()}
108
168
  >
109
- {!actualValue && (
110
- <Icon
111
- size="md"
112
- className="text-muted"
113
- icon={CoreIcons.colorSwatch}
114
- />
115
- )}
169
+ <ColorPicker {...props} onChange={handleChange} defaultColor={defaultColor} value={currentValue} />
170
+ <FloatingArrow
171
+ ref={arrowRef}
172
+ context={context}
173
+ strokeWidth={0.5}
174
+ className="stroke-muted"
175
+ style={{
176
+ fill: currentValue || "var(--bg-color-default)",
177
+ }}
178
+ />
116
179
  </div>
117
- </button>
118
- <ColorPicker
119
- {...rest}
120
- onChange={handleChange}
121
- defaultColor={defaultColor}
122
- value={actualValue}
123
- />
124
- </Dropdown>
125
- {!disabled && showPicker && (
126
- <Icon
127
- icon={CoreIcons.colorPicker}
128
- onClick={() =>
129
- (window as AnyObject).EyeDropper &&
130
- new (window as AnyObject).EyeDropper()
131
- .open()
132
- .then((resp: AnyObject) => handleChange(resp.sRGBHex))
133
- }
134
- />
180
+ </FloatingFocusManager>
135
181
  )}
136
- {decorateEnd && (
137
- <div
138
- className={classNames(
139
- "text-muted leading-none flex-content",
140
- isString(decorateEnd) && "px-2",
141
- )}
182
+ {!disabled && showPicker && (
183
+ <button
184
+ type="button"
185
+ className="size-fit leading-0 cursor-pointer"
186
+ onClick={() => {
187
+ const ED = (window as Window).EyeDropper;
188
+ if (ED) {
189
+ void new ED().open().then((resp) => handleChange(resp.sRGBHex));
190
+ }
191
+ }}
142
192
  >
143
- {decorateEnd}
144
- </div>
145
- )}
146
- {!disabled && allowClear && actualValue !== defaultColor && (
147
- <Icon
148
- icon={CoreIcons.close}
149
- onClick={() => handleChange()}
150
- className={classNames(
151
- "text-tint-500 hover:text-tint-700 p-[0.25em] flex-content",
152
- )}
153
- />
193
+ <Icon className="flex-content p-1" icon="icon-[mdi--eyedropper]" />
194
+ </button>
154
195
  )}
155
- </div>
196
+ </InputWrapper>
156
197
  );
157
- };
198
+ }