@react-fabric/form 1.0.0-beta.4 → 1.0.0-beta.40

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 (152) hide show
  1. package/{src/input/ArrayInput.module.css → css/_arrayInput.css} +8 -8
  2. package/css/_colorPicker.css +65 -0
  3. package/{src/internal/Field.module.css → css/_field.css} +26 -15
  4. package/{src/input/File.module.css → css/_file.css} +20 -6
  5. package/css/styles.css +27 -0
  6. package/dist/cjs/builder/FileField.js +37 -25
  7. package/dist/cjs/builder/FormField.js +44 -34
  8. package/dist/cjs/builder/SchemaEditor.js +72 -0
  9. package/dist/cjs/builder/components/AddSchemaFields.js +62 -0
  10. package/dist/cjs/builder/components/SchemaArrayForm.js +143 -0
  11. package/dist/cjs/builder/components/SchemaField.js +413 -0
  12. package/dist/cjs/form/Controller.js +1 -1
  13. package/dist/cjs/form/Form.js +19 -10
  14. package/dist/cjs/hooks/useFormBuilder.js +27 -15
  15. package/dist/cjs/index.js +9 -2
  16. package/dist/cjs/input/ArrayInput.js +36 -25
  17. package/dist/cjs/input/AvatarInput.js +1 -1
  18. package/dist/cjs/input/Checkbox.js +5 -4
  19. package/dist/cjs/input/ColorInput.js +70 -28
  20. package/dist/cjs/input/ColorPicker.js +1 -35
  21. package/dist/cjs/input/DateInput.js +183 -0
  22. package/dist/cjs/input/Field.js +4 -4
  23. package/dist/cjs/input/File.js +2 -3
  24. package/dist/cjs/input/Hidden.js +3 -1
  25. package/dist/cjs/input/Input.js +1 -1
  26. package/dist/cjs/input/Number.js +1 -1
  27. package/dist/cjs/input/Password.js +1 -1
  28. package/dist/cjs/input/Radio.js +1 -1
  29. package/dist/cjs/input/RadioGroup.js +6 -3
  30. package/dist/cjs/input/{Range.js → RangeSlider.js} +3 -3
  31. package/dist/cjs/input/Search.js +1 -1
  32. package/dist/cjs/input/Slider.js +1 -1
  33. package/dist/cjs/input/Switch.js +27 -5
  34. package/dist/cjs/input/Textarea.js +7 -3
  35. package/dist/cjs/internal/ErrorIcon.js +1 -1
  36. package/dist/cjs/internal/FieldWrapper.js +4 -3
  37. package/dist/cjs/internal/InputWrapper.js +14 -16
  38. package/dist/cjs/select/AutoComplete.js +1 -1
  39. package/dist/cjs/select/List.js +41 -20
  40. package/dist/cjs/select/Options.js +2 -2
  41. package/dist/cjs/select/Select.js +11 -8
  42. package/dist/cjs/select/useSelect.js +12 -7
  43. package/{src/input/ColorPicker.module.css → dist/cjs/types/schema.js} +18 -27
  44. package/dist/cjs/utils/getColor.js +6 -0
  45. package/dist/esm/builder/FileField.js +39 -27
  46. package/dist/esm/builder/FormField.js +44 -34
  47. package/dist/esm/builder/SchemaEditor.js +72 -0
  48. package/dist/esm/builder/components/AddSchemaFields.js +62 -0
  49. package/dist/esm/builder/components/SchemaArrayForm.js +143 -0
  50. package/dist/esm/builder/components/SchemaField.js +413 -0
  51. package/dist/esm/form/Controller.js +1 -1
  52. package/dist/esm/form/Form.js +19 -10
  53. package/dist/esm/hooks/useFormBuilder.js +25 -13
  54. package/dist/esm/index.js +4 -1
  55. package/dist/esm/input/ArrayInput.js +38 -27
  56. package/dist/esm/input/AvatarInput.js +1 -1
  57. package/dist/esm/input/Checkbox.js +5 -4
  58. package/dist/esm/input/ColorInput.js +72 -30
  59. package/dist/esm/input/ColorPicker.js +1 -35
  60. package/dist/esm/input/DateInput.js +183 -0
  61. package/dist/esm/input/Field.js +5 -5
  62. package/dist/esm/input/File.js +2 -3
  63. package/dist/esm/input/Hidden.js +3 -1
  64. package/dist/esm/input/Input.js +1 -1
  65. package/dist/esm/input/Number.js +1 -1
  66. package/dist/esm/input/Password.js +1 -1
  67. package/dist/esm/input/Radio.js +1 -1
  68. package/dist/esm/input/RadioGroup.js +7 -4
  69. package/dist/esm/input/{Range.js → RangeSlider.js} +3 -3
  70. package/dist/esm/input/Search.js +1 -1
  71. package/dist/esm/input/Slider.js +1 -1
  72. package/dist/esm/input/Switch.js +29 -7
  73. package/dist/esm/input/Textarea.js +7 -3
  74. package/dist/esm/internal/ErrorIcon.js +1 -1
  75. package/dist/esm/internal/FieldWrapper.js +4 -3
  76. package/dist/esm/internal/InputWrapper.js +14 -16
  77. package/dist/esm/select/AutoComplete.js +1 -1
  78. package/dist/esm/select/List.js +42 -21
  79. package/dist/esm/select/Options.js +2 -2
  80. package/dist/esm/select/Select.js +11 -8
  81. package/dist/esm/select/useSelect.js +13 -8
  82. package/dist/esm/types/schema.js +41 -0
  83. package/dist/esm/utils/getColor.js +6 -1
  84. package/dist/types/builder/FileField.d.ts +4 -2
  85. package/dist/types/builder/FormField.d.ts +3 -1
  86. package/dist/types/builder/SchemaEditor.d.ts +2 -0
  87. package/dist/types/builder/components/AddSchemaFields.d.ts +2 -0
  88. package/dist/types/builder/components/SchemaArrayForm.d.ts +2 -0
  89. package/dist/types/builder/components/SchemaField.d.ts +10 -0
  90. package/dist/types/form/Controller.d.ts +1 -1
  91. package/dist/types/hooks/useFormBuilder.d.ts +3 -1
  92. package/dist/types/index.d.ts +4 -2
  93. package/dist/types/input/ArrayInput.d.ts +20 -3
  94. package/dist/types/input/Checkbox.d.ts +4 -3
  95. package/dist/types/input/ColorInput.d.ts +1 -1
  96. package/dist/types/input/DateInput.d.ts +19 -0
  97. package/dist/types/input/RadioGroup.d.ts +18 -2
  98. package/dist/types/input/{Range.d.ts → RangeSlider.d.ts} +1 -1
  99. package/dist/types/input/Switch.d.ts +7 -3
  100. package/dist/types/input/Textarea.d.ts +5 -1
  101. package/dist/types/internal/InputWrapper.d.ts +1 -1
  102. package/dist/types/select/List.d.ts +1 -1
  103. package/dist/types/types/index.d.ts +9 -5
  104. package/dist/types/types/schema.d.ts +79 -33
  105. package/package.json +10 -7
  106. package/src/builder/FileField.tsx +53 -27
  107. package/src/builder/FormField.tsx +45 -32
  108. package/src/builder/SchemaEditor.tsx +72 -0
  109. package/src/builder/components/AddSchemaFields.tsx +74 -0
  110. package/src/builder/components/SchemaArrayForm.tsx +158 -0
  111. package/src/builder/components/SchemaField.tsx +435 -0
  112. package/src/form/Controller.tsx +2 -2
  113. package/src/form/Form.tsx +15 -8
  114. package/src/hooks/useFormBuilder.tsx +29 -15
  115. package/src/index.ts +10 -2
  116. package/src/input/ArrayInput.tsx +80 -42
  117. package/src/input/AvatarInput.tsx +1 -1
  118. package/src/input/Checkbox.tsx +5 -4
  119. package/src/input/ColorInput.tsx +102 -34
  120. package/src/input/ColorPicker.tsx +1 -35
  121. package/src/input/DateInput.tsx +248 -0
  122. package/src/input/Field.tsx +9 -11
  123. package/src/input/File.tsx +3 -6
  124. package/src/input/Hidden.tsx +4 -1
  125. package/src/input/Input.tsx +2 -2
  126. package/src/input/Number.tsx +2 -2
  127. package/src/input/Password.tsx +2 -2
  128. package/src/input/Radio.tsx +1 -1
  129. package/src/input/RadioGroup.tsx +27 -4
  130. package/src/input/{Range.tsx → RangeSlider.tsx} +2 -2
  131. package/src/input/Search.tsx +2 -2
  132. package/src/input/Slider.tsx +1 -1
  133. package/src/input/Switch.tsx +42 -6
  134. package/src/input/Textarea.tsx +11 -3
  135. package/src/internal/ErrorIcon.tsx +1 -1
  136. package/src/internal/FieldWrapper.tsx +3 -3
  137. package/src/internal/InputWrapper.tsx +21 -20
  138. package/src/select/AutoComplete.tsx +1 -1
  139. package/src/select/List.tsx +34 -7
  140. package/src/select/Options.tsx +1 -1
  141. package/src/select/Select.tsx +5 -2
  142. package/src/select/useSelect.tsx +27 -18
  143. package/src/types/index.ts +14 -7
  144. package/src/types/schema.ts +96 -41
  145. package/dist/cjs/input/ArrayInput.module.css +0 -1
  146. package/dist/cjs/input/ColorPicker.module.css +0 -1
  147. package/dist/cjs/input/File.module.css +0 -1
  148. package/dist/cjs/internal/Field.module.css +0 -1
  149. package/dist/esm/input/ArrayInput.module.css +0 -1
  150. package/dist/esm/input/ColorPicker.module.css +0 -1
  151. package/dist/esm/input/File.module.css +0 -1
  152. package/dist/esm/internal/Field.module.css +0 -1
