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