@react-fabric/form 1.0.0-beta.5 → 1.0.0-beta.51

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 (174) hide show
  1. package/{src/input/ArrayInput.module.css → css/_arrayInput.css} +8 -8
  2. package/css/_colorPicker.css +65 -0
  3. package/{src/internal/Field.module.css → css/_field.css} +64 -29
  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 +37 -25
  7. package/dist/cjs/builder/FormField.js +50 -34
  8. package/dist/cjs/builder/SchemaEditor.js +72 -0
  9. package/dist/cjs/builder/components/AddSchemaFields.js +62 -0
  10. package/dist/cjs/builder/components/SchemaArrayForm.js +143 -0
  11. package/dist/cjs/builder/components/SchemaField.js +414 -0
  12. package/dist/cjs/form/Controller.js +1 -1
  13. package/dist/cjs/form/Form.js +11 -6
  14. package/dist/cjs/hooks/useFormBuilder.js +27 -15
  15. package/dist/cjs/index.js +11 -2
  16. package/dist/cjs/input/ArrayInput.js +61 -33
  17. package/dist/cjs/input/AvatarInput.js +9 -8
  18. package/dist/cjs/input/Checkbox.js +6 -5
  19. package/dist/cjs/input/ColorInput.js +95 -56
  20. package/dist/cjs/input/ColorPicker.js +1 -35
  21. package/dist/cjs/input/DateInput.js +186 -0
  22. package/dist/cjs/input/Field.js +5 -4
  23. package/dist/cjs/input/File.js +4 -7
  24. package/dist/cjs/input/Hidden.js +3 -1
  25. package/dist/cjs/input/Input.js +11 -7
  26. package/dist/cjs/input/Number.js +9 -7
  27. package/dist/cjs/input/Password.js +10 -8
  28. package/dist/cjs/input/Radio.js +2 -2
  29. package/dist/cjs/input/RadioGroup.js +6 -3
  30. package/dist/cjs/input/{Range.js → RangeSlider.js} +5 -5
  31. package/dist/cjs/input/Search.js +16 -16
  32. package/dist/cjs/input/Slider.js +3 -3
  33. package/dist/cjs/input/Switch.js +30 -7
  34. package/dist/cjs/input/Textarea.js +13 -4
  35. package/dist/cjs/internal/ErrorIcon.js +1 -1
  36. package/dist/cjs/internal/FieldWrapper.js +11 -4
  37. package/dist/cjs/internal/InputWrapper.js +19 -16
  38. package/dist/cjs/select/AutoComplete.js +12 -8
  39. package/dist/cjs/select/DualList.js +4 -1
  40. package/dist/cjs/select/List.js +45 -20
  41. package/dist/cjs/select/Options.js +2 -2
  42. package/dist/cjs/select/Select.js +14 -4
  43. package/dist/cjs/select/useSelect.js +10 -6
  44. package/dist/cjs/types/schema.js +43 -0
  45. package/dist/cjs/utils/eventHandlers.js +11 -5
  46. package/dist/cjs/utils/getColor.js +6 -0
  47. package/dist/esm/builder/FileField.js +39 -27
  48. package/dist/esm/builder/FormField.js +50 -34
  49. package/dist/esm/builder/SchemaEditor.js +72 -0
  50. package/dist/esm/builder/components/AddSchemaFields.js +62 -0
  51. package/dist/esm/builder/components/SchemaArrayForm.js +143 -0
  52. package/dist/esm/builder/components/SchemaField.js +414 -0
  53. package/dist/esm/form/Controller.js +1 -1
  54. package/dist/esm/form/Form.js +11 -6
  55. package/dist/esm/hooks/useFormBuilder.js +25 -13
  56. package/dist/esm/index.js +5 -1
  57. package/dist/esm/input/ArrayInput.js +61 -33
  58. package/dist/esm/input/AvatarInput.js +9 -8
  59. package/dist/esm/input/Checkbox.js +6 -5
  60. package/dist/esm/input/ColorInput.js +97 -58
  61. package/dist/esm/input/ColorPicker.js +1 -35
  62. package/dist/esm/input/DateInput.js +186 -0
  63. package/dist/esm/input/Field.js +6 -5
  64. package/dist/esm/input/File.js +6 -9
  65. package/dist/esm/input/Hidden.js +3 -1
  66. package/dist/esm/input/Input.js +13 -9
  67. package/dist/esm/input/Number.js +11 -9
  68. package/dist/esm/input/Password.js +12 -10
  69. package/dist/esm/input/Radio.js +2 -2
  70. package/dist/esm/input/RadioGroup.js +7 -4
  71. package/dist/esm/input/{Range.js → RangeSlider.js} +5 -5
  72. package/dist/esm/input/Search.js +18 -18
  73. package/dist/esm/input/Slider.js +3 -3
  74. package/dist/esm/input/Switch.js +32 -9
  75. package/dist/esm/input/Textarea.js +14 -5
  76. package/dist/esm/internal/ErrorIcon.js +1 -1
  77. package/dist/esm/internal/FieldWrapper.js +11 -4
  78. package/dist/esm/internal/InputWrapper.js +19 -16
  79. package/dist/esm/select/AutoComplete.js +12 -8
  80. package/dist/esm/select/DualList.js +4 -1
  81. package/dist/esm/select/List.js +46 -21
  82. package/dist/esm/select/Options.js +2 -2
  83. package/dist/esm/select/Select.js +14 -4
  84. package/dist/esm/select/useSelect.js +11 -7
  85. package/{src/input/ColorPicker.module.css → dist/esm/types/schema.js} +18 -27
  86. package/dist/esm/utils/eventHandlers.js +11 -5
  87. package/dist/esm/utils/getColor.js +6 -1
  88. package/dist/types/builder/FileField.d.ts +4 -2
  89. package/dist/types/builder/FormField.d.ts +3 -1
  90. package/dist/types/builder/SchemaEditor.d.ts +2 -0
  91. package/dist/types/builder/components/AddSchemaFields.d.ts +2 -0
  92. package/dist/types/builder/components/SchemaArrayForm.d.ts +2 -0
  93. package/dist/types/builder/components/SchemaField.d.ts +10 -0
  94. package/dist/types/form/Controller.d.ts +2 -2
  95. package/dist/types/form/Form.d.ts +5 -1
  96. package/dist/types/hooks/useFormBuilder.d.ts +3 -1
  97. package/dist/types/index.d.ts +5 -2
  98. package/dist/types/input/ArrayInput.d.ts +39 -6
  99. package/dist/types/input/AvatarInput.d.ts +6 -3
  100. package/dist/types/input/Checkbox.d.ts +4 -4
  101. package/dist/types/input/ColorInput.d.ts +2 -2
  102. package/dist/types/input/ColorPicker.d.ts +1 -1
  103. package/dist/types/input/DateInput.d.ts +19 -0
  104. package/dist/types/input/Field.d.ts +0 -1
  105. package/dist/types/input/Input.d.ts +1 -1
  106. package/dist/types/input/Number.d.ts +1 -1
  107. package/dist/types/input/Password.d.ts +1 -1
  108. package/dist/types/input/Radio.d.ts +0 -1
  109. package/dist/types/input/RadioGroup.d.ts +18 -3
  110. package/dist/types/input/{Range.d.ts → RangeSlider.d.ts} +1 -1
  111. package/dist/types/input/Search.d.ts +5 -1
  112. package/dist/types/input/Switch.d.ts +7 -4
  113. package/dist/types/input/Textarea.d.ts +5 -1
  114. package/dist/types/internal/FieldWrapper.d.ts +1 -1
  115. package/dist/types/internal/InputWrapper.d.ts +1 -1
  116. package/dist/types/ranger/index.d.ts +2 -2
  117. package/dist/types/select/AutoComplete.d.ts +1 -1
  118. package/dist/types/select/DualList.d.ts +6 -2
  119. package/dist/types/select/List.d.ts +1 -1
  120. package/dist/types/select/Select.d.ts +1 -1
  121. package/dist/types/select/useSelect.d.ts +7 -15
  122. package/dist/types/types/index.d.ts +28 -7
  123. package/dist/types/types/schema.d.ts +78 -33
  124. package/dist/types/utils/eventHandlers.d.ts +1 -1
  125. package/package.json +15 -12
  126. package/src/builder/FileField.tsx +53 -27
  127. package/src/builder/FormField.tsx +52 -32
  128. package/src/builder/SchemaEditor.tsx +72 -0
  129. package/src/builder/components/AddSchemaFields.tsx +74 -0
  130. package/src/builder/components/SchemaArrayForm.tsx +158 -0
  131. package/src/builder/components/SchemaField.tsx +436 -0
  132. package/src/form/Controller.tsx +3 -3
  133. package/src/form/Form.tsx +14 -5
  134. package/src/hooks/useFormBuilder.tsx +29 -15
  135. package/src/index.ts +11 -2
  136. package/src/input/ArrayInput.tsx +130 -54
  137. package/src/input/AvatarInput.tsx +12 -8
  138. package/src/input/Checkbox.tsx +6 -5
  139. package/src/input/ColorInput.tsx +113 -69
  140. package/src/input/ColorPicker.tsx +2 -36
  141. package/src/input/DateInput.tsx +250 -0
  142. package/src/input/Field.tsx +10 -11
  143. package/src/input/File.tsx +7 -14
  144. package/src/input/Hidden.tsx +4 -1
  145. package/src/input/Input.tsx +15 -10
  146. package/src/input/Number.tsx +10 -10
  147. package/src/input/Password.tsx +13 -12
  148. package/src/input/Radio.tsx +2 -2
  149. package/src/input/RadioGroup.tsx +27 -4
  150. package/src/input/{Range.tsx → RangeSlider.tsx} +4 -4
  151. package/src/input/Search.tsx +20 -19
  152. package/src/input/Slider.tsx +3 -3
  153. package/src/input/Switch.tsx +43 -7
  154. package/src/input/Textarea.tsx +19 -5
  155. package/src/internal/ErrorIcon.tsx +1 -1
  156. package/src/internal/FieldWrapper.tsx +8 -4
  157. package/src/internal/InputWrapper.tsx +36 -20
  158. package/src/select/AutoComplete.tsx +5 -1
  159. package/src/select/DualList.tsx +8 -0
  160. package/src/select/List.tsx +41 -8
  161. package/src/select/Options.tsx +1 -1
  162. package/src/select/Select.tsx +13 -3
  163. package/src/select/useSelect.tsx +13 -5
  164. package/src/types/index.ts +34 -8
  165. package/src/types/schema.ts +96 -41
  166. package/src/utils/eventHandlers.ts +9 -3
  167. package/dist/cjs/input/ArrayInput.module.css +0 -1
  168. package/dist/cjs/input/ColorPicker.module.css +0 -1
  169. package/dist/cjs/input/File.module.css +0 -1
  170. package/dist/cjs/internal/Field.module.css +0 -1
  171. package/dist/esm/input/ArrayInput.module.css +0 -1
  172. package/dist/esm/input/ColorPicker.module.css +0 -1
  173. package/dist/esm/input/File.module.css +0 -1
  174. package/dist/esm/internal/Field.module.css +0 -1
