@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.
- 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} +64 -29
- package/{src/input/File.module.css → css/_file.css} +21 -6
- package/css/styles.css +27 -0
- package/dist/cjs/builder/FileField.js +37 -25
- package/dist/cjs/builder/FormField.js +50 -34
- package/dist/cjs/builder/SchemaEditor.js +72 -0
- package/dist/cjs/builder/components/AddSchemaFields.js +62 -0
- package/dist/cjs/builder/components/SchemaArrayForm.js +143 -0
- package/dist/cjs/builder/components/SchemaField.js +414 -0
- package/dist/cjs/form/Controller.js +46 -1
- package/dist/cjs/form/Form.js +43 -6
- package/dist/cjs/hooks/useFileUploader.js +16 -0
- package/dist/cjs/hooks/useFormBuilder.js +57 -15
- package/dist/cjs/i18n/ar.json.js +5 -4
- package/dist/cjs/i18n/en.json.js +5 -5
- package/dist/cjs/index.js +11 -2
- package/dist/cjs/input/ArrayInput.js +100 -33
- package/dist/cjs/input/AvatarInput.js +17 -8
- package/dist/cjs/input/Checkbox.js +15 -5
- package/dist/cjs/input/ColorInput.js +108 -56
- package/dist/cjs/input/ColorPicker.js +11 -35
- package/dist/cjs/input/DateInput.js +195 -0
- package/dist/cjs/input/Field.js +10 -4
- package/dist/cjs/input/File.js +14 -7
- package/dist/cjs/input/Hidden.js +11 -1
- package/dist/cjs/input/Input.js +19 -7
- package/dist/cjs/input/Number.js +17 -7
- package/dist/cjs/input/Password.js +16 -8
- package/dist/cjs/input/Radio.js +11 -2
- package/dist/cjs/input/RadioGroup.js +14 -3
- package/dist/cjs/input/{Range.js → RangeSlider.js} +14 -6
- package/dist/cjs/input/Search.js +25 -16
- package/dist/cjs/input/Slider.js +10 -4
- package/dist/cjs/input/Switch.js +39 -7
- package/dist/cjs/input/Textarea.js +23 -4
- package/dist/cjs/internal/ErrorIcon.js +1 -1
- package/dist/cjs/internal/FieldWrapper.js +11 -4
- package/dist/cjs/internal/InputWrapper.js +19 -16
- package/dist/cjs/select/AutoComplete.js +19 -8
- package/dist/cjs/select/DualList.js +15 -1
- package/dist/cjs/select/List.js +45 -20
- package/dist/cjs/select/Options.js +2 -2
- package/dist/cjs/select/Select.js +14 -4
- package/dist/cjs/select/useSelect.js +10 -6
- package/dist/cjs/types/schema.js +43 -0
- package/dist/cjs/utils/eventHandlers.js +11 -5
- package/dist/cjs/utils/getColor.js +6 -0
- package/dist/esm/builder/FileField.js +39 -27
- package/dist/esm/builder/FormField.js +50 -34
- package/dist/esm/builder/SchemaEditor.js +72 -0
- package/dist/esm/builder/components/AddSchemaFields.js +62 -0
- package/dist/esm/builder/components/SchemaArrayForm.js +143 -0
- package/dist/esm/builder/components/SchemaField.js +414 -0
- package/dist/esm/form/Controller.js +46 -1
- package/dist/esm/form/Form.js +43 -6
- package/dist/esm/hooks/useFileUploader.js +16 -0
- package/dist/esm/hooks/useFormBuilder.js +55 -13
- package/dist/esm/i18n/ar.json.js +5 -4
- package/dist/esm/i18n/en.json.js +5 -5
- package/dist/esm/index.js +5 -1
- package/dist/esm/input/ArrayInput.js +100 -33
- package/dist/esm/input/AvatarInput.js +17 -8
- package/dist/esm/input/Checkbox.js +15 -5
- package/dist/esm/input/ColorInput.js +110 -58
- package/dist/esm/input/ColorPicker.js +11 -35
- package/dist/esm/input/DateInput.js +195 -0
- package/dist/esm/input/Field.js +11 -5
- package/dist/esm/input/File.js +16 -9
- package/dist/esm/input/Hidden.js +11 -1
- package/dist/esm/input/Input.js +21 -9
- package/dist/esm/input/Number.js +19 -9
- package/dist/esm/input/Password.js +18 -10
- package/dist/esm/input/Radio.js +11 -2
- package/dist/esm/input/RadioGroup.js +15 -4
- package/dist/esm/input/{Range.js → RangeSlider.js} +14 -6
- package/dist/esm/input/Search.js +27 -18
- package/dist/esm/input/Slider.js +10 -4
- package/dist/esm/input/Switch.js +41 -9
- package/dist/esm/input/Textarea.js +24 -5
- package/dist/esm/internal/ErrorIcon.js +1 -1
- package/dist/esm/internal/FieldWrapper.js +11 -4
- package/dist/esm/internal/InputWrapper.js +19 -16
- package/dist/esm/select/AutoComplete.js +19 -8
- package/dist/esm/select/DualList.js +15 -1
- package/dist/esm/select/List.js +46 -21
- package/dist/esm/select/Options.js +2 -2
- package/dist/esm/select/Select.js +14 -4
- package/dist/esm/select/useSelect.js +11 -7
- package/{src/input/ColorPicker.module.css → dist/esm/types/schema.js} +18 -27
- package/dist/esm/utils/eventHandlers.js +11 -5
- package/dist/esm/utils/getColor.js +6 -1
- 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 +37 -2
- package/dist/types/hooks/useFileUploader.d.ts +16 -0
- package/dist/types/hooks/useFormBuilder.d.ts +32 -1
- package/dist/types/index.d.ts +5 -2
- package/dist/types/input/ArrayInput.d.ts +78 -6
- package/dist/types/input/AvatarInput.d.ts +14 -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 +8 -0
- 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 +26 -3
- package/dist/types/input/{Range.d.ts → RangeSlider.d.ts} +10 -2
- package/dist/types/input/Search.d.ts +14 -1
- package/dist/types/input/Slider.d.ts +7 -1
- package/dist/types/input/Switch.d.ts +16 -4
- 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 +17 -2
- package/dist/types/select/List.d.ts +9 -1
- package/dist/types/select/Select.d.ts +9 -1
- package/dist/types/select/useSelect.d.ts +7 -6
- package/dist/types/types/index.d.ts +28 -7
- package/dist/types/types/schema.d.ts +78 -33
- package/dist/types/utils/eventHandlers.d.ts +1 -1
- package/package.json +15 -12
- package/src/builder/FileField.tsx +53 -27
- package/src/builder/FormField.tsx +52 -32
- package/src/builder/SchemaEditor.tsx +79 -0
- package/src/builder/components/AddSchemaFields.tsx +74 -0
- package/src/builder/components/SchemaArrayForm.tsx +158 -0
- package/src/builder/components/SchemaField.tsx +436 -0
- package/src/form/Controller.tsx +48 -3
- package/src/form/Form.tsx +45 -5
- package/src/hooks/useFileUploader.ts +16 -0
- package/src/hooks/useFormBuilder.tsx +58 -15
- package/src/i18n/ar.json +5 -4
- package/src/i18n/en.json +5 -5
- package/src/index.ts +11 -2
- package/src/input/ArrayInput.tsx +169 -54
- package/src/input/AvatarInput.tsx +20 -8
- package/src/input/Checkbox.tsx +15 -5
- package/src/input/ColorInput.tsx +126 -69
- package/src/input/ColorPicker.tsx +12 -36
- package/src/input/DateInput.tsx +259 -0
- package/src/input/Field.tsx +15 -11
- package/src/input/File.tsx +16 -14
- package/src/input/Hidden.tsx +12 -1
- package/src/input/Input.tsx +23 -10
- package/src/input/Number.tsx +18 -10
- package/src/input/Password.tsx +19 -12
- package/src/input/Radio.tsx +11 -2
- package/src/input/RadioGroup.tsx +35 -4
- package/src/input/{Range.tsx → RangeSlider.tsx} +13 -5
- package/src/input/Search.tsx +29 -19
- package/src/input/Slider.tsx +10 -4
- package/src/input/Switch.tsx +52 -7
- package/src/input/Textarea.tsx +29 -5
- package/src/internal/ErrorIcon.tsx +1 -1
- package/src/internal/FieldWrapper.tsx +8 -4
- package/src/internal/InputWrapper.tsx +36 -20
- package/src/select/AutoComplete.tsx +12 -1
- package/src/select/DualList.tsx +19 -0
- package/src/select/List.tsx +49 -8
- package/src/select/Options.tsx +1 -1
- package/src/select/Select.tsx +21 -3
- package/src/select/useSelect.tsx +13 -5
- package/src/types/index.ts +34 -8
- package/src/types/schema.ts +96 -41
- package/src/utils/eventHandlers.ts +9 -3
- 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
|
@@ -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
|
-
|
|
106
|
-
invalid &&
|
|
107
|
+
"fabric-inputWrapper",
|
|
108
|
+
invalid && "fabric-invalidWrapper",
|
|
107
109
|
!noOutline &&
|
|
108
110
|
!noBorder &&
|
|
109
|
-
"bg-
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
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=
|
|
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
|
-
{
|
|
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-
|
|
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,
|
package/src/select/DualList.tsx
CHANGED
|
@@ -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}
|
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,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
|
|
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 (
|
|
171
|
-
|
|
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=
|
|
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) ??
|
|
324
|
+
{(item, label) => renderer?.(item) ?? label}
|
|
284
325
|
</Options>
|
|
285
326
|
</FloatingFocusManager>
|
|
286
327
|
</Wrapper>
|
package/src/select/Options.tsx
CHANGED
package/src/select/Select.tsx
CHANGED
|
@@ -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-
|
|
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>
|
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,
|
|
@@ -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:
|
|
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:
|
|
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) ===
|
|
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
|
package/src/types/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
/**
|