@dxos/react-ui-form 0.8.4-main.406dc2a → 0.8.4-main.422d1c7879

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