@react-fabric/form 1.0.0-beta.4 → 1.0.0-beta.41

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 (155) 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} +26 -15
  4. package/{src/input/File.module.css → css/_file.css} +20 -6
  5. package/css/styles.css +27 -0
  6. package/dist/cjs/builder/FileField.js +37 -25
  7. package/dist/cjs/builder/FormField.js +44 -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 +413 -0
  12. package/dist/cjs/form/Controller.js +1 -1
  13. package/dist/cjs/form/Form.js +19 -10
  14. package/dist/cjs/hooks/useFormBuilder.js +27 -15
  15. package/dist/cjs/index.js +9 -2
  16. package/dist/cjs/input/ArrayInput.js +36 -25
  17. package/dist/cjs/input/AvatarInput.js +1 -1
  18. package/dist/cjs/input/Checkbox.js +5 -4
  19. package/dist/cjs/input/ColorInput.js +70 -28
  20. package/dist/cjs/input/ColorPicker.js +1 -35
  21. package/dist/cjs/input/DateInput.js +183 -0
  22. package/dist/cjs/input/Field.js +4 -4
  23. package/dist/cjs/input/File.js +2 -3
  24. package/dist/cjs/input/Hidden.js +3 -1
  25. package/dist/cjs/input/Input.js +1 -1
  26. package/dist/cjs/input/Number.js +1 -1
  27. package/dist/cjs/input/Password.js +1 -1
  28. package/dist/cjs/input/Radio.js +1 -1
  29. package/dist/cjs/input/RadioGroup.js +6 -3
  30. package/dist/cjs/input/{Range.js → RangeSlider.js} +3 -3
  31. package/dist/cjs/input/Search.js +1 -1
  32. package/dist/cjs/input/Slider.js +1 -1
  33. package/dist/cjs/input/Switch.js +27 -5
  34. package/dist/cjs/input/Textarea.js +7 -3
  35. package/dist/cjs/internal/ErrorIcon.js +1 -1
  36. package/dist/cjs/internal/FieldWrapper.js +4 -3
  37. package/dist/cjs/internal/InputWrapper.js +14 -16
  38. package/dist/cjs/select/AutoComplete.js +9 -6
  39. package/dist/cjs/select/List.js +41 -20
  40. package/dist/cjs/select/Options.js +2 -2
  41. package/dist/cjs/select/Select.js +15 -9
  42. package/dist/cjs/select/useSelect.js +15 -8
  43. package/{src/input/ColorPicker.module.css → dist/cjs/types/schema.js} +18 -27
  44. package/dist/cjs/utils/getColor.js +6 -0
  45. package/dist/esm/builder/FileField.js +39 -27
  46. package/dist/esm/builder/FormField.js +44 -34
  47. package/dist/esm/builder/SchemaEditor.js +72 -0
  48. package/dist/esm/builder/components/AddSchemaFields.js +62 -0
  49. package/dist/esm/builder/components/SchemaArrayForm.js +143 -0
  50. package/dist/esm/builder/components/SchemaField.js +413 -0
  51. package/dist/esm/form/Controller.js +1 -1
  52. package/dist/esm/form/Form.js +19 -10
  53. package/dist/esm/hooks/useFormBuilder.js +25 -13
  54. package/dist/esm/index.js +4 -1
  55. package/dist/esm/input/ArrayInput.js +38 -27
  56. package/dist/esm/input/AvatarInput.js +1 -1
  57. package/dist/esm/input/Checkbox.js +5 -4
  58. package/dist/esm/input/ColorInput.js +72 -30
  59. package/dist/esm/input/ColorPicker.js +1 -35
  60. package/dist/esm/input/DateInput.js +183 -0
  61. package/dist/esm/input/Field.js +5 -5
  62. package/dist/esm/input/File.js +2 -3
  63. package/dist/esm/input/Hidden.js +3 -1
  64. package/dist/esm/input/Input.js +1 -1
  65. package/dist/esm/input/Number.js +1 -1
  66. package/dist/esm/input/Password.js +1 -1
  67. package/dist/esm/input/Radio.js +1 -1
  68. package/dist/esm/input/RadioGroup.js +7 -4
  69. package/dist/esm/input/{Range.js → RangeSlider.js} +3 -3
  70. package/dist/esm/input/Search.js +1 -1
  71. package/dist/esm/input/Slider.js +1 -1
  72. package/dist/esm/input/Switch.js +29 -7
  73. package/dist/esm/input/Textarea.js +7 -3
  74. package/dist/esm/internal/ErrorIcon.js +1 -1
  75. package/dist/esm/internal/FieldWrapper.js +4 -3
  76. package/dist/esm/internal/InputWrapper.js +14 -16
  77. package/dist/esm/select/AutoComplete.js +9 -6
  78. package/dist/esm/select/List.js +42 -21
  79. package/dist/esm/select/Options.js +2 -2
  80. package/dist/esm/select/Select.js +15 -9
  81. package/dist/esm/select/useSelect.js +16 -9
  82. package/dist/esm/types/schema.js +41 -0
  83. package/dist/esm/utils/getColor.js +6 -1
  84. package/dist/types/builder/FileField.d.ts +4 -2
  85. package/dist/types/builder/FormField.d.ts +3 -1
  86. package/dist/types/builder/SchemaEditor.d.ts +2 -0
  87. package/dist/types/builder/components/AddSchemaFields.d.ts +2 -0
  88. package/dist/types/builder/components/SchemaArrayForm.d.ts +2 -0
  89. package/dist/types/builder/components/SchemaField.d.ts +10 -0
  90. package/dist/types/form/Controller.d.ts +1 -1
  91. package/dist/types/hooks/useFormBuilder.d.ts +3 -1
  92. package/dist/types/index.d.ts +4 -2
  93. package/dist/types/input/ArrayInput.d.ts +20 -3
  94. package/dist/types/input/Checkbox.d.ts +4 -3
  95. package/dist/types/input/ColorInput.d.ts +1 -1
  96. package/dist/types/input/DateInput.d.ts +19 -0
  97. package/dist/types/input/RadioGroup.d.ts +18 -2
  98. package/dist/types/input/{Range.d.ts → RangeSlider.d.ts} +1 -1
  99. package/dist/types/input/Switch.d.ts +7 -3
  100. package/dist/types/input/Textarea.d.ts +5 -1
  101. package/dist/types/internal/InputWrapper.d.ts +1 -1
  102. package/dist/types/select/AutoComplete.d.ts +1 -1
  103. package/dist/types/select/List.d.ts +1 -1
  104. package/dist/types/select/Select.d.ts +1 -1
  105. package/dist/types/select/useSelect.d.ts +2 -1
  106. package/dist/types/types/index.d.ts +17 -5
  107. package/dist/types/types/schema.d.ts +79 -33
  108. package/package.json +10 -7
  109. package/src/builder/FileField.tsx +53 -27
  110. package/src/builder/FormField.tsx +45 -32
  111. package/src/builder/SchemaEditor.tsx +72 -0
  112. package/src/builder/components/AddSchemaFields.tsx +74 -0
  113. package/src/builder/components/SchemaArrayForm.tsx +158 -0
  114. package/src/builder/components/SchemaField.tsx +435 -0
  115. package/src/form/Controller.tsx +2 -2
  116. package/src/form/Form.tsx +15 -8
  117. package/src/hooks/useFormBuilder.tsx +29 -15
  118. package/src/index.ts +10 -2
  119. package/src/input/ArrayInput.tsx +80 -42
  120. package/src/input/AvatarInput.tsx +1 -1
  121. package/src/input/Checkbox.tsx +5 -4
  122. package/src/input/ColorInput.tsx +102 -34
  123. package/src/input/ColorPicker.tsx +1 -35
  124. package/src/input/DateInput.tsx +248 -0
  125. package/src/input/Field.tsx +9 -11
  126. package/src/input/File.tsx +3 -6
  127. package/src/input/Hidden.tsx +4 -1
  128. package/src/input/Input.tsx +2 -2
  129. package/src/input/Number.tsx +2 -2
  130. package/src/input/Password.tsx +2 -2
  131. package/src/input/Radio.tsx +1 -1
  132. package/src/input/RadioGroup.tsx +27 -4
  133. package/src/input/{Range.tsx → RangeSlider.tsx} +2 -2
  134. package/src/input/Search.tsx +2 -2
  135. package/src/input/Slider.tsx +1 -1
  136. package/src/input/Switch.tsx +42 -6
  137. package/src/input/Textarea.tsx +11 -3
  138. package/src/internal/ErrorIcon.tsx +1 -1
  139. package/src/internal/FieldWrapper.tsx +3 -3
  140. package/src/internal/InputWrapper.tsx +21 -20
  141. package/src/select/AutoComplete.tsx +4 -1
  142. package/src/select/List.tsx +37 -8
  143. package/src/select/Options.tsx +1 -1
  144. package/src/select/Select.tsx +9 -3
  145. package/src/select/useSelect.tsx +30 -19
  146. package/src/types/index.ts +22 -7
  147. package/src/types/schema.ts +96 -41
  148. package/dist/cjs/input/ArrayInput.module.css +0 -1
  149. package/dist/cjs/input/ColorPicker.module.css +0 -1
  150. package/dist/cjs/input/File.module.css +0 -1
  151. package/dist/cjs/internal/Field.module.css +0 -1
  152. package/dist/esm/input/ArrayInput.module.css +0 -1
  153. package/dist/esm/input/ColorPicker.module.css +0 -1
  154. package/dist/esm/input/File.module.css +0 -1
  155. package/dist/esm/internal/Field.module.css +0 -1
