@dxos/react-ui-form 0.8.4-main.72ec0f3 → 0.8.4-main.74a063c4e0

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