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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/{src/input/ArrayInput.module.css → css/_arrayInput.css} +8 -8
  2. package/css/_colorPicker.css +95 -0
  3. package/{src/internal/Field.module.css → css/_field.css} +94 -33
  4. package/{src/input/File.module.css → css/_file.css} +21 -6
  5. package/css/styles.css +27 -0
  6. package/dist/cjs/builder/FileField.js +107 -78
  7. package/dist/cjs/builder/FormField.js +60 -39
  8. package/dist/cjs/builder/SchemaEditor.js +72 -0
  9. package/dist/cjs/builder/components/AddSchemaFields.js +64 -0
  10. package/dist/cjs/builder/components/SchemaArrayForm.js +146 -0
  11. package/dist/cjs/builder/components/SchemaField.js +423 -0
  12. package/dist/cjs/form/Controller.js +66 -19
  13. package/dist/cjs/form/Form.js +97 -39
  14. package/dist/cjs/form/useForm.js +32 -0
  15. package/dist/cjs/hooks/useFileUploader.js +74 -45
  16. package/dist/cjs/hooks/useFormBuilder.js +85 -31
  17. package/dist/cjs/i18n/{ar.json.js → ar/form.json.js} +5 -4
  18. package/dist/cjs/i18n/{en.json.js → en/form.json.js} +5 -5
  19. package/dist/cjs/i18n/index.js +4 -4
  20. package/dist/cjs/index.js +41 -2
  21. package/dist/cjs/input/ArrayInput.js +259 -135
  22. package/dist/cjs/input/AvatarInput.js +99 -63
  23. package/dist/cjs/input/Checkbox.js +24 -25
  24. package/dist/cjs/input/ColorInput.js +138 -81
  25. package/dist/cjs/input/ColorPicker.js +78 -95
  26. package/dist/cjs/input/DateInput.js +192 -0
  27. package/dist/cjs/input/Field.js +21 -20
  28. package/dist/cjs/input/File.js +85 -80
  29. package/dist/cjs/input/Hidden.js +27 -13
  30. package/dist/cjs/input/Input.js +49 -48
  31. package/dist/cjs/input/Number.js +46 -44
  32. package/dist/cjs/input/Password.js +51 -49
  33. package/dist/cjs/input/Radio.js +26 -16
  34. package/dist/cjs/input/RadioGroup.js +26 -11
  35. package/dist/cjs/input/{Range.js → RangeSlider.js} +61 -66
  36. package/dist/cjs/input/Search.js +58 -64
  37. package/dist/cjs/input/Slider.js +57 -66
  38. package/dist/cjs/input/Switch.js +29 -27
  39. package/dist/cjs/input/Textarea.js +65 -53
  40. package/dist/cjs/internal/ErrorIcon.js +9 -9
  41. package/dist/cjs/internal/FieldWrapper.js +23 -13
  42. package/dist/cjs/internal/InputWrapper.js +85 -87
  43. package/dist/cjs/ranger/index.js +7 -7
  44. package/dist/cjs/ranger/utils.js +9 -4
  45. package/dist/cjs/select/AutoComplete.js +93 -76
  46. package/dist/cjs/select/DualList.js +235 -154
  47. package/dist/cjs/select/List.js +122 -89
  48. package/dist/cjs/select/Options.js +58 -54
  49. package/dist/cjs/select/Select.js +119 -89
  50. package/dist/cjs/select/helpers.js +5 -5
  51. package/dist/cjs/select/useSelect.js +83 -58
  52. package/dist/cjs/types/schema.js +43 -0
  53. package/dist/cjs/utils/eventHandlers.js +16 -6
  54. package/dist/cjs/utils/getColor.js +6 -6
  55. package/dist/cjs/utils/getProps.js +49 -0
  56. package/dist/esm/builder/FileField.js +109 -80
  57. package/dist/esm/builder/FormField.js +60 -39
  58. package/dist/esm/builder/SchemaEditor.js +72 -0
  59. package/dist/esm/builder/components/AddSchemaFields.js +64 -0
  60. package/dist/esm/builder/components/SchemaArrayForm.js +146 -0
  61. package/dist/esm/builder/components/SchemaField.js +423 -0
  62. package/dist/esm/form/Controller.js +66 -19
  63. package/dist/esm/form/Form.js +97 -39
  64. package/dist/esm/form/useForm.js +32 -0
  65. package/dist/esm/hooks/useFileUploader.js +75 -46
  66. package/dist/esm/hooks/useFormBuilder.js +84 -30
  67. package/dist/esm/i18n/{ar.json.js → ar/form.json.js} +5 -4
  68. package/dist/esm/i18n/{en.json.js → en/form.json.js} +5 -5
  69. package/dist/esm/i18n/index.js +2 -2
  70. package/dist/esm/index.js +36 -1
  71. package/dist/esm/input/ArrayInput.js +260 -136
  72. package/dist/esm/input/AvatarInput.js +99 -63
  73. package/dist/esm/input/Checkbox.js +26 -27
  74. package/dist/esm/input/ColorInput.js +141 -84
  75. package/dist/esm/input/ColorPicker.js +80 -97
  76. package/dist/esm/input/DateInput.js +192 -0
  77. package/dist/esm/input/Field.js +21 -20
  78. package/dist/esm/input/File.js +89 -84
  79. package/dist/esm/input/Hidden.js +28 -14
  80. package/dist/esm/input/Input.js +51 -50
  81. package/dist/esm/input/Number.js +48 -46
  82. package/dist/esm/input/Password.js +54 -52
  83. package/dist/esm/input/Radio.js +27 -17
  84. package/dist/esm/input/RadioGroup.js +27 -12
  85. package/dist/esm/input/{Range.js → RangeSlider.js} +62 -67
  86. package/dist/esm/input/Search.js +61 -67
  87. package/dist/esm/input/Slider.js +58 -67
  88. package/dist/esm/input/Switch.js +31 -29
  89. package/dist/esm/input/Textarea.js +67 -55
  90. package/dist/esm/internal/ErrorIcon.js +11 -11
  91. package/dist/esm/internal/FieldWrapper.js +23 -13
  92. package/dist/esm/internal/InputWrapper.js +88 -90
  93. package/dist/esm/ranger/index.js +7 -7
  94. package/dist/esm/ranger/utils.js +9 -4
  95. package/dist/esm/select/AutoComplete.js +95 -78
  96. package/dist/esm/select/DualList.js +233 -152
  97. package/dist/esm/select/List.js +125 -92
  98. package/dist/esm/select/Options.js +58 -54
  99. package/dist/esm/select/Select.js +122 -92
  100. package/dist/esm/select/helpers.js +5 -5
  101. package/dist/esm/select/useSelect.js +84 -59
  102. package/{src/input/ColorPicker.module.css → dist/esm/types/schema.js} +18 -27
  103. package/dist/esm/utils/eventHandlers.js +16 -6
  104. package/dist/esm/utils/getColor.js +6 -6
  105. package/dist/esm/utils/getProps.js +49 -0
  106. package/dist/types/builder/FileField.d.ts +7 -4
  107. package/dist/types/builder/FormField.d.ts +4 -2
  108. package/dist/types/builder/SchemaEditor.d.ts +6 -0
  109. package/dist/types/builder/components/AddSchemaFields.d.ts +2 -0
  110. package/dist/types/builder/components/SchemaArrayForm.d.ts +2 -0
  111. package/dist/types/builder/components/SchemaField.d.ts +12 -0
  112. package/dist/types/form/Controller.d.ts +45 -3
  113. package/dist/types/form/Form.d.ts +38 -7
  114. package/dist/types/form/useForm.d.ts +6 -0
  115. package/dist/types/hooks/useFileUploader.d.ts +24 -3
  116. package/dist/types/hooks/useFormBuilder.d.ts +41 -3
  117. package/dist/types/index.d.ts +32 -2
  118. package/dist/types/input/ArrayInput.d.ts +68 -13
  119. package/dist/types/input/AvatarInput.d.ts +23 -6
  120. package/dist/types/input/Checkbox.d.ts +12 -10
  121. package/dist/types/input/ColorInput.d.ts +15 -5
  122. package/dist/types/input/ColorPicker.d.ts +10 -2
  123. package/dist/types/input/DateInput.d.ts +21 -0
  124. package/dist/types/input/Field.d.ts +9 -36
  125. package/dist/types/input/File.d.ts +16 -4
  126. package/dist/types/input/Hidden.d.ts +7 -6
  127. package/dist/types/input/Input.d.ts +11 -4
  128. package/dist/types/input/Number.d.ts +9 -4
  129. package/dist/types/input/Password.d.ts +9 -4
  130. package/dist/types/input/Radio.d.ts +9 -4
  131. package/dist/types/input/RadioGroup.d.ts +14 -6
  132. package/dist/types/input/{Range.d.ts → RangeSlider.d.ts} +11 -6
  133. package/dist/types/input/Search.d.ts +14 -4
  134. package/dist/types/input/Slider.d.ts +9 -6
  135. package/dist/types/input/Switch.d.ts +13 -8
  136. package/dist/types/input/Textarea.d.ts +15 -4
  137. package/dist/types/internal/ErrorIcon.d.ts +4 -1
  138. package/dist/types/internal/FieldWrapper.d.ts +10 -1
  139. package/dist/types/internal/InputWrapper.d.ts +19 -1
  140. package/dist/types/ranger/index.d.ts +7 -3
  141. package/dist/types/ranger/utils.d.ts +2 -2
  142. package/dist/types/select/AutoComplete.d.ts +7 -2
  143. package/dist/types/select/DualList.d.ts +11 -37
  144. package/dist/types/select/List.d.ts +7 -2
  145. package/dist/types/select/Options.d.ts +8 -7
  146. package/dist/types/select/Select.d.ts +7 -2
  147. package/dist/types/select/helpers.d.ts +2 -2
  148. package/dist/types/select/useSelect.d.ts +10 -28
  149. package/dist/types/types/index.d.ts +73 -39
  150. package/dist/types/types/schema.d.ts +78 -32
  151. package/dist/types/utils/eventHandlers.d.ts +4 -4
  152. package/dist/types/utils/getColor.d.ts +4 -4
  153. package/dist/types/utils/getProps.d.ts +26 -0
  154. package/dist/types/utils/index.d.ts +1 -0
  155. package/package.json +25 -25
  156. package/src/builder/FileField.tsx +73 -56
  157. package/src/builder/FormField.tsx +66 -58
  158. package/src/builder/SchemaEditor.tsx +78 -0
  159. package/src/builder/components/AddSchemaFields.tsx +64 -0
  160. package/src/builder/components/SchemaArrayForm.tsx +146 -0
  161. package/src/builder/components/SchemaField.tsx +405 -0
  162. package/src/form/Controller.tsx +61 -24
  163. package/src/form/Form.tsx +74 -50
  164. package/src/form/useForm.ts +53 -0
  165. package/src/hooks/useFileUploader.ts +115 -84
  166. package/src/hooks/useFormBuilder.tsx +80 -41
  167. package/src/i18n/{ar.json → ar/form.json} +5 -4
  168. package/src/i18n/{en.json → en/form.json} +5 -5
  169. package/src/i18n/index.ts +2 -2
  170. package/src/index.ts +34 -20
  171. package/src/input/ArrayInput.tsx +255 -173
  172. package/src/input/AvatarInput.tsx +82 -39
  173. package/src/input/Checkbox.tsx +30 -54
  174. package/src/input/ColorInput.tsx +146 -105
  175. package/src/input/ColorPicker.tsx +76 -102
  176. package/src/input/DateInput.tsx +225 -0
  177. package/src/input/Field.tsx +23 -65
  178. package/src/input/File.tsx +76 -98
  179. package/src/input/Hidden.tsx +36 -24
  180. package/src/input/Input.tsx +55 -53
  181. package/src/input/Number.tsx +49 -54
  182. package/src/input/Password.tsx +61 -66
  183. package/src/input/Radio.tsx +28 -37
  184. package/src/input/RadioGroup.tsx +30 -25
  185. package/src/input/{Range.tsx → RangeSlider.tsx} +81 -109
  186. package/src/input/Search.tsx +64 -77
  187. package/src/input/Slider.tsx +80 -109
  188. package/src/input/Switch.tsx +39 -53
  189. package/src/input/Textarea.tsx +72 -59
  190. package/src/internal/ErrorIcon.tsx +22 -21
  191. package/src/internal/FieldWrapper.tsx +33 -23
  192. package/src/internal/InputWrapper.tsx +132 -143
  193. package/src/ranger/index.tsx +37 -117
  194. package/src/ranger/utils.ts +12 -11
  195. package/src/select/AutoComplete.tsx +113 -123
  196. package/src/select/DualList.tsx +218 -204
  197. package/src/select/List.tsx +130 -97
  198. package/src/select/Options.tsx +59 -57
  199. package/src/select/Select.tsx +148 -122
  200. package/src/select/helpers.ts +13 -8
  201. package/src/select/useSelect.tsx +110 -127
  202. package/src/types/index.ts +76 -45
  203. package/src/types/schema.ts +97 -40
  204. package/src/utils/eventHandlers.ts +24 -26
  205. package/src/utils/getColor.ts +9 -8
  206. package/src/utils/getProps.ts +47 -0
  207. package/src/utils/index.ts +1 -0
  208. package/dist/cjs/input/ArrayInput.module.css +0 -1
  209. package/dist/cjs/input/ColorPicker.module.css +0 -1
  210. package/dist/cjs/input/File.module.css +0 -1
  211. package/dist/cjs/internal/Field.module.css +0 -1
  212. package/dist/esm/input/ArrayInput.module.css +0 -1
  213. package/dist/esm/input/ColorPicker.module.css +0 -1
  214. package/dist/esm/input/File.module.css +0 -1
  215. package/dist/esm/internal/Field.module.css +0 -1