@@ -23,27 +23,32 @@
23
23
 
24
24
  import { DndContext, type DragEndEvent } from "@dnd-kit/core";
25
25
  import { SortableContext, useSortable } from "@dnd-kit/sortable";
26
- import { Button, CoreIcons, Icon, Tooltip } from "@react-fabric/core";
27
- import { isString } from "@react-fabric/utilities";
26
+ import {
27
+ Button,
28
+ cloneChildren,
29
+ CoreIcons,
30
+ Icon,
31
+ Tooltip,
32
+ } from "@react-fabric/core";
33
+ import { isFalse, isString } from "@react-fabric/utilities";
28
34
  import classNames from "classnames";
29
35
  import {
30
- Children,
31
36
  cloneElement,
32
37
  useCallback,
33
38
  useImperativeHandle,
34
39
  useMemo,
35
40
  type PropsWithChildren,
36
41
  type ReactNode,
37
- type RefObject,
42
+ type Ref,
38
43
  } from "react";
39
44
  import { useFieldArray, useFormContext } from "react-hook-form";
40
45
  import { useTranslation } from "react-i18next";
41
46
  import { FieldWrapper } from "../internal/FieldWrapper";
42
- import classes from "./ArrayInput.module.css";
43
47
 
44
48
  import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
45
49
  import { CSS } from "@dnd-kit/utilities";