@@ -22,7 +22,6 @@
22
22
  */
23
23
 
24
24
  import classNames from "classnames";
25
- import classes from "./Field.module.css";
26
25
 
27
26
  export const FieldWrapper = ({
28
27
  label,
@@ -41,7 +40,7 @@ export const FieldWrapper = ({
41
40
  return (
42
41
  <div
43
42
  className={classNames(
44
- classes.fieldWrapper,
43
+ "fabric-fieldWrapper",
45
44
  className,
46
45
  inline ? "flex flex-nowrap gap-1" : "block",
47
46
  )}
@@ -50,7 +49,7 @@ export const FieldWrapper = ({
50
49
  >
51
50
  {!isInner && (
52
51
  <div
53
- className="flex flex-nowrap flex-content gap-1 py-0.5 px-2"
52
+ className="flex flex-nowrap flex-content items-end gap-1 py-0.5 px-2"
54
53
  style={{ width: inline ? labelWidth : undefined }}
55
54
  >
56
55
  <label
@@ -73,6 +72,7 @@ export const FieldWrapper = ({
73
72
  ✽
74
73
  </span>
75
74
  )}
75
+ <div className="flex-1" />
76
76
  {appendLabel && <span className="flex-content">{appendLabel}</span>}
77
77
  </div>
78
78
  )}
@@ -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,14 @@ 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
52
  textarea = false,
53
+ textareaExpandable = false,
53
54
  "data-inner": isInner,
54
55
  "data-open": isOpen,
55
56
  // expand autocomplete
@@ -102,12 +103,12 @@ export const InputWrapper = ({
102
103
  role="none"
103
104
  ref={wrapperRef}
104
105
  className={classNames(
105
- classes.inputWrapper,
106
- invalid && classes.invalidWrapper,
106
+ "fabric-inputWrapper",
107
+ invalid && "fabric-invalidWrapper",
107
108
  !noOutline &&
108
109
  !noBorder &&
109
- "bg-[var(--fabric-input)] outline overflow-hidden",
110
- "flex flex-nowrap items-center",
110
+ "bg-(--fabric-input) outline overflow-hidden",
111
+ "flex flex-nowrap items-center flex-1",
111
112
  textarea ? "rounded-capped group" : "rounded",
112
113
  isExpanded
113
114
  ? "fixed inset-4 z-[99]"
@@ -117,13 +118,17 @@ export const InputWrapper = ({
117
118
  !noOutline &&
118
119
  "after:absolute after:bottom-0 after:h-px after:bg-primary-500 after:mx-auto",
119
120
  )}
120
- onKeyDown={(e) => e.key === "Escape" && setExpanded(false)}
121
+ onKeyDown={(e) =>
122
+ e.key === "Escape" &&
123
+ isExpanded &&
124
+ (setExpanded(false), e.stopPropagation())
125
+ }
121
126
  >
122
127
  {decorateStart && (
123
128
  <div
124
129
  onClickCapture={doFocus}
125
130
  className={classNames(
126
- classes.decorate,
131
+ "fabric-decorate",
127
132
  "text-muted leading-none flex-content",
128
133
  isString(decorateStart) && "px-1",
129
134
  )}
@@ -139,7 +144,7 @@ export const InputWrapper = ({
139
144
  onClick={handleClear}
140
145
  className={classNames(
141
146
  "text-tint-500 hover:text-tint-700 p-1 z-1 flex-content",
142
- classes.inputClear,
147
+ "fabric-inputClear",
143
148
  textarea && "self-start mt-1 me-1",
144
149
  )}
145
150
  />
@@ -148,15 +153,18 @@ export const InputWrapper = ({
148
153
  <div
149
154
  onClickCapture={doFocus}
150
155
  className={classNames(
151
- classes.decorate,
156
+ "fabric-decorate",
152
157
  "text-muted leading-none flex-content",
153
158
  isString(decorateEnd) && "px-1",
159
+ decorateEndHideWhenEmpty && "peer-placeholder-shown:hidden",
160
+ decorateEndShowWhenEmpty &&
161
+ "hidden peer-placeholder-shown:inline-block",
154
162
  )}
155
163
  >
156
164
  {decorateEnd}
157
165
  </div>
158
166
  )}
159
- {textarea && (
167
+ {textareaExpandable && (
160
168
  <Icon
161
169
  onClick={() => setExpanded(!isExpanded)}
162
170
  icon={isExpanded ? CoreIcons.arrowCollapse : CoreIcons.arrowExpand}
@@ -170,17 +178,10 @@ export const InputWrapper = ({
170
178
  <Wrapper {...wrapperProps}>
171
179
  <div
172
180
  ref={elRef}
173
- className="flex-1 relative min-h-8"
181
+ className="flex-1 relative min-h-8 flex flex-col flex-nowrap"
174
182
  style={{ width, flexBasis: width }}
175
183
  >
176
- {!isExpanded && el}
177
- {isExpanded && (
178
- <FloatingPortal
179
- root={elRef.current?.closest<HTMLElement>(".theme-base")}
180
- >
181
- {el}
182
- </FloatingPortal>
183
- )}
184
+ {el}
184
185
  </div>
185
186
  </Wrapper>
186
187
  );
@@ -328,7 +328,7 @@ export const AutoComplete = ({
328
328
  >
329
329
  <Options
330
330
  ref={refs.setFloating}
331
- className="outline shadow-lg max-h-[40vh] z-[var(--z-popover)]"
331
+ className="outline shadow-lg max-h-[40vh] z-(--z-popover)"
332
332
  style={{
333
333
  width: refs.reference.current?.getBoundingClientRect().width,
334
334
  ...floatingStyles,
@@ -32,7 +32,7 @@ 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";
@@ -55,6 +55,7 @@ export const List = <T extends AnyObject = string>({
55
55
  valueProperty,
56
56
  multiple,
57
57
  allowCreate,
58
+ searchable,
58
59
  createOption,
59
60
  matcher,
60
61
  renderer,
@@ -166,9 +167,12 @@ export const List = <T extends AnyObject = string>({
166
167
  }
167
168
  } else if (evt.key === "Backspace") {
168
169
  !state.query && handleRemove();
169
- } else if (["ArrowUp", "ArrowDown"].includes(evt.key)) {
170
- if (state.activeIndex === null) {
171
- setActiveIndex(state.selectedIndex);
170
+ } else if (!searchable || ["ArrowUp", "ArrowDown"].includes(evt.key)) {
171
+ if (
172
+ ["ArrowUp", "ArrowDown"].includes(evt.key) &&
173
+ state.activeIndex === null
174
+ ) {
175
+ setActiveIndex(state.selectedIndex ?? 0);
172
176
  }
173
177
  setTimeout(() => {
174
178
  props.onKeyDown?.(evt);
@@ -225,6 +229,15 @@ export const List = <T extends AnyObject = string>({
225
229
  }
226
230
  }, [state.value, state.query, placeholder, renderer]);
227
231
 
232
+ useLayoutEffect(() => {
233
+ setTimeout(() => {
234
+ state.selectedIndex &&
235
+ listRef.current[state.selectedIndex]?.scrollIntoView({
236
+ block: "center",
237
+ });
238
+ }, 100);
239
+ }, [state.selectedIndex]);
240
+
228
241
  return (
229
242
  <Wrapper {...wrapperProps}>
230
243
  <InputWrapper
@@ -240,22 +253,34 @@ export const List = <T extends AnyObject = string>({
240
253
  wrapperRef={refs.setReference}
241
254
  >
242
255
  <div
256
+ role="none"
243
257
  data-select-display="true"
244
- className="group flex-1 py-1 px-2 truncate text-start flex gap-1 flex-wrap"
258
+ className={classNames(
259
+ "group flex-1 py-1 px-2 truncate text-start flex gap-1 relative min-h-5 justify-start",
260
+ multiple ? "flex-wrap" : "flex-nowrap overflow-hidden",
261
+ )}
262
+ {...referenceProps}
263
+ onMouseUp={(e) =>
264
+ e.currentTarget.querySelector<HTMLElement>("input")?.focus()
265
+ }
245
266
  >
246
267
  {!hideSelected && displayValue}
247
268
  <input
269
+ readOnly={!searchable || readOnly}
248
270
  disabled={disabled}
249
271
  value={state.query ?? ""}
250
272
  aria-invalid={invalid}
251
273
  data-testid={name}
252
274
  name={name}
275
+ size={1}
276
+ ref={ref}
277
+ autoFocus={autoFocus}
253
278
  className={classNames(
254
279
  "appearance-none bg-transparent outline-none border-none ring-0 flex-1 min-w-24",
255
280
  disabled && "cursor-not-allowed pointer-events-none",
281
+ multiple && "min-w-24",
256
282
  )}
257
283
  placeholder={isEmpty(state.value) ? placeholder : ""}
258
- {...referenceProps}
259
284
  onChange={(evt) => handleQuery(evt.target.value)}
260
285
  />
261
286
  </div>
@@ -277,10 +302,12 @@ export const List = <T extends AnyObject = string>({
277
302
  {...floatingProps}
278
303
  items={state.items}
279
304
  active={state.activeIndex}
305
+ valueProperty={valueProperty}
306
+ labelProperty={labelProperty}
280
307
  itemRef={setItemRef}
281
308
  itemProps={makeItemProps}
282
309
  >
283
- {(item) => renderer?.(item) ?? item[labelProperty] ?? item}
310
+ {(item, label) => renderer?.(item) ?? label}
284
311
  </Options>
285
312
  </FloatingFocusManager>
286
313
  </Wrapper>
@@ -93,7 +93,7 @@ export const Options = ({
93
93
  >
94
94
  <Trans
95
95
  i18nKey="form:select.createOption"
96
- values={{ query: item.value }}
96
+ values={{ query: item.value ?? item }}
97
97
  components={[<strong key="t0">query</strong>]}
98
98
  />
99
99
  </div>
@@ -172,6 +172,9 @@ export const Select = <T extends AnyObject = string>({
172
172
  return false;
173
173
  } else if (state.query) {
174
174
  handleQuery("");
175
+ evt.preventDefault();
176
+ evt.stopPropagation();
177
+ return false;
175
178
  } else {
176
179
  onEnterPressed?.(evt);
177
180
  }
@@ -273,6 +276,7 @@ export const Select = <T extends AnyObject = string>({
273
276
  "group flex-1 py-1 px-2 truncate text-start flex gap-1 relative min-h-5 justify-start",
274
277
  multiple ? "flex-wrap" : "flex-nowrap overflow-hidden",
275
278
  )}
279
+ {...referenceProps}
276
280
  onMouseUp={(e) =>
277
281
  e.currentTarget.querySelector<HTMLElement>("input")?.focus()
278
282
  }
@@ -294,7 +298,6 @@ export const Select = <T extends AnyObject = string>({
294
298
  multiple && "min-w-24",
295
299
  )}
296
300
  placeholder={isEmpty(state.value) ? placeholder : ""}
297
- {...referenceProps}
298
301
  onChange={(evt) => handleQuery(evt.target.value)}
299
302
  />
300
303
  </div>
@@ -327,7 +330,7 @@ export const Select = <T extends AnyObject = string>({
327
330
  >
328
331
  <Options
329
332
  ref={refs.setFloating}
330
- className="outline shadow-lg max-h-[40vh] z-[var(--z-popover)]"
333
+ className="outline shadow-lg max-h-[40vh] z-(--z-popover)"
331
334
  style={{
332
335
  width: refs.reference.current?.getBoundingClientRect().width,
333
336
  ...floatingStyles,
@@ -23,10 +23,12 @@
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,
29
30
  isArray,
31
+ isString,
30
32
  matchString,
31
33
  } from "@react-fabric/utilities";
32
34
  import {
@@ -173,9 +175,9 @@ export const useSelect = ({
173
175
  if (action.value !== undefined) {
174
176
  if (multiple) {
175
177
  const values = [];
176
- if (action.value && autoComplete)
178
+ if (isString(action.value))
177
179
  values.push(...action.value.split(/,\s?/));
178
- else if (action.value) values.push(action.value);
180
+ else values.push(action.value);
179
181
  newState.value = [...state.value];
180
182
  values.forEach((value: AnyObject) => {
181
183
  newState.value = newState.value.includes?.(value)
@@ -222,6 +224,7 @@ export const useSelect = ({
222
224
  if (autoComplete && action.query !== undefined) {
223
225
  optionList = dedupe([action.query, ...optionList]);
224
226
  }
227
+ optionList = optionList.sort(compareValues("asc", labelProperty));
225
228
  const grouped = groupBy(optionList, groupProperty, "");
226
229
  if (action.open) newState.open = true;
227
230
  newState.items = [];
@@ -229,7 +232,7 @@ export const useSelect = ({
229
232
  if (allowCreate && action.query)
230
233
  newState.items.push({
231
234
  ___create___: true,
232
- value: createOption(action.query),
235
+ value: action.query,
233
236
  });
234
237
  Object.entries(grouped).forEach(([key, list]) => {
235
238
  if (key) {
@@ -282,24 +285,26 @@ export const useSelect = ({
282
285
  dispatch({ type: "options", options: optionsRef.current });
283
286
  dispatch({
284
287
  type: "setValue",
285
- value: hasValue ?? newOption ?? "",
288
+ value: hasValue ?? newOption ?? deferred,
286
289
  });
287
290
  }
288
291
  if (multiple) {
289
292
  optionsRef.current = [];
290
- const value = (deferred ?? []).map((item: AnyObject) => {
291
- const newOption =
292
- (allowCreate || autoComplete) && item
293
- ? createOption(item)
294
- : undefined;
295
- const hasValue = options.find(
296
- (opt: AnyObject) => (opt[valueProperty] ?? opt) === deferred,
297
- );
298
- if (!hasValue && newOption) {
299
- optionsRef.current.push(newOption);
300
- }
301
- return hasValue ?? newOption ?? "";
302
- });
293
+ const value = (isArray(deferred) ? deferred : []).map(
294
+ (item: AnyObject) => {
295
+ const newOption =
296
+ (allowCreate || autoComplete) && item
297
+ ? createOption(item)
298
+ : undefined;
299
+ const hasValue = options.find(
300
+ (opt: AnyObject) => (opt[valueProperty] ?? opt) === item,
301
+ );
302
+ if (!hasValue && newOption) {
303
+ optionsRef.current.push(newOption);
304
+ }
305
+ return hasValue ?? newOption ?? item;
306
+ },
307
+ );
303
308
  optionsRef.current = [...optionsRef.current, ...(options ?? [])];
304
309
  dispatch({ type: "options", options: optionsRef.current });
305
310
  dispatch({
@@ -367,7 +372,11 @@ export const useSelect = ({
367
372
  value: value?.___create___ ? value.value : value,
368
373
  });
369
374
  if (value?.___create___) {
370
- optionsRef.current = [...optionsRef.current, createOption(value.value)];
375
+ let val = [value.value];
376
+ if (multiple) {
377
+ val = value.value.split(/,\s?/);
378
+ }
379
+ optionsRef.current = [...optionsRef.current, ...val.map(createOption)];
371
380
  }
372
381
  if (autoComplete) {
373
382
  optionsRef.current = dedupe([
@@ -21,9 +21,12 @@
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";
24
+ import {
25
+ type RefProp,
26
+ type TestProps,
27
+ } from "@react-fabric/core/dist/types/types";
25
28
 
26
- export interface InputProps<V = string> {
29
+ export interface InputProps<V = string> extends TestProps {
27
30
  /**
28
31
  * value
29
32
  */
@@ -31,7 +34,7 @@ export interface InputProps<V = string> {
31
34
  /**
32
35
  * change handler
33
36
  */
34
- onChange?: (value?: V) => void;
37
+ onChange?: (value: V | null) => void;
35
38
  /**
36
39
  * on focus event handler
37
40
  */
@@ -69,6 +72,8 @@ export interface InputProps<V = string> {
69
72
  *
70
73
  */
71
74
  decorateEnd?: JSX.Element | string | number | boolean;
75
+ decorateEndShowWhenEmpty?: boolean;
76
+ decorateEndHideWhenEmpty?: boolean;
72
77
  /**
73
78
  * info label at bottom
74
79
  */
@@ -117,9 +122,7 @@ export interface InputProps<V = string> {
117
122
  labelWidth?: string;
118
123
  }
119
124
 
120
- export interface BaseSelectProps<T>
121
- extends RefProp<HTMLInputElement>,
122
- Omit<InputProps, "value" | "onChange"> {
125
+ export interface BaseSelectProps<T extends AnyObject = string> {
123
126
  /**
124
127
  * list options
125
128
  */
@@ -162,7 +165,11 @@ export interface BaseSelectProps<T>
162
165
  matcher?: (option: T, query: string) => boolean;
163
166
  }
164
167
 
165
- export type SelectProps<T> = BaseSelectProps<T> &
168
+ type BaseSelectType<T> = BaseSelectProps<T> &
169
+ RefProp<HTMLInputElement> &
170
+ Omit<InputProps, "value" | "onChange">;
171
+
172
+ export type SelectProps<T> = BaseSelectType<T> &
166
173
  (
167
174
  | {
168
175
  /**
@@ -21,80 +21,135 @@
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
- interface BaseSchemaDef {
24
+ import { type Elements } from "@react-fabric/core/dist/types/types";
25
+ import { type DateLike } from "@react-fabric/date";
26
+
27
+ export enum DATA_TYPES {
28
+ TEXT = "text",
29
+ COLOR = "color",
30
+ BOOL = "bool",
31
+ DATE = "date",
32
+ STRING = "string",
33
+ NUMBER = "number",
34
+ RANGE = "range",
35
+ FILE = "file",
36
+ AVATAR = "avatar",
37
+ SCHEMA = "schema",
38
+ }
39
+
40
+ export const SCHEMA_CUSTOM_LIST = "--CUSTOM--";
41
+
42
+ interface SingleValue {
43
+ multiple?: false;
44
+ minItems?: never;
45
+ maxItems?: never;
46
+ }
47
+
48
+ interface MultipleValue {
49
+ multiple: true;
50
+ minItems?: number;
51
+ maxItems?: number;
52
+ }
53
+
54
+ export type ValueType = SingleValue | MultipleValue;
55
+
56
+ interface BaseSchema {
25
57
  id: string;
26
58
  label: string;
27
59
  datatype: string;
28
60
  required?: boolean;
29
- multiple?: boolean;
30
- defaultValue?: AnyObject;
31
61
  }
32
62
 
33
- interface SchemaDef extends BaseSchemaDef {
34
- datatype: "text" | "color";
63
+ export interface DefaultSchema extends BaseSchema {
64
+ datatype: DATA_TYPES.TEXT | DATA_TYPES.COLOR;
35
65
  }
36
66
 
37
- interface BooleanSchema extends BaseSchemaDef {
38
- datatype: "boolean";
39
- defaultValue?: boolean;
67
+ export interface BooleanSchema extends BaseSchema {
68
+ datatype: DATA_TYPES.BOOL;
40
69
  }
41
70
 
42
- interface StringSchema extends BaseSchemaDef {
43
- datatype: "string";
44
- options?: string[];
71
+ export interface StringSchema extends BaseSchema {
72
+ datatype: DATA_TYPES.STRING;
45
73
  regex?: string;
46
- identifierType?: string;
74
+ options?: string[];
75
+ optionList?: string;
76
+ allowCustom?: boolean;
47
77
  }
48
78
 
49
- interface RangeSchema extends BaseSchemaDef {
50
- datatype: "range";
79
+ export interface RangeSchema extends BaseSchema {
80
+ datatype: DATA_TYPES.RANGE;
51
81
  min?: number;
52
82
  max?: number;
53
83
  step?: number;
54
- defaultValue?: [number, number];
55
84
  }
56
85
 
57
- interface NumberSchema extends BaseSchemaDef {
58
- datatype: "number";
59
- min?: number;
60
- max?: number;
61
- step?: number;
62
- defaultValue?: number;
86
+ export interface DateSchema extends BaseSchema {
87
+ datatype: DATA_TYPES.DATE;
88
+ type?: "date" | "datetime";
89
+ min?: DateLike;
90
+ max?: DateLike;
63
91
  }
64
92
 
65
- interface DecimalSchema extends BaseSchemaDef {
66
- datatype: "decimal";
93
+ export interface NumberSchema extends BaseSchema {
94
+ datatype: DATA_TYPES.NUMBER;
67
95
  min?: number;
68
96
  max?: number;
69
97
  step?: number;
70
- defaultValue?: number;
71
98
  }
72
99
 
73
- export interface FileSchema extends BaseSchemaDef {
74
- datatype: "file";
100
+ export interface FileSchema extends BaseSchema {
101
+ datatype: DATA_TYPES.FILE;
75
102
  accept?: string;
76
103
  }
77
104
 
78
- export interface AvatarSchema extends BaseSchemaDef {
79
- datatype: "avatar";
105
+ export interface AvatarSchema extends BaseSchema {
106
+ datatype: DATA_TYPES.AVATAR;
80
107
  multiple?: false;
81
108
  }
82
109
 
83
110
  export type SchemaType =
84
- | SchemaDef
85
- | BooleanSchema
86
- | StringSchema
87
- | RangeSchema
88
- | NumberSchema
89
- | DecimalSchema
90
- | FileSchema
91
- | AvatarSchema;
92
-
93
- export interface ChildSchema extends BaseSchemaDef {
94
- datatype: "schema";
111
+ | (DefaultSchema & ValueType)
112
+ | (BooleanSchema & ValueType)
113
+ | (StringSchema & ValueType)
114
+ | (RangeSchema & ValueType)
115
+ | (DateSchema & ValueType)
116
+ | (NumberSchema & ValueType)
117
+ | (FileSchema & ValueType)
118
+ | (AvatarSchema & ValueType);
119
+
120
+ export interface ChildSchema extends BaseSchema {
121
+ datatype: DATA_TYPES.SCHEMA;
95
122
  inline?: boolean;
96
123
  schema: SchemaType[];
97
- defaultValue?: never;
98
124
  }
99
125
 
100
- export type FormSchema = SchemaType | ChildSchema;
126
+ export type SchemaDef = SchemaType | (ChildSchema & ValueType);
127
+ export type FormSchema = SchemaDef[];
128
+
129
+ export interface SchemaEditorProps {
130
+ /**
131
+ * default form schema
132
+ */
133
+ schemaDef: FormSchema;
134
+ /**
135
+ * is dynamic schema (allow add/remove)
136
+ */
137
+ dynamic?: boolean;
138
+ /**
139
+ * acceptable list of data types
140
+ */
141
+ acceptableTypes?: DATA_TYPES[];
142
+
143
+ /**
144
+ * available option lists in application
145
+ */
146
+ optionLists?: string[];
147
+
148
+ /**
149
+ * footer actions
150
+ */
151
+ children?: Elements<JSX.Element> | Array<Elements<JSX.Element>>;
152
+
153
+ onChange?: (schema: FormSchema) => void;
154
+ onSubmit?: (schema: FormSchema) => void;
155
+ }
@@ -1 +0,0 @@
1
- @layer var{.arrayInput{& [data-ref=fieldWrapper]{flex:1 1 0%}& .addButton :global(.buttonInner){color:theme(colors.tint.600);outline:1px dashed theme(colors.tint.600);&[data-invalid=true]{background-color:theme(colors.danger.100);color:theme(colors.danger.600);outline:1px dashed theme(colors.danger.600)}}}}
@@ -1 +0,0 @@
1
- @layer comp{.colorPicker{--color-shadow:theme(borderColor.base);& input{@apply bg-base text-base !border !border-solid !rounded-sm !border-base focus:!border-primary !shadow-none;line-height:2;padding:0 4px!important}& label{@apply !text-base}& :global(.flexbox-fix){@apply !border-t-muted justify-center;&>div>div{@apply text-center}& span>div{box-shadow:inset 0 0 1px 1px theme(borderColor.base)!important}}}}
@@ -1 +0,0 @@
1
- @layer comp{.fileInput{&::-webkit-file-upload-button{@apply bg-primary-500 opacity-80 hover:opacity-90 text-white rounded border-none py-0 cursor-pointer}&[aria-invalid=true]::-webkit-file-upload-button{@apply bg-danger-500}&:disabled::-webkit-file-upload-button{@apply bg-muted pointer-events-none}&[readOnly]{@apply pointer-events-none}}}
@@ -1 +0,0 @@
1
- @layer comp{}@layer var{.inputWrapper{&:after{inset-inline:50%;transition:all .3s;transition-delay:.1s}& .decorate:has([data-ref=button]){align-self:stretch;display:flex}&:has(input[disabled],textarea[disabled]){background-color:theme(colors.tint.50);cursor:not-allowed;& input,& textarea{cursor:not-allowed}}&:focus-within,&:has([data-open=true]){background-color:var(--fabric-input-focus);outline-color:var(--primary);z-index:1;&:not(:has([aria-readonly=true]:not([hidden]))):after{inset-inline:0}}&:global(.fixed){box-shadow:0 0 2rem .5rem var(--fabric-shadow)}& input,& textarea{max-width:100%}&>input[type=number]+.inputClear{margin-inline:-3rem 2rem}& input::-ms-clear,& select::-ms-clear,& textarea::-ms-clear{display:none}& input[type=search]::-webkit-search-cancel-button{display:none}& input[type=password]:not(:placeholder-shown){font-family:caption;letter-spacing:2px}& input:-webkit-autofill{box-shadow:0 0 0 999em var(--fabric-input) inset;-webkit-text-fill-color:theme(colors.accent.700)}& input:-webkit-autofill:focus{box-shadow:0 0 0 999em var(--fabric-input-focus) inset;-webkit-text-fill-color:theme(colors.accent.700)}&.invalidWrapper{outline-color:theme(colors.danger.500);&:after{background-color:theme(colors.danger.500)}}}.fieldGroup{>*{line-height:1}>:not(:first-child){&,&>[data-ref=button]>[role=button],&>[role=button],&>div{border-end-start-radius:0;border-start-start-radius:0}}>:not(:last-child){&,&>[data-ref=button]>[role=button],&>[role=button],&>div{border-end-end-radius:0;border-start-end-radius:0}}}}
@@ -1 +0,0 @@
1
- @layer var{.arrayInput{& [data-ref=fieldWrapper]{flex:1 1 0%}& .addButton :global(.buttonInner){color:theme(colors.tint.600);outline:1px dashed theme(colors.tint.600);&[data-invalid=true]{background-color:theme(colors.danger.100);color:theme(colors.danger.600);outline:1px dashed theme(colors.danger.600)}}}}
@@ -1 +0,0 @@
1
- @layer comp{.colorPicker{--color-shadow:theme(borderColor.base);& input{@apply bg-base text-base !border !border-solid !rounded-sm !border-base focus:!border-primary !shadow-none;line-height:2;padding:0 4px!important}& label{@apply !text-base}& :global(.flexbox-fix){@apply !border-t-muted justify-center;&>div>div{@apply text-center}& span>div{box-shadow:inset 0 0 1px 1px theme(borderColor.base)!important}}}}
@@ -1 +0,0 @@
1
- @layer comp{.fileInput{&::-webkit-file-upload-button{@apply bg-primary-500 opacity-80 hover:opacity-90 text-white rounded border-none py-0 cursor-pointer}&[aria-invalid=true]::-webkit-file-upload-button{@apply bg-danger-500}&:disabled::-webkit-file-upload-button{@apply bg-muted pointer-events-none}&[readOnly]{@apply pointer-events-none}}}
@@ -1 +0,0 @@
1
- @layer comp{}@layer var{.inputWrapper{&:after{inset-inline:50%;transition:all .3s;transition-delay:.1s}& .decorate:has([data-ref=button]){align-self:stretch;display:flex}&:has(input[disabled],textarea[disabled]){background-color:theme(colors.tint.50);cursor:not-allowed;& input,& textarea{cursor:not-allowed}}&:focus-within,&:has([data-open=true]){background-color:var(--fabric-input-focus);outline-color:var(--primary);z-index:1;&:not(:has([aria-readonly=true]:not([hidden]))):after{inset-inline:0}}&:global(.fixed){box-shadow:0 0 2rem .5rem var(--fabric-shadow)}& input,& textarea{max-width:100%}&>input[type=number]+.inputClear{margin-inline:-3rem 2rem}& input::-ms-clear,& select::-ms-clear,& textarea::-ms-clear{display:none}& input[type=search]::-webkit-search-cancel-button{display:none}& input[type=password]:not(:placeholder-shown){font-family:caption;letter-spacing:2px}& input:-webkit-autofill{box-shadow:0 0 0 999em var(--fabric-input) inset;-webkit-text-fill-color:theme(colors.accent.700)}& input:-webkit-autofill:focus{box-shadow:0 0 0 999em var(--fabric-input-focus) inset;-webkit-text-fill-color:theme(colors.accent.700)}&.invalidWrapper{outline-color:theme(colors.danger.500);&:after{background-color:theme(colors.danger.500)}}}.fieldGroup{>*{line-height:1}>:not(:first-child){&,&>[data-ref=button]>[role=button],&>[role=button],&>div{border-end-start-radius:0;border-start-start-radius:0}}>:not(:last-child){&,&>[data-ref=button]>[role=button],&>[role=button],&>div{border-end-end-radius:0;border-start-end-radius:0}}}}