@@ -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 { type RefProp } from "@react-fabric/core/dist/types/types";
25
- import {
26
- useCallback,
27
- useDeferredValue,
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 { type InputProps } from "../types";
35
- import { handleEnter } from "../utils";
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
- export const Textarea = ({
55
- ref,
56
- value,
57
- name,
58
- invalid,
59
- readOnly,
60
- disabled,
61
- required,
62
- placeholder,
63
- autoFocus,
64
- rows = 5,
65
- maxRows = 12,
66
- variableRows,
67
- onChange,
68
- onEnterPressed,
69
- ...rest
70
- }: TextareaProps) => {
71
- const [actualValue, setActualValue] = useState("");
72
- const [fixedRows, setFixedRows] = useState(rows);
73
- const deferred = useDeferredValue(value);
74
- const wrapperRef = useRef(null);
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
- useEffect(() => {
77
- setActualValue(deferred ?? "");
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
- setActualValue(value ?? "");
84
- onChange?.(value ?? undefined);
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 = actualValue?.split("\n").length ?? 1;
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
- }, [actualValue, variableRows, rows, maxRows]);
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
- showClear={!!actualValue}
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
- {...rest}
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
- className="appearance-none self-stretch bg-transparent py-1 px-2 flex-1 border-none outline-none ring-0 min-h-20 resize-none"
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={actualValue}
135
+ value={currentValue ?? ""}
136
+ onBlur={onBlur}
137
+ onFocus={onFocus}
125
138
  onChange={handleChange}
126
- onKeyDown={handleEnter(onEnterPressed)}
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 { CoreIcons, Icon, Tooltip } from "@react-fabric/core";
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
- export const ErrorIcon = ({ error = "", invalid }: KeyValue) => {
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
- isString(error)
36
- ? `${error}`
37
- : (t(
38
- error?.key,
39
- error.values
41
+ isObject<ErrorObject>(error)
42
+ ? t(
43
+ error.key,
44
+ isObject(error.values)
40
45
  ? {
41
- ...error?.values,
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
- ) as string)
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 classNames from "classnames";
25
- import classes from "./Field.module.css";
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 const FieldWrapper = ({
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
- className,
39
- "data-inner": isInner,
40
- }: KeyValue) => {
49
+ invalid,
50
+ disabled,
51
+ error,
52
+ showError,
53
+ }: FieldProps) {
41
54
  return (
42
55
  <div
43
- className={classNames(
44
- classes.fieldWrapper,
45
- className,
46
- inline ? "flex flex-nowrap gap-1" : "block",
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
- {!isInner && (
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
- className={classNames(
59
- "flex-initial truncate text-sm",
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
- <span className="text-danger-500 select-none flex-content text-[10px]">
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 { FloatingPortal } from "@floating-ui/react";
25
- import { CoreIcons, Icon } from "@react-fabric/core";
26
- import { isString } from "@react-fabric/utilities";
27
- import classNames from "classnames";
28
- import {
29
- useCallback,
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 const InputWrapper = ({
42
- invalid,
43
- error,
44
- allowClear,
45
- showClear,
46
- decorateStart,
47
- decorateEnd,
48
- children,
49
- onClear,
50
- noOutline,
51
- noBorder,
52
- textarea = false,
53
- "data-inner": isInner,
54
- "data-open": isOpen,
55
- // expand autocomplete
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
- const Wrapper = useMemo(
65
- () => (isInner || !rest.label ? Fragment : FieldWrapper),
66
- [isInner, rest.label],
67
- );
68
- const wrapperProps = useMemo(
69
- () => (isInner || !rest.label ? {} : { ...rest, width }),
70
- [isInner, rest, width],
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<HTMLDivElement>) => {
75
- e.currentTarget.parentElement
76
- ?.querySelector<HTMLElement>("input,textarea,button")
77
- ?.focus();
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
- (e: MouseEvent<HTMLDivElement>) => {
85
- e.currentTarget.parentElement
86
- ?.querySelector<HTMLElement>("input,textarea,button")
87
- ?.focus();
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
- () => wrapperRef?.current?.querySelector("textarea")?.focus(),
96
- 50,
97
- );
98
- }, [isExpanded]);
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
- <Wrapper {...wrapperProps}>
101
+ <FieldWrapper {...props}>
171
102
  <div
172
- ref={elRef}
173
- className="flex-1 relative min-h-8"
174
- style={{ width, flexBasis: width }}
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
- {!isExpanded && el}
177
- {isExpanded && (
178
- <FloatingPortal
179
- root={elRef.current?.closest<HTMLElement>(".theme-base")}
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
- {el}
182
- </FloatingPortal>
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
- </Wrapper>
174
+ </FieldWrapper>
186
175
  );
187
- };
176
+ }