@react-fabric/form 1.0.0-beta.6 → 1.0.0-beta.60

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 (187) 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} +66 -30
  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 +40 -28
  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 +145 -0
  11. package/dist/cjs/builder/components/SchemaField.js +414 -0
  12. package/dist/cjs/form/Controller.js +46 -1
  13. package/dist/cjs/form/Form.js +70 -9
  14. package/dist/cjs/hooks/useFileUploader.js +16 -0
  15. package/dist/cjs/hooks/useFormBuilder.js +57 -15
  16. package/dist/cjs/i18n/ar.json.js +5 -4
  17. package/dist/cjs/i18n/en.json.js +5 -5
  18. package/dist/cjs/index.js +12 -2
  19. package/dist/cjs/input/ArrayInput.js +126 -36
  20. package/dist/cjs/input/AvatarInput.js +43 -23
  21. package/dist/cjs/input/Checkbox.js +16 -6
  22. package/dist/cjs/input/ColorInput.js +113 -58
  23. package/dist/cjs/input/ColorPicker.js +60 -76
  24. package/dist/cjs/input/DateInput.js +197 -0
  25. package/dist/cjs/input/Field.js +10 -4
  26. package/dist/cjs/input/File.js +14 -7
  27. package/dist/cjs/input/Hidden.js +13 -2
  28. package/dist/cjs/input/Input.js +24 -10
  29. package/dist/cjs/input/Number.js +22 -10
  30. package/dist/cjs/input/Password.js +22 -12
  31. package/dist/cjs/input/Radio.js +12 -3
  32. package/dist/cjs/input/RadioGroup.js +10 -1
  33. package/dist/cjs/input/{Range.js → RangeSlider.js} +16 -8
  34. package/dist/cjs/input/Search.js +30 -19
  35. package/dist/cjs/input/Slider.js +12 -6
  36. package/dist/cjs/input/Switch.js +20 -9
  37. package/dist/cjs/input/Textarea.js +29 -8
  38. package/dist/cjs/internal/ErrorIcon.js +1 -1
  39. package/dist/cjs/internal/FieldWrapper.js +11 -4
  40. package/dist/cjs/internal/InputWrapper.js +27 -20
  41. package/dist/cjs/select/AutoComplete.js +21 -11
  42. package/dist/cjs/select/DualList.js +34 -12
  43. package/dist/cjs/select/List.js +68 -22
  44. package/dist/cjs/select/Options.js +49 -42
  45. package/dist/cjs/select/Select.js +40 -10
  46. package/dist/cjs/select/useSelect.js +22 -11
  47. package/dist/cjs/types/schema.js +43 -0
  48. package/dist/cjs/utils/eventHandlers.js +15 -5
  49. package/dist/esm/builder/FileField.js +42 -30
  50. package/dist/esm/builder/FormField.js +50 -34
  51. package/dist/esm/builder/SchemaEditor.js +72 -0
  52. package/dist/esm/builder/components/AddSchemaFields.js +62 -0
  53. package/dist/esm/builder/components/SchemaArrayForm.js +145 -0
  54. package/dist/esm/builder/components/SchemaField.js +414 -0
  55. package/dist/esm/form/Controller.js +46 -1
  56. package/dist/esm/form/Form.js +72 -12
  57. package/dist/esm/hooks/useFileUploader.js +16 -0
  58. package/dist/esm/hooks/useFormBuilder.js +55 -13
  59. package/dist/esm/i18n/ar.json.js +5 -4
  60. package/dist/esm/i18n/en.json.js +5 -5
  61. package/dist/esm/index.js +6 -2
  62. package/dist/esm/input/ArrayInput.js +126 -36
  63. package/dist/esm/input/AvatarInput.js +44 -24
  64. package/dist/esm/input/Checkbox.js +16 -6
  65. package/dist/esm/input/ColorInput.js +115 -60
  66. package/dist/esm/input/ColorPicker.js +60 -76
  67. package/dist/esm/input/DateInput.js +197 -0
  68. package/dist/esm/input/Field.js +11 -5
  69. package/dist/esm/input/File.js +16 -9
  70. package/dist/esm/input/Hidden.js +13 -2
  71. package/dist/esm/input/Input.js +26 -12
  72. package/dist/esm/input/Number.js +24 -12
  73. package/dist/esm/input/Password.js +24 -14
  74. package/dist/esm/input/Radio.js +12 -3
  75. package/dist/esm/input/RadioGroup.js +11 -2
  76. package/dist/esm/input/{Range.js → RangeSlider.js} +16 -8
  77. package/dist/esm/input/Search.js +32 -21
  78. package/dist/esm/input/Slider.js +12 -6
  79. package/dist/esm/input/Switch.js +20 -9
  80. package/dist/esm/input/Textarea.js +30 -9
  81. package/dist/esm/internal/ErrorIcon.js +1 -1
  82. package/dist/esm/internal/FieldWrapper.js +11 -4
  83. package/dist/esm/internal/InputWrapper.js +27 -20
  84. package/dist/esm/select/AutoComplete.js +21 -11
  85. package/dist/esm/select/DualList.js +34 -12
  86. package/dist/esm/select/List.js +69 -23
  87. package/dist/esm/select/Options.js +49 -42
  88. package/dist/esm/select/Select.js +40 -10
  89. package/dist/esm/select/useSelect.js +23 -12
  90. package/{src/input/ColorPicker.module.css → dist/esm/types/schema.js} +18 -27
  91. package/dist/esm/utils/eventHandlers.js +15 -5
  92. package/dist/types/builder/FileField.d.ts +4 -2
  93. package/dist/types/builder/FormField.d.ts +3 -1
  94. package/dist/types/builder/SchemaEditor.d.ts +9 -0
  95. package/dist/types/builder/components/AddSchemaFields.d.ts +2 -0
  96. package/dist/types/builder/components/SchemaArrayForm.d.ts +2 -0
  97. package/dist/types/builder/components/SchemaField.d.ts +10 -0
  98. package/dist/types/form/Controller.d.ts +47 -2
  99. package/dist/types/form/Form.d.ts +48 -2
  100. package/dist/types/hooks/useFileUploader.d.ts +16 -0
  101. package/dist/types/hooks/useFormBuilder.d.ts +32 -1
  102. package/dist/types/index.d.ts +6 -3
  103. package/dist/types/input/ArrayInput.d.ts +69 -12
  104. package/dist/types/input/AvatarInput.d.ts +23 -3
  105. package/dist/types/input/Checkbox.d.ts +13 -4
  106. package/dist/types/input/ColorInput.d.ts +15 -2
  107. package/dist/types/input/ColorPicker.d.ts +11 -1
  108. package/dist/types/input/DateInput.d.ts +28 -0
  109. package/dist/types/input/Field.d.ts +5 -1
  110. package/dist/types/input/File.d.ts +9 -0
  111. package/dist/types/input/Hidden.d.ts +13 -1
  112. package/dist/types/input/Input.d.ts +9 -1
  113. package/dist/types/input/Number.d.ts +9 -1
  114. package/dist/types/input/Password.d.ts +7 -1
  115. package/dist/types/input/Radio.d.ts +9 -1
  116. package/dist/types/input/RadioGroup.d.ts +8 -1
  117. package/dist/types/input/RangeSlider.d.ts +67 -0
  118. package/dist/types/input/Search.d.ts +14 -1
  119. package/dist/types/input/Slider.d.ts +8 -2
  120. package/dist/types/input/Switch.d.ts +10 -2
  121. package/dist/types/input/Textarea.d.ts +15 -1
  122. package/dist/types/internal/FieldWrapper.d.ts +1 -1
  123. package/dist/types/internal/InputWrapper.d.ts +1 -1
  124. package/dist/types/ranger/index.d.ts +2 -2
  125. package/dist/types/select/AutoComplete.d.ts +8 -1
  126. package/dist/types/select/DualList.d.ts +17 -2
  127. package/dist/types/select/List.d.ts +9 -1
  128. package/dist/types/select/Options.d.ts +4 -3
  129. package/dist/types/select/Select.d.ts +9 -1
  130. package/dist/types/select/useSelect.d.ts +10 -6
  131. package/dist/types/types/index.d.ts +34 -5
  132. package/dist/types/types/schema.d.ts +78 -33
  133. package/dist/types/utils/eventHandlers.d.ts +2 -2
  134. package/package.json +22 -20
  135. package/src/builder/FileField.tsx +62 -30
  136. package/src/builder/FormField.tsx +52 -32
  137. package/src/builder/SchemaEditor.tsx +79 -0
  138. package/src/builder/components/AddSchemaFields.tsx +74 -0
  139. package/src/builder/components/SchemaArrayForm.tsx +161 -0
  140. package/src/builder/components/SchemaField.tsx +436 -0
  141. package/src/form/Controller.tsx +48 -3
  142. package/src/form/Form.tsx +80 -8
  143. package/src/hooks/useFileUploader.ts +16 -0
  144. package/src/hooks/useFormBuilder.tsx +58 -15
  145. package/src/i18n/ar.json +5 -4
  146. package/src/i18n/en.json +5 -5
  147. package/src/index.ts +12 -3
  148. package/src/input/ArrayInput.tsx +199 -64
  149. package/src/input/AvatarInput.tsx +67 -19
  150. package/src/input/Checkbox.tsx +16 -6
  151. package/src/input/ColorInput.tsx +131 -71
  152. package/src/input/ColorPicker.tsx +54 -72
  153. package/src/input/DateInput.tsx +260 -0
  154. package/src/input/Field.tsx +15 -11
  155. package/src/input/File.tsx +16 -14
  156. package/src/input/Hidden.tsx +17 -2
  157. package/src/input/Input.tsx +27 -13
  158. package/src/input/Number.tsx +22 -13
  159. package/src/input/Password.tsx +24 -16
  160. package/src/input/Radio.tsx +12 -3
  161. package/src/input/RadioGroup.tsx +12 -3
  162. package/src/input/{Range.tsx → RangeSlider.tsx} +17 -9
  163. package/src/input/Search.tsx +33 -22
  164. package/src/input/Slider.tsx +20 -11
  165. package/src/input/Switch.tsx +17 -6
  166. package/src/input/Textarea.tsx +34 -9
  167. package/src/internal/ErrorIcon.tsx +1 -1
  168. package/src/internal/FieldWrapper.tsx +8 -4
  169. package/src/internal/InputWrapper.tsx +49 -27
  170. package/src/select/AutoComplete.tsx +14 -3
  171. package/src/select/DualList.tsx +31 -16
  172. package/src/select/List.tsx +52 -8
  173. package/src/select/Options.tsx +50 -46
  174. package/src/select/Select.tsx +25 -5
  175. package/src/select/useSelect.tsx +26 -9
  176. package/src/types/index.ts +37 -6
  177. package/src/types/schema.ts +96 -41
  178. package/src/utils/eventHandlers.ts +14 -4
  179. package/dist/cjs/input/ArrayInput.module.css +0 -1
  180. package/dist/cjs/input/ColorPicker.module.css +0 -1
  181. package/dist/cjs/input/File.module.css +0 -1
  182. package/dist/cjs/internal/Field.module.css +0 -1
  183. package/dist/esm/input/ArrayInput.module.css +0 -1
  184. package/dist/esm/input/ColorPicker.module.css +0 -1
  185. package/dist/esm/input/File.module.css +0 -1
  186. package/dist/esm/internal/Field.module.css +0 -1
  187. package/dist/types/input/Range.d.ts +0 -59
