@react-fabric/form 1.0.0-beta.9 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/{src/input/ArrayInput.module.css → css/_arrayInput.css} +8 -8
  2. package/css/_colorPicker.css +95 -0
  3. package/{src/internal/Field.module.css → css/_field.css} +94 -33
  4. package/{src/input/File.module.css → css/_file.css} +21 -6
  5. package/css/styles.css +27 -0
  6. package/dist/cjs/builder/FileField.js +107 -78
  7. package/dist/cjs/builder/FormField.js +60 -39
  8. package/dist/cjs/builder/SchemaEditor.js +72 -0
  9. package/dist/cjs/builder/components/AddSchemaFields.js +64 -0
  10. package/dist/cjs/builder/components/SchemaArrayForm.js +146 -0
  11. package/dist/cjs/builder/components/SchemaField.js +423 -0
  12. package/dist/cjs/form/Controller.js +66 -19
  13. package/dist/cjs/form/Form.js +97 -39
  14. package/dist/cjs/form/useForm.js +32 -0
  15. package/dist/cjs/hooks/useFileUploader.js +74 -45
  16. package/dist/cjs/hooks/useFormBuilder.js +85 -31
  17. package/dist/cjs/i18n/{ar.json.js → ar/form.json.js} +5 -4
  18. package/dist/cjs/i18n/{en.json.js → en/form.json.js} +5 -5
  19. package/dist/cjs/i18n/index.js +4 -4
  20. package/dist/cjs/index.js +41 -2
  21. package/dist/cjs/input/ArrayInput.js +259 -135
  22. package/dist/cjs/input/AvatarInput.js +99 -63
  23. package/dist/cjs/input/Checkbox.js +24 -25
  24. package/dist/cjs/input/ColorInput.js +138 -81
  25. package/dist/cjs/input/ColorPicker.js +78 -95
  26. package/dist/cjs/input/DateInput.js +192 -0
  27. package/dist/cjs/input/Field.js +21 -20
  28. package/dist/cjs/input/File.js +85 -80
  29. package/dist/cjs/input/Hidden.js +27 -13
  30. package/dist/cjs/input/Input.js +49 -48
  31. package/dist/cjs/input/Number.js +46 -44
  32. package/dist/cjs/input/Password.js +51 -49
  33. package/dist/cjs/input/Radio.js +26 -16
  34. package/dist/cjs/input/RadioGroup.js +26 -11
  35. package/dist/cjs/input/{Range.js → RangeSlider.js} +61 -66
  36. package/dist/cjs/input/Search.js +58 -64
  37. package/dist/cjs/input/Slider.js +57 -66
  38. package/dist/cjs/input/Switch.js +29 -27
  39. package/dist/cjs/input/Textarea.js +65 -53
  40. package/dist/cjs/internal/ErrorIcon.js +9 -9
  41. package/dist/cjs/internal/FieldWrapper.js +23 -13
  42. package/dist/cjs/internal/InputWrapper.js +85 -87
  43. package/dist/cjs/ranger/index.js +7 -7
  44. package/dist/cjs/ranger/utils.js +9 -4
  45. package/dist/cjs/select/AutoComplete.js +93 -76
  46. package/dist/cjs/select/DualList.js +235 -154
  47. package/dist/cjs/select/List.js +122 -89
  48. package/dist/cjs/select/Options.js +58 -54
  49. package/dist/cjs/select/Select.js +119 -89
  50. package/dist/cjs/select/helpers.js +5 -5
  51. package/dist/cjs/select/useSelect.js +83 -58
  52. package/dist/cjs/types/schema.js +43 -0
  53. package/dist/cjs/utils/eventHandlers.js +16 -6
  54. package/dist/cjs/utils/getColor.js +6 -6
  55. package/dist/cjs/utils/getProps.js +49 -0
  56. package/dist/esm/builder/FileField.js +109 -80
  57. package/dist/esm/builder/FormField.js +60 -39
  58. package/dist/esm/builder/SchemaEditor.js +72 -0
  59. package/dist/esm/builder/components/AddSchemaFields.js +64 -0
  60. package/dist/esm/builder/components/SchemaArrayForm.js +146 -0
  61. package/dist/esm/builder/components/SchemaField.js +423 -0
  62. package/dist/esm/form/Controller.js +66 -19
  63. package/dist/esm/form/Form.js +97 -39
  64. package/dist/esm/form/useForm.js +32 -0
  65. package/dist/esm/hooks/useFileUploader.js +75 -46
  66. package/dist/esm/hooks/useFormBuilder.js +84 -30
  67. package/dist/esm/i18n/{ar.json.js → ar/form.json.js} +5 -4
  68. package/dist/esm/i18n/{en.json.js → en/form.json.js} +5 -5
  69. package/dist/esm/i18n/index.js +2 -2
  70. package/dist/esm/index.js +36 -1
  71. package/dist/esm/input/ArrayInput.js +260 -136
  72. package/dist/esm/input/AvatarInput.js +99 -63
  73. package/dist/esm/input/Checkbox.js +26 -27
  74. package/dist/esm/input/ColorInput.js +141 -84
  75. package/dist/esm/input/ColorPicker.js +80 -97
  76. package/dist/esm/input/DateInput.js +192 -0
  77. package/dist/esm/input/Field.js +21 -20
  78. package/dist/esm/input/File.js +89 -84
  79. package/dist/esm/input/Hidden.js +28 -14
  80. package/dist/esm/input/Input.js +51 -50
  81. package/dist/esm/input/Number.js +48 -46
  82. package/dist/esm/input/Password.js +54 -52
  83. package/dist/esm/input/Radio.js +27 -17
  84. package/dist/esm/input/RadioGroup.js +27 -12
  85. package/dist/esm/input/{Range.js → RangeSlider.js} +62 -67
  86. package/dist/esm/input/Search.js +61 -67
  87. package/dist/esm/input/Slider.js +58 -67
  88. package/dist/esm/input/Switch.js +31 -29
  89. package/dist/esm/input/Textarea.js +67 -55
  90. package/dist/esm/internal/ErrorIcon.js +11 -11
  91. package/dist/esm/internal/FieldWrapper.js +23 -13
  92. package/dist/esm/internal/InputWrapper.js +88 -90
  93. package/dist/esm/ranger/index.js +7 -7
  94. package/dist/esm/ranger/utils.js +9 -4
  95. package/dist/esm/select/AutoComplete.js +95 -78
  96. package/dist/esm/select/DualList.js +233 -152
  97. package/dist/esm/select/List.js +125 -92
  98. package/dist/esm/select/Options.js +58 -54
  99. package/dist/esm/select/Select.js +122 -92
  100. package/dist/esm/select/helpers.js +5 -5
  101. package/dist/esm/select/useSelect.js +84 -59
  102. package/{src/input/ColorPicker.module.css → dist/esm/types/schema.js} +18 -27
  103. package/dist/esm/utils/eventHandlers.js +16 -6
  104. package/dist/esm/utils/getColor.js +6 -6
  105. package/dist/esm/utils/getProps.js +49 -0
  106. package/dist/types/builder/FileField.d.ts +7 -4
  107. package/dist/types/builder/FormField.d.ts +4 -2
  108. package/dist/types/builder/SchemaEditor.d.ts +6 -0
  109. package/dist/types/builder/components/AddSchemaFields.d.ts +2 -0
  110. package/dist/types/builder/components/SchemaArrayForm.d.ts +2 -0
  111. package/dist/types/builder/components/SchemaField.d.ts +12 -0
  112. package/dist/types/form/Controller.d.ts +45 -3
  113. package/dist/types/form/Form.d.ts +38 -7
  114. package/dist/types/form/useForm.d.ts +6 -0
  115. package/dist/types/hooks/useFileUploader.d.ts +24 -3
  116. package/dist/types/hooks/useFormBuilder.d.ts +41 -3
  117. package/dist/types/index.d.ts +32 -2
  118. package/dist/types/input/ArrayInput.d.ts +68 -13
  119. package/dist/types/input/AvatarInput.d.ts +23 -6
  120. package/dist/types/input/Checkbox.d.ts +12 -10
  121. package/dist/types/input/ColorInput.d.ts +15 -5
  122. package/dist/types/input/ColorPicker.d.ts +10 -2
  123. package/dist/types/input/DateInput.d.ts +21 -0
  124. package/dist/types/input/Field.d.ts +9 -36
  125. package/dist/types/input/File.d.ts +16 -4
  126. package/dist/types/input/Hidden.d.ts +7 -6
  127. package/dist/types/input/Input.d.ts +11 -4
  128. package/dist/types/input/Number.d.ts +9 -4
  129. package/dist/types/input/Password.d.ts +9 -4
  130. package/dist/types/input/Radio.d.ts +9 -4
  131. package/dist/types/input/RadioGroup.d.ts +14 -6
  132. package/dist/types/input/{Range.d.ts → RangeSlider.d.ts} +11 -6
  133. package/dist/types/input/Search.d.ts +14 -4
  134. package/dist/types/input/Slider.d.ts +9 -6
  135. package/dist/types/input/Switch.d.ts +13 -8
  136. package/dist/types/input/Textarea.d.ts +15 -4
  137. package/dist/types/internal/ErrorIcon.d.ts +4 -1
  138. package/dist/types/internal/FieldWrapper.d.ts +10 -1
  139. package/dist/types/internal/InputWrapper.d.ts +19 -1
  140. package/dist/types/ranger/index.d.ts +7 -3
  141. package/dist/types/ranger/utils.d.ts +2 -2
  142. package/dist/types/select/AutoComplete.d.ts +7 -2
  143. package/dist/types/select/DualList.d.ts +11 -37
  144. package/dist/types/select/List.d.ts +7 -2
  145. package/dist/types/select/Options.d.ts +8 -7
  146. package/dist/types/select/Select.d.ts +7 -2
  147. package/dist/types/select/helpers.d.ts +2 -2
  148. package/dist/types/select/useSelect.d.ts +10 -28
  149. package/dist/types/types/index.d.ts +73 -39
  150. package/dist/types/types/schema.d.ts +78 -32
  151. package/dist/types/utils/eventHandlers.d.ts +4 -4
  152. package/dist/types/utils/getColor.d.ts +4 -4
  153. package/dist/types/utils/getProps.d.ts +26 -0
  154. package/dist/types/utils/index.d.ts +1 -0
  155. package/package.json +25 -25
  156. package/src/builder/FileField.tsx +73 -56
  157. package/src/builder/FormField.tsx +66 -58
  158. package/src/builder/SchemaEditor.tsx +78 -0
  159. package/src/builder/components/AddSchemaFields.tsx +64 -0
  160. package/src/builder/components/SchemaArrayForm.tsx +146 -0
  161. package/src/builder/components/SchemaField.tsx +405 -0
  162. package/src/form/Controller.tsx +61 -24
  163. package/src/form/Form.tsx +74 -50
  164. package/src/form/useForm.ts +53 -0
  165. package/src/hooks/useFileUploader.ts +115 -84
  166. package/src/hooks/useFormBuilder.tsx +80 -41
  167. package/src/i18n/{ar.json → ar/form.json} +5 -4
  168. package/src/i18n/{en.json → en/form.json} +5 -5
  169. package/src/i18n/index.ts +2 -2
  170. package/src/index.ts +34 -20
  171. package/src/input/ArrayInput.tsx +255 -173
  172. package/src/input/AvatarInput.tsx +82 -39
  173. package/src/input/Checkbox.tsx +30 -54
  174. package/src/input/ColorInput.tsx +146 -105
  175. package/src/input/ColorPicker.tsx +76 -102
  176. package/src/input/DateInput.tsx +225 -0
  177. package/src/input/Field.tsx +23 -65
  178. package/src/input/File.tsx +76 -98
  179. package/src/input/Hidden.tsx +36 -24
  180. package/src/input/Input.tsx +55 -53
  181. package/src/input/Number.tsx +49 -54
  182. package/src/input/Password.tsx +61 -66
  183. package/src/input/Radio.tsx +28 -37
  184. package/src/input/RadioGroup.tsx +30 -25
  185. package/src/input/{Range.tsx → RangeSlider.tsx} +81 -109
  186. package/src/input/Search.tsx +64 -77
  187. package/src/input/Slider.tsx +80 -109
  188. package/src/input/Switch.tsx +39 -53
  189. package/src/input/Textarea.tsx +72 -59
  190. package/src/internal/ErrorIcon.tsx +22 -21
  191. package/src/internal/FieldWrapper.tsx +33 -23
  192. package/src/internal/InputWrapper.tsx +132 -143
  193. package/src/ranger/index.tsx +37 -117
  194. package/src/ranger/utils.ts +12 -11
  195. package/src/select/AutoComplete.tsx +113 -123
  196. package/src/select/DualList.tsx +218 -204
  197. package/src/select/List.tsx +130 -97
  198. package/src/select/Options.tsx +59 -57
  199. package/src/select/Select.tsx +148 -122
  200. package/src/select/helpers.ts +13 -8
  201. package/src/select/useSelect.tsx +110 -127
  202. package/src/types/index.ts +76 -45
  203. package/src/types/schema.ts +97 -40
  204. package/src/utils/eventHandlers.ts +24 -26
  205. package/src/utils/getColor.ts +9 -8
  206. package/src/utils/getProps.ts +47 -0
  207. package/src/utils/index.ts +1 -0
  208. package/dist/cjs/input/ArrayInput.module.css +0 -1
  209. package/dist/cjs/input/ColorPicker.module.css +0 -1
  210. package/dist/cjs/input/File.module.css +0 -1
  211. package/dist/cjs/internal/Field.module.css +0 -1
  212. package/dist/esm/input/ArrayInput.module.css +0 -1
  213. package/dist/esm/input/ColorPicker.module.css +0 -1
  214. package/dist/esm/input/File.module.css +0 -1
  215. package/dist/esm/internal/Field.module.css +0 -1
