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

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
@@ -30,54 +30,57 @@ import {
30
30
  useTypeahead,
31
31
  } from "@floating-ui/react";
32
32
  import { Chip } from "@react-fabric/core";
33
- import { isArray, isEmpty } from "@react-fabric/utilities";
34
- import classNames from "classnames";
35
- import { Fragment, useCallback, useId, useMemo } from "react";
36
- import { FieldWrapper } from "../internal/FieldWrapper";
33
+ import { cn, getByPath, isArray, isEmpty } from "@react-fabric/utilities";
34
+ import { useCallback, useId, useLayoutEffect, useMemo } from "react";
37
35
  import { InputWrapper } from "../internal/InputWrapper";
38
36
  import { type SelectProps } from "../types";
37
+ import { getWrapperProps } from "../utils";
39
38
  import { Options } from "./Options";
40
39
  import { useSelect } from "./useSelect";
41
40
 
42
- export const List = <T extends AnyObject = string>({
43
- ref,
44
- name,
45
- value,
46
- readOnly,
47
- disabled,
48
- required,
49
- placeholder,
50
- autoFocus,
51
- options,
52
- height,
53
- groupProperty,
54
- labelProperty,
55
- valueProperty,
56
- multiple,
57
- allowCreate,
58
- createOption,
59
- matcher,
60
- renderer,
61
- onQuery,
62
- onChange,
63
- onSelect,
64
- onEnterPressed,
65
- invalid,
66
- error,
67
- allowClear,
68
- decorateStart,
69
- decorateEnd,
70
- // @ts-expect-error ignore
71
- "data-inner": isInner,
72
- // @ts-expect-error ignore
73
- "data-hide-selected": hideSelected,
74
- ...rest
75
- }: SelectProps<T> & { height?: string | number }) => {
76
- const Wrapper = useMemo(() => (isInner ? Fragment : FieldWrapper), [isInner]);
77
- const wrapperProps = useMemo(
78
- () => (isInner ? {} : { ...rest }),
79
- [isInner, rest],
80
- );
41
+ /**
42
+ * List component for selecting items from a list.
43
+ * It supports single and multiple selections, searching, creating new options,
44
+ * and custom rendering of options.
45
+ */
46
+ export function List<T extends AnyObject = string>(
47
+ props: Omit<SelectProps<T>, "defaultOpen" | "hideHandle"> & {
48
+ height?: string | number;
49
+ },
50
+ ) {
51
+ const {
52
+ ref,
53
+ name,
54
+ value,
55
+ readOnly,
56
+ disabled,
57
+ placeholder,
58
+ autoFocus,
59
+ options,
60
+ height,
61
+ groupProperty,
62
+ labelProperty = "label",
63
+ valueProperty = "value",
64
+ infoProperty,
65
+ sortProperty,
66
+ multiple,
67
+ allowCreate,
68
+ allowClear,
69
+ searchable,
70
+ createOption,
71
+ matcher,
72
+ renderer,
73
+ onQuery,
74
+ onChange,
75
+ onSelect,
76
+ onEnterPressed,
77
+ emptyDisplay,
78
+ invalid,
79
+ // @ts-expect-error ignore
80
+ "data-inner": isInner,
81
+ // @ts-expect-error ignore
82
+ "data-hide-selected": hideSelected,
83
+ } = props;
81
84
 
82
85
  const {
83
86
  state,
@@ -88,14 +91,17 @@ export const List = <T extends AnyObject = string>({
88
91
  handleRemove,
89
92
  handleQuery,
90
93
  setItemRef,
94
+ clearActiveIndex,
91
95
  setActiveIndex,
92
96
  } = useSelect({
93
97
  value,
94
98
  options,
95
99
  multiple,
100
+ alwaysOpen: true,
96
101
  groupProperty,
97
102
  labelProperty,
98
103
  valueProperty,
104
+ sortProperty,
99
105
  allowCreate,
100
106
  createOption,
101
107
  matcher,
@@ -104,7 +110,7 @@ export const List = <T extends AnyObject = string>({
104
110
  onQuery,
105
111
  });
106
112
 
107
- const dropdownKey = useId();
113
+ const dropdownKeyId = useId();
108
114
 
109
115
  const { context, refs } = useFloating({
110
116
  open: true,
@@ -132,29 +138,25 @@ export const List = <T extends AnyObject = string>({
132
138
  return list.find((item) => macthOption(item, typedString));
133
139
  },
134
140
  onMatch(index) {
135
- state.open && setActiveIndex(index);
141
+ if (state.open) setActiveIndex(index);
136
142
  },
137
143
  });
138
144
 
139
- const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions(
140
- [navigation, typeahead],
141
- );
145
+ const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([navigation, typeahead]);
142
146
 
143
147
  const referenceProps = useMemo(() => {
144
- const props: AnyObject = getReferenceProps();
148
+ const props = getReferenceProps();
145
149
 
146
150
  return {
147
151
  ...props,
148
152
  onBlur(evt: React.FocusEvent) {
149
- if (
150
- evt.relatedTarget?.closest(`[data-select-dropdown="${dropdownKey}"]`)
151
- ) {
153
+ if (evt.relatedTarget?.closest(`[data-select-dropdown="${dropdownKeyId}"]`)) {
152
154
  refs.domReference.current?.querySelector("input")?.focus();
153
155
  return;
154
156
  }
155
157
  handleQuery("");
156
158
  },
157
- onKeyDown(evt: React.KeyboardEvent) {
159
+ onKeyDown(evt: React.KeyboardEvent<HTMLInputElement>) {
158
160
  if (evt.key === "Enter") {
159
161
  if (state.activeIndex !== null) {
160
162
  handleChange(listContentRef.current[state.activeIndex]);
@@ -165,44 +167,50 @@ export const List = <T extends AnyObject = string>({
165
167
  onEnterPressed?.(evt);
166
168
  }
167
169
  } else if (evt.key === "Backspace") {
168
- !state.query && handleRemove();
169
- } else if (["ArrowUp", "ArrowDown"].includes(evt.key)) {
170
- if (state.activeIndex === null) {
171
- setActiveIndex(state.selectedIndex);
170
+ if (!state.query) handleRemove();
171
+ } else if (!searchable || ["ArrowUp", "ArrowDown"].includes(evt.key)) {
172
+ if (["ArrowUp", "ArrowDown"].includes(evt.key) && state.activeIndex === null) {
173
+ setActiveIndex(state.selectedIndex ?? 0);
172
174
  }
173
175
  setTimeout(() => {
176
+ // @ts-expect-error ignore
174
177
  props.onKeyDown?.(evt);
175
178
  }, 50);
176
179
  }
177
180
  },
178
- };
181
+ } as KeyValue;
179
182
  }, [
180
183
  getReferenceProps,
181
- onEnterPressed,
184
+ dropdownKeyId,
182
185
  handleQuery,
183
- refs,
184
- state.query,
186
+ refs.domReference,
187
+ searchable,
185
188
  state.activeIndex,
189
+ state.query,
190
+ state.selectedIndex,
191
+ handleChange,
192
+ listContentRef,
193
+ onEnterPressed,
194
+ handleRemove,
195
+ setActiveIndex,
186
196
  ]);
187
197
 
188
198
  const floatingProps = useMemo(() => {
189
199
  return getFloatingProps({
190
- "data-select-dropdown": dropdownKey,
191
- } as AnyObject);
192
- }, [getFloatingProps]);
200
+ "data-select-dropdown": dropdownKeyId,
201
+ } as KeyValue);
202
+ }, [dropdownKeyId, getFloatingProps]);
193
203
 
194
204
  const makeItemProps = useCallback(
195
205
  (item: AnyObject) => {
196
- const selected = isArray(state.value)
197
- ? state.value?.includes?.(item)
198
- : state.value === item;
206
+ const selected = isArray(state.value) ? state.value?.includes?.(item) : state.value === item;
199
207
  return {
200
208
  ...getItemProps({
201
209
  "data-selected": selected ? true : undefined,
202
210
  onClick() {
203
211
  handleChange(item);
204
212
  },
205
- } as AnyObject),
213
+ } as KeyValue),
206
214
  "aria-hidden": undefined,
207
215
  };
208
216
  },
@@ -211,64 +219,85 @@ export const List = <T extends AnyObject = string>({
211
219
 
212
220
  const displayValue = useMemo(() => {
213
221
  if (isArray(state.value)) {
214
- return state.value.map((item: AnyObject, index) => (
222
+ return state.value.map((item: T, index) => (
223
+ // eslint-disable-next-line @eslint-react/no-array-index-key
215
224
  <Chip key={index} onRemove={() => handleRemove(index)}>
225
+ {/* @ts-expect-error ignore */}
216
226
  {renderer?.(item) ?? item[labelProperty] ?? item}
217
227
  </Chip>
218
228
  ));
219
229
  } else if (!isEmpty(state.value) && !state.query) {
220
230
  return (
221
- <span>
222
- {renderer?.(state.value) ?? state.value[labelProperty] ?? state.value}
231
+ <span className="self-center pointer-events-none select-none truncate leading-tight">
232
+ {/* eslint-disable-next-line @typescript-eslint/no-unsafe-member-access */}
233
+ {renderer?.(state.value as T) ?? state.value[labelProperty] ?? state.value}
223
234
  </span>
224
235
  );
225
236
  }
226
- }, [state.value, state.query, placeholder, renderer]);
237
+ }, [state.value, state.query, renderer, labelProperty, handleRemove]);
238
+
239
+ useLayoutEffect(() => {
240
+ if (state.open) {
241
+ const tmr = setTimeout(() => {
242
+ if (state.selectedIndex)
243
+ listRef.current[state.selectedIndex]?.scrollIntoView({
244
+ block: "nearest",
245
+ });
246
+ }, 100);
247
+ return () => {
248
+ clearTimeout(tmr);
249
+ };
250
+ }
251
+ }, [listRef, state.open, state.selectedIndex]);
227
252
 
228
253
  return (
229
- <Wrapper {...wrapperProps}>
254
+ <>
230
255
  <InputWrapper
231
- showClear={!isEmpty(state.value)}
256
+ showClear={allowClear && !isEmpty(state.value)}
232
257
  onClear={handleChange}
233
- invalid={invalid}
234
- readOnly={readOnly}
235
- disabled={disabled}
236
- required={required}
237
- error={error}
238
- className="z-1 rounded-b-none"
239
- data-inner={true}
240
- wrapperRef={refs.setReference}
258
+ ref={(el) => refs.setReference(el)}
259
+ className="z-1 rounded-capped rounded-b-none outline"
260
+ {...getWrapperProps(props)}
261
+ borderless={!!isInner}
241
262
  >
242
263
  <div
264
+ role="none"
243
265
  data-select-display="true"
244
- className="group flex-1 py-1 px-2 truncate text-start flex gap-1 flex-wrap"
266
+ className={cn(
267
+ "group flex-1 py-1 px-2 truncate text-start flex gap-1 relative min-h-5 justify-start",
268
+ multiple ? "flex-wrap" : "flex-nowrap overflow-hidden",
269
+ )}
270
+ {...referenceProps}
271
+ onMouseUp={(e) => e.currentTarget.querySelector<HTMLElement>("input")?.focus()}
245
272
  >
246
273
  {!hideSelected && displayValue}
247
274
  <input
275
+ readOnly={!searchable || readOnly}
248
276
  disabled={disabled}
249
277
  value={state.query ?? ""}
250
278
  aria-invalid={invalid}
251
279
  data-testid={name}
252
280
  name={name}
253
- className={classNames(
281
+ size={1}
282
+ ref={ref}
283
+ autoComplete="off"
284
+ {...{ autoFocus }}
285
+ className={cn(
254
286
  "appearance-none bg-transparent outline-none border-none ring-0 flex-1 min-w-24",
255
287
  disabled && "cursor-not-allowed pointer-events-none",
288
+ multiple && "min-w-24",
256
289
  )}
257
- placeholder={isEmpty(state.value) ? placeholder : ""}
258
- {...referenceProps}
290
+ placeholder={!hideSelected && !isEmpty(state.value) ? "" : placeholder}
259
291
  onChange={(evt) => handleQuery(evt.target.value)}
260
292
  />
261
293
  </div>
262
294
  </InputWrapper>
263
- <FloatingFocusManager
264
- context={context}
265
- initialFocus={-1}
266
- visuallyHiddenDismiss
267
- >
295
+ <FloatingFocusManager context={context} initialFocus={-1} visuallyHiddenDismiss closeOnFocusOut>
268
296
  <Options
269
- ref={refs.setFloating}
270
- className={classNames(
271
- "max-h-96 z-0 rounded-capped rounded-t-none outline",
297
+ ref={(el) => refs.setFloating(el)}
298
+ onMouseOut={() => clearActiveIndex()}
299
+ className={cn(
300
+ "max-h-96 z-0 rounded-capped! rounded-t-none! outline peer-focus-within:outline-primary-500",
272
301
  disabled && "pointer-events-none opacity-65",
273
302
  )}
274
303
  style={{
@@ -277,12 +306,16 @@ export const List = <T extends AnyObject = string>({
277
306
  {...floatingProps}
278
307
  items={state.items}
279
308
  active={state.activeIndex}
309
+ valueProperty={valueProperty}
310
+ labelProperty={labelProperty}
280
311
  itemRef={setItemRef}
281
312
  itemProps={makeItemProps}
313
+ empty={emptyDisplay}
314
+ info={getByPath(state.activeItem as KeyValue, infoProperty ?? "info")}
282
315
  >
283
- {(item) => renderer?.(item) ?? item[labelProperty] ?? item}
316
+ {(item, label) => renderer?.(item as T) ?? label}
284
317
  </Options>
285
318
  </FloatingFocusManager>
286
- </Wrapper>
319
+ </>
287
320
  );
288
- };
321
+ }
@@ -21,12 +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
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
25
+ /* eslint-disable @typescript-eslint/no-unsafe-argument */
26
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
27
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
28
+ /* eslint-disable @typescript-eslint/no-unsafe-return */
29
+ /* eslint-disable @eslint-react/no-array-index-key */
30
+
24
31
  import { EmptyContent } from "@react-fabric/core";
25
- import classNames from "classnames";
26
- import { type ReactElement, type ReactNode } from "react";
32
+ import { cn, getByPath } from "@react-fabric/utilities";
27
33
  import { Trans } from "react-i18next";
28
34
 
29
- export const Options = ({
35
+ export function Options({
30
36
  ref,
31
37
  style,
32
38
  items,
@@ -34,50 +40,65 @@ export const Options = ({
34
40
  className,
35
41
  itemRef,
36
42
  empty,
37
- valueProperty = "id",
43
+ info,
38
44
  labelProperty = "label",
39
45
  itemProps,
40
46
  children,
41
- ...rest
47
+ onMouseOut,
42
48
  }: {
43
- ref: AnyObject;
49
+ ref: React.Ref<HTMLDivElement | null>;
50
+ info?: AnyObject;
44
51
  itemRef: AnyObject;
45
- style?: AnyObject;
52
+ style?: React.CSSProperties;
46
53
  items: AnyObject[];
47
54
  active: number | null;
48
55
  className?: string;
49
- empty?: ReactElement;
56
+ empty?: React.ReactNode;
50
57
  valueProperty?: AnyObject;
51
58
  labelProperty?: AnyObject;
59
+ onMouseOut?: () => void;
52
60
  itemProps: (item: AnyObject) => AnyObject;
53
- children: (item: AnyObject, label: string) => ReactNode;
54
- } & KeyValue) => {
61
+ children: (item: AnyObject, label: string) => React.ReactNode;
62
+ }) {
55
63
  return (
56
64
  <div
57
65
  ref={ref}
58
- className={classNames(
59
- "bg-base select-none overflow-x-hidden overflow-y-auto",
60
- className,
61
- )}
66
+ className={cn("bg-default select-none flex flex-col overflow-hidden", className)}
62
67
  style={style}
63
- {...rest}
68
+ onMouseLeave={onMouseOut}
64
69
  >
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
- );
70
+ <div className={cn("overflow-x-hidden overflow-y-auto flex-1")}>
71
+ {items.length === 0 && (empty ?? <EmptyContent message={<Trans i18nKey="form:select.emptyList" />} />)}
72
+ {items.map((item, index) => {
73
+ if (item.___group___)
74
+ return (
75
+ <p key={index} className="px-2 py-1 text-muted text-xs sticky -top-px z-2 bg-default border-t">
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={cn(
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"
@@ -85,37 +106,18 @@ export const Options = ({
85
106
  tabIndex={active === index ? 0 : -1}
86
107
  ref={(el: AnyObject) => itemRef(index, el)}
87
108
  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",
109
+ className={cn(
110
+ "block text-start truncate px-2 py-1 leading-normal cursor-pointer",
111
+ "active:bg-primary-200 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, getByPath(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 && <div className="px-2 py-1 text-sm bg-dimmed text-muted">{info}</div>}
119
121
  </div>
120
122
  );
121
- };
123
+ }