@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.
- package/{src/input/ArrayInput.module.css → css/_arrayInput.css} +8 -8
- package/css/_colorPicker.css +95 -0
- package/{src/internal/Field.module.css → css/_field.css} +94 -33
- package/{src/input/File.module.css → css/_file.css} +21 -6
- package/css/styles.css +27 -0
- package/dist/cjs/builder/FileField.js +107 -78
- package/dist/cjs/builder/FormField.js +60 -39
- package/dist/cjs/builder/SchemaEditor.js +72 -0
- package/dist/cjs/builder/components/AddSchemaFields.js +64 -0
- package/dist/cjs/builder/components/SchemaArrayForm.js +146 -0
- package/dist/cjs/builder/components/SchemaField.js +423 -0
- package/dist/cjs/form/Controller.js +66 -19
- package/dist/cjs/form/Form.js +97 -39
- package/dist/cjs/form/useForm.js +32 -0
- package/dist/cjs/hooks/useFileUploader.js +74 -45
- package/dist/cjs/hooks/useFormBuilder.js +85 -31
- package/dist/cjs/i18n/{ar.json.js → ar/form.json.js} +5 -4
- package/dist/cjs/i18n/{en.json.js → en/form.json.js} +5 -5
- package/dist/cjs/i18n/index.js +4 -4
- package/dist/cjs/index.js +41 -2
- package/dist/cjs/input/ArrayInput.js +259 -135
- package/dist/cjs/input/AvatarInput.js +99 -63
- package/dist/cjs/input/Checkbox.js +24 -25
- package/dist/cjs/input/ColorInput.js +138 -81
- package/dist/cjs/input/ColorPicker.js +78 -95
- package/dist/cjs/input/DateInput.js +192 -0
- package/dist/cjs/input/Field.js +21 -20
- package/dist/cjs/input/File.js +85 -80
- package/dist/cjs/input/Hidden.js +27 -13
- package/dist/cjs/input/Input.js +49 -48
- package/dist/cjs/input/Number.js +46 -44
- package/dist/cjs/input/Password.js +51 -49
- package/dist/cjs/input/Radio.js +26 -16
- package/dist/cjs/input/RadioGroup.js +26 -11
- package/dist/cjs/input/{Range.js → RangeSlider.js} +61 -66
- package/dist/cjs/input/Search.js +58 -64
- package/dist/cjs/input/Slider.js +57 -66
- package/dist/cjs/input/Switch.js +29 -27
- package/dist/cjs/input/Textarea.js +65 -53
- package/dist/cjs/internal/ErrorIcon.js +9 -9
- package/dist/cjs/internal/FieldWrapper.js +23 -13
- package/dist/cjs/internal/InputWrapper.js +85 -87
- package/dist/cjs/ranger/index.js +7 -7
- package/dist/cjs/ranger/utils.js +9 -4
- package/dist/cjs/select/AutoComplete.js +93 -76
- package/dist/cjs/select/DualList.js +235 -154
- package/dist/cjs/select/List.js +122 -89
- package/dist/cjs/select/Options.js +58 -54
- package/dist/cjs/select/Select.js +119 -89
- package/dist/cjs/select/helpers.js +5 -5
- package/dist/cjs/select/useSelect.js +83 -58
- package/dist/cjs/types/schema.js +43 -0
- package/dist/cjs/utils/eventHandlers.js +16 -6
- package/dist/cjs/utils/getColor.js +6 -6
- package/dist/cjs/utils/getProps.js +49 -0
- package/dist/esm/builder/FileField.js +109 -80
- package/dist/esm/builder/FormField.js +60 -39
- package/dist/esm/builder/SchemaEditor.js +72 -0
- package/dist/esm/builder/components/AddSchemaFields.js +64 -0
- package/dist/esm/builder/components/SchemaArrayForm.js +146 -0
- package/dist/esm/builder/components/SchemaField.js +423 -0
- package/dist/esm/form/Controller.js +66 -19
- package/dist/esm/form/Form.js +97 -39
- package/dist/esm/form/useForm.js +32 -0
- package/dist/esm/hooks/useFileUploader.js +75 -46
- package/dist/esm/hooks/useFormBuilder.js +84 -30
- package/dist/esm/i18n/{ar.json.js → ar/form.json.js} +5 -4
- package/dist/esm/i18n/{en.json.js → en/form.json.js} +5 -5
- package/dist/esm/i18n/index.js +2 -2
- package/dist/esm/index.js +36 -1
- package/dist/esm/input/ArrayInput.js +260 -136
- package/dist/esm/input/AvatarInput.js +99 -63
- package/dist/esm/input/Checkbox.js +26 -27
- package/dist/esm/input/ColorInput.js +141 -84
- package/dist/esm/input/ColorPicker.js +80 -97
- package/dist/esm/input/DateInput.js +192 -0
- package/dist/esm/input/Field.js +21 -20
- package/dist/esm/input/File.js +89 -84
- package/dist/esm/input/Hidden.js +28 -14
- package/dist/esm/input/Input.js +51 -50
- package/dist/esm/input/Number.js +48 -46
- package/dist/esm/input/Password.js +54 -52
- package/dist/esm/input/Radio.js +27 -17
- package/dist/esm/input/RadioGroup.js +27 -12
- package/dist/esm/input/{Range.js → RangeSlider.js} +62 -67
- package/dist/esm/input/Search.js +61 -67
- package/dist/esm/input/Slider.js +58 -67
- package/dist/esm/input/Switch.js +31 -29
- package/dist/esm/input/Textarea.js +67 -55
- package/dist/esm/internal/ErrorIcon.js +11 -11
- package/dist/esm/internal/FieldWrapper.js +23 -13
- package/dist/esm/internal/InputWrapper.js +88 -90
- package/dist/esm/ranger/index.js +7 -7
- package/dist/esm/ranger/utils.js +9 -4
- package/dist/esm/select/AutoComplete.js +95 -78
- package/dist/esm/select/DualList.js +233 -152
- package/dist/esm/select/List.js +125 -92
- package/dist/esm/select/Options.js +58 -54
- package/dist/esm/select/Select.js +122 -92
- package/dist/esm/select/helpers.js +5 -5
- package/dist/esm/select/useSelect.js +84 -59
- package/{src/input/ColorPicker.module.css → dist/esm/types/schema.js} +18 -27
- package/dist/esm/utils/eventHandlers.js +16 -6
- package/dist/esm/utils/getColor.js +6 -6
- package/dist/esm/utils/getProps.js +49 -0
- package/dist/types/builder/FileField.d.ts +7 -4
- package/dist/types/builder/FormField.d.ts +4 -2
- package/dist/types/builder/SchemaEditor.d.ts +6 -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 +12 -0
- package/dist/types/form/Controller.d.ts +45 -3
- package/dist/types/form/Form.d.ts +38 -7
- package/dist/types/form/useForm.d.ts +6 -0
- package/dist/types/hooks/useFileUploader.d.ts +24 -3
- package/dist/types/hooks/useFormBuilder.d.ts +41 -3
- package/dist/types/index.d.ts +32 -2
- package/dist/types/input/ArrayInput.d.ts +68 -13
- package/dist/types/input/AvatarInput.d.ts +23 -6
- package/dist/types/input/Checkbox.d.ts +12 -10
- package/dist/types/input/ColorInput.d.ts +15 -5
- package/dist/types/input/ColorPicker.d.ts +10 -2
- package/dist/types/input/DateInput.d.ts +21 -0
- package/dist/types/input/Field.d.ts +9 -36
- package/dist/types/input/File.d.ts +16 -4
- package/dist/types/input/Hidden.d.ts +7 -6
- package/dist/types/input/Input.d.ts +11 -4
- package/dist/types/input/Number.d.ts +9 -4
- package/dist/types/input/Password.d.ts +9 -4
- package/dist/types/input/Radio.d.ts +9 -4
- package/dist/types/input/RadioGroup.d.ts +14 -6
- package/dist/types/input/{Range.d.ts → RangeSlider.d.ts} +11 -6
- package/dist/types/input/Search.d.ts +14 -4
- package/dist/types/input/Slider.d.ts +9 -6
- package/dist/types/input/Switch.d.ts +13 -8
- package/dist/types/input/Textarea.d.ts +15 -4
- package/dist/types/internal/ErrorIcon.d.ts +4 -1
- package/dist/types/internal/FieldWrapper.d.ts +10 -1
- package/dist/types/internal/InputWrapper.d.ts +19 -1
- package/dist/types/ranger/index.d.ts +7 -3
- package/dist/types/ranger/utils.d.ts +2 -2
- package/dist/types/select/AutoComplete.d.ts +7 -2
- package/dist/types/select/DualList.d.ts +11 -37
- package/dist/types/select/List.d.ts +7 -2
- package/dist/types/select/Options.d.ts +8 -7
- package/dist/types/select/Select.d.ts +7 -2
- package/dist/types/select/helpers.d.ts +2 -2
- package/dist/types/select/useSelect.d.ts +10 -28
- package/dist/types/types/index.d.ts +73 -39
- package/dist/types/types/schema.d.ts +78 -32
- package/dist/types/utils/eventHandlers.d.ts +4 -4
- package/dist/types/utils/getColor.d.ts +4 -4
- package/dist/types/utils/getProps.d.ts +26 -0
- package/dist/types/utils/index.d.ts +1 -0
- package/package.json +25 -25
- package/src/builder/FileField.tsx +73 -56
- package/src/builder/FormField.tsx +66 -58
- package/src/builder/SchemaEditor.tsx +78 -0
- package/src/builder/components/AddSchemaFields.tsx +64 -0
- package/src/builder/components/SchemaArrayForm.tsx +146 -0
- package/src/builder/components/SchemaField.tsx +405 -0
- package/src/form/Controller.tsx +61 -24
- package/src/form/Form.tsx +74 -50
- package/src/form/useForm.ts +53 -0
- package/src/hooks/useFileUploader.ts +115 -84
- package/src/hooks/useFormBuilder.tsx +80 -41
- package/src/i18n/{ar.json → ar/form.json} +5 -4
- package/src/i18n/{en.json → en/form.json} +5 -5
- package/src/i18n/index.ts +2 -2
- package/src/index.ts +34 -20
- package/src/input/ArrayInput.tsx +255 -173
- package/src/input/AvatarInput.tsx +82 -39
- package/src/input/Checkbox.tsx +30 -54
- package/src/input/ColorInput.tsx +146 -105
- package/src/input/ColorPicker.tsx +76 -102
- package/src/input/DateInput.tsx +225 -0
- package/src/input/Field.tsx +23 -65
- package/src/input/File.tsx +76 -98
- package/src/input/Hidden.tsx +36 -24
- package/src/input/Input.tsx +55 -53
- package/src/input/Number.tsx +49 -54
- package/src/input/Password.tsx +61 -66
- package/src/input/Radio.tsx +28 -37
- package/src/input/RadioGroup.tsx +30 -25
- package/src/input/{Range.tsx → RangeSlider.tsx} +81 -109
- package/src/input/Search.tsx +64 -77
- package/src/input/Slider.tsx +80 -109
- package/src/input/Switch.tsx +39 -53
- package/src/input/Textarea.tsx +72 -59
- package/src/internal/ErrorIcon.tsx +22 -21
- package/src/internal/FieldWrapper.tsx +33 -23
- package/src/internal/InputWrapper.tsx +132 -143
- package/src/ranger/index.tsx +37 -117
- package/src/ranger/utils.ts +12 -11
- package/src/select/AutoComplete.tsx +113 -123
- package/src/select/DualList.tsx +218 -204
- package/src/select/List.tsx +130 -97
- package/src/select/Options.tsx +59 -57
- package/src/select/Select.tsx +148 -122
- package/src/select/helpers.ts +13 -8
- package/src/select/useSelect.tsx +110 -127
- package/src/types/index.ts +76 -45
- package/src/types/schema.ts +97 -40
- package/src/utils/eventHandlers.ts +24 -26
- package/src/utils/getColor.ts +9 -8
- package/src/utils/getProps.ts +47 -0
- package/src/utils/index.ts +1 -0
- 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/src/input/Textarea.tsx
CHANGED
|
@@ -21,22 +21,15 @@
|
|
|
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 {
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
useEffect,
|
|
29
|
-
useRef,
|
|
30
|
-
useState,
|
|
31
|
-
type ChangeEvent,
|
|
32
|
-
} from "react";
|
|
24
|
+
import { useControlledValue } from "@react-fabric/core";
|
|
25
|
+
import type { RefProp } from "@react-fabric/core/dist/types/types";
|
|
26
|
+
import type { KeyboardEventHandler } from "react";
|
|
27
|
+
import { useCallback, useEffect, useId, useRef, useState } from "react";
|
|
33
28
|
import { InputWrapper } from "../internal/InputWrapper";
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
29
|
+
import type { InputProps, WrapperProps } from "../types";
|
|
30
|
+
import { getWrapperProps, useHandleEnter } from "../utils";
|
|
36
31
|
|
|
37
|
-
export interface TextareaProps
|
|
38
|
-
extends InputProps,
|
|
39
|
-
RefProp<HTMLTextAreaElement> {
|
|
32
|
+
export interface TextareaProps extends InputProps, WrapperProps, RefProp<HTMLTextAreaElement> {
|
|
40
33
|
/**
|
|
41
34
|
* textarae rows
|
|
42
35
|
*/
|
|
@@ -49,66 +42,84 @@ export interface TextareaProps
|
|
|
49
42
|
* increment rows on value change
|
|
50
43
|
*/
|
|
51
44
|
variableRows?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* expandable textarea
|
|
47
|
+
*/
|
|
48
|
+
expandable?: boolean;
|
|
52
49
|
}
|
|
53
50
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Textarea component that allows users to input multi-line text.
|
|
53
|
+
* It supports features like variable row count, expandable behavior,
|
|
54
|
+
* and handling of focus, blur, and change events.
|
|
55
|
+
* This component is designed to be flexible and user-friendly,
|
|
56
|
+
* providing a clear interface for text input with optional validation and accessibility features.
|
|
57
|
+
*/
|
|
58
|
+
export function Textarea(props: TextareaProps) {
|
|
59
|
+
const {
|
|
60
|
+
value,
|
|
61
|
+
defaultValue,
|
|
62
|
+
onEnterPressed,
|
|
63
|
+
onChange,
|
|
64
|
+
onBlur,
|
|
65
|
+
onFocus,
|
|
66
|
+
disabled,
|
|
67
|
+
readOnly,
|
|
68
|
+
invalid,
|
|
69
|
+
rows = 5,
|
|
70
|
+
maxRows = 12,
|
|
71
|
+
placeholder,
|
|
72
|
+
name,
|
|
73
|
+
autoFocus,
|
|
74
|
+
expandable,
|
|
75
|
+
variableRows,
|
|
76
|
+
allowClear,
|
|
77
|
+
ref,
|
|
78
|
+
} = props;
|
|
79
|
+
|
|
80
|
+
const id = useId();
|
|
81
|
+
const { currentValue, updateValue } = useControlledValue(value, defaultValue);
|
|
75
82
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}, [deferred]);
|
|
83
|
+
const [fixedRows, setFixedRows] = useState(() => rows);
|
|
84
|
+
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
79
85
|
|
|
80
86
|
const handleChange = useCallback(
|
|
81
|
-
(e?: ChangeEvent<HTMLTextAreaElement>) => {
|
|
82
|
-
const value = e?.target.value;
|
|
83
|
-
|
|
84
|
-
onChange?.(value
|
|
87
|
+
(e?: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
88
|
+
const value = e?.target.value ?? defaultValue;
|
|
89
|
+
updateValue(value);
|
|
90
|
+
onChange?.(value);
|
|
85
91
|
},
|
|
86
|
-
[onChange],
|
|
92
|
+
[defaultValue, updateValue, onChange],
|
|
87
93
|
);
|
|
88
94
|
|
|
89
95
|
useEffect(() => {
|
|
90
96
|
if (variableRows) {
|
|
91
|
-
const lines =
|
|
97
|
+
const lines = currentValue?.split("\n").length ?? 1;
|
|
98
|
+
// eslint-disable-next-line @eslint-react/set-state-in-effect
|
|
92
99
|
setFixedRows(Math.max(rows, Math.min(maxRows, lines)));
|
|
93
|
-
} else {
|
|
94
|
-
setFixedRows(rows);
|
|
95
100
|
}
|
|
96
|
-
}, [
|
|
101
|
+
}, [currentValue, variableRows, rows, maxRows]);
|
|
102
|
+
|
|
103
|
+
const handleEnterPressed = useHandleEnter<HTMLTextAreaElement>(
|
|
104
|
+
onEnterPressed as unknown as KeyboardEventHandler<HTMLTextAreaElement>,
|
|
105
|
+
{
|
|
106
|
+
preventDefault: !!onEnterPressed,
|
|
107
|
+
},
|
|
108
|
+
);
|
|
97
109
|
|
|
98
110
|
return (
|
|
99
111
|
<InputWrapper
|
|
100
|
-
|
|
101
|
-
onClear={handleChange}
|
|
102
|
-
invalid={invalid}
|
|
103
|
-
readOnly={readOnly}
|
|
104
|
-
disabled={disabled}
|
|
105
|
-
required={required}
|
|
106
|
-
wrapperRef={wrapperRef}
|
|
112
|
+
id={id}
|
|
107
113
|
textarea
|
|
108
|
-
{
|
|
114
|
+
textareaExpandable={expandable}
|
|
115
|
+
showClear={allowClear && (currentValue ?? "") !== (defaultValue ?? "") && !disabled && !readOnly}
|
|
116
|
+
onClear={handleChange}
|
|
117
|
+
ref={wrapperRef}
|
|
118
|
+
{...getWrapperProps(props)}
|
|
109
119
|
>
|
|
110
120
|
<textarea
|
|
111
|
-
|
|
121
|
+
id={id}
|
|
122
|
+
className="appearance-none self-stretch bg-transparent py-1 px-4 not-first:ps-2 not-last:pe-2 flex-1 border-none outline-none ring-0 min-h-12 resize-none"
|
|
112
123
|
aria-invalid={invalid}
|
|
113
124
|
aria-disabled={disabled}
|
|
114
125
|
aria-readonly={readOnly}
|
|
@@ -121,10 +132,12 @@ export const Textarea = ({
|
|
|
121
132
|
ref={ref}
|
|
122
133
|
autoComplete="off"
|
|
123
134
|
{...{ autoFocus }}
|
|
124
|
-
value={
|
|
135
|
+
value={currentValue ?? ""}
|
|
136
|
+
onBlur={onBlur}
|
|
137
|
+
onFocus={onFocus}
|
|
125
138
|
onChange={handleChange}
|
|
126
|
-
onKeyDown={
|
|
139
|
+
onKeyDown={handleEnterPressed}
|
|
127
140
|
/>
|
|
128
141
|
</InputWrapper>
|
|
129
142
|
);
|
|
130
|
-
}
|
|
143
|
+
}
|
|
@@ -21,41 +21,42 @@
|
|
|
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 {
|
|
25
|
-
import { isString } from "@react-fabric/utilities";
|
|
24
|
+
import { Icon, Tooltip } from "@react-fabric/core";
|
|
25
|
+
import { isObject, isString } from "@react-fabric/utilities";
|
|
26
26
|
import { useTranslation } from "react-i18next";
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
interface ErrorObject {
|
|
29
|
+
key: string;
|
|
30
|
+
values: Record<string, string>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function ErrorIcon({ error = "", invalid }: { error?: string | KeyValue; invalid?: boolean }) {
|
|
29
34
|
const { t } = useTranslation();
|
|
30
35
|
|
|
31
36
|
return (
|
|
32
|
-
invalid &&
|
|
37
|
+
invalid &&
|
|
38
|
+
error && (
|
|
33
39
|
<Tooltip
|
|
34
40
|
content={
|
|
35
|
-
|
|
36
|
-
?
|
|
37
|
-
|
|
38
|
-
error
|
|
39
|
-
error.values
|
|
41
|
+
isObject<ErrorObject>(error)
|
|
42
|
+
? t(
|
|
43
|
+
error.key,
|
|
44
|
+
isObject(error.values)
|
|
40
45
|
? {
|
|
41
|
-
...error
|
|
42
|
-
label: t(
|
|
43
|
-
error.values?.label ?? "form:badkey",
|
|
44
|
-
error.values?.path,
|
|
45
|
-
),
|
|
46
|
+
...error.values,
|
|
47
|
+
label: t(error.values.label ?? "form:badkey", error.values.path),
|
|
46
48
|
}
|
|
47
49
|
: {},
|
|
48
|
-
)
|
|
50
|
+
)
|
|
51
|
+
: isString(error)
|
|
52
|
+
? error
|
|
53
|
+
: ""
|
|
49
54
|
}
|
|
50
55
|
placement="bottom"
|
|
51
56
|
color="danger"
|
|
52
57
|
>
|
|
53
|
-
<Icon
|
|
54
|
-
className="flex-content first:ms-2 self-center"
|
|
55
|
-
color="danger"
|
|
56
|
-
icon={CoreIcons.alert}
|
|
57
|
-
/>
|
|
58
|
+
<Icon className="flex-content first:ms-2 self-center" color="danger" icon="icon-[mdi--alert-circle-outline]" />
|
|
58
59
|
</Tooltip>
|
|
59
60
|
)
|
|
60
61
|
);
|
|
61
|
-
}
|
|
62
|
+
}
|
|
@@ -21,44 +21,56 @@
|
|
|
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
|
|
25
|
-
import
|
|
24
|
+
import type { CssProp } from "@react-fabric/core/dist/types/types";
|
|
25
|
+
import { cn } from "@react-fabric/utilities";
|
|
26
|
+
import type { ReactNode } from "react";
|
|
27
|
+
import type { WrapperProps } from "../types";
|
|
28
|
+
import { ErrorIcon } from "./ErrorIcon";
|
|
26
29
|
|
|
27
|
-
export
|
|
30
|
+
export interface FieldProps extends CssProp, WrapperProps {
|
|
31
|
+
id?: string;
|
|
32
|
+
children: ReactNode;
|
|
33
|
+
showError?: boolean;
|
|
34
|
+
listing?: ReactNode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function FieldWrapper({
|
|
38
|
+
id,
|
|
39
|
+
name,
|
|
28
40
|
label,
|
|
29
41
|
appendLabel,
|
|
30
42
|
width,
|
|
31
43
|
labelWidth = 192,
|
|
32
44
|
inline,
|
|
33
45
|
info,
|
|
34
|
-
disabled,
|
|
35
46
|
required,
|
|
36
47
|
children,
|
|
37
48
|
listing,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
invalid,
|
|
50
|
+
disabled,
|
|
51
|
+
error,
|
|
52
|
+
showError,
|
|
53
|
+
}: FieldProps) {
|
|
41
54
|
return (
|
|
42
55
|
<div
|
|
43
|
-
className={
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
inline ? "flex flex-nowrap
|
|
56
|
+
className={cn(
|
|
57
|
+
"fabric-fieldWrapper peer group relative",
|
|
58
|
+
width ? "flex-content" : "flex-1",
|
|
59
|
+
inline ? "flex flex-nowrap" : "flex flex-col",
|
|
47
60
|
)}
|
|
48
61
|
data-ref="fieldWrapper"
|
|
62
|
+
data-testid={`field_${name}`}
|
|
49
63
|
style={{ width, flexBasis: width }}
|
|
50
64
|
>
|
|
51
|
-
{
|
|
65
|
+
{label && (
|
|
52
66
|
<div
|
|
53
|
-
className="flex flex-nowrap flex-content gap-1 py-0.5 px-2"
|
|
67
|
+
className="fabric-fieldLabel flex flex-nowrap flex-content items-end gap-1 py-0.5 px-2"
|
|
54
68
|
style={{ width: inline ? labelWidth : undefined }}
|
|
55
69
|
>
|
|
56
70
|
<label
|
|
57
71
|
role="none"
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
disabled && "text-muted",
|
|
61
|
-
)}
|
|
72
|
+
htmlFor={id}
|
|
73
|
+
className={cn("flex-initial truncate text-sm", disabled && "text-muted")}
|
|
62
74
|
onClick={(e) =>
|
|
63
75
|
e.currentTarget
|
|
64
76
|
.closest<HTMLElement>('[data-ref="fieldWrapper"]')
|
|
@@ -68,11 +80,9 @@ export const FieldWrapper = ({
|
|
|
68
80
|
>
|
|
69
81
|
{label}
|
|
70
82
|
</label>
|
|
71
|
-
{required &&
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
</span>
|
|
75
|
-
)}
|
|
83
|
+
{required && <span className="text-danger-500 select-none self-center flex-content text-[10px]">✽</span>}
|
|
84
|
+
{showError && <ErrorIcon invalid={invalid} error={error} />}
|
|
85
|
+
<div className="flex-1" />
|
|
76
86
|
{appendLabel && <span className="flex-content">{appendLabel}</span>}
|
|
77
87
|
</div>
|
|
78
88
|
)}
|
|
@@ -81,4 +91,4 @@ export const FieldWrapper = ({
|
|
|
81
91
|
{info && <div className="text-sm text-muted flex-content">{info}</div>}
|
|
82
92
|
</div>
|
|
83
93
|
);
|
|
84
|
-
}
|
|
94
|
+
}
|
|
@@ -21,167 +21,156 @@
|
|
|
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 {
|
|
25
|
-
import {
|
|
26
|
-
import { isString } from "@react-fabric/utilities";
|
|
27
|
-
import
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
useEffect,
|
|
31
|
-
useMemo,
|
|
32
|
-
useRef,
|
|
33
|
-
useState,
|
|
34
|
-
type MouseEvent,
|
|
35
|
-
} from "react";
|
|
36
|
-
import { Fragment } from "react/jsx-runtime";
|
|
24
|
+
import { Icon } from "@react-fabric/core";
|
|
25
|
+
import type { CssProp } from "@react-fabric/core/dist/types/types";
|
|
26
|
+
import { cn, isString, mergeRefs } from "@react-fabric/utilities";
|
|
27
|
+
import type { ReactNode } from "react";
|
|
28
|
+
import { useCallback, useEffect, useRef, useState, type MouseEvent } from "react";
|
|
29
|
+
import type { WrapperProps } from "../types";
|
|
37
30
|
import { ErrorIcon } from "./ErrorIcon";
|
|
38
|
-
import classes from "./Field.module.css";
|
|
39
31
|
import { FieldWrapper } from "./FieldWrapper";
|
|
40
32
|
|
|
41
|
-
export
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
floatingExpand,
|
|
57
|
-
wrapperRef,
|
|
58
|
-
width,
|
|
59
|
-
...rest
|
|
60
|
-
}: KeyValue) => {
|
|
61
|
-
const elRef = useRef<HTMLDivElement>(null);
|
|
62
|
-
const [isExpanded, setExpanded] = useState(false);
|
|
33
|
+
export interface InputWrapperProps extends CssProp, WrapperProps {
|
|
34
|
+
id?: string;
|
|
35
|
+
children: ReactNode;
|
|
36
|
+
showClear?: boolean;
|
|
37
|
+
onClear?: () => void;
|
|
38
|
+
textarea?: boolean;
|
|
39
|
+
hasStepper?: boolean;
|
|
40
|
+
actionButton?: ReactNode;
|
|
41
|
+
textareaExpandable?: boolean;
|
|
42
|
+
isOpen?: boolean;
|
|
43
|
+
focusRing?: boolean;
|
|
44
|
+
floatingExpand?: boolean;
|
|
45
|
+
listing?: ReactNode;
|
|
46
|
+
ref?: React.Ref<HTMLDivElement | null>;
|
|
47
|
+
}
|
|
63
48
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
49
|
+
export function InputWrapper(props: InputWrapperProps) {
|
|
50
|
+
const {
|
|
51
|
+
invalid,
|
|
52
|
+
error,
|
|
53
|
+
showClear,
|
|
54
|
+
decorateStart,
|
|
55
|
+
decorateEnd,
|
|
56
|
+
decorateEndShowWhenEmpty,
|
|
57
|
+
decorateEndHideWhenEmpty,
|
|
58
|
+
children,
|
|
59
|
+
isOpen,
|
|
60
|
+
className,
|
|
61
|
+
focusRing,
|
|
62
|
+
onClear,
|
|
63
|
+
borderless,
|
|
64
|
+
hasStepper,
|
|
65
|
+
actionButton,
|
|
66
|
+
textarea = false,
|
|
67
|
+
textareaExpandable = false,
|
|
68
|
+
// expand autocomplete
|
|
69
|
+
floatingExpand,
|
|
70
|
+
ref,
|
|
71
|
+
} = props;
|
|
72
|
+
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
73
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
72
74
|
|
|
73
75
|
const handleClear = useCallback(
|
|
74
|
-
(e: MouseEvent<
|
|
75
|
-
e.currentTarget.parentElement
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
(e: MouseEvent<HTMLButtonElement>) => {
|
|
77
|
+
const parent = e.currentTarget.parentElement?.querySelector("& > :not(.fabric-decorate)");
|
|
78
|
+
const el = (parent?.querySelector<HTMLElement>("input,textarea,button") ?? parent) as HTMLElement;
|
|
79
|
+
el?.focus();
|
|
78
80
|
onClear?.();
|
|
79
81
|
},
|
|
80
82
|
[onClear],
|
|
81
83
|
);
|
|
82
84
|
|
|
83
|
-
const doFocus = useCallback(
|
|
84
|
-
(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
},
|
|
89
|
-
[onClear],
|
|
90
|
-
);
|
|
85
|
+
const doFocus = useCallback((e: MouseEvent<HTMLDivElement>) => {
|
|
86
|
+
const parent = e.currentTarget.parentElement?.querySelector("& > :not(.fabric-decorate)");
|
|
87
|
+
const el = (parent?.querySelector<HTMLElement>("input,textarea,button") ?? parent) as HTMLElement;
|
|
88
|
+
el?.focus();
|
|
89
|
+
}, []);
|
|
91
90
|
|
|
92
91
|
useEffect(() => {
|
|
93
|
-
isExpanded
|
|
94
|
-
setTimeout(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const el = (
|
|
101
|
-
<div
|
|
102
|
-
role="none"
|
|
103
|
-
ref={wrapperRef}
|
|
104
|
-
className={classNames(
|
|
105
|
-
classes.inputWrapper,
|
|
106
|
-
invalid && classes.invalidWrapper,
|
|
107
|
-
!noOutline &&
|
|
108
|
-
!noBorder &&
|
|
109
|
-
"bg-[var(--fabric-input)] outline overflow-hidden",
|
|
110
|
-
"flex flex-nowrap items-center",
|
|
111
|
-
textarea ? "rounded-capped group" : "rounded",
|
|
112
|
-
isExpanded
|
|
113
|
-
? "fixed inset-4 z-[99]"
|
|
114
|
-
: floatingExpand
|
|
115
|
-
? "absolute inset-x-0"
|
|
116
|
-
: "relative",
|
|
117
|
-
!noOutline &&
|
|
118
|
-
"after:absolute after:bottom-0 after:h-px after:bg-primary-500 after:mx-auto",
|
|
119
|
-
)}
|
|
120
|
-
onKeyDown={(e) => e.key === "Escape" && setExpanded(false)}
|
|
121
|
-
>
|
|
122
|
-
{decorateStart && (
|
|
123
|
-
<div
|
|
124
|
-
onClickCapture={doFocus}
|
|
125
|
-
className={classNames(
|
|
126
|
-
classes.decorate,
|
|
127
|
-
"text-muted leading-none flex-content",
|
|
128
|
-
isString(decorateStart) && "px-1",
|
|
129
|
-
)}
|
|
130
|
-
>
|
|
131
|
-
{decorateStart}
|
|
132
|
-
</div>
|
|
133
|
-
)}
|
|
134
|
-
<ErrorIcon invalid={invalid} error={error} />
|
|
135
|
-
{children}
|
|
136
|
-
{allowClear && showClear && (
|
|
137
|
-
<Icon
|
|
138
|
-
icon={CoreIcons.close}
|
|
139
|
-
onClick={handleClear}
|
|
140
|
-
className={classNames(
|
|
141
|
-
"text-tint-500 hover:text-tint-700 p-1 z-1 flex-content",
|
|
142
|
-
classes.inputClear,
|
|
143
|
-
textarea && "self-start mt-1 me-1",
|
|
144
|
-
)}
|
|
145
|
-
/>
|
|
146
|
-
)}
|
|
147
|
-
{decorateEnd && (
|
|
148
|
-
<div
|
|
149
|
-
onClickCapture={doFocus}
|
|
150
|
-
className={classNames(
|
|
151
|
-
classes.decorate,
|
|
152
|
-
"text-muted leading-none flex-content",
|
|
153
|
-
isString(decorateEnd) && "px-1",
|
|
154
|
-
)}
|
|
155
|
-
>
|
|
156
|
-
{decorateEnd}
|
|
157
|
-
</div>
|
|
158
|
-
)}
|
|
159
|
-
{textarea && (
|
|
160
|
-
<Icon
|
|
161
|
-
onClick={() => setExpanded(!isExpanded)}
|
|
162
|
-
icon={isExpanded ? CoreIcons.arrowCollapse : CoreIcons.arrowExpand}
|
|
163
|
-
className="hidden group-hover:block absolute top-0 end-0 bg-base hover:bg-tint-200 p-1 opacity-65 rounded-capped"
|
|
164
|
-
/>
|
|
165
|
-
)}
|
|
166
|
-
</div>
|
|
167
|
-
);
|
|
92
|
+
if (isExpanded) {
|
|
93
|
+
const tmr = setTimeout(() => wrapperRef?.current?.querySelector("textarea")?.focus(), 50);
|
|
94
|
+
return () => {
|
|
95
|
+
clearTimeout(tmr);
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}, [isExpanded, wrapperRef]);
|
|
168
99
|
|
|
169
100
|
return (
|
|
170
|
-
<
|
|
101
|
+
<FieldWrapper {...props}>
|
|
171
102
|
<div
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
103
|
+
role="none"
|
|
104
|
+
ref={mergeRefs(ref, wrapperRef)}
|
|
105
|
+
className={cn(
|
|
106
|
+
"fabric-inputWrapper",
|
|
107
|
+
invalid && "fabric-invalidWrapper",
|
|
108
|
+
!borderless && "outline",
|
|
109
|
+
textarea ? "rounded-capped" : "rounded",
|
|
110
|
+
className,
|
|
111
|
+
"flex flex-1 flex-nowrap items-center bg-(--fabric-input)",
|
|
112
|
+
isExpanded ? "fixed inset-4 z-99" : floatingExpand ? "absolute inset-x-0" : "relative",
|
|
113
|
+
focusRing
|
|
114
|
+
? "focus-within:ring ring-primary-500"
|
|
115
|
+
: "after:absolute after:bottom-0 after:h-px after:bg-primary-500 after:mx-auto",
|
|
116
|
+
isOpen && "outline-primary-500",
|
|
117
|
+
)}
|
|
118
|
+
onKeyDown={(e) => e.key === "Escape" && isExpanded && (setIsExpanded(false), e.stopPropagation())}
|
|
175
119
|
>
|
|
176
|
-
{
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
120
|
+
{decorateStart && (
|
|
121
|
+
<div
|
|
122
|
+
onClickCapture={doFocus}
|
|
123
|
+
className={cn("fabric-decorate", "text-muted leading-none flex-content", isString(decorateStart) && "px-1")}
|
|
124
|
+
>
|
|
125
|
+
{decorateStart}
|
|
126
|
+
</div>
|
|
127
|
+
)}
|
|
128
|
+
<ErrorIcon invalid={invalid} error={error} />
|
|
129
|
+
{children}
|
|
130
|
+
{showClear && (
|
|
131
|
+
<button
|
|
132
|
+
type="button"
|
|
133
|
+
className={cn("size-fit leading-0 cursor-pointer", hasStepper && "relative -ms-12")}
|
|
134
|
+
onClick={handleClear}
|
|
135
|
+
>
|
|
136
|
+
<Icon
|
|
137
|
+
icon="icon-[mdi--close]"
|
|
138
|
+
className={cn("text-tint-500 hover:text-tint-700 p-1 z-1 flex-content", "fabric-inputClear")}
|
|
139
|
+
/>
|
|
140
|
+
</button>
|
|
141
|
+
)}
|
|
142
|
+
{decorateEnd && (
|
|
143
|
+
<div
|
|
144
|
+
onClickCapture={doFocus}
|
|
145
|
+
className={cn(
|
|
146
|
+
"fabric-decorate",
|
|
147
|
+
"text-muted leading-none flex-content",
|
|
148
|
+
isString(decorateEnd) && "px-1",
|
|
149
|
+
decorateEndHideWhenEmpty && "peer-placeholder-shown:hidden",
|
|
150
|
+
decorateEndShowWhenEmpty && "hidden peer-placeholder-shown:inline-block",
|
|
151
|
+
)}
|
|
152
|
+
>
|
|
153
|
+
{decorateEnd}
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
156
|
+
{actionButton && (
|
|
157
|
+
<div onClickCapture={doFocus} className={cn("fabric-decorate", "text-muted leading-none flex-content")}>
|
|
158
|
+
{actionButton}
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
161
|
+
{textareaExpandable && (
|
|
162
|
+
<button
|
|
163
|
+
type="button"
|
|
164
|
+
className="size-fit cursor-pointer leading-0 absolute top-0 inset-e-0"
|
|
165
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
180
166
|
>
|
|
181
|
-
|
|
182
|
-
|
|
167
|
+
<Icon
|
|
168
|
+
icon={isExpanded ? "icon-[mdi--arrow-collapse]" : "icon-[mdi--arrow-expand]"}
|
|
169
|
+
className="hidden group-hover:block bg-default hover:bg-tint-200 p-1 opacity-65 rounded-capped"
|
|
170
|
+
/>
|
|
171
|
+
</button>
|
|
183
172
|
)}
|
|
184
173
|
</div>
|
|
185
|
-
</
|
|
174
|
+
</FieldWrapper>
|
|
186
175
|
);
|
|
187
|
-
}
|
|
176
|
+
}
|