@@ -21,107 +21,60 @@
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 { Button, ButtonGroup, CoreIcons } from "@react-fabric/core";
25
- import { type RefProp } from "@react-fabric/core/dist/types/types";
26
- import {
27
- useCallback,
28
- useDeferredValue,
29
- useEffect,
30
- useMemo,
31
- useState,
32
- } from "react";
33
- import { Field, type FieldProps } from "../input/Field";
24
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
25
+ /* eslint-disable @typescript-eslint/no-unsafe-argument */
26
+
27
+ import { RestrictToHorizontalAxis } from "@dnd-kit/abstract/modifiers";
28
+ import { DragDropProvider, DragOverlay, useDraggable, useDroppable } from "@dnd-kit/react";
29
+ import { Button, ButtonGroup, Icon } from "@react-fabric/core";
30
+ import { cn, mergeRefs } from "@react-fabric/utilities";
31
+ import { useCallback, useDeferredValue, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
32
+ import { Field } from "../input/Field";
33
+ import type { DualSelectProps } from "../types";
34
+ import { getWrapperProps } from "../utils";
34
35
  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
36
 
44
- export interface DualListProps<T>
45
- extends Omit<FieldProps, "children">,
46
- RefProp<HTMLInputElement> {
47
- /**
48
- * list height
49
- */
50
- height?: string | number;
51
- /**
52
- * list options
53
- */
54
- options: T[];
55
- /**
56
- * selected value
57
- */
58
- value?: string[];
59
- /**
60
- * group by property name
61
- */
62
- groupProperty?: keyof T;
63
- /**
64
- * label property name
65
- */
66
- labelProperty?: keyof T;
67
- /**
68
- * value property name
69
- */
70
- valueProperty?: keyof T;
71
- /**
72
- * item renderer
73
- */
74
- renderer?: (option: T) => React.ReactNode;
75
- /**
76
- * change handler
77
- */
78
- onChange?: (value?: string[]) => void;
79
- }
37
+ type DualRef = { moveLeftSelected: () => void; moveRightSelected: () => void };
80
38
 
81
- export const DualList = <T extends AnyObject = string>({
82
- ref,
83
- value,
84
- options,
85
- groupProperty,
86
- height = "18rem",
87
- labelProperty = "label" as AnyObject,
88
- valueProperty = "id" as AnyObject,
89
- renderer,
90
- onChange,
91
- // @ts-expect-error ignore
92
- name,
93
- // @ts-expect-error ignore
94
- invalid,
95
- // @ts-expect-error ignore
96
- readOnly,
97
- // @ts-expect-error ignore
98
- disabled,
99
- // @ts-expect-error ignore
100
- placeholder,
101
- // @ts-expect-error ignore
102
- autoFocus,
103
- // @ts-expect-error ignore
104
- error,
105
- ...rest
106
- }: DualListProps<T>) => {
107
- const [actualValue, setActualValue] = useState<T[]>([]);
108
- const [leftSelected, setLeftSelected] = useState<T[]>([]);
109
- const [rightSelected, setRightSelected] = useState<T[]>([]);
39
+ function DualListComponent<T extends AnyObject = string>(
40
+ props: DualSelectProps<T> & {
41
+ height?: string | number;
42
+ onSelectedChange: (left: T[], right: T[]) => void;
43
+ dualRef: React.Ref<DualRef>;
44
+ },
45
+ ) {
46
+ const {
47
+ ref,
48
+ value,
49
+ options,
50
+ emptyDisplay,
51
+ groupProperty,
52
+ height = "18rem",
53
+ labelProperty,
54
+ valueProperty = "value",
55
+ renderer,
56
+ onChange,
57
+ name,
58
+ invalid,
59
+ readOnly,
60
+ disabled,
61
+ autoFocus,
62
+ error,
63
+ dualRef,
64
+ onSelectedChange,
65
+ } = props;
66
+ const [actualValue, setActualValue] = useState<T[]>(() => []);
67
+ const [leftSelected, setLeftSelected] = useState<T[]>(() => []);
68
+ const [rightSelected, setRightSelected] = useState<T[]>(() => []);
110
69
  const deferred = useDeferredValue(value);
111
70
 
112
71
  useEffect(() => {
113
- setActualValue(
114
- options.filter((opt: AnyObject) =>
115
- deferred?.includes(opt[valueProperty] ?? opt),
116
- ),
117
- );
118
- }, [deferred, options]);
72
+ // eslint-disable-next-line @eslint-react/set-state-in-effect
73
+ setActualValue(options.filter((opt: AnyObject) => deferred?.includes(opt[valueProperty] ?? opt)));
74
+ }, [deferred, options, valueProperty]);
119
75
 
120
76
  const [leftOptions, rightOptions] = useMemo(() => {
121
- return [
122
- options.filter((opt: AnyObject) => !actualValue?.includes(opt)),
123
- actualValue,
124
- ];
77
+ return [options.filter((opt: AnyObject) => !actualValue?.includes(opt)), actualValue];
125
78
  }, [options, actualValue]);
126
79
 
127
80
  const moveAllLeft = useCallback(() => {
@@ -141,146 +94,207 @@ export const DualList = <T extends AnyObject = string>({
141
94
  const moveLeftSelected = useCallback(() => {
142
95
  const selected = [...actualValue, ...leftSelected];
143
96
  setActualValue(selected);
144
- onChange?.(
145
- selected.map((opt: AnyObject) => `${opt[valueProperty] ?? opt}`),
146
- );
97
+ onChange?.(selected.map((opt: AnyObject) => `${opt[valueProperty] ?? opt}`));
147
98
  setLeftSelected([]);
148
- }, [onChange, leftSelected, actualValue]);
99
+ }, [actualValue, leftSelected, onChange, valueProperty]);
149
100
 
150
101
  const moveRightSelected = useCallback(() => {
151
102
  const selected = actualValue.filter((opt) => !rightSelected.includes(opt));
152
103
  setActualValue(selected);
153
- onChange?.(
154
- selected.map((opt: AnyObject) => `${opt[valueProperty] ?? opt}`),
155
- );
104
+ onChange?.(selected.map((opt: AnyObject) => `${opt[valueProperty] ?? opt}`));
156
105
  setRightSelected([]);
157
- }, [onChange, rightSelected, actualValue]);
106
+ }, [actualValue, onChange, rightSelected, valueProperty]);
158
107
 
159
- const [dragItems, setDragItems] = useState<AnyObject[]>([]);
160
108
  const dragLeft = useDraggable({
161
109
  id: "unselected",
110
+ type: "unselected",
162
111
  disabled: !leftSelected.length,
163
- data: { supports: ["selected"] },
164
- attributes: { role: "none" },
165
112
  });
166
113
  const dragRight = useDraggable({
167
114
  id: "selected",
115
+ type: "selected",
168
116
  disabled: !rightSelected.length,
169
- data: { supports: ["unselected"] },
170
- attributes: { role: "none" },
171
117
  });
172
118
  const dropLeft = useDroppable({
173
119
  id: "unselected",
174
- data: { accepts: ["selected"] },
120
+ accept: ["selected"],
175
121
  });
176
122
  const dropRight = useDroppable({
177
123
  id: "selected",
178
- data: { accepts: ["unselected"] },
124
+ accept: ["unselected"],
179
125
  });
180
126
 
127
+ useImperativeHandle(
128
+ dualRef,
129
+ () => ({
130
+ moveLeftSelected,
131
+ moveRightSelected,
132
+ }),
133
+ [moveLeftSelected, moveRightSelected],
134
+ );
135
+
136
+ useEffect(() => {
137
+ onSelectedChange(leftSelected, rightSelected);
138
+ }, [leftSelected, onSelectedChange, rightSelected]);
139
+
181
140
  return (
182
- <Field {...rest} noOutline plain>
183
- <DndContext
184
- modifiers={[restrictToHorizontalAxis]}
141
+ <>
142
+ <div
143
+ className={cn(
144
+ "flex-1 flex flex-col overflow-hidden p-px border-none!",
145
+ dropLeft.isDropTarget && "outline outline-primary-500 rounded-capped!",
146
+ )}
147
+ data-id="unselected"
148
+ ref={mergeRefs(dragLeft.ref, dropLeft.ref)}
149
+ >
150
+ <List
151
+ ref={ref}
152
+ multiple
153
+ allowClear
154
+ name={name}
155
+ invalid={invalid}
156
+ readOnly={readOnly}
157
+ disabled={disabled}
158
+ {...{ autoFocus }}
159
+ error={error}
160
+ height={height}
161
+ options={leftOptions}
162
+ emptyDisplay={emptyDisplay}
163
+ groupProperty={groupProperty}
164
+ labelProperty={labelProperty}
165
+ searchable
166
+ placeholder="Filter"
167
+ decorateStart={<Icon icon="icon-[mdi--filter-variant]" className="ps-2" />}
168
+ // @ts-expect-error ignore
169
+ valueProperty={valueProperty}
170
+ renderer={renderer}
171
+ onSelect={setLeftSelected}
172
+ data-inner={true}
173
+ data-hide-selected={true}
174
+ />
175
+ </div>
176
+ <div className="self-center mx-2">
177
+ <ButtonGroup vertical className="outline overflow-hidden bg-default">
178
+ <Button
179
+ size="sm"
180
+ variant="link"
181
+ aria-label="scroll to top"
182
+ onClick={moveAllLeft}
183
+ icon={{ icon: "icon-[mdi--chevron-double-left]", rtlFlip: true }}
184
+ disabled={!rightOptions.length}
185
+ />
186
+ <Button
187
+ size="sm"
188
+ variant="link"
189
+ aria-label="scroll up"
190
+ onClick={moveRightSelected}
191
+ icon={{ icon: "icon-[mdi--chevron-left]", rtlFlip: true }}
192
+ disabled={!rightSelected.length}
193
+ />
194
+ <Button
195
+ size="sm"
196
+ variant="link"
197
+ aria-label="scroll down"
198
+ onClick={moveLeftSelected}
199
+ icon={{ icon: "icon-[mdi--chevron-right]", rtlFlip: true }}
200
+ disabled={!leftSelected.length}
201
+ />
202
+ <Button
203
+ size="sm"
204
+ variant="link"
205
+ aria-label="scroll to bottom"
206
+ onClick={moveAllRight}
207
+ icon={{ icon: "icon-[mdi--chevron-double-right]", rtlFlip: true }}
208
+ disabled={!leftOptions.length}
209
+ />
210
+ </ButtonGroup>
211
+ </div>
212
+ <div
213
+ className={cn(
214
+ "flex-1 flex flex-col overflow-hidden p-px border-none!",
215
+ dropRight.isDropTarget && "outline outline-primary-500 rounded-capped!",
216
+ )}
217
+ data-id="selected"
218
+ ref={mergeRefs(dropRight.ref, dragRight.ref)}
219
+ >
220
+ <List
221
+ multiple
222
+ allowClear
223
+ height={height}
224
+ options={rightOptions}
225
+ emptyDisplay={emptyDisplay}
226
+ groupProperty={groupProperty}
227
+ labelProperty={labelProperty}
228
+ placeholder="Filter"
229
+ decorateStart={<Icon icon="icon-[mdi--filter-variant]" className="ps-2" />}
230
+ // @ts-expect-error ignore
231
+ valueProperty={valueProperty}
232
+ searchable
233
+ renderer={renderer}
234
+ onSelect={setRightSelected}
235
+ data-inner={true}
236
+ data-hide-selected={true}
237
+ />
238
+ </div>
239
+ </>
240
+ );
241
+ }
242
+
243
+ /**
244
+ * DualList component allows users to select items from two lists.
245
+ * It displays two lists side by side, one for unselected items and one for selected items.
246
+ * Users can move items between the lists using buttons or drag-and-drop functionality.
247
+ * It supports multiple selections and provides options for grouping, labeling, and rendering items.
248
+ * It also includes features for handling empty states and custom rendering of items.
249
+ * This component is useful for scenarios where users need to manage selections from a larger set of options, such as in forms or settings.
250
+ */
251
+ export function DualList<T extends AnyObject = string>(props: DualSelectProps<T> & { height?: string | number }) {
252
+ const [dragCount, setDragCount] = useState(0);
253
+ const [leftSelected, setLeftSelected] = useState<T[]>(() => []);
254
+ const [rightSelected, setRightSelected] = useState<T[]>(() => []);
255
+ const dualRef = useRef<DualRef>(null);
256
+
257
+ return (
258
+ <Field {...getWrapperProps(props)} borderless>
259
+ <DragDropProvider
260
+ modifiers={[RestrictToHorizontalAxis]}
185
261
  onDragStart={(e) => {
186
- setDragItems(
187
- e.active.id === "selected" ? rightSelected : leftSelected,
188
- );
262
+ setDragCount(e.operation.source?.id === "selected" ? rightSelected.length : leftSelected.length);
189
263
  }}
190
264
  onDragEnd={(e) => {
191
- setDragItems([]);
265
+ if (e.operation.source?.id === "selected") dualRef.current?.moveRightSelected();
266
+ if (e.operation.source?.id === "unselected") dualRef.current?.moveLeftSelected();
267
+ setDragCount(0);
192
268
  }}
193
269
  >
194
- <div
195
- className="flex-1 flex flex-col overflow-hidden p-px"
196
- ref={mergeRefs(dragLeft.setNodeRef, dropLeft.setNodeRef)}
197
- {...dragLeft.attributes}
198
- {...dragLeft.listeners}
199
- >
200
- <List
201
- ref={ref}
202
- multiple
203
- allowClear
204
- name={name}
205
- invalid={invalid}
206
- readOnly={readOnly}
207
- disabled={disabled}
208
- placeholder={placeholder}
209
- autoFocus={autoFocus}
210
- error={error}
211
- height={height}
212
- options={leftOptions}
213
- groupProperty={groupProperty}
214
- labelProperty={labelProperty}
215
- valueProperty={valueProperty}
216
- renderer={renderer}
217
- onSelect={setLeftSelected}
218
- data-inner={true}
219
- data-hide-selected={true}
220
- />
221
- </div>
222
- <div className="self-center mx-2">
223
- <ButtonGroup vertical className="outline overflow-hidden bg-base">
224
- <Button
225
- rtlFlip
226
- size="sm"
227
- variant="link"
228
- aria-label="scroll to top"
229
- onClick={moveAllLeft}
230
- icon={CoreIcons.chevronsLeft}
231
- disabled={!rightOptions.length}
232
- />
233
- <Button
234
- rtlFlip
235
- size="sm"
236
- variant="link"
237
- aria-label="scroll up"
238
- onClick={moveRightSelected}
239
- icon={CoreIcons.chevronLeft}
240
- disabled={!rightSelected.length}
241
- />
242
- <Button
243
- rtlFlip
244
- size="sm"
245
- variant="link"
246
- aria-label="scroll down"
247
- onClick={moveLeftSelected}
248
- icon={CoreIcons.chevronRight}
249
- disabled={!leftSelected.length}
250
- />
251
- <Button
252
- rtlFlip
253
- size="sm"
254
- variant="link"
255
- aria-label="scroll to bottom"
256
- onClick={moveAllRight}
257
- icon={CoreIcons.chevronsRight}
258
- disabled={!leftOptions.length}
259
- />
260
- </ButtonGroup>
261
- </div>
262
- <div
263
- className="flex-1 flex flex-col overflow-hidden p-px"
264
- ref={mergeRefs(dropRight.setNodeRef, dragRight.setNodeRef)}
270
+ <DualListComponent
271
+ {...props}
272
+ dualRef={dualRef}
273
+ onSelectedChange={(l, r) => (setLeftSelected(l), setRightSelected(r))}
274
+ />
275
+ <DragOverlay
276
+ className="mt-8! w-fit! h-fit!"
277
+ dropAnimation={({ element, feedbackElement, source }) => {
278
+ const target = element.parentElement?.querySelector(
279
+ `[data-id="${source.id === "selected" ? "unselected" : "selected"}"]`,
280
+ );
281
+
282
+ const overlayRect = element.getBoundingClientRect();
283
+ const targetRect = target?.getBoundingClientRect();
284
+
285
+ const deltaX = (targetRect?.left ?? 0) - overlayRect.left;
286
+ const deltaY = (targetRect?.top ?? 0) - overlayRect.top;
287
+
288
+ (feedbackElement as HTMLElement).style.setProperty("--dnd-translate", `${deltaX}px ${deltaY}px 0`);
289
+ }}
265
290
  >
266
- <List
267
- multiple
268
- allowClear
269
- height={height}
270
- options={rightOptions}
271
- groupProperty={groupProperty}
272
- labelProperty={labelProperty}
273
- valueProperty={valueProperty}
274
- renderer={renderer}
275
- onSelect={setRightSelected}
276
- data-inner={true}
277
- data-hide-selected={true}
278
- />
279
- </div>
280
- <DragOverlay>
281
- {dragItems.length > 0 && <span>{dragItems.length} Item(s)</span>}
291
+ {(evt) => {
292
+ if (!evt) return null;
293
+ // Render a clean visual preview without the drag hook styles
294
+ return <div className="bg-default text-xs outline px-2 py-1 size-fit rounded!">Moving {dragCount} item(s)</div>;
295
+ }}
282
296
  </DragOverlay>
283
- </DndContext>
297
+ </DragDropProvider>
284
298
  </Field>
285
299
  );
286
- };
300
+ }