46
- export interface ArrayInputProps {
50
+
51
+ export interface ArrayInputProps<T extends AnyObject = string> {
47
52
  /**
48
53
  * input label
49
54
  */
@@ -77,6 +82,10 @@ export interface ArrayInputProps {
77
82
  * minimum required length
78
83
  */
79
84
  minItems?: number;
85
+ /**
86
+ * maximum length
87
+ */
88
+ maxItems?: number;
80
89
  /**
81
90
  * field name to focus on add
82
91
  */
@@ -92,34 +101,65 @@ export interface ArrayInputProps {
92
101
  * add item button label
93
102
  */
94
103
  addLabel?: string;
104
+ buttonPosition?: "top" | "bottom" | "both";
95
105
  /**
96
106
  * fixed length list
97
107
  */
98
108
  fixedList?: boolean;
99
- arrayRef?: RefObject<{
100
- addItem: (item: AnyObject) => void;
109
+ arrayRef?: Ref<{
110
+ addItem: (item: T) => void;
101
111
  removeItem: (idx: number) => void;
102
112
  }>;
103
113
  /**
104
- * Add item
114
+ * add new item
105
115
  */
106
116
  onAdd?: () => AnyObject;
117
+ /**
118
+ * on item remove
119
+ */
120
+ onRemove?: (item: T, idx: number) => void;
121
+ /**
122
+ * before item remove
123
+ */
124
+ onBeforeRemove?: (item: T, idx: number) => Promise<boolean> | boolean;
125
+ /**
126
+ * can remove item
127
+ */
128
+ canRemove?: (props: { item: T; index: number; lastItem: boolean }) => boolean;
129
+
130
+ /**
131
+ * field width
132
+ */
133
+ width?: number | string;
134
+ /**
135
+ * inline label and input
136
+ */
137
+ inline?: boolean;
138
+ /**
139
+ * label width for inline
140
+ */
141
+ labelWidth?: string;
107
142
  }
108
143
 
109
- export const ArrayInput = ({
144
+ export const ArrayInput = <T extends AnyObject = string>({
110
145
  name,
111
146
  children,
112
147
  addLabel,
113
148
  fixedList,
114
149
  focusName = "",
150
+ buttonPosition = "bottom",
115
151
  onAdd,
152
+ onRemove,
153
+ onBeforeRemove,
154
+ canRemove,
116
155
  arrayRef,
117
156
  enableSorting,
118
157
  disabled = false,
119
158
  readOnly = false,
120
159
  minItems = 0,
160
+ maxItems = Number.MAX_SAFE_INTEGER,
121
161
  ...rest
122
- }: ArrayInputProps) => {
162
+ }: ArrayInputProps<T>) => {
123
163
  const { t } = useTranslation("form");
124
164
  const form = useFormContext();
125
165
 
@@ -128,6 +168,7 @@ export const ArrayInput = ({
128
168
 
129
169
  const { fields, append, remove, move } = useFieldArray({
130
170
  name,
171
+ keyName: "__ID__",
131
172
  control: form.control,
132
173
  });
133
174
 
@@ -139,7 +180,7 @@ export const ArrayInput = ({
139
180
  );
140
181
 
141
182
  const handleAdd = useCallback(
142
- (item: AnyObject) => {
183
+ (item: T) => {
143
184
  append(item);
144
185
  setTimeout(() => {
145
186
  form.setFocus(
@@ -150,6 +191,19 @@ export const ArrayInput = ({
150
191
  [name, fields, focusName],
151
192
  );
152
193
 
194
+ const handleRemove = useCallback(
195
+ (item: T, index: number) => {
196
+ const ret = onBeforeRemove?.(item, index);
197
+ void Promise.resolve(ret).then((b) => {
198
+ if (!isFalse(b)) {
199
+ remove(index);
200
+ onRemove?.(item, index);
201
+ }
202
+ });
203
+ },
204
+ [remove, onRemove, onBeforeRemove],
205
+ );
206
+
153
207
  useImperativeHandle(arrayRef, () => ({
154
208
  addItem: handleAdd,
155
209
  removeItem: remove,
@@ -159,7 +213,7 @@ export const ArrayInput = ({
159
213
  ({ children }: PropsWithChildren) => {
160
214
  if (enableSorting) {
161
215
  // pass id list to dnd context
162
- const idMap = fields.map((item) => item.id);
216
+ const idMap = fields.map((item) => item.__ID__);
163
217
  const handleDragEnd = (e: DragEndEvent) => {
164
218
  if (e.over && e.active.id !== e.over.id) {
165
219
  // find index of item and drop over
@@ -227,14 +281,60 @@ export const ArrayInput = ({
227
281
  // @ts-expect-error ignore
228
282
  SortableItem.displayName = "SortableItem";
229
283
 
284
+ const addButton = useMemo(
285
+ () => (
286
+ <div className="flex justify-end">
287
+ <Tooltip
288
+ color="danger-200"
289
+ content={
290
+ isString(error)
291
+ ? `${error}`
292
+ : (t(
293
+ error?.key,
294
+ error?.values
295
+ ? {
296
+ ...error?.values,
297
+ label: t(
298
+ error.values?.label ?? "form:badkey",
299
+ error.values?.path,
300
+ ),
301
+ }
302
+ : {},
303
+ ) as string)
304
+ }
305
+ >
306
+ <Button
307
+ size="sm"
308
+ aria-label="Add item"
309
+ icon={CoreIcons.insert}
310
+ onClick={() => handleAdd(onAdd?.())}
311
+ data-invalid={!!error}
312
+ disabled={!!disabled || readOnly || fields.length > maxItems}
313
+ className={classNames("fabric-addButton", "me-10")}
314
+ >
315
+ {addLabel ?? t("addArray")}
316
+ </Button>
317
+ </Tooltip>
318
+ </div>
319
+ ),
320
+ [error, fields, handleAdd, disabled, readOnly, addLabel, maxItems],
321
+ );
322
+
230
323
  return (
231
- <div className={classes.arrayInput}>
232
- <FieldWrapper {...rest}>
324
+ <div className={"fabric-arrayInput"}>
325
+ <FieldWrapper
326
+ {...rest}
327
+ appendLabel={
328
+ <div className="-me-2">
329
+ {!fixedList && onAdd && buttonPosition !== "bottom" && addButton}
330
+ </div>
331
+ }
332
+ >
233
333
  <Wrapper>
234
334
  {fields.map((item, index) => (
235
335
  <SortableItem
236
- key={item.id}
237
- id={item.id}
336
+ key={item.__ID__}
337
+ id={item.__ID__}
238
338
  className="flex items-center flex-nowrap gap-2 mb-1"
239
339
  >
240
340
  {typeof children === "function"
@@ -242,7 +342,7 @@ export const ArrayInput = ({
242
342
  index,
243
343
  name: `${name}.${index}`,
244
344
  })
245
- : Children.map(children, (child: AnyObject) =>
345
+ : cloneChildren(children, (child: AnyObject) =>
246
346
  cloneElement(
247
347
  child,
248
348
  Object.assign(
@@ -271,48 +371,24 @@ export const ArrayInput = ({
271
371
  color="danger"
272
372
  variant="link"
273
373
  className="self-end"
274
- disabled={disabled || readOnly || fields.length <= minItems}
275
- onClick={() => remove(index)}
374
+ disabled={
375
+ disabled ||
376
+ readOnly ||
377
+ fields.length <= minItems ||
378
+ canRemove?.({
379
+ item: item as T,
380
+ index,
381
+ lastItem: index + 1 === fields.length,
382
+ }) !== false
383
+ }
384
+ onClick={() => handleRemove(item as T, index)}
276
385
  />
277
386
  )}
278
387
  </SortableItem>
279
388
  ))}
280
389
  </Wrapper>
281
390
  </FieldWrapper>
282
- {!fixedList && onAdd && (
283
- <div className="flex justify-end">
284
- <Tooltip
285
- color="danger"
286
- content={
287
- isString(error)
288
- ? `${error}`
289
- : (t(
290
- error?.key,
291
- error?.values
292
- ? {
293
- ...error?.values,
294
- label: t(
295
- error.values?.label ?? "form:badkey",
296
- error.values?.path,
297
- ),
298
- }
299
- : {},
300
- ) as string)
301
- }
302
- >
303
- <Button
304
- aria-label="Add item"
305
- icon={CoreIcons.insert}
306
- onClick={() => handleAdd(onAdd())}
307
- data-invalid={!!error}
308
- disabled={!!disabled || readOnly}
309
- className={classNames(classes.addButton, "me-10")}
310
- >
311
- {addLabel ?? t("addArray")}
312
- </Button>
313
- </Tooltip>
314
- </div>
315
- )}
391
+ {!fixedList && onAdd && buttonPosition !== "top" && addButton}
316
392
  </div>
317
393
  );
318
394
  };
@@ -37,14 +37,17 @@ export interface Props extends RefProp {
37
37
  */
38
38
  value?: string;
39
39
  /**
40
- * avatar stream src
40
+ * avatar name
41
41
  */
42
- src?: string;
42
+ avatarName?: string;
43
43
  size?: string | number;
44
44
  /**
45
45
  * upload handler to store file in temp storage
46
46
  */
47
47
  uploadHandler: UploadHandler;
48
+ /**
49
+ * file stream path
50
+ */
48
51
  fileUrl?: (path: string) => string;
49
52
  /**
50
53
  * change handler
@@ -57,15 +60,16 @@ export interface Props extends RefProp {
57
60
  }
58
61
 
59
62
  export const AvatarInput = ({
60
- src,
61
63
  size = "6rem",
64
+ avatarName,
65
+ value,
62
66
  fileUrl,
63
67
  uploadHandler,
64
68
  ...rest
65
69
  }: Props) => {
66
70
  const { files, list, upload, remove } = useFileUploader(
67
71
  async (data, config) => await uploadHandler?.(data, config),
68
- rest.value,
72
+ value,
69
73
  false,
70
74
  );
71
75
 
@@ -92,14 +96,14 @@ export const AvatarInput = ({
92
96
  }, [files, list]);
93
97
 
94
98
  useEffect(() => {
95
- src && setBase64(fileUrl?.(src) ?? src);
96
- }, [src]);
99
+ value && setBase64(fileUrl?.(value) ?? value);
100
+ }, [value]);
97
101
 
98
102
  return (
99
103
  <div className="flex flex-nowrap items-end">
100
104
  <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">
105
+ <Avatar name={avatarName ?? "temp"} size={size} avatar={base64} />
106
+ <div className="absolute inset-x-0 bottom-0 cursor-pointer bg-black/20 hover:bg-black/50 text-white text-xs text-center py-1">
103
107
  <span>Edit</span>
104
108
  <input
105
109
  type="file"
@@ -34,7 +34,7 @@ import {
34
34
  } from "react";
35
35
  import { ErrorIcon } from "../internal/ErrorIcon";
36
36
 
37
- export interface CheckboxProps extends RefProp<HTMLInputElement> {
37
+ interface CheckboxProps extends RefProp<HTMLInputElement> {
38
38
  /**
39
39
  * checkbox label
40
40
  */
@@ -50,8 +50,7 @@ export interface CheckboxProps extends RefProp<HTMLInputElement> {
50
50
  /**
51
51
  * on checked change
52
52
  */
53
- onChange?: (checked: boolean | string | number) => void;
54
-
53
+ onChange?: (checked: boolean) => void;
55
54
  /**
56
55
  * disabled input
57
56
  */
@@ -94,6 +93,8 @@ export const Checkbox = ({
94
93
  width,
95
94
  icon,
96
95
  iconChecked,
96
+ // @ts-expect-error ignore
97
+ required,
97
98
  onChange,
98
99
  ...rest
99
100
  }: CheckboxProps) => {
@@ -150,7 +151,7 @@ export const Checkbox = ({
150
151
  <Icon
151
152
  className={classNames(
152
153
  "p-0.5",
153
- actualValue && icon && !iconChecked && "bg-primary-500 text-white",
154
+ actualValue && icon && !iconChecked && "bg-primary-500 text-white!",
154
155
  )}
155
156
  size="1.25em"
156
157
  color={invalid ? "danger" : "primary"}
@@ -158,7 +159,7 @@ export const Checkbox = ({
158
159
  />
159
160
  <ErrorIcon invalid={invalid} error={error} />
160
161
  {label && (
161
- <span className="flex-initial leading-none truncate">{label}</span>
162
+ <span className="flex-initial leading-tight truncate">{label}</span>
162
163
  )}
163
164
  </label>
164
165
  );
@@ -21,21 +21,36 @@
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";
24
+ import {
25
+ arrow,
26
+ autoUpdate,
27
+ flip,
28
+ FloatingArrow,
29
+ offset,
30
+ shift,
31
+ useClick,
32
+ useDismiss,
33
+ useFloating,
34
+ useInteractions,
35
+ } from "@floating-ui/react";
36
+ import { CoreIcons, Icon } from "@react-fabric/core";
25
37
  import { type RefProp } from "@react-fabric/core/dist/types/types";
26
- import { isString } from "@react-fabric/utilities";
27
- import classNames from "classnames";
28
38
  import {
29
39
  startTransition,
30
40
  useCallback,
31
41
  useDeferredValue,
32
42
  useEffect,
43
+ useRef,
33
44
  useState,
34
45
  } from "react";
46
+ import { InputWrapper } from "../internal/InputWrapper";
35
47
  import { type InputProps } from "../types";
36
48
  import { ColorPicker, type ColorProps } from "./ColorPicker";
37
49
 
38
- export interface ColorInputProps extends InputProps, ColorProps, RefProp {
50
+ export interface ColorInputProps
51
+ extends InputProps,
52
+ Omit<ColorProps, "onChange">,
53
+ RefProp {
39
54
  /**
40
55
  * show eydrop picker
41
56
  */
@@ -46,14 +61,14 @@ export const ColorInput = ({
46
61
  ref,
47
62
  required,
48
63
  disabled,
49
- allowClear,
64
+ readOnly,
50
65
  invalid,
51
66
  error,
52
67
  value,
53
68
  showPicker,
54
69
  defaultColor = "",
55
- decorateEnd,
56
- decorateStart,
70
+ onBlur,
71
+ onFocus,
57
72
  onChange,
58
73
  // @ts-expect-error ignore
59
74
  noOutline,
@@ -69,59 +84,107 @@ export const ColorInput = ({
69
84
  const handleChange = useCallback(
70
85
  (e?: string) => {
71
86
  setActualValue(e ?? defaultColor);
72
- onChange != null && startTransition(() => onChange(e));
87
+ onChange != null && startTransition(() => onChange(e ?? null));
73
88
  },
74
89
  [onChange],
75
90
  );
76
91
 
92
+ const arrowRef = useRef(null);
93
+ const [isOpen, setIsOpen] = useState(false);
94
+ const { refs, floatingStyles, context } = useFloating({
95
+ open: isOpen,
96
+ onOpenChange: (open, _, reason) => {
97
+ if (reason === "reference-press") return;
98
+ setIsOpen(open);
99
+ },
100
+ strategy: "fixed",
101
+ placement: "bottom",
102
+ whileElementsMounted: autoUpdate,
103
+ middleware: [
104
+ shift({ padding: 8 }),
105
+ flip(),
106
+ offset(9),
107
+ arrow({
108
+ element: arrowRef,
109
+ }),
110
+ ],
111
+ });
112
+ const click = useClick(context, {
113
+ enabled: !disabled && !readOnly,
114
+ });
115
+ const dismiss = useDismiss(context, {
116
+ referencePress: true,
117
+ });
118
+
119
+ const { getReferenceProps, getFloatingProps } = useInteractions([
120
+ dismiss,
121
+ click,
122
+ ]);
123
+
77
124
  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
- )}
125
+ <InputWrapper
126
+ showClear={actualValue !== defaultColor && !disabled && !readOnly}
127
+ onClear={handleChange}
128
+ invalid={invalid}
129
+ readOnly={readOnly}
130
+ disabled={disabled}
131
+ required={required}
132
+ error={error}
133
+ {...rest}
84
134
  >
85
- {decorateStart && (
135
+ <button
136
+ data-ref="button"
137
+ className="flex-1 flex items-center"
138
+ ref={refs.setReference}
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",
147
+ role="none"
148
+ 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"
149
+ style={{
150
+ backgroundColor: actualValue,
151
+ opacity: disabled ? 0.65 : 1,
152
+ pointerEvents: disabled ? "none" : undefined,
153
+ }}
154
+ >
155
+ {!actualValue && (
156
+ <Icon
157
+ size="md"
158
+ className="text-muted"
159
+ icon={CoreIcons.colorSwatch}
160
+ />
90
161
  )}
162
+ </div>
163
+ </button>
164
+ {isOpen && (
165
+ <div
166
+ ref={refs.setFloating}
167
+ style={{
168
+ zIndex: "var(--z-popover)",
169
+ ...floatingStyles,
170
+ }}
171
+ data-ref="dropdownBody"
172
+ {...getFloatingProps()}
91
173
  >
92
- {decorateStart}
174
+ <ColorPicker
175
+ {...rest}
176
+ onChange={handleChange}
177
+ defaultColor={defaultColor}
178
+ value={actualValue}
179
+ />
180
+ <FloatingArrow
181
+ ref={arrowRef}
182
+ context={context}
183
+ strokeWidth={0.5}
184
+ className="fill-base stroke-muted"
185
+ />
93
186
  </div>
94
187
  )}
95
- <Dropdown showArrow disabled={disabled}>
96
- <button data-ref="button" className="flex-1 flex items-center">
97
- <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"
100
- style={{
101
- backgroundColor: actualValue,
102
- opacity: disabled ? 0.65 : 1,
103
- pointerEvents: disabled ? "none" : undefined,
104
- }}
105
- onClick={(e) =>
106
- (e.currentTarget.nextElementSibling as HTMLElement)?.focus()
107
- }
108
- >
109
- {!actualValue && (
110
- <Icon
111
- size="md"
112
- className="text-muted"
113
- icon={CoreIcons.colorSwatch}
114
- />
115
- )}
116
- </div>
117
- </button>
118
- <ColorPicker
119
- {...rest}
120
- onChange={handleChange}
121
- defaultColor={defaultColor}
122
- value={actualValue}
123
- />
124
- </Dropdown>
125
188
  {!disabled && showPicker && (
126
189
  <Icon
127
190
  icon={CoreIcons.colorPicker}
@@ -133,25 +196,6 @@ export const ColorInput = ({
133
196
  }
134
197
  />
135
198
  )}
136
- {decorateEnd && (
137
- <div
138
- className={classNames(
139
- "text-muted leading-none flex-content",
140
- isString(decorateEnd) && "px-2",
141
- )}
142
- >
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
- />
154
- )}
155
- </div>
199
+ </InputWrapper>
156
200
  );
157
201
  };