@@ -23,7 +23,7 @@
23
23
 
24
24
  import { EmptyContent } from "@react-fabric/core";
25
25
  import classNames from "classnames";
26
- import { type ReactElement, type ReactNode } from "react";
26
+ import { type ReactElement } from "react";
27
27
  import { Trans } from "react-i18next";
28
28
 
29
29
  export const Options = ({
@@ -34,6 +34,7 @@ export const Options = ({
34
34
  className,
35
35
  itemRef,
36
36
  empty,
37
+ info,
37
38
  valueProperty = "id",
38
39
  labelProperty = "label",
39
40
  itemProps,
@@ -41,6 +42,7 @@ export const Options = ({
41
42
  ...rest
42
43
  }: {
43
44
  ref: AnyObject;
45
+ info?: AnyObject;
44
46
  itemRef: AnyObject;
45
47
  style?: AnyObject;
46
48
  items: AnyObject[];
@@ -50,34 +52,53 @@ export const Options = ({
50
52
  valueProperty?: AnyObject;
51
53
  labelProperty?: AnyObject;
52
54
  itemProps: (item: AnyObject) => AnyObject;
53
- children: (item: AnyObject, label: string) => ReactNode;
55
+ children: (item: AnyObject, label: string) => React.ReactNode;
54
56
  } & KeyValue) => {
55
57
  return (
56
58
  <div
57
59
  ref={ref}
58
- className={classNames(
59
- "bg-base select-none overflow-x-hidden overflow-y-auto",
60
- className,
61
- )}
60
+ className={classNames("bg-base select-none flex flex-col", className)}
62
61
  style={style}
63
62
  {...rest}
64
63
  >
65
- {items.length === 0 &&
66
- (empty ?? (
67
- <EmptyContent message={<Trans i18nKey="form:select.emptyList" />} />
68
- ))}
69
- {items.map((item, index) => {
70
- if (item.___group___)
71
- return (
72
- <p
73
- key={index}
74
- className="px-2 py-1 text-muted text-xs sticky -top-px z-2 bg-base border-t"
75
- >
76
- {item.label}
77
- </p>
78
- );
64
+ <div className={classNames("overflow-x-hidden overflow-y-auto flex-1")}>
65
+ {items.length === 0 &&
66
+ (empty ?? (
67
+ <EmptyContent message={<Trans i18nKey="form:select.emptyList" />} />
68
+ ))}
69
+ {items.map((item, index) => {
70
+ if (item.___group___)
71
+ return (
72
+ <p
73
+ key={index}
74
+ className="px-2 py-1 text-muted text-xs sticky -top-px z-2 bg-base border-t"
75
+ >
76
+ {item.label}
77
+ </p>
78
+ );
79
+
80
+ if (item.___create___)
81
+ return (
82
+ <div
83
+ role="none"
84
+ key={index}
85
+ tabIndex={active === index ? 0 : -1}
86
+ ref={(el: AnyObject) => itemRef(index, el)}
87
+ data-active={active === index ? true : undefined}
88
+ className={classNames(
89
+ "block text-start truncate px-2 py-1 leading-normal cursor-pointer select-none",
90
+ "data-active:bg-primary-200 data-selected:bg-primary-600 data-selected:text-white data-active:data-selected:underline",
91
+ )}
92
+ {...itemProps(item)}
93
+ >
94
+ <Trans
95
+ i18nKey="form:select.createOption"
96
+ values={{ query: item.value ?? item }}
97
+ components={[<strong key="t0">query</strong>]}
98
+ />
99
+ </div>
100
+ );
79
101
 
80
- if (item.___create___)
81
102
  return (
82
103
  <div
83
104
  role="none"
@@ -86,36 +107,19 @@ export const Options = ({
86
107
  ref={(el: AnyObject) => itemRef(index, el)}
87
108
  data-active={active === index ? true : undefined}
88
109
  className={classNames(
89
- "block text-start truncate px-2 py-1 leading-normal cursor-pointer select-none",
90
- "data-[active]:bg-primary-200 data-[selected]:bg-primary-600 data-[selected]:text-white data-[active]:data-[selected]:underline",
110
+ "block text-start truncate px-2 py-1 leading-normal cursor-pointer",
111
+ "data-active:bg-primary-200 data-selected:bg-primary-600 data-selected:text-white data-active:data-selected:underline",
91
112
  )}
92
113
  {...itemProps(item)}
93
114
  >
94
- <Trans
95
- i18nKey="form:select.createOption"
96
- values={{ query: item.value }}
97
- components={[<strong key="t0">query</strong>]}
98
- />
115
+ {children(item, item[labelProperty] ?? item)}
99
116
  </div>
100
117
  );
101
-
102
- return (
103
- <div
104
- role="none"
105
- key={index}
106
- tabIndex={active === index ? 0 : -1}
107
- ref={(el: AnyObject) => itemRef(index, el)}
108
- data-active={active === index ? true : undefined}
109
- className={classNames(
110
- "block text-start truncate px-2 py-1 leading-normal cursor-pointer",
111
- "data-[active]:bg-primary-200 data-[selected]:bg-primary-600 data-[selected]:text-white data-[active]:data-[selected]:underline",
112
- )}
113
- {...itemProps(item)}
114
- >
115
- {children(item, item[labelProperty] ?? item)}
116
- </div>
117
- );
118
- })}
118
+ })}
119
+ </div>
120
+ {info && (
121
+ <div className="px-2 py-1 text-sm bg-dimmed text-muted">{info}</div>
122
+ )}
119
123
  </div>
120
124
  );
121
125
  };
@@ -44,6 +44,14 @@ import { type SelectProps } from "../types";
44
44
  import { Options } from "./Options";
45
45
  import { useSelect } from "./useSelect";
46
46
 
47
+ /**
48
+ * Select component for selecting items from a dropdown list.
49
+ * It supports single and multiple selections, searching, creating new options,
50
+ * and custom rendering of options.
51
+ *
52
+ * @param {SelectProps<T>} props - The properties for the Select component.
53
+ * @returns {JSX.Element} The rendered Select component.
54
+ */
47
55
  export const Select = <T extends AnyObject = string>({
48
56
  ref,
49
57
  name,
@@ -56,12 +64,17 @@ export const Select = <T extends AnyObject = string>({
56
64
  autoFocus,
57
65
  error,
58
66
  options,
67
+ defaultOpen,
68
+ hideHandle,
59
69
  groupProperty,
60
70
  labelProperty = "label" as AnyObject,
61
71
  valueProperty = "id" as AnyObject,
72
+ sortProperty,
73
+ infoProperty,
62
74
  multiple,
63
75
  searchable,
64
76
  allowCreate,
77
+ emptyDisplay,
65
78
  createOption,
66
79
  matcher,
67
80
  renderer,
@@ -86,9 +99,11 @@ export const Select = <T extends AnyObject = string>({
86
99
  value,
87
100
  options,
88
101
  multiple,
102
+ defaultOpen,
89
103
  groupProperty,
90
104
  labelProperty,
91
105
  valueProperty,
106
+ sortProperty,
92
107
  allowCreate,
93
108
  createOption,
94
109
  matcher,
@@ -172,6 +187,9 @@ export const Select = <T extends AnyObject = string>({
172
187
  return false;
173
188
  } else if (state.query) {
174
189
  handleQuery("");
190
+ evt.preventDefault();
191
+ evt.stopPropagation();
192
+ return false;
175
193
  } else {
176
194
  onEnterPressed?.(evt);
177
195
  }
@@ -263,7 +281,7 @@ export const Select = <T extends AnyObject = string>({
263
281
  required={required}
264
282
  error={error}
265
283
  wrapperRef={refs.setReference}
266
- className={state.open ? "!z-[99]" : ""}
284
+ className={state.open ? "z-99!" : ""}
267
285
  {...rest}
268
286
  >
269
287
  <div
@@ -290,7 +308,7 @@ export const Select = <T extends AnyObject = string>({
290
308
  ref={ref}
291
309
  autoFocus={autoFocus}
292
310
  className={classNames(
293
- "appearance-none bg-transparent outline-none border-none ring-0 flex-1",
311
+ "appearance-none bg-transparent outline-none border-none ring-0 flex-1 peer",
294
312
  disabled && "cursor-not-allowed pointer-events-none",
295
313
  multiple && "min-w-24",
296
314
  )}
@@ -298,7 +316,7 @@ export const Select = <T extends AnyObject = string>({
298
316
  onChange={(evt) => handleQuery(evt.target.value)}
299
317
  />
300
318
  </div>
301
- {!state.loading && (
319
+ {!state.loading && !hideHandle && (
302
320
  <Icon
303
321
  className={classNames(
304
322
  "flex-content p-2 z-0 order-9 text-muted cursor-pointer self-stretch flex items-center h-auto",
@@ -327,7 +345,7 @@ export const Select = <T extends AnyObject = string>({
327
345
  >
328
346
  <Options
329
347
  ref={refs.setFloating}
330
- className="outline shadow-lg max-h-[40vh] z-[var(--z-popover)]"
348
+ className="outline shadow-lg max-h-[40vh] z-(--z-popover)"
331
349
  style={{
332
350
  width: refs.reference.current?.getBoundingClientRect().width,
333
351
  ...floatingStyles,
@@ -339,8 +357,10 @@ export const Select = <T extends AnyObject = string>({
339
357
  labelProperty={labelProperty}
340
358
  itemRef={setItemRef}
341
359
  itemProps={makeItemProps}
360
+ empty={emptyDisplay}
361
+ info={state.activeItem?.[infoProperty ?? "info"]}
342
362
  >
343
- {(item, label) => renderer?.(item) ?? label}
363
+ {async (item, label) => await (renderer?.(item) ?? label)}
344
364
  </Options>
345
365
  </FloatingFocusManager>
346
366
  </FloatingPortal>
@@ -23,6 +23,7 @@
23
23
 
24
24
  import { useDebounce } from "@react-fabric/core";
25
25
  import {
26
+ compareValues,
26
27
  dedupe,
27
28
  EMPTY_ARRAY,
28
29
  groupBy,
@@ -64,6 +65,7 @@ interface State {
64
65
  open: boolean;
65
66
  loading: boolean;
66
67
  activeIndex: number | null;
68
+ activeItem: AnyObject;
67
69
  selectedIndex: number | null;
68
70
  value: AnyObject;
69
71
  // filter
@@ -76,11 +78,14 @@ export const useSelect = ({
76
78
  options = EMPTY_ARRAY,
77
79
  value,
78
80
  autoComplete,
81
+ alwaysOpen = false,
82
+ defaultOpen = false,
79
83
  multiple = false,
80
84
  allowCreate = false,
81
85
  groupProperty = "",
82
86
  valueProperty = "id",
83
87
  labelProperty = "label",
88
+ sortProperty,
84
89
  createOption = makeOption,
85
90
  matcher,
86
91
  onQuery,
@@ -90,10 +95,13 @@ export const useSelect = ({
90
95
  value?: AnyObject;
91
96
  options?: AnyObject[];
92
97
  multiple?: boolean;
98
+ alwaysOpen?: boolean;
99
+ defaultOpen?: boolean;
93
100
  allowCreate?: boolean;
94
101
  groupProperty?: AnyObject;
95
102
  valueProperty?: AnyObject;
96
103
  labelProperty?: AnyObject;
104
+ sortProperty?: AnyObject;
97
105
  autoComplete?: boolean;
98
106
  matcher?: (item: AnyObject, query: string) => boolean;
99
107
  createOption?: (query: string) => AnyObject;
@@ -144,12 +152,16 @@ export const useSelect = ({
144
152
  };
145
153
  }
146
154
  if (action.type === "close") {
147
- return { ...state, activeIndex: null, open: false };
155
+ return { ...state, activeIndex: null, activeItem: null, open: false };
148
156
  }
149
157
  if (action.type === "active" && state.open) {
150
158
  if (autoComplete && action.index !== null)
151
159
  state.query = state.items[action.index];
152
- return { ...state, activeIndex: action.index };
160
+ return {
161
+ ...state,
162
+ activeIndex: action.index,
163
+ activeItem: state.items[action.index ?? -1],
164
+ };
153
165
  }
154
166
  if (action.type === "setValue") {
155
167
  return {
@@ -165,6 +177,7 @@ export const useSelect = ({
165
177
  ...state,
166
178
  value: state.emptyValue,
167
179
  activeIndex: null,
180
+ activeItem: null,
168
181
  selectedIndex: autoComplete
169
182
  ? state.multiple
170
183
  ? action.value?.length
@@ -187,7 +200,7 @@ export const useSelect = ({
187
200
  newState.value = action.value;
188
201
  }
189
202
  }
190
- if (!state.multiple) newState.open = false;
203
+ if (!state.multiple) newState.open = alwaysOpen;
191
204
  fireChange(newState.value);
192
205
  return newState;
193
206
  }
@@ -223,6 +236,8 @@ export const useSelect = ({
223
236
  if (autoComplete && action.query !== undefined) {
224
237
  optionList = dedupe([action.query, ...optionList]);
225
238
  }
239
+ if (sortProperty)
240
+ optionList = optionList.sort(compareValues("asc", sortProperty));
226
241
  const grouped = groupBy(optionList, groupProperty, "");
227
242
  if (action.open) newState.open = true;
228
243
  newState.items = [];
@@ -230,7 +245,7 @@ export const useSelect = ({
230
245
  if (allowCreate && action.query)
231
246
  newState.items.push({
232
247
  ___create___: true,
233
- value: createOption(action.query),
248
+ value: action.query,
234
249
  });
235
250
  Object.entries(grouped).forEach(([key, list]) => {
236
251
  if (key) {
@@ -254,9 +269,10 @@ export const useSelect = ({
254
269
  {
255
270
  multiple,
256
271
  emptyValue,
257
- open: false,
272
+ open: alwaysOpen || defaultOpen,
258
273
  loading: false,
259
274
  activeIndex: null,
275
+ activeItem: null,
260
276
  selectedIndex: null,
261
277
  value: emptyValue,
262
278
  // filter
@@ -283,7 +299,7 @@ export const useSelect = ({
283
299
  dispatch({ type: "options", options: optionsRef.current });
284
300
  dispatch({
285
301
  type: "setValue",
286
- value: hasValue ?? newOption ?? "",
302
+ value: hasValue ?? newOption ?? deferred,
287
303
  });
288
304
  }
289
305
  if (multiple) {
@@ -295,12 +311,12 @@ export const useSelect = ({
295
311
  ? createOption(item)
296
312
  : undefined;
297
313
  const hasValue = options.find(
298
- (opt: AnyObject) => (opt[valueProperty] ?? opt) === deferred,
314
+ (opt: AnyObject) => (opt[valueProperty] ?? opt) === item,
299
315
  );
300
316
  if (!hasValue && newOption) {
301
317
  optionsRef.current.push(newOption);
302
318
  }
303
- return hasValue ?? newOption ?? "";
319
+ return hasValue ?? newOption ?? item;
304
320
  },
305
321
  );
306
322
  optionsRef.current = [...optionsRef.current, ...(options ?? [])];
@@ -334,6 +350,7 @@ export const useSelect = ({
334
350
  });
335
351
  },
336
352
  [onQuery],
353
+ 300,
337
354
  );
338
355
 
339
356
  // handlers
@@ -399,7 +416,7 @@ export const useSelect = ({
399
416
  }, []);
400
417
 
401
418
  const setActiveIndex = useCallback((index: number | null) => {
402
- dispatch({ type: "active", index });
419
+ index && index > -1 && dispatch({ type: "active", index });
403
420
  }, []);
404
421
 
405
422
  return {
@@ -25,16 +25,21 @@ import {
25
25
  type RefProp,
26
26
  type TestProps,
27
27
  } from "@react-fabric/core/dist/types/types";
28
+ import { type ReactElement } from "react";
28
29
 
29
30
  export interface InputProps<V = string> extends TestProps {
30
31
  /**
31
32
  * value
32
33
  */
33
- value?: V;
34
+ value?: V | null;
35
+ /**
36
+ * default value
37
+ */
38
+ defaultValue?: V | null;
34
39
  /**
35
40
  * change handler
36
41
  */
37
- onChange?: (value?: V) => void;
42
+ onChange?: (value: V | null) => void;
38
43
  /**
39
44
  * on focus event handler
40
45
  */
@@ -72,11 +77,15 @@ export interface InputProps<V = string> extends TestProps {
72
77
  *
73
78
  */
74
79
  decorateEnd?: JSX.Element | string | number | boolean;
80
+ decorateEndShowWhenEmpty?: boolean;
81
+ decorateEndHideWhenEmpty?: boolean;
75
82
  /**
76
83
  * info label at bottom
77
84
  */
78
85
  info?: string;
79
86
 
87
+ monospace?: boolean;
88
+
80
89
  /**
81
90
  * required
82
91
  */
@@ -120,9 +129,15 @@ export interface InputProps<V = string> extends TestProps {
120
129
  labelWidth?: string;
121
130
  }
122
131
 
123
- export interface BaseSelectProps<T>
124
- extends RefProp<HTMLInputElement>,
125
- Omit<InputProps, "value" | "onChange"> {
132
+ export interface BaseSelectProps<T extends AnyObject = string> {
133
+ /**
134
+ * open dropdown by default
135
+ */
136
+ defaultOpen?: boolean;
137
+ /**
138
+ * hide dropdown handle
139
+ */
140
+ hideHandle?: boolean;
126
141
  /**
127
142
  * list options
128
143
  */
@@ -139,6 +154,14 @@ export interface BaseSelectProps<T>
139
154
  * value property name
140
155
  */
141
156
  valueProperty?: keyof T;
157
+ /**
158
+ * value property name
159
+ */
160
+ infoProperty?: keyof T;
161
+ /**
162
+ * value property name
163
+ */
164
+ sortProperty?: keyof T | true;
142
165
  /**
143
166
  * searchable list
144
167
  */
@@ -147,6 +170,10 @@ export interface BaseSelectProps<T>
147
170
  * allow create item
148
171
  */
149
172
  allowCreate?: boolean;
173
+ /**
174
+ * empty list display
175
+ */
176
+ emptyDisplay?: ReactElement;
150
177
  /**
151
178
  * create new item object
152
179
  */
@@ -165,7 +192,11 @@ export interface BaseSelectProps<T>
165
192
  matcher?: (option: T, query: string) => boolean;
166
193
  }
167
194
 
168
- export type SelectProps<T> = BaseSelectProps<T> &
195
+ type BaseSelectType<T> = BaseSelectProps<T> &
196
+ RefProp<HTMLInputElement> &
197
+ Omit<InputProps, "value" | "onChange">;
198
+
199
+ export type SelectProps<T> = BaseSelectType<T> &
169
200
  (
170
201
  | {
171
202
  /**
@@ -21,80 +21,135 @@
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
- interface BaseSchemaDef {
24
+ import { type Elements } from "@react-fabric/core/dist/types/types";
25
+ import { type DateLike } from "@react-fabric/date";
26
+
27
+ export enum DATA_TYPES {
28
+ TEXT = "text",
29
+ COLOR = "color",
30
+ BOOL = "bool",
31
+ DATE = "date",
32
+ DATETIME = "datetime",
33
+ STRING = "string",
34
+ NUMBER = "number",
35
+ RANGE = "range",
36
+ FILE = "file",
37
+ AVATAR = "avatar",
38
+ SCHEMA = "schema",
39
+ }
40
+
41
+ export const SCHEMA_CUSTOM_LIST = "--CUSTOM--";
42
+
43
+ interface SingleValue {
44
+ multiple?: false;
45
+ minItems?: never;
46
+ maxItems?: never;
47
+ }
48
+
49
+ interface MultipleValue {
50
+ multiple: true;
51
+ minItems?: number;
52
+ maxItems?: number;
53
+ }
54
+
55
+ export type ValueType = SingleValue | MultipleValue;
56
+
57
+ interface BaseSchema {
25
58
  id: string;
26
59
  label: string;
27
60
  datatype: string;
28
61
  required?: boolean;
29
- multiple?: boolean;
30
- defaultValue?: AnyObject;
31
62
  }
32
63
 
33
- interface SchemaDef extends BaseSchemaDef {
34
- datatype: "text" | "color";
64
+ export interface DefaultSchema extends BaseSchema {
65
+ datatype: DATA_TYPES.TEXT | DATA_TYPES.COLOR;
35
66
  }
36
67
 
37
- interface BooleanSchema extends BaseSchemaDef {
38
- datatype: "boolean";
39
- defaultValue?: boolean;
68
+ export interface BooleanSchema extends BaseSchema {
69
+ datatype: DATA_TYPES.BOOL;
40
70
  }
41
71
 
42
- interface StringSchema extends BaseSchemaDef {
43
- datatype: "string";
44
- options?: string[];
72
+ export interface StringSchema extends BaseSchema {
73
+ datatype: DATA_TYPES.STRING;
45
74
  regex?: string;
46
- identifierType?: string;
75
+ options?: string[];
76
+ optionList?: string;
77
+ allowCustom?: boolean;
47
78
  }
48
79
 
49
- interface RangeSchema extends BaseSchemaDef {
50
- datatype: "range";
80
+ export interface RangeSchema extends BaseSchema {
81
+ datatype: DATA_TYPES.RANGE;
51
82
  min?: number;
52
83
  max?: number;
53
84
  step?: number;
54
- defaultValue?: [number, number];
55
85
  }
56
86
 
57
- interface NumberSchema extends BaseSchemaDef {
58
- datatype: "number";
59
- min?: number;
60
- max?: number;
61
- step?: number;
62
- defaultValue?: number;
87
+ export interface DateSchema extends BaseSchema {
88
+ datatype: DATA_TYPES.DATE | DATA_TYPES.DATETIME;
89
+ min?: DateLike;
90
+ max?: DateLike;
63
91
  }
64
92
 
65
- interface DecimalSchema extends BaseSchemaDef {
66
- datatype: "decimal";
93
+ export interface NumberSchema extends BaseSchema {
94
+ datatype: DATA_TYPES.NUMBER;
67
95
  min?: number;
68
96
  max?: number;
69
97
  step?: number;
70
- defaultValue?: number;
71
98
  }
72
99
 
73
- export interface FileSchema extends BaseSchemaDef {
74
- datatype: "file";
100
+ export interface FileSchema extends BaseSchema {
101
+ datatype: DATA_TYPES.FILE;
75
102
  accept?: string;
76
103
  }
77
104
 
78
- export interface AvatarSchema extends BaseSchemaDef {
79
- datatype: "avatar";
105
+ export interface AvatarSchema extends BaseSchema {
106
+ datatype: DATA_TYPES.AVATAR;
80
107
  multiple?: false;
81
108
  }
82
109
 
83
110
  export type SchemaType =
84
- | SchemaDef
85
- | BooleanSchema
86
- | StringSchema
87
- | RangeSchema
88
- | NumberSchema
89
- | DecimalSchema
90
- | FileSchema
91
- | AvatarSchema;
92
-
93
- export interface ChildSchema extends BaseSchemaDef {
94
- datatype: "schema";
111
+ | (DefaultSchema & ValueType)
112
+ | (BooleanSchema & ValueType)
113
+ | (StringSchema & ValueType)
114
+ | (RangeSchema & ValueType)
115
+ | (DateSchema & ValueType)
116
+ | (NumberSchema & ValueType)
117
+ | (FileSchema & ValueType)
118
+ | (AvatarSchema & ValueType);
119
+
120
+ export interface ChildSchema extends BaseSchema {
121
+ datatype: DATA_TYPES.SCHEMA;
95
122
  inline?: boolean;
96
123
  schema: SchemaType[];
97
- defaultValue?: never;
98
124
  }
99
125
 
100
- export type FormSchema = SchemaType | ChildSchema;
126
+ export type SchemaDef = SchemaType | (ChildSchema & ValueType);
127
+ export type FormSchema = SchemaDef[];
128
+
129
+ export interface SchemaEditorProps {
130
+ /**
131
+ * default form schema
132
+ */
133
+ schemaDef: FormSchema;
134
+ /**
135
+ * is dynamic schema (allow add/remove)
136
+ */
137
+ dynamic?: boolean;
138
+ /**
139
+ * acceptable list of data types
140
+ */
141
+ acceptableTypes?: DATA_TYPES[];
142
+
143
+ /**
144
+ * available option lists in application
145
+ */
146
+ optionLists?: string[];
147
+
148
+ /**
149
+ * footer actions
150
+ */
151
+ children?: Elements<JSX.Element> | Array<Elements<JSX.Element>>;
152
+
153
+ onChange?: (schema: FormSchema) => void;
154
+ onSubmit?: (schema: FormSchema) => void;
155
+ }
@@ -23,19 +23,29 @@
23
23
 
24
24
  /* istanbul ignore file */
25
25
 
26
- import { debounce } from "@react-fabric/utilities";
26
+ import { useFloatingTree } from "@floating-ui/react";
27
+ import { useDebounce } from "@react-fabric/core";
27
28
  import { type KeyboardEvent, type MouseEvent } from "react";
28
29
 
29
30
  /** ***************** common handler for enter press *******************/
30
- export const handleEnter = (
31
+ export const useHandleEnter = (
31
32
  callback?: (e: KeyboardEvent) => void,
32
33
  {
33
34
  stopPropagation = false,
34
35
  preventDefault = false,
35
36
  }: { stopPropagation?: boolean; preventDefault?: boolean } = {},
36
37
  ) => {
37
- const cbHandle = debounce(callback ?? (() => undefined), 100);
38
- return (e: KeyboardEvent) => {
38
+ const cbHandle = useDebounce(callback ?? (() => undefined), [callback], 100);
39
+ const tree = useFloatingTree();
40
+ return (e: KeyboardEvent<HTMLInputElement>) => {
41
+ // prevent propagation of event if input within dropdown (work around for floating-ui typeahead)
42
+ if (tree && e.key !== "Escape") {
43
+ e?.stopPropagation();
44
+ }
45
+ // blur input on escape key press
46
+ if (e.key === "Escape") {
47
+ e.currentTarget.blur();
48
+ }
39
49
  if (
40
50
  !e.shiftKey &&
41
51
  !e.altKey &&