@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
@@ -48,7 +48,7 @@ export const ErrorIcon = ({ error = "", invalid }: KeyValue) => {
48
48
  ) as string)
49
49
  }
50
50
  placement="bottom"
51
- color="danger"
51
+ color="danger-200"
52
52
  >
53
53
  <Icon
54
54
  className="flex-content first:ms-2 self-center"
@@ -22,7 +22,7 @@
22
22
  */
23
23
 
24
24
  import classNames from "classnames";
25
- import classes from "./Field.module.css";
25
+ import { ErrorIcon } from "./ErrorIcon";
26
26
 
27
27
  export const FieldWrapper = ({
28
28
  label,
@@ -36,21 +36,23 @@ export const FieldWrapper = ({
36
36
  children,
37
37
  listing,
38
38
  className,
39
+ invalid,
40
+ error,
39
41
  "data-inner": isInner,
40
42
  }: KeyValue) => {
41
43
  return (
42
44
  <div
43
45
  className={classNames(
44
- classes.fieldWrapper,
46
+ "fabric-fieldWrapper",
45
47
  className,
46
48
  inline ? "flex flex-nowrap gap-1" : "block",
47
49
  )}
48
50
  data-ref="fieldWrapper"
49
51
  style={{ width, flexBasis: width }}
50
52
  >
51
- {!isInner && (
53
+ {!isInner && label && (
52
54
  <div
53
- className="flex flex-nowrap flex-content gap-1 py-0.5 px-2"
55
+ className="flex flex-nowrap flex-content items-end gap-1 py-0.5 px-2"
54
56
  style={{ width: inline ? labelWidth : undefined }}
55
57
  >
56
58
  <label
@@ -73,6 +75,8 @@ export const FieldWrapper = ({
73
75
  ✽
74
76
  </span>
75
77
  )}
78
+ <ErrorIcon invalid={invalid} error={error} />
79
+ <div className="flex-1" />
76
80
  {appendLabel && <span className="flex-content">{appendLabel}</span>}
77
81
  </div>
78
82
  )}
@@ -21,7 +21,6 @@
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 { FloatingPortal } from "@floating-ui/react";
25
24
  import { CoreIcons, Icon } from "@react-fabric/core";
26
25
  import { isString } from "@react-fabric/utilities";
27
26
  import classNames from "classnames";
@@ -35,7 +34,6 @@ import {
35
34
  } from "react";
36
35
  import { Fragment } from "react/jsx-runtime";
37
36
  import { ErrorIcon } from "./ErrorIcon";
38
- import classes from "./Field.module.css";
39
37
  import { FieldWrapper } from "./FieldWrapper";
40
38
 
41
39
  export const InputWrapper = ({
@@ -45,11 +43,15 @@ export const InputWrapper = ({
45
43
  showClear,
46
44
  decorateStart,
47
45
  decorateEnd,
46
+ decorateEndShowWhenEmpty,
47
+ decorateEndHideWhenEmpty,
48
48
  children,
49
49
  onClear,
50
50
  noOutline,
51
51
  noBorder,
52
+ actionButton,
52
53
  textarea = false,
54
+ textareaExpandable = false,
53
55
  "data-inner": isInner,
54
56
  "data-open": isOpen,
55
57
  // expand autocomplete
@@ -72,9 +74,12 @@ export const InputWrapper = ({
72
74
 
73
75
  const handleClear = useCallback(
74
76
  (e: MouseEvent<HTMLDivElement>) => {
75
- e.currentTarget.parentElement
76
- ?.querySelector<HTMLElement>("input,textarea,button")
77
- ?.focus();
77
+ const parent = e.currentTarget.parentElement?.querySelector(
78
+ "& > :not(.fabric-decorate)",
79
+ );
80
+ const el = (parent?.querySelector<HTMLElement>("input,textarea,button") ??
81
+ parent) as HTMLElement;
82
+ el?.focus();
78
83
  onClear?.();
79
84
  },
80
85
  [onClear],
@@ -82,9 +87,12 @@ export const InputWrapper = ({
82
87
 
83
88
  const doFocus = useCallback(
84
89
  (e: MouseEvent<HTMLDivElement>) => {
85
- e.currentTarget.parentElement
86
- ?.querySelector<HTMLElement>("input,textarea,button")
87
- ?.focus();
90
+ const parent = e.currentTarget.parentElement?.querySelector(
91
+ "& > :not(.fabric-decorate)",
92
+ );
93
+ const el = (parent?.querySelector<HTMLElement>("input,textarea,button") ??
94
+ parent) as HTMLElement;
95
+ el?.focus();
88
96
  },
89
97
  [onClear],
90
98
  );
@@ -102,28 +110,32 @@ export const InputWrapper = ({
102
110
  role="none"
103
111
  ref={wrapperRef}
104
112
  className={classNames(
105
- classes.inputWrapper,
106
- invalid && classes.invalidWrapper,
113
+ "fabric-inputWrapper",
114
+ invalid && "fabric-invalidWrapper",
107
115
  !noOutline &&
108
116
  !noBorder &&
109
- "bg-[var(--fabric-input)] outline overflow-hidden",
110
- "flex flex-nowrap items-center",
117
+ "bg-(--fabric-input) outline overflow-hidden",
118
+ "flex flex-nowrap items-center flex-1",
111
119
  textarea ? "rounded-capped group" : "rounded",
112
120
  isExpanded
113
- ? "fixed inset-4 z-[99]"
121
+ ? "fixed inset-4 z-99"
114
122
  : floatingExpand
115
123
  ? "absolute inset-x-0"
116
124
  : "relative",
117
125
  !noOutline &&
118
126
  "after:absolute after:bottom-0 after:h-px after:bg-primary-500 after:mx-auto",
119
127
  )}
120
- onKeyDown={(e) => e.key === "Escape" && setExpanded(false)}
128
+ onKeyDown={(e) =>
129
+ e.key === "Escape" &&
130
+ isExpanded &&
131
+ (setExpanded(false), e.stopPropagation())
132
+ }
121
133
  >
122
134
  {decorateStart && (
123
135
  <div
124
136
  onClickCapture={doFocus}
125
137
  className={classNames(
126
- classes.decorate,
138
+ "fabric-decorate",
127
139
  "text-muted leading-none flex-content",
128
140
  isString(decorateStart) && "px-1",
129
141
  )}
@@ -139,7 +151,7 @@ export const InputWrapper = ({
139
151
  onClick={handleClear}
140
152
  className={classNames(
141
153
  "text-tint-500 hover:text-tint-700 p-1 z-1 flex-content",
142
- classes.inputClear,
154
+ "fabric-inputClear",
143
155
  textarea && "self-start mt-1 me-1",
144
156
  )}
145
157
  />
@@ -148,15 +160,29 @@ export const InputWrapper = ({
148
160
  <div
149
161
  onClickCapture={doFocus}
150
162
  className={classNames(
151
- classes.decorate,
163
+ "fabric-decorate",
152
164
  "text-muted leading-none flex-content",
153
165
  isString(decorateEnd) && "px-1",
166
+ decorateEndHideWhenEmpty && "peer-placeholder-shown:hidden",
167
+ decorateEndShowWhenEmpty &&
168
+ "hidden peer-placeholder-shown:inline-block",
154
169
  )}
155
170
  >
156
171
  {decorateEnd}
157
172
  </div>
158
173
  )}
159
- {textarea && (
174
+ {actionButton && (
175
+ <div
176
+ onClickCapture={doFocus}
177
+ className={classNames(
178
+ "fabric-decorate",
179
+ "text-muted leading-none flex-content",
180
+ )}
181
+ >
182
+ {actionButton}
183
+ </div>
184
+ )}
185
+ {textareaExpandable && (
160
186
  <Icon
161
187
  onClick={() => setExpanded(!isExpanded)}
162
188
  icon={isExpanded ? CoreIcons.arrowCollapse : CoreIcons.arrowExpand}
@@ -170,17 +196,13 @@ export const InputWrapper = ({
170
196
  <Wrapper {...wrapperProps}>
171
197
  <div
172
198
  ref={elRef}
173
- className="flex-1 relative min-h-8"
199
+ className={classNames(
200
+ "relative min-h-8 flex flex-col flex-nowrap",
201
+ width ? "flex-auto" : "flex-1",
202
+ )}
174
203
  style={{ width, flexBasis: width }}
175
204
  >
176
- {!isExpanded && el}
177
- {isExpanded && (
178
- <FloatingPortal
179
- root={elRef.current?.closest<HTMLElement>(".theme-base")}
180
- >
181
- {el}
182
- </FloatingPortal>
183
- )}
205
+ {el}
184
206
  </div>
185
207
  </Wrapper>
186
208
  );
@@ -61,6 +61,13 @@ type AutoCompleteProps = Omit<
61
61
  onInput?: (query: string) => void;
62
62
  };
63
63
 
64
+ /**
65
+ * AutoComplete component for selecting or entering values with history suggestions.
66
+ * It supports multiple selections, history suggestions, and custom styling.
67
+ *
68
+ * @param {AutoCompleteProps} props - The properties for the AutoComplete component.
69
+ * @returns {JSX.Element} The rendered AutoComplete component.
70
+ */
64
71
  export const AutoComplete = ({
65
72
  ref,
66
73
  name,
@@ -80,6 +87,7 @@ export const AutoComplete = ({
80
87
  onInput,
81
88
  onFocus,
82
89
  onBlur,
90
+ onQuery,
83
91
  onEnterPressed,
84
92
  ...rest
85
93
  }: AutoCompleteProps) => {
@@ -104,6 +112,7 @@ export const AutoComplete = ({
104
112
  multiple,
105
113
  onChange,
106
114
  onSelect,
115
+ onQuery,
107
116
  });
108
117
 
109
118
  const dropdownKey = useId();
@@ -181,7 +190,7 @@ export const AutoComplete = ({
181
190
  return false;
182
191
  } else {
183
192
  onEnterPressed?.(evt);
184
- submitRef.current?.click();
193
+ setOpen(false);
185
194
  }
186
195
  } else if (evt.key === "Backspace") {
187
196
  !state.query && multiple && handleRemove();
@@ -267,7 +276,7 @@ export const AutoComplete = ({
267
276
  required={required}
268
277
  error={error}
269
278
  wrapperRef={refs.setReference}
270
- className={!multiple && expandOnEdit && state.open ? "!z-[99]" : ""}
279
+ className={!multiple && expandOnEdit && state.open ? "z-99!" : ""}
271
280
  floatingExpand={!multiple && expandOnEdit && state.open}
272
281
  {...rest}
273
282
  >
@@ -291,7 +300,9 @@ export const AutoComplete = ({
291
300
  data-testid={name}
292
301
  name={name}
293
302
  size={1}
303
+ ref={ref}
294
304
  onInput={onInput}
305
+ autoFocus={autoFocus}
295
306
  rows={!multiple && expandOnEdit && state.open ? 5 : 1}
296
307
  className={classNames(
297
308
  "appearance-none bg-transparent outline-none border-none ring-0 flex-1 resize-none",
@@ -328,7 +339,7 @@ export const AutoComplete = ({
328
339
  >
329
340
  <Options
330
341
  ref={refs.setFloating}
331
- className="outline shadow-lg max-h-[40vh] z-[var(--z-popover)]"
342
+ className="outline shadow-lg max-h-[40vh] z-(--z-popover)"
332
343
  style={{
333
344
  width: refs.reference.current?.getBoundingClientRect().width,
334
345
  ...floatingStyles,
@@ -21,9 +21,18 @@
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 {
25
+ DndContext,
26
+ DragOverlay,
27
+ useDraggable,
28
+ useDroppable,
29
+ } from "@dnd-kit/core";
30
+ import { restrictToHorizontalAxis } from "@dnd-kit/modifiers";
24
31
  import { Button, ButtonGroup, CoreIcons } from "@react-fabric/core";
25
32
  import { type RefProp } from "@react-fabric/core/dist/types/types";
33
+ import { mergeRefs } from "@react-fabric/utilities";
26
34
  import {
35
+ type ReactElement,
27
36
  useCallback,
28
37
  useDeferredValue,
29
38
  useEffect,
@@ -32,14 +41,6 @@ import {
32
41
  } from "react";
33
42
  import { Field, type FieldProps } from "../input/Field";
34
43
  import { List } from "./List";
35
- import { restrictToHorizontalAxis } from "@dnd-kit/modifiers";
36
- import {
37
- DndContext,
38
- DragOverlay,
39
- useDraggable,
40
- useDroppable,
41
- } from "@dnd-kit/core";
42
- import { mergeRefs } from "@react-fabric/utilities";
43
44
 
44
45
  export interface DualListProps<T>
45
46
  extends Omit<FieldProps, "children">,
@@ -68,6 +69,10 @@ export interface DualListProps<T>
68
69
  * value property name
69
70
  */
70
71
  valueProperty?: keyof T;
72
+ /**
73
+ * empty list display
74
+ */
75
+ emptyDisplay?: ReactElement;
71
76
  /**
72
77
  * item renderer
73
78
  */
@@ -78,10 +83,22 @@ export interface DualListProps<T>
78
83
  onChange?: (value?: string[]) => void;
79
84
  }
80
85
 
86
+ /**
87
+ * DualList component allows users to select items from two lists.
88
+ * It displays two lists side by side, one for unselected items and one for selected items.
89
+ * Users can move items between the lists using buttons or drag-and-drop functionality.
90
+ * It supports multiple selections and provides options for grouping, labeling, and rendering items.
91
+ * It also includes features for handling empty states and custom rendering of items.
92
+ * This component is useful for scenarios where users need to manage selections from a larger set of options, such as in forms or settings.
93
+ *
94
+ * @param {DualListProps<T>} props - The properties for the DualList component.
95
+ * @returns {JSX.Element} The rendered DualList component.
96
+ */
81
97
  export const DualList = <T extends AnyObject = string>({
82
98
  ref,
83
99
  value,
84
100
  options,
101
+ emptyDisplay,
85
102
  groupProperty,
86
103
  height = "18rem",
87
104
  labelProperty = "label" as AnyObject,
@@ -210,6 +227,7 @@ export const DualList = <T extends AnyObject = string>({
210
227
  error={error}
211
228
  height={height}
212
229
  options={leftOptions}
230
+ emptyDisplay={emptyDisplay}
213
231
  groupProperty={groupProperty}
214
232
  labelProperty={labelProperty}
215
233
  valueProperty={valueProperty}
@@ -222,39 +240,35 @@ export const DualList = <T extends AnyObject = string>({
222
240
  <div className="self-center mx-2">
223
241
  <ButtonGroup vertical className="outline overflow-hidden bg-base">
224
242
  <Button
225
- rtlFlip
226
243
  size="sm"
227
244
  variant="link"
228
245
  aria-label="scroll to top"
229
246
  onClick={moveAllLeft}
230
- icon={CoreIcons.chevronsLeft}
247
+ icon={{ icon: CoreIcons.chevronsLeft, rtlFlip: true }}
231
248
  disabled={!rightOptions.length}
232
249
  />
233
250
  <Button
234
- rtlFlip
235
251
  size="sm"
236
252
  variant="link"
237
253
  aria-label="scroll up"
238
254
  onClick={moveRightSelected}
239
- icon={CoreIcons.chevronLeft}
255
+ icon={{ icon: CoreIcons.chevronLeft, rtlFlip: true }}
240
256
  disabled={!rightSelected.length}
241
257
  />
242
258
  <Button
243
- rtlFlip
244
259
  size="sm"
245
260
  variant="link"
246
261
  aria-label="scroll down"
247
262
  onClick={moveLeftSelected}
248
- icon={CoreIcons.chevronRight}
263
+ icon={{ icon: CoreIcons.chevronRight, rtlFlip: true }}
249
264
  disabled={!leftSelected.length}
250
265
  />
251
266
  <Button
252
- rtlFlip
253
267
  size="sm"
254
268
  variant="link"
255
269
  aria-label="scroll to bottom"
256
270
  onClick={moveAllRight}
257
- icon={CoreIcons.chevronsRight}
271
+ icon={{ icon: CoreIcons.chevronsRight, rtlFlip: true }}
258
272
  disabled={!leftOptions.length}
259
273
  />
260
274
  </ButtonGroup>
@@ -268,6 +282,7 @@ export const DualList = <T extends AnyObject = string>({
268
282
  allowClear
269
283
  height={height}
270
284
  options={rightOptions}
285
+ emptyDisplay={emptyDisplay}
271
286
  groupProperty={groupProperty}
272
287
  labelProperty={labelProperty}
273
288
  valueProperty={valueProperty}
@@ -32,13 +32,21 @@ import {
32
32
  import { Chip } from "@react-fabric/core";
33
33
  import { isArray, isEmpty } from "@react-fabric/utilities";
34
34
  import classNames from "classnames";
35
- import { Fragment, useCallback, useId, useMemo } from "react";
35
+ import { Fragment, useCallback, useId, useLayoutEffect, useMemo } from "react";
36
36
  import { FieldWrapper } from "../internal/FieldWrapper";
37
37
  import { InputWrapper } from "../internal/InputWrapper";
38
38
  import { type SelectProps } from "../types";
39
39
  import { Options } from "./Options";
40
40
  import { useSelect } from "./useSelect";
41
41
 
42
+ /**
43
+ * List component for selecting items from a list.
44
+ * It supports single and multiple selections, searching, creating new options,
45
+ * and custom rendering of options.
46
+ *
47
+ * @param {SelectProps<T>} props - The properties for the List component.
48
+ * @returns {JSX.Element} The rendered List component.
49
+ */
42
50
  export const List = <T extends AnyObject = string>({
43
51
  ref,
44
52
  name,
@@ -53,8 +61,11 @@ export const List = <T extends AnyObject = string>({
53
61
  groupProperty,
54
62
  labelProperty,
55
63
  valueProperty,
64
+ infoProperty,
65
+ sortProperty,
56
66
  multiple,
57
67
  allowCreate,
68
+ searchable,
58
69
  createOption,
59
70
  matcher,
60
71
  renderer,
@@ -62,6 +73,7 @@ export const List = <T extends AnyObject = string>({
62
73
  onChange,
63
74
  onSelect,
64
75
  onEnterPressed,
76
+ emptyDisplay,
65
77
  invalid,
66
78
  error,
67
79
  allowClear,
@@ -72,7 +84,9 @@ export const List = <T extends AnyObject = string>({
72
84
  // @ts-expect-error ignore
73
85
  "data-hide-selected": hideSelected,
74
86
  ...rest
75
- }: SelectProps<T> & { height?: string | number }) => {
87
+ }: Omit<SelectProps<T>, "defaultOpen" | "hideHandle"> & {
88
+ height?: string | number;
89
+ }) => {
76
90
  const Wrapper = useMemo(() => (isInner ? Fragment : FieldWrapper), [isInner]);
77
91
  const wrapperProps = useMemo(
78
92
  () => (isInner ? {} : { ...rest }),
@@ -93,9 +107,11 @@ export const List = <T extends AnyObject = string>({
93
107
  value,
94
108
  options,
95
109
  multiple,
110
+ alwaysOpen: true,
96
111
  groupProperty,
97
112
  labelProperty,
98
113
  valueProperty,
114
+ sortProperty,
99
115
  allowCreate,
100
116
  createOption,
101
117
  matcher,
@@ -166,9 +182,12 @@ export const List = <T extends AnyObject = string>({
166
182
  }
167
183
  } else if (evt.key === "Backspace") {
168
184
  !state.query && handleRemove();
169
- } else if (["ArrowUp", "ArrowDown"].includes(evt.key)) {
170
- if (state.activeIndex === null) {
171
- setActiveIndex(state.selectedIndex);
185
+ } else if (!searchable || ["ArrowUp", "ArrowDown"].includes(evt.key)) {
186
+ if (
187
+ ["ArrowUp", "ArrowDown"].includes(evt.key) &&
188
+ state.activeIndex === null
189
+ ) {
190
+ setActiveIndex(state.selectedIndex ?? 0);
172
191
  }
173
192
  setTimeout(() => {
174
193
  props.onKeyDown?.(evt);
@@ -225,6 +244,15 @@ export const List = <T extends AnyObject = string>({
225
244
  }
226
245
  }, [state.value, state.query, placeholder, renderer]);
227
246
 
247
+ useLayoutEffect(() => {
248
+ setTimeout(() => {
249
+ state.selectedIndex &&
250
+ listRef.current[state.selectedIndex]?.scrollIntoView({
251
+ block: "center",
252
+ });
253
+ }, 100);
254
+ }, [state.selectedIndex]);
255
+
228
256
  return (
229
257
  <Wrapper {...wrapperProps}>
230
258
  <InputWrapper
@@ -240,22 +268,34 @@ export const List = <T extends AnyObject = string>({
240
268
  wrapperRef={refs.setReference}
241
269
  >
242
270
  <div
271
+ role="none"
243
272
  data-select-display="true"
244
- className="group flex-1 py-1 px-2 truncate text-start flex gap-1 flex-wrap"
273
+ className={classNames(
274
+ "group flex-1 py-1 px-2 truncate text-start flex gap-1 relative min-h-5 justify-start",
275
+ multiple ? "flex-wrap" : "flex-nowrap overflow-hidden",
276
+ )}
277
+ {...referenceProps}
278
+ onMouseUp={(e) =>
279
+ e.currentTarget.querySelector<HTMLElement>("input")?.focus()
280
+ }
245
281
  >
246
282
  {!hideSelected && displayValue}
247
283
  <input
284
+ readOnly={!searchable || readOnly}
248
285
  disabled={disabled}
249
286
  value={state.query ?? ""}
250
287
  aria-invalid={invalid}
251
288
  data-testid={name}
252
289
  name={name}
290
+ size={1}
291
+ ref={ref}
292
+ autoFocus={autoFocus}
253
293
  className={classNames(
254
294
  "appearance-none bg-transparent outline-none border-none ring-0 flex-1 min-w-24",
255
295
  disabled && "cursor-not-allowed pointer-events-none",
296
+ multiple && "min-w-24",
256
297
  )}
257
298
  placeholder={isEmpty(state.value) ? placeholder : ""}
258
- {...referenceProps}
259
299
  onChange={(evt) => handleQuery(evt.target.value)}
260
300
  />
261
301
  </div>
@@ -277,10 +317,14 @@ export const List = <T extends AnyObject = string>({
277
317
  {...floatingProps}
278
318
  items={state.items}
279
319
  active={state.activeIndex}
320
+ valueProperty={valueProperty}
321
+ labelProperty={labelProperty}
280
322
  itemRef={setItemRef}
281
323
  itemProps={makeItemProps}
324
+ empty={emptyDisplay}
325
+ info={state.activeItem?.[infoProperty ?? "info"]}
282
326
  >
283
- {(item) => renderer?.(item) ?? item[labelProperty] ?? item}
327
+ {async (item, label) => await (renderer?.(item) ?? label)}
284
328
  </Options>
285
329
  </FloatingFocusManager>
286
330
  </Wrapper>