@dxos/react-ui-form 0.8.4-main.f9ba587 → 0.8.4-main.fcc0d83b33

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 (246) hide show
  1. package/README.md +0 -1
  2. package/dist/lib/browser/index.mjs +2787 -2137
  3. package/dist/lib/browser/index.mjs.map +4 -4
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/browser/translations.mjs +82 -0
  6. package/dist/lib/browser/translations.mjs.map +7 -0
  7. package/dist/lib/node-esm/index.mjs +2787 -2137
  8. package/dist/lib/node-esm/index.mjs.map +4 -4
  9. package/dist/lib/node-esm/meta.json +1 -1
  10. package/dist/lib/node-esm/translations.mjs +84 -0
  11. package/dist/lib/node-esm/translations.mjs.map +7 -0
  12. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +9 -9
  13. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  14. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +86 -7
  15. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  16. package/dist/types/src/components/Form/Form.d.ts +109 -25
  17. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  18. package/dist/types/src/components/Form/Form.stories.d.ts +130 -56
  19. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  20. package/dist/types/src/components/Form/FormField.d.ts +43 -0
  21. package/dist/types/src/components/Form/FormField.d.ts.map +1 -0
  22. package/dist/types/src/components/Form/FormFieldComponent.d.ts +79 -0
  23. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -0
  24. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +9 -0
  25. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -0
  26. package/dist/types/src/components/Form/FormFieldSet.d.ts +15 -0
  27. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -0
  28. package/dist/types/src/components/Form/fields/ArrayField.d.ts +18 -0
  29. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -0
  30. package/dist/types/src/components/Form/fields/BooleanField.d.ts +4 -0
  31. package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -0
  32. package/dist/types/src/components/Form/fields/GeoPointField.d.ts +5 -0
  33. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -0
  34. package/dist/types/src/components/Form/fields/MarkdownField.d.ts +11 -0
  35. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -0
  36. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +83 -0
  37. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +1 -0
  38. package/dist/types/src/components/Form/fields/NumberField.d.ts +4 -0
  39. package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -0
  40. package/dist/types/src/components/Form/fields/RefField.d.ts +22 -0
  41. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -0
  42. package/dist/types/src/components/Form/fields/SelectField.d.ts +10 -0
  43. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -0
  44. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +5 -0
  45. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -0
  46. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +84 -0
  47. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -0
  48. package/dist/types/src/components/Form/fields/TextAreaField.d.ts +4 -0
  49. package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -0
  50. package/dist/types/src/components/Form/fields/TextField.d.ts +4 -0
  51. package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -0
  52. package/dist/types/src/components/Form/fields/TupleField.d.ts +6 -0
  53. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -0
  54. package/dist/types/src/components/Form/fields/index.d.ts +12 -0
  55. package/dist/types/src/components/Form/fields/index.d.ts.map +1 -0
  56. package/dist/types/src/components/Form/index.d.ts +2 -4
  57. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  58. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +9 -0
  59. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
  60. package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
  61. package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
  62. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +68 -0
  63. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -0
  64. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +87 -0
  65. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -0
  66. package/dist/types/src/components/ObjectPicker/index.d.ts +2 -0
  67. package/dist/types/src/components/ObjectPicker/index.d.ts.map +1 -0
  68. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
  69. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
  70. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +127 -0
  71. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
  72. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts +2 -0
  73. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts.map +1 -0
  74. package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
  75. package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
  76. package/dist/types/src/components/Settings/Settings.d.ts +51 -0
  77. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
  78. package/dist/types/src/components/Settings/Settings.stories.d.ts +84 -0
  79. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
  80. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +43 -0
  81. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -0
  82. package/dist/types/src/components/Settings/SettingsItem.d.ts +16 -0
  83. package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -0
  84. package/dist/types/src/components/Settings/SettingsSelectField.d.ts +18 -0
  85. package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +1 -0
  86. package/dist/types/src/components/Settings/index.d.ts +4 -0
  87. package/dist/types/src/components/Settings/index.d.ts.map +1 -0
  88. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +22 -10
  89. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  90. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +87 -9
  91. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  92. package/dist/types/src/components/index.d.ts +4 -2
  93. package/dist/types/src/components/index.d.ts.map +1 -1
  94. package/dist/types/src/components/testing.d.ts +5 -4
  95. package/dist/types/src/components/testing.d.ts.map +1 -1
  96. package/dist/types/src/hooks/index.d.ts +2 -3
  97. package/dist/types/src/hooks/index.d.ts.map +1 -1
  98. package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
  99. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
  100. package/dist/types/src/hooks/useKeyHandler.d.ts +7 -0
  101. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
  102. package/dist/types/src/index.d.ts +0 -2
  103. package/dist/types/src/index.d.ts.map +1 -1
  104. package/dist/types/src/translations.d.ts +65 -54
  105. package/dist/types/src/translations.d.ts.map +1 -1
  106. package/dist/types/src/util/field-type.d.ts +19 -0
  107. package/dist/types/src/util/field-type.d.ts.map +1 -0
  108. package/dist/types/src/util/field.d.ts +3 -3
  109. package/dist/types/src/util/field.d.ts.map +1 -1
  110. package/dist/types/src/util/formatting.d.ts +2 -2
  111. package/dist/types/src/util/formatting.d.ts.map +1 -1
  112. package/dist/types/src/util/index.d.ts +2 -1
  113. package/dist/types/src/util/index.d.ts.map +1 -1
  114. package/dist/types/src/util/properties.d.ts +7 -0
  115. package/dist/types/src/util/properties.d.ts.map +1 -0
  116. package/dist/types/src/util/refs.d.ts +3 -3
  117. package/dist/types/src/util/refs.d.ts.map +1 -1
  118. package/dist/types/tsconfig.tsbuildinfo +1 -1
  119. package/package.json +57 -46
  120. package/src/components/FieldEditor/FieldEditor.stories.tsx +52 -31
  121. package/src/components/FieldEditor/FieldEditor.test.tsx +13 -13
  122. package/src/components/FieldEditor/FieldEditor.tsx +90 -95
  123. package/src/components/Form/AUDIT.md +17 -0
  124. package/src/components/Form/Form.stories.tsx +147 -223
  125. package/src/components/Form/Form.tsx +376 -95
  126. package/src/components/Form/FormField.tsx +306 -0
  127. package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
  128. package/src/components/Form/FormFieldComponent.tsx +174 -0
  129. package/src/components/Form/FormFieldSet.tsx +144 -0
  130. package/src/components/Form/README.md +3 -0
  131. package/src/components/Form/fields/ArrayField.tsx +159 -0
  132. package/src/components/Form/fields/BooleanField.tsx +22 -0
  133. package/src/components/Form/fields/GeoPointField.tsx +121 -0
  134. package/src/components/Form/fields/MarkdownField.stories.tsx +152 -0
  135. package/src/components/Form/fields/MarkdownField.tsx +168 -0
  136. package/src/components/Form/fields/NumberField.tsx +39 -0
  137. package/src/components/Form/fields/RefField.tsx +229 -0
  138. package/src/components/Form/fields/SelectField.tsx +65 -0
  139. package/src/components/Form/fields/SelectOptionField.stories.tsx +66 -0
  140. package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +50 -45
  141. package/src/components/Form/fields/TextAreaField.tsx +38 -0
  142. package/src/components/Form/fields/TextField.tsx +38 -0
  143. package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +17 -10
  144. package/src/components/Form/fields/index.ts +15 -0
  145. package/src/components/Form/index.ts +3 -4
  146. package/src/components/ObjectForm/ObjectForm.tsx +114 -0
  147. package/src/components/{ControlSection → ObjectForm}/index.ts +1 -1
  148. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +212 -0
  149. package/src/components/ObjectPicker/ObjectPicker.tsx +194 -0
  150. package/src/components/ObjectPicker/index.ts +5 -0
  151. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +362 -0
  152. package/src/components/ObjectProperties/ObjectProperties.test.tsx +101 -0
  153. package/src/components/ObjectProperties/ObjectProperties.tsx +141 -0
  154. package/src/components/ObjectProperties/index.ts +5 -0
  155. package/src/components/Settings/AUDIT.md +52 -0
  156. package/src/components/Settings/Settings.stories.tsx +104 -0
  157. package/src/components/Settings/Settings.tsx +82 -0
  158. package/src/components/Settings/SettingsFieldSet.tsx +257 -0
  159. package/src/components/Settings/SettingsItem.tsx +62 -0
  160. package/src/components/Settings/SettingsSelectField.tsx +59 -0
  161. package/src/components/Settings/index.ts +7 -0
  162. package/src/components/ViewEditor/ViewEditor.stories.tsx +145 -61
  163. package/src/components/ViewEditor/ViewEditor.test.tsx +22 -22
  164. package/src/components/ViewEditor/ViewEditor.tsx +355 -219
  165. package/src/components/index.ts +4 -3
  166. package/src/components/testing.tsx +31 -12
  167. package/src/hooks/index.ts +2 -3
  168. package/src/hooks/useFormHandler.ts +380 -0
  169. package/src/hooks/useKeyHandler.tsx +37 -0
  170. package/src/index.ts +0 -4
  171. package/src/translations.ts +70 -58
  172. package/src/util/field-type.ts +60 -0
  173. package/src/util/field.ts +33 -43
  174. package/src/util/formatting.ts +22 -22
  175. package/src/util/index.ts +2 -1
  176. package/src/util/properties.ts +18 -0
  177. package/src/util/refs.ts +17 -19
  178. package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
  179. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
  180. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -8
  181. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
  182. package/dist/types/src/components/ControlSection/index.d.ts +0 -2
  183. package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
  184. package/dist/types/src/components/Form/ArrayField.d.ts +0 -16
  185. package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
  186. package/dist/types/src/components/Form/Defaults.d.ts +0 -14
  187. package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
  188. package/dist/types/src/components/Form/FormActions.d.ts +0 -9
  189. package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
  190. package/dist/types/src/components/Form/FormContent.d.ts +0 -57
  191. package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
  192. package/dist/types/src/components/Form/FormContext.d.ts +0 -23
  193. package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
  194. package/dist/types/src/components/Form/Input.d.ts +0 -26
  195. package/dist/types/src/components/Form/Input.d.ts.map +0 -1
  196. package/dist/types/src/components/Form/RefField.d.ts +0 -12
  197. package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
  198. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
  199. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
  200. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
  201. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
  202. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +0 -7
  203. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
  204. package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
  205. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
  206. package/dist/types/src/components/Form/custom/index.d.ts +0 -4
  207. package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
  208. package/dist/types/src/components/Form/factory.d.ts +0 -8
  209. package/dist/types/src/components/Form/factory.d.ts.map +0 -1
  210. package/dist/types/src/components/deprecated/Form.d.ts +0 -15
  211. package/dist/types/src/components/deprecated/Form.d.ts.map +0 -1
  212. package/dist/types/src/components/deprecated/Form.stories.d.ts +0 -13
  213. package/dist/types/src/components/deprecated/Form.stories.d.ts.map +0 -1
  214. package/dist/types/src/components/deprecated/FormInput.d.ts +0 -16
  215. package/dist/types/src/components/deprecated/FormInput.d.ts.map +0 -1
  216. package/dist/types/src/components/deprecated/index.d.ts +0 -2
  217. package/dist/types/src/components/deprecated/index.d.ts.map +0 -1
  218. package/dist/types/src/hooks/useForm.d.ts +0 -67
  219. package/dist/types/src/hooks/useForm.d.ts.map +0 -1
  220. package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -23
  221. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
  222. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
  223. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
  224. package/dist/types/src/util/schema.d.ts +0 -3
  225. package/dist/types/src/util/schema.d.ts.map +0 -1
  226. package/src/components/ControlSection/ControlSection.stories.tsx +0 -41
  227. package/src/components/ControlSection/ControlSection.tsx +0 -139
  228. package/src/components/Form/ArrayField.tsx +0 -124
  229. package/src/components/Form/Defaults.tsx +0 -203
  230. package/src/components/Form/FormActions.tsx +0 -53
  231. package/src/components/Form/FormContent.tsx +0 -240
  232. package/src/components/Form/FormContext.tsx +0 -110
  233. package/src/components/Form/Input.tsx +0 -51
  234. package/src/components/Form/RefField.tsx +0 -213
  235. package/src/components/Form/custom/GeoPointInput.tsx +0 -72
  236. package/src/components/Form/custom/SelectOptionsInput.stories.tsx +0 -61
  237. package/src/components/Form/custom/index.ts +0 -7
  238. package/src/components/Form/factory.tsx +0 -31
  239. package/src/components/deprecated/Form.stories.tsx +0 -62
  240. package/src/components/deprecated/Form.tsx +0 -119
  241. package/src/components/deprecated/FormInput.tsx +0 -71
  242. package/src/components/deprecated/index.ts +0 -5
  243. package/src/hooks/useForm.ts +0 -291
  244. package/src/hooks/useQueryRefOptions.ts +0 -53
  245. package/src/hooks/useRefQueryLookupHandler.ts +0 -39
  246. package/src/util/schema.ts +0 -14