@@ -21,38 +21,47 @@
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 { EmptyContent } from "@react-fabric/core";
24
25
  import { isEmpty, isNil, yup } from "@react-fabric/utilities";
25
26
  import { Fragment, useMemo } from "react";
26
27
  import { FormField } from "../builder/FormField";
27
- import { type FormSchema } from "../types/schema";
28
+ import { type BaseSelectProps } from "../types";
29
+ import { DATA_TYPES, type FormSchema, type SchemaDef } from "../types/schema";
28
30
  import { type UploadHandler } from "./useFileUploader";
29
31
 
30
- const makeYupField = (field: FormSchema) => {
31
- let yupField: AnyObject = yup.string();
32
- if (field.datatype === "schema") {
32
+ const makeYupField = (field: SchemaDef) => {
33
+ let yupField: AnyObject = yup.string().trim();
34
+ if (field.datatype === DATA_TYPES.SCHEMA) {
33
35
  const child: yup.ObjectShape = Object.fromEntries(
34
36
  field.schema.map((field) => [field.id, makeYupField(field)]),
35
37
  );
36
38
  yupField = yup.object(child);
37
39
  }
38
- if (field.datatype === "boolean") yupField = yup.boolean();
40
+ if (field.datatype === DATA_TYPES.BOOL) {
41
+ yupField = yup.boolean();
42
+ if (field.required)
43
+ yupField = yupField.test({
44
+ test: (value: boolean) => !!value,
45
+ });
46
+ }
39
47
  if (
40
- field.datatype === "decimal" ||
41
- field.datatype === "number" ||
42
- field.datatype === "range"
48
+ field.datatype === DATA_TYPES.NUMBER ||
49
+ field.datatype === DATA_TYPES.RANGE
43
50
  ) {
44
51
  yupField = yup.number();
45
52
  !isNil(field.min) && (yupField = yupField.min(field.min));
46
53
  !isNil(field.max) && (yupField = yupField.max(field.max));
47
54
 
48
- if (field.datatype === "range") yupField = yup.array(yupField);
55
+ if (field.datatype === DATA_TYPES.RANGE) yupField = yup.array(yupField);
49
56
  }
50
- if (field.datatype === "string") {
51
- field.options && (yupField = yupField.oneOf(field.options));
57
+ if (field.datatype === DATA_TYPES.STRING) {
58
+ field.options &&
59
+ !field.allowCustom &&
60
+ (yupField = yupField.oneOf(field.options));
52
61
  field.regex && (yupField = yupField.matches(new RegExp(field.regex)));
53
62
  }
54
63
 
55
- if (field.datatype === "file") {
64
+ if (field.datatype === DATA_TYPES.FILE) {
56
65
  yupField = yup
57
66
  .object({
58
67
  filename: yup.string().required(),
@@ -83,13 +92,14 @@ const makeYupField = (field: FormSchema) => {
83
92
  return yupField.label(field.label);
84
93
  };
85
94
 
86
- export const useFormBuilder = (
87
- schema: FormSchema[],
95
+ export const useFormBuilder = <T extends AnyObject = string>(
96
+ schema: FormSchema,
88
97
  options: {
89
98
  prefix?: string;
90
99
  inline?: boolean;
91
100
  fileUrl?: (path: string) => string;
92
101
  uploadHandler?: UploadHandler;
102
+ optionLists?: KeyValue<BaseSelectProps<T>>;
93
103
  } = {},
94
104
  ) => {
95
105
  const filteredSchema = useMemo(
@@ -106,11 +116,12 @@ export const useFormBuilder = (
106
116
  <Fragment>
107
117
  {filteredSchema.map((field, idx) => (
108
118
  <div className="mb-4" key={field.id}>
109
- {field.datatype === "schema" ? null : (
119
+ {field.datatype === DATA_TYPES.SCHEMA ? null : (
110
120
  <FormField
111
121
  {...field}
112
122
  autoFocus={idx === 0}
113
123
  fileUrl={options.fileUrl}
124
+ optionLists={options.optionLists}
114
125
  uploadHandler={options.uploadHandler}
115
126
  inline={options.inline}
116
127
  prefix={options.prefix}
@@ -118,6 +129,9 @@ export const useFormBuilder = (
118
129
  )}
119
130
  </div>
120
131
  ))}
132
+ {filteredSchema.length === 0 && (
133
+ <EmptyContent message="No schema defined" />
134
+ )}
121
135
  </Fragment>
122
136
  );
123
137
 
package/src/index.ts CHANGED
@@ -26,15 +26,23 @@ import "./i18n";
26
26
  export { Controller } from "./form/Controller";
27
27
  export { Form, type FormRef } from "./form/Form";
28
28
 
29
+ export { SchemaEditor } from "./builder/SchemaEditor";
30
+
29
31
  export { useFileUploader } from "./hooks/useFileUploader";
30
32
  export { useFormBuilder } from "./hooks/useFormBuilder";
31
33
 
32
- export { type FormSchema } from "./types/schema";
34
+ export {
35
+ DATA_TYPES,
36
+ SCHEMA_CUSTOM_LIST,
37
+ type FormSchema,
38
+ type SchemaDef,
39
+ } from "./types/schema";
33
40
 
34
41
  export { ArrayInput } from "./input/ArrayInput";
35
42
  export { Checkbox } from "./input/Checkbox";
36
43
  export { ColorInput } from "./input/ColorInput";
37
44
  export { ColorPicker } from "./input/ColorPicker";
45
+ export { DateInput } from "./input/DateInput";
38
46
  export { Field } from "./input/Field";
39
47
  export { FileInput } from "./input/File";
40
48
  export { HiddenInput } from "./input/Hidden";
@@ -43,7 +51,7 @@ export { Number } from "./input/Number";
43
51
  export { Password } from "./input/Password";
44
52
  export { Radio } from "./input/Radio";
45
53
  export { RadioGroup } from "./input/RadioGroup";
46
- export { Range } from "./input/Range";
54
+ export { RangeSlider } from "./input/RangeSlider";
47
55
  export { Search } from "./input/Search";
48
56
  export { Slider } from "./input/Slider";
49
57
  export { Switch } from "./input/Switch";
@@ -23,26 +23,31 @@
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";
26
+ import {
27
+ Button,
28
+ cloneChildren,
29
+ CoreIcons,
30
+ Icon,
31
+ Tooltip,
32
+ } from "@react-fabric/core";
27
33
  import { 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";
50
+
46
51
  export interface ArrayInputProps {
47
52
  /**
48
53
  * input label
@@ -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,11 +101,12 @@ 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<{
109
+ arrayRef?: Ref<{
100
110
  addItem: (item: AnyObject) => void;
101
111
  removeItem: (idx: number) => void;
102
112
  }>;
@@ -104,6 +114,19 @@ export interface ArrayInputProps {
104
114
  * Add item
105
115
  */
106
116
  onAdd?: () => AnyObject;
117
+
118
+ /**
119
+ * field width
120
+ */
121
+ width?: number | string;
122
+ /**
123
+ * inline label and input
124
+ */
125
+ inline?: boolean;
126
+ /**
127
+ * label width for inline
128
+ */
129
+ labelWidth?: string;
107
130
  }
108
131
 
109
132
  export const ArrayInput = ({
@@ -112,12 +135,14 @@ export const ArrayInput = ({
112
135
  addLabel,
113
136
  fixedList,
114
137
  focusName = "",
138
+ buttonPosition = "bottom",
115
139
  onAdd,
116
140
  arrayRef,
117
141
  enableSorting,
118
142
  disabled = false,
119
143
  readOnly = false,
120
144
  minItems = 0,
145
+ maxItems = Number.MAX_SAFE_INTEGER,
121
146
  ...rest
122
147
  }: ArrayInputProps) => {
123
148
  const { t } = useTranslation("form");
@@ -227,9 +252,55 @@ export const ArrayInput = ({
227
252
  // @ts-expect-error ignore
228
253
  SortableItem.displayName = "SortableItem";
229
254
 
255
+ const addButton = useMemo(
256
+ () => (
257
+ <div className="flex justify-end">
258
+ <Tooltip
259
+ color="danger-200"
260
+ content={
261
+ isString(error)
262
+ ? `${error}`
263
+ : (t(
264
+ error?.key,
265
+ error?.values
266
+ ? {
267
+ ...error?.values,
268
+ label: t(
269
+ error.values?.label ?? "form:badkey",
270
+ error.values?.path,
271
+ ),
272
+ }
273
+ : {},
274
+ ) as string)
275
+ }
276
+ >
277
+ <Button
278
+ size="sm"
279
+ aria-label="Add item"
280
+ icon={CoreIcons.insert}
281
+ onClick={() => handleAdd(onAdd?.())}
282
+ data-invalid={!!error}
283
+ disabled={!!disabled || readOnly || fields.length > maxItems}
284
+ className={classNames("fabric-addButton", "me-10")}
285
+ >
286
+ {addLabel ?? t("addArray")}
287
+ </Button>
288
+ </Tooltip>
289
+ </div>
290
+ ),
291
+ [error, fields, handleAdd, disabled, readOnly, addLabel, maxItems],
292
+ );
293
+
230
294
  return (
231
- <div className={classes.arrayInput}>
232
- <FieldWrapper {...rest}>
295
+ <div className={"fabric-arrayInput"}>
296
+ <FieldWrapper
297
+ {...rest}
298
+ appendLabel={
299
+ <div className="-me-2">
300
+ {!fixedList && onAdd && buttonPosition !== "bottom" && addButton}
301
+ </div>
302
+ }
303
+ >
233
304
  <Wrapper>
234
305
  {fields.map((item, index) => (
235
306
  <SortableItem
@@ -242,7 +313,7 @@ export const ArrayInput = ({
242
313
  index,
243
314
  name: `${name}.${index}`,
244
315
  })
245
- : Children.map(children, (child: AnyObject) =>
316
+ : cloneChildren(children, (child: AnyObject) =>
246
317
  cloneElement(
247
318
  child,
248
319
  Object.assign(
@@ -279,40 +350,7 @@ export const ArrayInput = ({
279
350
  ))}
280
351
  </Wrapper>
281
352
  </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
- )}
353
+ {!fixedList && onAdd && buttonPosition !== "top" && addButton}
316
354
  </div>
317
355
  );
318
356
  };
@@ -99,7 +99,7 @@ export const AvatarInput = ({
99
99
  <div className="flex flex-nowrap items-end">
100
100
  <div className="inline-block leading-none rounded-full relative outline overflow-hidden">
101
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">
102
+ <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
103
  <span>Edit</span>
104
104
  <input
105
105
  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"}
@@ -21,7 +21,19 @@
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
38
  import { isString } from "@react-fabric/utilities";
27
39
  import classNames from "classnames";
@@ -30,12 +42,16 @@ import {
30
42
  useCallback,
31
43
  useDeferredValue,
32
44
  useEffect,
45
+ useRef,
33
46
  useState,
34
47
  } from "react";
35
48
  import { type InputProps } from "../types";
36
49
  import { ColorPicker, type ColorProps } from "./ColorPicker";
37
50
 
38
- export interface ColorInputProps extends InputProps, ColorProps, RefProp {
51
+ export interface ColorInputProps
52
+ extends InputProps,
53
+ Omit<ColorProps, "onChange">,
54
+ RefProp {
39
55
  /**
40
56
  * show eydrop picker
41
57
  */
@@ -69,17 +85,49 @@ export const ColorInput = ({
69
85
  const handleChange = useCallback(
70
86
  (e?: string) => {
71
87
  setActualValue(e ?? defaultColor);
72
- onChange != null && startTransition(() => onChange(e));
88
+ onChange != null && startTransition(() => onChange(e ?? null));
73
89
  },
74
90
  [onChange],
75
91
  );
76
92
 
93
+ const arrowRef = useRef(null);
94
+ const [isOpen, setIsOpen] = useState(false);
95
+ const { refs, floatingStyles, context } = useFloating({
96
+ open: isOpen,
97
+ onOpenChange: (open, _, reason) => {
98
+ if (reason === "reference-press") return;
99
+ setIsOpen(open);
100
+ },
101
+ strategy: "fixed",
102
+ placement: "bottom",
103
+ whileElementsMounted: autoUpdate,
104
+ middleware: [
105
+ shift({ padding: 8 }),
106
+ flip(),
107
+ offset(9),
108
+ arrow({
109
+ element: arrowRef,
110
+ }),
111
+ ],
112
+ });
113
+ const click = useClick(context, {
114
+ enabled: !disabled,
115
+ });
116
+ const dismiss = useDismiss(context, {
117
+ referencePress: true,
118
+ });
119
+
120
+ const { getReferenceProps, getFloatingProps } = useInteractions([
121
+ dismiss,
122
+ click,
123
+ ]);
124
+
77
125
  return (
78
126
  <div
79
127
  className={classNames(
80
128
  "flex-content min-h-[1.5em] min-w-[3.5em] self-stretch flex items-center outline-tint-200 rounded",
81
129
  !disabled && "bg-base",
82
- !noOutline && "bg-[var(--fabric-input)] outline overflow-hidden",
130
+ !noOutline && "bg-(--fabric-input) outline overflow-hidden",
83
131
  )}
84
132
  >
85
133
  {decorateStart && (
@@ -92,36 +140,56 @@ export const ColorInput = ({
92
140
  {decorateStart}
93
141
  </div>
94
142
  )}
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>
143
+ <button
144
+ data-ref="button"
145
+ className="flex-1 flex items-center"
146
+ ref={refs.setReference}
147
+ {...getReferenceProps()}
148
+ data-inner-clickable="true"
149
+ data-dropdown-open={isOpen ? true : undefined}
150
+ >
151
+ <div
152
+ role="none"
153
+ 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"
154
+ style={{
155
+ backgroundColor: actualValue,
156
+ opacity: disabled ? 0.65 : 1,
157
+ pointerEvents: disabled ? "none" : undefined,
158
+ }}
159
+ >
160
+ {!actualValue && (
161
+ <Icon
162
+ size="md"
163
+ className="text-muted"
164
+ icon={CoreIcons.colorSwatch}
165
+ />
166
+ )}
167
+ </div>
168
+ </button>
169
+ {isOpen && (
170
+ <div
171
+ ref={refs.setFloating}
172
+ style={{
173
+ zIndex: "var(--z-popover)",
174
+ ...floatingStyles,
175
+ }}
176
+ data-ref="dropdownBody"
177
+ {...getFloatingProps()}
178
+ >
179
+ <ColorPicker
180
+ {...rest}
181
+ onChange={handleChange}
182
+ defaultColor={defaultColor}
183
+ value={actualValue}
184
+ />
185
+ <FloatingArrow
186
+ ref={arrowRef}
187
+ context={context}
188
+ strokeWidth={0.5}
189
+ className="fill-base stroke-muted"
190
+ />
191
+ </div>
192
+ )}
125
193
  {!disabled && showPicker && (
126
194
  <Icon
127
195
  icon={CoreIcons.colorPicker}
@@ -32,7 +32,6 @@ import {
32
32
  type FC,
33
33
  } from "react";
34
34
  import { SketchPicker } from "react-color";
35
- import classes from "./ColorPicker.module.css";
36
35
 
37
36
  export interface ColorProps {
38
37
  /**
@@ -130,17 +129,14 @@ export const ColorPicker: FC<ColorProps> = memo(
130
129
  return (
131
130
  <SketchPicker
132
131
  width="16rem"
133
- className={classes.colorPicker}
132
+ className={"fabric-colorPicker"}
134
133
  styles={
135
134
  {
136
135
  default: {
137
136
  hue: {
138
- borderRadius: "99px",
139
137
  boxShadow: "0 0 1px 1px var(--color-shadow)",
140
138
  },
141
139
  alpha: {
142
- height: "1rem",
143
- borderRadius: "99px",
144
140
  backgroundColor: "var(--fabric-bg)",
145
141
  boxShadow: "0 0 1px 1px var(--color-shadow)",
146
142
  },
@@ -158,36 +154,6 @@ export const ColorPicker: FC<ColorProps> = memo(
158
154
  boxShadow:
159
155
  "0 0 0 1px var(--color-shadow), 0 0 8px var(--fabric-shadow)",
160
156
  },
161
- Alpha: {
162
- alpha: {
163
- paddingRight: "calc(1rem - 2px)",
164
- },
165
- slider: {
166
- width: "calc(1rem - 2px)",
167
- borderRadius: "99px",
168
- height: "calc(1rem - 2px)",
169
- transform: "none",
170
- },
171
- },
172
- Hue: {
173
- hue: {
174
- paddingRight: "calc(1rem - 2px)",
175
- },
176
- slider: {
177
- width: "calc(1rem - 2px)",
178
- borderRadius: "99px",
179
- height: "calc(1rem - 2px)",
180
- transform: "none",
181
- },
182
- },
183
- Saturation: {
184
- circle: {
185
- width: "0.5rem",
186
- borderRadius: "99px",
187
- height: "0.5rem",
188
- transform: "translate(-0.25rem, -0.25rem)",
189
- },
190
- },
191
157
  checkboard: {
192
158
  white: "transparent",
193
159
  black: "red",