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

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} +64 -29
  4. package/{src/input/File.module.css → css/_file.css} +21 -6
  5. package/css/styles.css +27 -0
  6. package/dist/cjs/builder/FileField.js +37 -25
  7. package/dist/cjs/builder/FormField.js +50 -34
  8. package/dist/cjs/builder/SchemaEditor.js +72 -0
  9. package/dist/cjs/builder/components/AddSchemaFields.js +62 -0
  10. package/dist/cjs/builder/components/SchemaArrayForm.js +143 -0
  11. package/dist/cjs/builder/components/SchemaField.js +414 -0
  12. package/dist/cjs/form/Controller.js +46 -1
  13. package/dist/cjs/form/Form.js +43 -6
  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 +11 -2
  19. package/dist/cjs/input/ArrayInput.js +100 -33
  20. package/dist/cjs/input/AvatarInput.js +17 -8
  21. package/dist/cjs/input/Checkbox.js +15 -5
  22. package/dist/cjs/input/ColorInput.js +108 -56
  23. package/dist/cjs/input/ColorPicker.js +11 -35
  24. package/dist/cjs/input/DateInput.js +195 -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 +11 -1
  28. package/dist/cjs/input/Input.js +19 -7
  29. package/dist/cjs/input/Number.js +17 -7
  30. package/dist/cjs/input/Password.js +16 -8
  31. package/dist/cjs/input/Radio.js +11 -2
  32. package/dist/cjs/input/RadioGroup.js +14 -3
  33. package/dist/cjs/input/{Range.js → RangeSlider.js} +14 -6
  34. package/dist/cjs/input/Search.js +25 -16
  35. package/dist/cjs/input/Slider.js +10 -4
  36. package/dist/cjs/input/Switch.js +39 -7
  37. package/dist/cjs/input/Textarea.js +23 -4
  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 +19 -16
  41. package/dist/cjs/select/AutoComplete.js +19 -8
  42. package/dist/cjs/select/DualList.js +15 -1
  43. package/dist/cjs/select/List.js +45 -20
  44. package/dist/cjs/select/Options.js +2 -2
  45. package/dist/cjs/select/Select.js +14 -4
  46. package/dist/cjs/select/useSelect.js +10 -6
  47. package/dist/cjs/types/schema.js +43 -0
  48. package/dist/cjs/utils/eventHandlers.js +11 -5
  49. package/dist/cjs/utils/getColor.js +6 -0
  50. package/dist/esm/builder/FileField.js +39 -27
  51. package/dist/esm/builder/FormField.js +50 -34
  52. package/dist/esm/builder/SchemaEditor.js +72 -0
  53. package/dist/esm/builder/components/AddSchemaFields.js +62 -0
  54. package/dist/esm/builder/components/SchemaArrayForm.js +143 -0
  55. package/dist/esm/builder/components/SchemaField.js +414 -0
  56. package/dist/esm/form/Controller.js +46 -1
  57. package/dist/esm/form/Form.js +43 -6
  58. package/dist/esm/hooks/useFileUploader.js +16 -0
  59. package/dist/esm/hooks/useFormBuilder.js +55 -13
  60. package/dist/esm/i18n/ar.json.js +5 -4
  61. package/dist/esm/i18n/en.json.js +5 -5
  62. package/dist/esm/index.js +5 -1
  63. package/dist/esm/input/ArrayInput.js +100 -33
  64. package/dist/esm/input/AvatarInput.js +17 -8
  65. package/dist/esm/input/Checkbox.js +15 -5
  66. package/dist/esm/input/ColorInput.js +110 -58
  67. package/dist/esm/input/ColorPicker.js +11 -35
  68. package/dist/esm/input/DateInput.js +195 -0
  69. package/dist/esm/input/Field.js +11 -5
  70. package/dist/esm/input/File.js +16 -9
  71. package/dist/esm/input/Hidden.js +11 -1
  72. package/dist/esm/input/Input.js +21 -9
  73. package/dist/esm/input/Number.js +19 -9
  74. package/dist/esm/input/Password.js +18 -10
  75. package/dist/esm/input/Radio.js +11 -2
  76. package/dist/esm/input/RadioGroup.js +15 -4
  77. package/dist/esm/input/{Range.js → RangeSlider.js} +14 -6
  78. package/dist/esm/input/Search.js +27 -18
  79. package/dist/esm/input/Slider.js +10 -4
  80. package/dist/esm/input/Switch.js +41 -9
  81. package/dist/esm/input/Textarea.js +24 -5
  82. package/dist/esm/internal/ErrorIcon.js +1 -1
  83. package/dist/esm/internal/FieldWrapper.js +11 -4
  84. package/dist/esm/internal/InputWrapper.js +19 -16
  85. package/dist/esm/select/AutoComplete.js +19 -8
  86. package/dist/esm/select/DualList.js +15 -1
  87. package/dist/esm/select/List.js +46 -21
  88. package/dist/esm/select/Options.js +2 -2
  89. package/dist/esm/select/Select.js +14 -4
  90. package/dist/esm/select/useSelect.js +11 -7
  91. package/{src/input/ColorPicker.module.css → dist/esm/types/schema.js} +18 -27
  92. package/dist/esm/utils/eventHandlers.js +11 -5
  93. package/dist/esm/utils/getColor.js +6 -1
  94. package/dist/types/builder/FileField.d.ts +4 -2
  95. package/dist/types/builder/FormField.d.ts +3 -1
  96. package/dist/types/builder/SchemaEditor.d.ts +9 -0
  97. package/dist/types/builder/components/AddSchemaFields.d.ts +2 -0
  98. package/dist/types/builder/components/SchemaArrayForm.d.ts +2 -0
  99. package/dist/types/builder/components/SchemaField.d.ts +10 -0
  100. package/dist/types/form/Controller.d.ts +47 -2
  101. package/dist/types/form/Form.d.ts +37 -2
  102. package/dist/types/hooks/useFileUploader.d.ts +16 -0
  103. package/dist/types/hooks/useFormBuilder.d.ts +32 -1
  104. package/dist/types/index.d.ts +5 -2
  105. package/dist/types/input/ArrayInput.d.ts +78 -6
  106. package/dist/types/input/AvatarInput.d.ts +14 -3
  107. package/dist/types/input/Checkbox.d.ts +13 -4
  108. package/dist/types/input/ColorInput.d.ts +15 -2
  109. package/dist/types/input/ColorPicker.d.ts +11 -1
  110. package/dist/types/input/DateInput.d.ts +28 -0
  111. package/dist/types/input/Field.d.ts +5 -1
  112. package/dist/types/input/File.d.ts +9 -0
  113. package/dist/types/input/Hidden.d.ts +8 -0
  114. package/dist/types/input/Input.d.ts +9 -1
  115. package/dist/types/input/Number.d.ts +9 -1
  116. package/dist/types/input/Password.d.ts +7 -1
  117. package/dist/types/input/Radio.d.ts +9 -1
  118. package/dist/types/input/RadioGroup.d.ts +26 -3
  119. package/dist/types/input/{Range.d.ts → RangeSlider.d.ts} +10 -2
  120. package/dist/types/input/Search.d.ts +14 -1
  121. package/dist/types/input/Slider.d.ts +7 -1
  122. package/dist/types/input/Switch.d.ts +16 -4
  123. package/dist/types/input/Textarea.d.ts +15 -1
  124. package/dist/types/internal/FieldWrapper.d.ts +1 -1
  125. package/dist/types/internal/InputWrapper.d.ts +1 -1
  126. package/dist/types/ranger/index.d.ts +2 -2
  127. package/dist/types/select/AutoComplete.d.ts +8 -1
  128. package/dist/types/select/DualList.d.ts +17 -2
  129. package/dist/types/select/List.d.ts +9 -1
  130. package/dist/types/select/Select.d.ts +9 -1
  131. package/dist/types/select/useSelect.d.ts +7 -6
  132. package/dist/types/types/index.d.ts +28 -7
  133. package/dist/types/types/schema.d.ts +78 -33
  134. package/dist/types/utils/eventHandlers.d.ts +1 -1
  135. package/package.json +15 -12
  136. package/src/builder/FileField.tsx +53 -27
  137. package/src/builder/FormField.tsx +52 -32
  138. package/src/builder/SchemaEditor.tsx +79 -0
  139. package/src/builder/components/AddSchemaFields.tsx +74 -0
  140. package/src/builder/components/SchemaArrayForm.tsx +158 -0
  141. package/src/builder/components/SchemaField.tsx +436 -0
  142. package/src/form/Controller.tsx +48 -3
  143. package/src/form/Form.tsx +45 -5
  144. package/src/hooks/useFileUploader.ts +16 -0
  145. package/src/hooks/useFormBuilder.tsx +58 -15
  146. package/src/i18n/ar.json +5 -4
  147. package/src/i18n/en.json +5 -5
  148. package/src/index.ts +11 -2
  149. package/src/input/ArrayInput.tsx +169 -54
  150. package/src/input/AvatarInput.tsx +20 -8
  151. package/src/input/Checkbox.tsx +15 -5
  152. package/src/input/ColorInput.tsx +126 -69
  153. package/src/input/ColorPicker.tsx +12 -36
  154. package/src/input/DateInput.tsx +259 -0
  155. package/src/input/Field.tsx +15 -11
  156. package/src/input/File.tsx +16 -14
  157. package/src/input/Hidden.tsx +12 -1
  158. package/src/input/Input.tsx +23 -10
  159. package/src/input/Number.tsx +18 -10
  160. package/src/input/Password.tsx +19 -12
  161. package/src/input/Radio.tsx +11 -2
  162. package/src/input/RadioGroup.tsx +35 -4
  163. package/src/input/{Range.tsx → RangeSlider.tsx} +13 -5
  164. package/src/input/Search.tsx +29 -19
  165. package/src/input/Slider.tsx +10 -4
  166. package/src/input/Switch.tsx +52 -7
  167. package/src/input/Textarea.tsx +29 -5
  168. package/src/internal/ErrorIcon.tsx +1 -1
  169. package/src/internal/FieldWrapper.tsx +8 -4
  170. package/src/internal/InputWrapper.tsx +36 -20
  171. package/src/select/AutoComplete.tsx +12 -1
  172. package/src/select/DualList.tsx +19 -0
  173. package/src/select/List.tsx +49 -8
  174. package/src/select/Options.tsx +1 -1
  175. package/src/select/Select.tsx +21 -3
  176. package/src/select/useSelect.tsx +13 -5
  177. package/src/types/index.ts +34 -8
  178. package/src/types/schema.ts +96 -41
  179. package/src/utils/eventHandlers.ts +9 -3
  180. package/dist/cjs/input/ArrayInput.module.css +0 -1
  181. package/dist/cjs/input/ColorPicker.module.css +0 -1
  182. package/dist/cjs/input/File.module.css +0 -1
  183. package/dist/cjs/internal/Field.module.css +0 -1
  184. package/dist/esm/input/ArrayInput.module.css +0 -1
  185. package/dist/esm/input/ColorPicker.module.css +0 -1
  186. package/dist/esm/input/File.module.css +0 -1
  187. package/dist/esm/internal/Field.module.css +0 -1