@@ -1,108 +1,389 @@
1
1
  //
2
- // Copyright 2024 DXOS.org
3
- //
4
-
5
- import { type Schema } from 'effect';
6
- import React, { type ReactElement, useEffect, useMemo, useRef } from 'react';
7
-
8
- import { type BaseObject, type PropertyKey } from '@dxos/echo-schema';
9
- import { type ThemedClassName } from '@dxos/react-ui';
10
- import { cardDialogOverflow, cardSpacing } from '@dxos/react-ui-stack';
11
- import { type SchemaProperty } from '@dxos/schema';
12
-
13
- import { FormActions, type FormOuterSpacing } from './FormActions';
14
- import { FormFields, type FormFieldsProps } from './FormContent';
15
- import { FormProvider } from './FormContext';
16
- import { type InputProps, type InputComponent } from './Input';
17
- import { type FormOptions } from '../../hooks';
18
-
19
- export type PropsFilter<T extends BaseObject> = (props: SchemaProperty<T>[]) => SchemaProperty<T>[];
20
-
21
- export type ComponentLookup = (args: {
22
- prop: string;
23
- schema: Schema.Schema<any>;
24
- inputProps: InputProps;
25
- }) => ReactElement | undefined;
26
-
27
- export type CustomInputMap = Partial<Record<string, InputComponent>>;
28
-
29
- export type FormProps<T extends BaseObject> = ThemedClassName<{
30
- values: Partial<T>;
31
- // TODO(burdon): Autofocus first input.
32
- autoFocus?: boolean;
33
- // TODO(burdon): Change to JsonPath includes/excludes.
34
- filter?: PropsFilter<T>;
35
- sort?: PropertyKey<T>[];
36
- autoSave?: boolean;
37
- outerSpacing?: FormOuterSpacing;
38
- onCancel?: () => void;
39
- }> &
40
- Pick<FormOptions<T>, 'schema' | 'onValuesChanged' | 'onValidate' | 'onSave'> &
41
- FormFieldsProps;
42
-
43
- export const Form = <T extends BaseObject>({
44
- classNames,
45
- testId,
46
- values: initialValues,
47
- autoFocus,
48
- readonly,
49
- autoSave,
50
- outerSpacing = true,
51
- onCancel,
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { createContext } from '@radix-ui/react-context';
6
+ import * as Option from 'effect/Option';
7
+ import * as Schema from 'effect/Schema';
8
+ import * as SchemaAST from 'effect/SchemaAST';
9
+ import React, { type PropsWithChildren, useEffect, useMemo, useRef } from 'react';
10
+
11
+ import { Annotation as EchoAnnotation } from '@dxos/echo';
12
+ import { type AnyProperties } from '@dxos/echo/internal';
13
+ import { createJsonPath, getValue as getValue$ } from '@dxos/effect';
14
+ import {
15
+ IconButton,
16
+ type IconButtonProps,
17
+ ScrollArea,
18
+ type ThemedClassName,
19
+ useMergeRefs,
20
+ useTranslation,
21
+ } from '@dxos/react-ui';
22
+ import { composable, composableProps, mx, withColumn } from '@dxos/ui-theme';
23
+
24
+ import { translationKey } from '#translations';
25
+
26
+ import {
27
+ type FormHandler,
28
+ type FormHandlerProps,
29
+ type FormUpdateMeta,
30
+ useFormHandler,
31
+ useKeyHandler,
32
+ } from '../../hooks';
33
+ import { FormFieldLabel, type FormFieldLabelProps, type FormFieldStateProps } from './FormFieldComponent';
34
+ import {
35
+ FormFieldSet as NaturalFormFieldSet,
36
+ type FormFieldSetProps as NaturalFormFieldSetProps,
37
+ } from './FormFieldSet';
38
+
39
+ // TODO(burdon): Move styles to form.ts (as with ui-theme).
40
+
41
+ // TODO(burdon): Reconcile with @dxos/echo.
42
+ export type ExcludeId<S extends Schema.Schema.AnyNoContext> = Omit<Schema.Schema.Type<S>, 'id'>;
43
+
44
+ // TODO(burdon): Move to @dxos/schema (re-export here).
45
+ export const omitId = <S extends Schema.Schema.AnyNoContext>(schema: S): Schema.Schema<ExcludeId<S>, ExcludeId<S>> =>
46
+ schema.pipe(Schema.omit('id')) as any;
47
+
48
+ /**
49
+ * Drop fields annotated with `FormInputAnnotation.set(false)` from a schema so
50
+ * the form's validator doesn't trip on required-but-hidden fields. Used by
51
+ * the picker's inline create form, where a `FactoryAnnotation` typically
52
+ * supplies the hidden values (e.g. a backing-object Ref) outside the form.
53
+ */
54
+ export const omitHiddenFormFields = <S extends Schema.Schema.AnyNoContext>(schema: S): S => {
55
+ const properties = SchemaAST.getPropertySignatures(schema.ast);
56
+ const hidden = properties
57
+ .filter((prop) => Option.getOrElse(EchoAnnotation.FormInputAnnotation.getFromAst(prop.type), () => true) === false)
58
+ .map((prop) => prop.name as string);
59
+ return hidden.length === 0 ? schema : (schema.pipe(Schema.omit(...(hidden as [string, ...string[]]))) as any);
60
+ };
61
+
62
+ //
63
+ // Context
64
+ //
65
+
66
+ type FormContextValue<T extends AnyProperties = any> = {
67
+ /**
68
+ * Form handler.
69
+ */
70
+ form: FormHandler<T>;
71
+
72
+ /**
73
+ * Show debug info.
74
+ */
75
+ debug?: boolean;
76
+
77
+ /**
78
+ * Testing.
79
+ */
80
+ testId?: string;
81
+ } & Pick<
82
+ NaturalFormFieldSetProps<T>,
83
+ 'readonly' | 'layout' | 'fieldMap' | 'fieldProvider' | 'projection' | 'createTypename' | 'createFieldMap' | 'db'
84
+ >;
85
+
86
+ const [FormContextProvider, useFormContext] = createContext<FormContextValue>('Form');
87
+
88
+ /**
89
+ * Get the current form values.
90
+ */
91
+ const useFormValues: {
92
+ <T extends AnyProperties>(componentName: string, path?: (string | number)[]): T;
93
+ <T extends AnyProperties>(
94
+ componentName: string,
95
+ path: (string | number)[] | undefined,
96
+ defaultValue: () => T,
97
+ ): T | undefined;
98
+ } = (componentName: string, path?: (string | number)[], defaultValue?: () => any) => {
99
+ const stablePath = useMemo(() => path ?? [], [path ? path.join('.') : undefined]);
100
+ const jsonPath = createJsonPath(stablePath);
101
+ const {
102
+ form: { values, onValueChange },
103
+ } = useFormContext(componentName);
104
+
105
+ const value = getValue$(values, jsonPath);
106
+
107
+ // Apply default value once when the field has no value. lastAppliedPathRef prevents
108
+ // re-applying on every render (e.g. when defaultValue() returns null) and ensures
109
+ // we apply per path when the hook is used for different fields.
110
+ const lastAppliedPathRef = useRef<string | null>(null);
111
+ useEffect(() => {
112
+ if (value == null && defaultValue && lastAppliedPathRef.current !== jsonPath) {
113
+ lastAppliedPathRef.current = jsonPath;
114
+ onValueChange(stablePath, SchemaAST.stringKeyword, defaultValue());
115
+ }
116
+ }, [value, defaultValue, onValueChange, stablePath, jsonPath]);
117
+
118
+ return value;
119
+ };
120
+
121
+ /**
122
+ * Get the state props for the given field.
123
+ */
124
+ const useFormFieldState = (componentName: string, path: (string | number)[] = []): FormFieldStateProps => {
125
+ const stablePath = useMemo(() => path, [Array.isArray(path) ? path.join('.') : path]);
126
+ const {
127
+ form: { getStatus, getValue, onBlur, onValueChange },
128
+ } = useFormContext(componentName);
129
+
130
+ return useMemo(
131
+ () => ({
132
+ getStatus: () => getStatus(stablePath),
133
+ getValue: () => getValue(stablePath),
134
+ onBlur: () => onBlur(stablePath),
135
+ onValueChange: (ast: SchemaAST.AST, value: any) => onValueChange(stablePath, ast, value),
136
+ }),
137
+ [getStatus, getValue, onBlur, onValueChange, stablePath],
138
+ );
139
+ };
140
+
141
+ //
142
+ // Root
143
+ //
144
+
145
+ type FormRootProps<T extends AnyProperties = AnyProperties> = PropsWithChildren<
146
+ {
147
+ /**
148
+ * Called when the form is submitted and passes validation.
149
+ */
150
+ onSave?: (values: T, meta: FormUpdateMeta<T>) => void;
151
+
152
+ /**
153
+ * Called when the form is canceled to abandon/undo any pending changes.
154
+ */
155
+ onCancel?: () => void;
156
+ } & Omit<FormContextValue<T>, 'form'> &
157
+ Pick<FormHandlerProps<T>, 'schema' | 'autoSave' | 'values' | 'defaultValues' | 'onValidate' | 'onValuesChanged'> &
158
+ Omit<NaturalFormFieldSetProps<T>, 'schema' | 'path'>
159
+ >;
160
+
161
+ const FormRoot = <T extends AnyProperties = AnyProperties>({
162
+ children,
52
163
  schema,
53
- onValuesChanged,
54
- onValidate,
164
+ values,
55
165
  onSave,
166
+ onCancel,
56
167
  ...props
57
- }: FormProps<T>) => {
58
- const formRef = useRef<HTMLDivElement>(null);
168
+ }: FormRootProps<T>) => {
169
+ const form = useFormHandler({ schema, values, onSave, onCancel, ...props });
59
170
 
60
- // TODO(burdon): Rename.
61
- const handleValid = useMemo(() => (autoSave ? onSave : undefined), [autoSave, onSave]);
171
+ return (
172
+ <FormContextProvider form={form} {...props}>
173
+ {children}
174
+ </FormContextProvider>
175
+ );
176
+ };
62
177
 
63
- // Focus the first input element within this form.
64
- useEffect(() => {
65
- if (autoFocus && formRef.current) {
66
- const input = formRef.current.querySelector('input');
67
- if (input) {
68
- input.focus();
69
- }
70
- }
71
- }, [autoFocus]);
178
+ FormRoot.displayName = 'Form.Root';
72
179
 
180
+ //
181
+ // Viewport
182
+ //
183
+
184
+ const FORM_VIEWPORT_NAME = 'Form.Viewport';
185
+
186
+ type FormViewportProps = {};
187
+
188
+ const FormViewport = composable<HTMLDivElement>(({ children, ...props }, forwardedRef) => {
73
189
  return (
74
- <FormProvider
75
- formRef={formRef}
76
- schema={schema}
77
- autoSave={autoSave}
78
- initialValues={initialValues}
79
- onValuesChanged={onValuesChanged}
80
- onValidate={onValidate}
81
- onValid={handleValid}
82
- onSave={onSave}
190
+ <ScrollArea.Root {...composableProps(props)} orientation='vertical' centered padding thin ref={forwardedRef}>
191
+ <ScrollArea.Viewport>{children}</ScrollArea.Viewport>
192
+ </ScrollArea.Root>
193
+ );
194
+ });
195
+
196
+ FormViewport.displayName = FORM_VIEWPORT_NAME;
197
+
198
+ //
199
+ // Content
200
+ //
201
+
202
+ const FORM_CONTENT_NAME = 'Form.Content';
203
+
204
+ type FormContentProps = ThemedClassName<PropsWithChildren<{}>>;
205
+
206
+ const FormContent = composable<HTMLDivElement, FormContentProps>(({ children, ...props }, forwardedRef) => {
207
+ const { form, testId } = useFormContext(FORM_CONTENT_NAME);
208
+ const localRef = useRef<HTMLDivElement>(null);
209
+ const mergedRef = useMergeRefs([forwardedRef, localRef]);
210
+ useKeyHandler(localRef, form);
211
+
212
+ return (
213
+ <div
214
+ {...composableProps(props, {
215
+ role: 'form',
216
+ classNames: mx(withColumn.center(), 'flex flex-col w-full pb-form-gap'),
217
+ })}
218
+ data-testid={testId}
219
+ ref={mergedRef}
83
220
  >
84
- <div role='none' className='contents' data-testid={testId}>
85
- <FormFields
86
- {...props}
87
- ref={formRef}
88
- classNames={[
89
- outerSpacing === 'blockStart-0'
90
- ? 'pli-cardSpacingInline mbe-cardSpacingBlock [&>.mbs-inputSpacingBlock:first-child]:!mbs-0'
91
- : outerSpacing === 'scroll-fields'
92
- ? 'pli-cardSpacingInline pbe-cardSpacingBlock'
93
- : outerSpacing
94
- ? cardSpacing
95
- : false,
96
- outerSpacing === 'scroll-fields' && cardDialogOverflow,
97
- classNames,
98
- ]}
99
- readonly={readonly}
100
- schema={schema}
221
+ {children}
222
+ </div>
223
+ );
224
+ });
225
+
226
+ FormContent.displayName = FORM_CONTENT_NAME;
227
+
228
+ //
229
+ // FieldSet
230
+ //
231
+
232
+ const FORM_FIELDSET_NAME = 'Form.FieldSet';
233
+
234
+ type FormFieldSetProps = ThemedClassName<NaturalFormFieldSetProps<any>>;
235
+
236
+ const FormFieldSet = (props: FormFieldSetProps) => {
237
+ const { form, ...contextProps } = useFormContext(FORM_FIELDSET_NAME);
238
+
239
+ return <NaturalFormFieldSet schema={form.schema} {...contextProps} {...props} />;
240
+ };
241
+
242
+ FormFieldSet.displayName = FORM_FIELDSET_NAME;
243
+
244
+ //
245
+ // Actions
246
+ //
247
+
248
+ const FORM_ACTIONS_NAME = 'Form.Actions';
249
+
250
+ type FormActionsProps = ThemedClassName<{}>;
251
+
252
+ const FormActions = ({ classNames }: FormActionsProps) => {
253
+ const { t } = useTranslation(translationKey);
254
+ const {
255
+ form: { canSave, onSave, onCancel },
256
+ readonly,
257
+ layout,
258
+ } = useFormContext(FORM_ACTIONS_NAME);
259
+
260
+ if (readonly || layout === 'static') {
261
+ return null;
262
+ }
263
+
264
+ // TODO(burdon): Currently onCancel is a no-op; implement "revert values".
265
+ // Deprecate FormSubmit ans use FormActions without Cancel button if no callback is supplied.
266
+
267
+ return (
268
+ <div
269
+ role='none'
270
+ className={mx(withColumn.center(), 'grid grid-flow-col gap-form-gap auto-cols-fr py-form-padding', classNames)}
271
+ >
272
+ {onCancel && (
273
+ <IconButton
274
+ icon='ph--x--regular'
275
+ iconEnd
276
+ label={t('cancel-button.label')}
277
+ onClick={onCancel}
278
+ data-testid='cancel-button'
101
279
  />
102
- {(onCancel || onSave) && !autoSave && (
103
- <FormActions readonly={readonly} onCancel={onCancel} outerSpacing={outerSpacing} />
104
- )}
280
+ )}
281
+ {onSave && (
282
+ <IconButton
283
+ type='submit'
284
+ variant='primary'
285
+ disabled={!canSave}
286
+ icon='ph--check--regular'
287
+ iconEnd
288
+ label={t('save-button.label')}
289
+ onClick={onSave}
290
+ data-testid='save-button'
291
+ />
292
+ )}
293
+ </div>
294
+ );
295
+ };
296
+
297
+ FormActions.displayName = FORM_ACTIONS_NAME;
298
+
299
+ //
300
+ // Section
301
+ //
302
+
303
+ const FORM_SECTION_NAME = 'Form.Section';
304
+
305
+ type FormSectionProps = ThemedClassName<{ label: string; description?: string }>;
306
+
307
+ const FormSection = composable<HTMLDivElement, FormSectionProps>(
308
+ ({ children, label, description, ...props }, forwardedRef) => {
309
+ return (
310
+ <div
311
+ {...composableProps(props, { classNames: 'flex flex-col pt-form-section-gap first:pt-0' })}
312
+ ref={forwardedRef}
313
+ >
314
+ <h2 className='text-lg'>{label}</h2>
315
+ {description && <p className='text-description'>{description}</p>}
316
+ {children}
105
317
  </div>
106
- </FormProvider>
318
+ );
319
+ },
320
+ );
321
+
322
+ FormSection.displayName = FORM_SECTION_NAME;
323
+
324
+ //
325
+ // Submit
326
+ //
327
+
328
+ const FORM_SUBMIT_NAME = 'Form.Submit';
329
+
330
+ type FormSubmitProps = ThemedClassName<Partial<Pick<IconButtonProps, 'icon' | 'label' | 'disabled'>>>;
331
+
332
+ const FormSubmit = ({ classNames, label, icon, disabled }: FormSubmitProps) => {
333
+ const { t } = useTranslation(translationKey);
334
+ const {
335
+ form: { canSave, onSave },
336
+ readonly,
337
+ layout,
338
+ } = useFormContext(FORM_SUBMIT_NAME);
339
+
340
+ if (readonly || layout === 'static') {
341
+ return null;
342
+ }
343
+
344
+ return (
345
+ <div role='none' className={mx('flex w-full pt-form-padding', classNames)}>
346
+ <IconButton
347
+ classNames='w-full'
348
+ type='submit'
349
+ variant='primary'
350
+ disabled={disabled ?? !canSave}
351
+ icon={icon ?? 'ph--check--regular'}
352
+ label={label ?? t('save-button.label')}
353
+ onClick={onSave}
354
+ data-testid='save-button'
355
+ />
356
+ </div>
107
357
  );
108
358
  };
359
+
360
+ FormSubmit.displayName = FORM_SUBMIT_NAME;
361
+
362
+ //
363
+ // Form
364
+ // https://www.radix-ui.com/primitives/docs/guides/composition
365
+ //
366
+
367
+ export const Form = {
368
+ Root: FormRoot,
369
+ Viewport: FormViewport,
370
+ Content: FormContent,
371
+ Section: FormSection,
372
+ FieldSet: FormFieldSet,
373
+ Label: FormFieldLabel,
374
+ Actions: FormActions,
375
+ Submit: FormSubmit,
376
+ };
377
+
378
+ export { useFormContext, useFormValues, useFormFieldState };
379
+
380
+ export type {
381
+ FormRootProps,
382
+ FormViewportProps,
383
+ FormContentProps,
384
+ FormSectionProps,
385
+ FormFieldSetProps,
386
+ FormFieldLabelProps,
387
+ FormActionsProps,
388
+ FormSubmitProps,
389
+ };