@dxos/react-ui-form 0.8.4-main.f5c0578 → 0.8.4-main.fcfe5033a5

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