@@ -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
@@ -102,12 +104,12 @@ export const InputWrapper = ({
102
104
  role="none"
103
105
  ref={wrapperRef}
104
106
  className={classNames(
105
- classes.inputWrapper,
106
- invalid && classes.invalidWrapper,
107
+ "fabric-inputWrapper",
108
+ invalid && "fabric-invalidWrapper",
107
109
  !noOutline &&
108
110
  !noBorder &&
109
- "bg-[var(--fabric-input)] outline overflow-hidden",
110
- "flex flex-nowrap items-center",
111
+ "bg-(--fabric-input) outline overflow-hidden",
112
+ "flex flex-nowrap items-center flex-1",
111
113
  textarea ? "rounded-capped group" : "rounded",
112
114
  isExpanded
113
115
  ? "fixed inset-4 z-[99]"
@@ -117,13 +119,17 @@ export const InputWrapper = ({
117
119
  !noOutline &&
118
120
  "after:absolute after:bottom-0 after:h-px after:bg-primary-500 after:mx-auto",
119
121
  )}
120
- onKeyDown={(e) => e.key === "Escape" && setExpanded(false)}
122
+ onKeyDown={(e) =>
123
+ e.key === "Escape" &&
124
+ isExpanded &&
125
+ (setExpanded(false), e.stopPropagation())
126
+ }
121
127
  >
122
128
  {decorateStart && (
123
129
  <div
124
130
  onClickCapture={doFocus}
125
131
  className={classNames(
126
- classes.decorate,
132
+ "fabric-decorate",
127
133
  "text-muted leading-none flex-content",
128
134
  isString(decorateStart) && "px-1",
129
135
  )}
@@ -139,7 +145,7 @@ export const InputWrapper = ({
139
145
  onClick={handleClear}
140
146
  className={classNames(
141
147
  "text-tint-500 hover:text-tint-700 p-1 z-1 flex-content",
142
- classes.inputClear,
148
+ "fabric-inputClear",
143
149
  textarea && "self-start mt-1 me-1",
144
150
  )}
145
151
  />
@@ -148,15 +154,29 @@ export const InputWrapper = ({
148
154
  <div
149
155
  onClickCapture={doFocus}
150
156
  className={classNames(
151
- classes.decorate,
157
+ "fabric-decorate",
152
158
  "text-muted leading-none flex-content",
153
159
  isString(decorateEnd) && "px-1",
160
+ decorateEndHideWhenEmpty && "peer-placeholder-shown:hidden",
161
+ decorateEndShowWhenEmpty &&
162
+ "hidden peer-placeholder-shown:inline-block",
154
163
  )}
155
164
  >
156
165
  {decorateEnd}
157
166
  </div>
158
167
  )}
159
- {textarea && (
168
+ {actionButton && (
169
+ <div
170
+ onClickCapture={doFocus}
171
+ className={classNames(
172
+ "fabric-decorate",
173
+ "text-muted leading-none flex-content",
174
+ )}
175
+ >
176
+ {actionButton}
177
+ </div>
178
+ )}
179
+ {textareaExpandable && (
160
180
  <Icon
161
181
  onClick={() => setExpanded(!isExpanded)}
162
182
  icon={isExpanded ? CoreIcons.arrowCollapse : CoreIcons.arrowExpand}
@@ -170,17 +190,13 @@ export const InputWrapper = ({
170
190
  <Wrapper {...wrapperProps}>
171
191
  <div
172
192
  ref={elRef}
173
- className="flex-1 relative min-h-8"
193
+ className={classNames(
194
+ "relative min-h-8 flex flex-col flex-nowrap",
195
+ width ? "flex-auto" : "flex-1",
196
+ )}
174
197
  style={{ width, flexBasis: width }}
175
198
  >
176
- {!isExpanded && el}
177
- {isExpanded && (
178
- <FloatingPortal
179
- root={elRef.current?.closest<HTMLElement>(".theme-base")}
180
- >
181
- {el}
182
- </FloatingPortal>
183
- )}
199
+ {el}
184
200
  </div>
185
201
  </Wrapper>
186
202
  );
@@ -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();
@@ -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,
@@ -24,6 +24,7 @@
24
24
  import { Button, ButtonGroup, CoreIcons } from "@react-fabric/core";
25
25
  import { type RefProp } from "@react-fabric/core/dist/types/types";
26
26
  import {
27
+ type ReactElement,
27
28
  useCallback,
28
29
  useDeferredValue,
29
30
  useEffect,
@@ -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}
@@ -268,6 +286,7 @@ export const DualList = <T extends AnyObject = string>({
268
286
  allowClear
269
287
  height={height}
270
288
  options={rightOptions}
289
+ emptyDisplay={emptyDisplay}
271
290
  groupProperty={groupProperty}
272
291
  labelProperty={labelProperty}
273
292
  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,10 @@ export const List = <T extends AnyObject = string>({
53
61
  groupProperty,
54
62
  labelProperty,
55
63
  valueProperty,
64
+ sortProperty,
56
65
  multiple,
57
66
  allowCreate,
67
+ searchable,
58
68
  createOption,
59
69
  matcher,
60
70
  renderer,
@@ -62,6 +72,7 @@ export const List = <T extends AnyObject = string>({
62
72
  onChange,
63
73
  onSelect,
64
74
  onEnterPressed,
75
+ emptyDisplay,
65
76
  invalid,
66
77
  error,
67
78
  allowClear,
@@ -72,7 +83,9 @@ export const List = <T extends AnyObject = string>({
72
83
  // @ts-expect-error ignore
73
84
  "data-hide-selected": hideSelected,
74
85
  ...rest
75
- }: SelectProps<T> & { height?: string | number }) => {
86
+ }: Omit<SelectProps<T>, "defaultOpen" | "hideHandle"> & {
87
+ height?: string | number;
88
+ }) => {
76
89
  const Wrapper = useMemo(() => (isInner ? Fragment : FieldWrapper), [isInner]);
77
90
  const wrapperProps = useMemo(
78
91
  () => (isInner ? {} : { ...rest }),
@@ -96,6 +109,7 @@ export const List = <T extends AnyObject = string>({
96
109
  groupProperty,
97
110
  labelProperty,
98
111
  valueProperty,
112
+ sortProperty,
99
113
  allowCreate,
100
114
  createOption,
101
115
  matcher,
@@ -166,9 +180,12 @@ export const List = <T extends AnyObject = string>({
166
180
  }
167
181
  } else if (evt.key === "Backspace") {
168
182
  !state.query && handleRemove();
169
- } else if (["ArrowUp", "ArrowDown"].includes(evt.key)) {
170
- if (state.activeIndex === null) {
171
- setActiveIndex(state.selectedIndex);
183
+ } else if (!searchable || ["ArrowUp", "ArrowDown"].includes(evt.key)) {
184
+ if (
185
+ ["ArrowUp", "ArrowDown"].includes(evt.key) &&
186
+ state.activeIndex === null
187
+ ) {
188
+ setActiveIndex(state.selectedIndex ?? 0);
172
189
  }
173
190
  setTimeout(() => {
174
191
  props.onKeyDown?.(evt);
@@ -225,6 +242,15 @@ export const List = <T extends AnyObject = string>({
225
242
  }
226
243
  }, [state.value, state.query, placeholder, renderer]);
227
244
 
245
+ useLayoutEffect(() => {
246
+ setTimeout(() => {
247
+ state.selectedIndex &&
248
+ listRef.current[state.selectedIndex]?.scrollIntoView({
249
+ block: "center",
250
+ });
251
+ }, 100);
252
+ }, [state.selectedIndex]);
253
+
228
254
  return (
229
255
  <Wrapper {...wrapperProps}>
230
256
  <InputWrapper
@@ -240,22 +266,34 @@ export const List = <T extends AnyObject = string>({
240
266
  wrapperRef={refs.setReference}
241
267
  >
242
268
  <div
269
+ role="none"
243
270
  data-select-display="true"
244
- className="group flex-1 py-1 px-2 truncate text-start flex gap-1 flex-wrap"
271
+ className={classNames(
272
+ "group flex-1 py-1 px-2 truncate text-start flex gap-1 relative min-h-5 justify-start",
273
+ multiple ? "flex-wrap" : "flex-nowrap overflow-hidden",
274
+ )}
275
+ {...referenceProps}
276
+ onMouseUp={(e) =>
277
+ e.currentTarget.querySelector<HTMLElement>("input")?.focus()
278
+ }
245
279
  >
246
280
  {!hideSelected && displayValue}
247
281
  <input
282
+ readOnly={!searchable || readOnly}
248
283
  disabled={disabled}
249
284
  value={state.query ?? ""}
250
285
  aria-invalid={invalid}
251
286
  data-testid={name}
252
287
  name={name}
288
+ size={1}
289
+ ref={ref}
290
+ autoFocus={autoFocus}
253
291
  className={classNames(
254
292
  "appearance-none bg-transparent outline-none border-none ring-0 flex-1 min-w-24",
255
293
  disabled && "cursor-not-allowed pointer-events-none",
294
+ multiple && "min-w-24",
256
295
  )}
257
296
  placeholder={isEmpty(state.value) ? placeholder : ""}
258
- {...referenceProps}
259
297
  onChange={(evt) => handleQuery(evt.target.value)}
260
298
  />
261
299
  </div>
@@ -277,10 +315,13 @@ export const List = <T extends AnyObject = string>({
277
315
  {...floatingProps}
278
316
  items={state.items}
279
317
  active={state.activeIndex}
318
+ valueProperty={valueProperty}
319
+ labelProperty={labelProperty}
280
320
  itemRef={setItemRef}
281
321
  itemProps={makeItemProps}
322
+ empty={emptyDisplay}
282
323
  >
283
- {(item) => renderer?.(item) ?? item[labelProperty] ?? item}
324
+ {(item, label) => renderer?.(item) ?? label}
284
325
  </Options>
285
326
  </FloatingFocusManager>
286
327
  </Wrapper>
@@ -93,7 +93,7 @@ export const Options = ({
93
93
  >
94
94
  <Trans
95
95
  i18nKey="form:select.createOption"
96
- values={{ query: item.value }}
96
+ values={{ query: item.value ?? item }}
97
97
  components={[<strong key="t0">query</strong>]}
98
98
  />
99
99
  </div>
@@ -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,16 @@ 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,
62
73
  multiple,
63
74
  searchable,
64
75
  allowCreate,
76
+ emptyDisplay,
65
77
  createOption,
66
78
  matcher,
67
79
  renderer,
@@ -86,9 +98,11 @@ export const Select = <T extends AnyObject = string>({
86
98
  value,
87
99
  options,
88
100
  multiple,
101
+ defaultOpen,
89
102
  groupProperty,
90
103
  labelProperty,
91
104
  valueProperty,
105
+ sortProperty,
92
106
  allowCreate,
93
107
  createOption,
94
108
  matcher,
@@ -172,6 +186,9 @@ export const Select = <T extends AnyObject = string>({
172
186
  return false;
173
187
  } else if (state.query) {
174
188
  handleQuery("");
189
+ evt.preventDefault();
190
+ evt.stopPropagation();
191
+ return false;
175
192
  } else {
176
193
  onEnterPressed?.(evt);
177
194
  }
@@ -290,7 +307,7 @@ export const Select = <T extends AnyObject = string>({
290
307
  ref={ref}
291
308
  autoFocus={autoFocus}
292
309
  className={classNames(
293
- "appearance-none bg-transparent outline-none border-none ring-0 flex-1",
310
+ "appearance-none bg-transparent outline-none border-none ring-0 flex-1 peer",
294
311
  disabled && "cursor-not-allowed pointer-events-none",
295
312
  multiple && "min-w-24",
296
313
  )}
@@ -298,7 +315,7 @@ export const Select = <T extends AnyObject = string>({
298
315
  onChange={(evt) => handleQuery(evt.target.value)}
299
316
  />
300
317
  </div>
301
- {!state.loading && (
318
+ {!state.loading && !hideHandle && (
302
319
  <Icon
303
320
  className={classNames(
304
321
  "flex-content p-2 z-0 order-9 text-muted cursor-pointer self-stretch flex items-center h-auto",
@@ -327,7 +344,7 @@ export const Select = <T extends AnyObject = string>({
327
344
  >
328
345
  <Options
329
346
  ref={refs.setFloating}
330
- className="outline shadow-lg max-h-[40vh] z-[var(--z-popover)]"
347
+ className="outline shadow-lg max-h-[40vh] z-(--z-popover)"
331
348
  style={{
332
349
  width: refs.reference.current?.getBoundingClientRect().width,
333
350
  ...floatingStyles,
@@ -339,6 +356,7 @@ export const Select = <T extends AnyObject = string>({
339
356
  labelProperty={labelProperty}
340
357
  itemRef={setItemRef}
341
358
  itemProps={makeItemProps}
359
+ empty={emptyDisplay}
342
360
  >
343
361
  {(item, label) => renderer?.(item) ?? label}
344
362
  </Options>
@@ -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,
@@ -75,12 +76,14 @@ interface State {
75
76
  export const useSelect = ({
76
77
  options = EMPTY_ARRAY,
77
78
  value,
79
+ defaultOpen = false,
78
80
  autoComplete,
79
81
  multiple = false,
80
82
  allowCreate = false,
81
83
  groupProperty = "",
82
84
  valueProperty = "id",
83
85
  labelProperty = "label",
86
+ sortProperty,
84
87
  createOption = makeOption,
85
88
  matcher,
86
89
  onQuery,
@@ -90,10 +93,12 @@ export const useSelect = ({
90
93
  value?: AnyObject;
91
94
  options?: AnyObject[];
92
95
  multiple?: boolean;
96
+ defaultOpen?: boolean;
93
97
  allowCreate?: boolean;
94
98
  groupProperty?: AnyObject;
95
99
  valueProperty?: AnyObject;
96
100
  labelProperty?: AnyObject;
101
+ sortProperty?: AnyObject;
97
102
  autoComplete?: boolean;
98
103
  matcher?: (item: AnyObject, query: string) => boolean;
99
104
  createOption?: (query: string) => AnyObject;
@@ -223,6 +228,8 @@ export const useSelect = ({
223
228
  if (autoComplete && action.query !== undefined) {
224
229
  optionList = dedupe([action.query, ...optionList]);
225
230
  }
231
+ if (sortProperty)
232
+ optionList = optionList.sort(compareValues("asc", sortProperty));
226
233
  const grouped = groupBy(optionList, groupProperty, "");
227
234
  if (action.open) newState.open = true;
228
235
  newState.items = [];
@@ -230,7 +237,7 @@ export const useSelect = ({
230
237
  if (allowCreate && action.query)
231
238
  newState.items.push({
232
239
  ___create___: true,
233
- value: createOption(action.query),
240
+ value: action.query,
234
241
  });
235
242
  Object.entries(grouped).forEach(([key, list]) => {
236
243
  if (key) {
@@ -254,7 +261,7 @@ export const useSelect = ({
254
261
  {
255
262
  multiple,
256
263
  emptyValue,
257
- open: false,
264
+ open: defaultOpen,
258
265
  loading: false,
259
266
  activeIndex: null,
260
267
  selectedIndex: null,
@@ -283,7 +290,7 @@ export const useSelect = ({
283
290
  dispatch({ type: "options", options: optionsRef.current });
284
291
  dispatch({
285
292
  type: "setValue",
286
- value: hasValue ?? newOption ?? "",
293
+ value: hasValue ?? newOption ?? deferred,
287
294
  });
288
295
  }
289
296
  if (multiple) {
@@ -295,12 +302,12 @@ export const useSelect = ({
295
302
  ? createOption(item)
296
303
  : undefined;
297
304
  const hasValue = options.find(
298
- (opt: AnyObject) => (opt[valueProperty] ?? opt) === deferred,
305
+ (opt: AnyObject) => (opt[valueProperty] ?? opt) === item,
299
306
  );
300
307
  if (!hasValue && newOption) {
301
308
  optionsRef.current.push(newOption);
302
309
  }
303
- return hasValue ?? newOption ?? "";
310
+ return hasValue ?? newOption ?? item;
304
311
  },
305
312
  );
306
313
  optionsRef.current = [...optionsRef.current, ...(options ?? [])];
@@ -334,6 +341,7 @@ export const useSelect = ({
334
341
  });
335
342
  },
336
343
  [onQuery],
344
+ 300,
337
345
  );
338
346
 
339
347
  // handlers
@@ -21,17 +21,21 @@
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 { type RefProp } from "@react-fabric/core/dist/types/types";
24
+ import {
25
+ type RefProp,
26
+ type TestProps,
27
+ } from "@react-fabric/core/dist/types/types";
28
+ import { type ReactElement } from "react";
25
29
 
26
- export interface InputProps<V = string> {
30
+ export interface InputProps<V = string> extends TestProps {
27
31
  /**
28
32
  * value
29
33
  */
30
- value?: V;
34
+ value?: V | null;
31
35
  /**
32
36
  * change handler
33
37
  */
34
- onChange?: (value?: V) => void;
38
+ onChange?: (value: V | null) => void;
35
39
  /**
36
40
  * on focus event handler
37
41
  */
@@ -69,11 +73,15 @@ export interface InputProps<V = string> {
69
73
  *
70
74
  */
71
75
  decorateEnd?: JSX.Element | string | number | boolean;
76
+ decorateEndShowWhenEmpty?: boolean;
77
+ decorateEndHideWhenEmpty?: boolean;
72
78
  /**
73
79
  * info label at bottom
74
80
  */
75
81
  info?: string;
76
82
 
83
+ monospace?: boolean;
84
+
77
85
  /**
78
86
  * required
79
87
  */
@@ -117,9 +125,15 @@ export interface InputProps<V = string> {
117
125
  labelWidth?: string;
118
126
  }
119
127
 
120
- export interface BaseSelectProps<T>
121
- extends RefProp<HTMLInputElement>,
122
- Omit<InputProps, "value" | "onChange"> {
128
+ export interface BaseSelectProps<T extends AnyObject = string> {
129
+ /**
130
+ * open dropdown by default
131
+ */
132
+ defaultOpen?: boolean;
133
+ /**
134
+ * hide dropdown handle
135
+ */
136
+ hideHandle?: boolean;
123
137
  /**
124
138
  * list options
125
139
  */
@@ -136,6 +150,10 @@ export interface BaseSelectProps<T>
136
150
  * value property name
137
151
  */
138
152
  valueProperty?: keyof T;
153
+ /**
154
+ * value property name
155
+ */
156
+ sortProperty?: keyof T | true;
139
157
  /**
140
158
  * searchable list
141
159
  */
@@ -144,6 +162,10 @@ export interface BaseSelectProps<T>
144
162
  * allow create item
145
163
  */
146
164
  allowCreate?: boolean;
165
+ /**
166
+ * empty list display
167
+ */
168
+ emptyDisplay?: ReactElement;
147
169
  /**
148
170
  * create new item object
149
171
  */
@@ -162,7 +184,11 @@ export interface BaseSelectProps<T>
162
184
  matcher?: (option: T, query: string) => boolean;
163
185
  }
164
186
 
165
- export type SelectProps<T> = BaseSelectProps<T> &
187
+ type BaseSelectType<T> = BaseSelectProps<T> &
188
+ RefProp<HTMLInputElement> &
189
+ Omit<InputProps, "value" | "onChange">;
190
+
191
+ export type SelectProps<T> = BaseSelectType<T> &
166
192
  (
167
193
  | {
168
194
  /**