@dxos/react-ui-form 0.8.4-main.406dc2a → 0.8.4-main.52d7546f51

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 (190) hide show
  1. package/dist/lib/browser/index.mjs +2228 -2254
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +2228 -2254
  5. package/dist/lib/node-esm/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +6 -5
  8. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  9. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +13 -7
  10. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  11. package/dist/types/src/components/Form/Form-old.stories.d.ts +131 -0
  12. package/dist/types/src/components/Form/Form-old.stories.d.ts.map +1 -0
  13. package/dist/types/src/components/Form/Form.d.ts +81 -26
  14. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  15. package/dist/types/src/components/Form/Form.stories.d.ts +59 -84
  16. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  17. package/dist/types/src/components/Form/FormField.d.ts +37 -0
  18. package/dist/types/src/components/Form/FormField.d.ts.map +1 -0
  19. package/dist/types/src/components/Form/FormFieldComponent.d.ts +78 -0
  20. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -0
  21. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +9 -0
  22. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -0
  23. package/dist/types/src/components/Form/FormFieldSet.d.ts +19 -0
  24. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -0
  25. package/dist/types/src/components/Form/fields/ArrayField.d.ts +18 -0
  26. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -0
  27. package/dist/types/src/components/Form/fields/BooleanField.d.ts +4 -0
  28. package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -0
  29. package/dist/types/src/components/Form/fields/GeoPointField.d.ts +5 -0
  30. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -0
  31. package/dist/types/src/components/Form/fields/MarkdownField.d.ts +4 -0
  32. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -0
  33. package/dist/types/src/components/Form/fields/NumberField.d.ts +4 -0
  34. package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -0
  35. package/dist/types/src/components/Form/fields/RefField.d.ts +14 -0
  36. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -0
  37. package/dist/types/src/components/Form/fields/SelectField.d.ts +10 -0
  38. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -0
  39. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +5 -0
  40. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -0
  41. package/dist/types/src/components/{ControlSection/ControlSection.stories.d.ts → Form/fields/SelectOptionField.stories.d.ts} +12 -14
  42. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -0
  43. package/dist/types/src/components/Form/fields/TextAreaField.d.ts +4 -0
  44. package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -0
  45. package/dist/types/src/components/Form/fields/TextField.d.ts +4 -0
  46. package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -0
  47. package/dist/types/src/components/Form/fields/TupleField.d.ts +6 -0
  48. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -0
  49. package/dist/types/src/components/Form/fields/index.d.ts +12 -0
  50. package/dist/types/src/components/Form/fields/index.d.ts.map +1 -0
  51. package/dist/types/src/components/Form/index.d.ts +2 -4
  52. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  53. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +14 -9
  54. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  55. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +9 -6
  56. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  57. package/dist/types/src/components/Settings/Settings.d.ts +56 -0
  58. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
  59. package/dist/types/src/components/{Form/custom/SelectOptionsInput.stories.d.ts → Settings/Settings.stories.d.ts} +10 -8
  60. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
  61. package/dist/types/src/components/Settings/index.d.ts +2 -0
  62. package/dist/types/src/components/Settings/index.d.ts.map +1 -0
  63. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +11 -13
  64. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  65. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +12 -9
  66. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  67. package/dist/types/src/components/index.d.ts +1 -1
  68. package/dist/types/src/components/index.d.ts.map +1 -1
  69. package/dist/types/src/components/testing.d.ts +0 -2
  70. package/dist/types/src/components/testing.d.ts.map +1 -1
  71. package/dist/types/src/hooks/index.d.ts +2 -3
  72. package/dist/types/src/hooks/index.d.ts.map +1 -1
  73. package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
  74. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
  75. package/dist/types/src/hooks/useKeyHandler.d.ts +6 -0
  76. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
  77. package/dist/types/src/translations.d.ts +10 -7
  78. package/dist/types/src/translations.d.ts.map +1 -1
  79. package/dist/types/src/util/echo.d.ts +6 -0
  80. package/dist/types/src/util/echo.d.ts.map +1 -0
  81. package/dist/types/src/util/field.d.ts +3 -3
  82. package/dist/types/src/util/field.d.ts.map +1 -1
  83. package/dist/types/src/util/formatting.d.ts +2 -2
  84. package/dist/types/src/util/formatting.d.ts.map +1 -1
  85. package/dist/types/src/util/index.d.ts +2 -1
  86. package/dist/types/src/util/index.d.ts.map +1 -1
  87. package/dist/types/src/util/properties.d.ts +7 -0
  88. package/dist/types/src/util/properties.d.ts.map +1 -0
  89. package/dist/types/src/util/refs.d.ts +1 -2
  90. package/dist/types/src/util/refs.d.ts.map +1 -1
  91. package/dist/types/tsconfig.tsbuildinfo +1 -1
  92. package/package.json +49 -43
  93. package/src/components/FieldEditor/FieldEditor.stories.tsx +43 -22
  94. package/src/components/FieldEditor/FieldEditor.tsx +73 -76
  95. package/src/components/Form/Form-old.stories.tsx +314 -0
  96. package/src/components/Form/Form.stories.tsx +151 -248
  97. package/src/components/Form/Form.tsx +329 -108
  98. package/src/components/Form/FormField.tsx +286 -0
  99. package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
  100. package/src/components/Form/FormFieldComponent.tsx +174 -0
  101. package/src/components/Form/FormFieldSet.tsx +127 -0
  102. package/src/components/Form/README.md +3 -0
  103. package/src/components/Form/fields/ArrayField.tsx +154 -0
  104. package/src/components/Form/fields/BooleanField.tsx +22 -0
  105. package/src/components/Form/fields/GeoPointField.tsx +120 -0
  106. package/src/components/Form/fields/MarkdownField.tsx +78 -0
  107. package/src/components/Form/fields/NumberField.tsx +39 -0
  108. package/src/components/Form/fields/RefField.tsx +189 -0
  109. package/src/components/Form/fields/SelectField.tsx +65 -0
  110. package/src/components/Form/{custom/SelectOptionsInput.stories.tsx → fields/SelectOptionField.stories.tsx} +18 -15
  111. package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +21 -15
  112. package/src/components/Form/fields/TextAreaField.tsx +38 -0
  113. package/src/components/Form/fields/TextField.tsx +38 -0
  114. package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +15 -8
  115. package/src/components/Form/fields/index.ts +15 -0
  116. package/src/components/Form/index.ts +3 -4
  117. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +21 -14
  118. package/src/components/ObjectPicker/ObjectPicker.tsx +76 -62
  119. package/src/components/Settings/Settings.stories.tsx +56 -0
  120. package/src/components/Settings/Settings.tsx +234 -0
  121. package/src/components/{ControlSection → Settings}/index.ts +1 -1
  122. package/src/components/ViewEditor/ViewEditor.stories.tsx +43 -41
  123. package/src/components/ViewEditor/ViewEditor.test.tsx +5 -5
  124. package/src/components/ViewEditor/ViewEditor.tsx +245 -192
  125. package/src/components/index.ts +1 -1
  126. package/src/components/testing.tsx +9 -7
  127. package/src/hooks/index.ts +2 -3
  128. package/src/hooks/useFormHandler.ts +362 -0
  129. package/src/hooks/useKeyHandler.tsx +37 -0
  130. package/src/translations.ts +12 -10
  131. package/src/util/echo.ts +25 -0
  132. package/src/util/field.ts +25 -25
  133. package/src/util/formatting.ts +22 -22
  134. package/src/util/index.ts +2 -1
  135. package/src/util/properties.ts +18 -0
  136. package/src/util/refs.ts +11 -17
  137. package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
  138. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
  139. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
  140. package/dist/types/src/components/ControlSection/index.d.ts +0 -2
  141. package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
  142. package/dist/types/src/components/Form/ArrayField.d.ts +0 -16
  143. package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
  144. package/dist/types/src/components/Form/Defaults.d.ts +0 -14
  145. package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
  146. package/dist/types/src/components/Form/FormActions.d.ts +0 -9
  147. package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
  148. package/dist/types/src/components/Form/FormContent.d.ts +0 -63
  149. package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
  150. package/dist/types/src/components/Form/FormContext.d.ts +0 -23
  151. package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
  152. package/dist/types/src/components/Form/Input.d.ts +0 -29
  153. package/dist/types/src/components/Form/Input.d.ts.map +0 -1
  154. package/dist/types/src/components/Form/RefField.d.ts +0 -22
  155. package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
  156. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
  157. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
  158. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
  159. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
  160. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
  161. package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
  162. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
  163. package/dist/types/src/components/Form/custom/index.d.ts +0 -4
  164. package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
  165. package/dist/types/src/components/Form/factory.d.ts +0 -8
  166. package/dist/types/src/components/Form/factory.d.ts.map +0 -1
  167. package/dist/types/src/hooks/useForm.d.ts +0 -67
  168. package/dist/types/src/hooks/useForm.d.ts.map +0 -1
  169. package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -29
  170. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
  171. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
  172. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
  173. package/dist/types/src/util/schema.d.ts +0 -3
  174. package/dist/types/src/util/schema.d.ts.map +0 -1
  175. package/src/components/ControlSection/ControlSection.stories.tsx +0 -38
  176. package/src/components/ControlSection/ControlSection.tsx +0 -153
  177. package/src/components/Form/ArrayField.tsx +0 -125
  178. package/src/components/Form/Defaults.tsx +0 -205
  179. package/src/components/Form/FormActions.tsx +0 -54
  180. package/src/components/Form/FormContent.tsx +0 -322
  181. package/src/components/Form/FormContext.tsx +0 -110
  182. package/src/components/Form/Input.tsx +0 -54
  183. package/src/components/Form/RefField.tsx +0 -283
  184. package/src/components/Form/custom/GeoPointInput.tsx +0 -80
  185. package/src/components/Form/custom/index.ts +0 -7
  186. package/src/components/Form/factory.tsx +0 -31
  187. package/src/hooks/useForm.ts +0 -296
  188. package/src/hooks/useQueryRefOptions.ts +0 -62
  189. package/src/hooks/useRefQueryLookupHandler.ts +0 -39
  190. package/src/util/schema.ts +0 -12
@@ -1,120 +1,341 @@
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 Schema from 'effect/Schema';
7
+ import type * 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 { IconButton, type IconButtonProps, ScrollArea, type ThemedClassName, useTranslation } from '@dxos/react-ui';
13
+ import { mx } from '@dxos/ui-theme';
14
+
15
+ import {
16
+ type FormHandler,
17
+ type FormHandlerProps,
18
+ type FormUpdateMeta,
19
+ useFormHandler,
20
+ useKeyHandler,
21
+ } from '../../hooks';
22
+ import { translationKey } from '../../translations';
23
+ import { setValueEchoAware } from '../../util';
24
+
25
+ import { FormFieldLabel, type FormFieldLabelProps, type FormFieldStateProps } from './FormFieldComponent';
26
+ import {
27
+ FormFieldSet as NaturalFormFieldSet,
28
+ type FormFieldSetProps as NaturalFormFieldSetProps,
29
+ } from './FormFieldSet';
30
+
31
+ // New features/polish
32
+ // [x] Unify readonly/inline modes
33
+ // [ ] Don't call save/autoSave if value hasn't changed
34
+ // [ ] Fix onCancel (restore values)
35
+ // [ ] Fix useSchema Type.Obj.Any cast
36
+ // [ ] Remove @dxos/echo-db deps
37
+ // [ ] TableCellEditor (handleEnter/ModalController).
38
+ // [ ] Use FormFieldWrapper uniformly
39
+ // [ ] Inline tables for object arrays
40
+ // [ ] Defer query until popover
41
+ // [ ] Omit id from sub properties.
42
+ // [x] Refs
43
+ // [x] Single-select (fix popover)
44
+ // [x] Multi-select (array)
45
+ // [ ] auto save doesn't work for combobox + select due to only firing on blur (workaround is to use onValuesChanged)
46
+
47
+ // TODO(burdon): Move to @dxos/schema (re-export here).
48
+ export type ExcludeId<S extends Schema.Schema.AnyNoContext> = Omit<Schema.Schema.Type<S>, 'id'>;
49
+
50
+ // TODO(burdon): Move to @dxos/schema (re-export here).
51
+ export const omitId = <S extends Schema.Schema.AnyNoContext>(schema: S): Schema.Schema<ExcludeId<S>, ExcludeId<S>> =>
52
+ schema.pipe(Schema.omit('id')) as any;
53
+
54
+ //
55
+ // Context
56
+ //
57
+
58
+ type FormContextValue<T extends AnyProperties = any> = {
59
+ /**
60
+ * Form handler.
61
+ */
62
+ form: FormHandler<T>;
63
+
64
+ /**
65
+ * Show debug info.
66
+ */
67
+ debug?: boolean;
68
+
69
+ /**
70
+ * Testing.
71
+ */
72
+ testId?: string;
73
+ } & Pick<NaturalFormFieldSetProps<T>, 'readonly' | 'layout' | 'fieldMap' | 'fieldProvider'>;
74
+
75
+ const [FormContextProvider, useFormContext] = createContext<FormContextValue>('Form');
76
+
77
+ /**
78
+ * Get the current form values.
79
+ */
80
+ const useFormValues: {
81
+ <T extends AnyProperties>(componentName: string, path?: (string | number)[]): T;
82
+ <T extends AnyProperties>(
83
+ componentName: string,
84
+ path: (string | number)[] | undefined,
85
+ defaultValue: () => T,
86
+ ): T | undefined;
87
+ } = (componentName: string, path: (string | number)[] = [], defaultValue?: () => any) => {
88
+ const jsonPath = createJsonPath(path);
89
+ const {
90
+ form: { values },
91
+ } = useFormContext(componentName);
92
+
93
+ const value = getValue$(values, jsonPath);
94
+
95
+ useEffect(() => {
96
+ if (!value && defaultValue) {
97
+ setValueEchoAware(values, jsonPath, defaultValue());
98
+ }
99
+ }, [value, defaultValue]);
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,
55
146
  schema,
56
- onCancel,
57
- onValuesChanged,
58
- onValidate,
147
+ values,
59
148
  onSave,
149
+ onCancel,
60
150
  ...props
61
- }: FormProps<T>) => {
62
- const formRef = useRef<HTMLDivElement>(null);
151
+ }: FormRootProps<T>) => {
152
+ const form = useFormHandler({ schema, values, onSave, onCancel, ...props });
63
153
 
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],
154
+ return (
155
+ <FormContextProvider form={form} {...props}>
156
+ {children}
157
+ </FormContextProvider>
83
158
  );
159
+ };
84
160
 
161
+ FormRoot.displayName = 'Form.Root';
162
+
163
+ //
164
+ // Viewport
165
+ //
166
+
167
+ const FORM_VIEWPORT_NAME = 'Form.Viewport';
168
+
169
+ type FormViewportProps = PropsWithChildren<{}>;
170
+
171
+ // TODO(burdon): Ref and props (allow asChild).
172
+ const FormViewport = ({ children }: FormViewportProps) => {
85
173
  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}
174
+ <ScrollArea.Root orientation='vertical'>
175
+ <ScrollArea.Viewport>{children}</ScrollArea.Viewport>
176
+ </ScrollArea.Root>
177
+ );
178
+ };
179
+
180
+ FormViewport.displayName = FORM_VIEWPORT_NAME;
181
+
182
+ //
183
+ // Content
184
+ //
185
+
186
+ const FORM_CONTENT_NAME = 'Form.Content';
187
+
188
+ type FormContentProps = ThemedClassName<PropsWithChildren<{}>>;
189
+
190
+ // TOOD(burdon): Figure out nesting (indent and testId).
191
+ const FormContent = ({ classNames, children }: FormContentProps) => {
192
+ const { form, testId } = useFormContext(FORM_CONTENT_NAME);
193
+ const ref = useRef<HTMLDivElement>(null);
194
+ useKeyHandler(ref.current, form);
195
+
196
+ return (
197
+ <div
198
+ ref={ref}
199
+ role='form'
200
+ className={mx('flex flex-col is-full pli-cardSpacingInline density-fine', classNames)}
201
+ data-testid={testId}
95
202
  >
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}
203
+ {children}
204
+ </div>
205
+ );
206
+ };
207
+
208
+ FormContent.displayName = FORM_CONTENT_NAME;
209
+
210
+ //
211
+ // FieldSet
212
+ //
213
+
214
+ const FORM_FIELDSET_NAME = 'Form.FieldSet';
215
+
216
+ type FormFieldSetProps = ThemedClassName<{}>;
217
+
218
+ const FormFieldSet = ({ classNames }: FormFieldSetProps) => {
219
+ const { form, ...props } = useFormContext(FORM_FIELDSET_NAME);
220
+
221
+ return <NaturalFormFieldSet classNames={classNames} schema={form.schema} {...props} />;
222
+ };
223
+
224
+ FormFieldSet.displayName = FORM_FIELDSET_NAME;
225
+
226
+ //
227
+ // Actions
228
+ //
229
+
230
+ const FORM_ACTIONS_NAME = 'Form.Actions';
231
+
232
+ type FormActionsProps = ThemedClassName<{}>;
233
+
234
+ const FormActions = ({ classNames }: FormActionsProps) => {
235
+ const { t } = useTranslation(translationKey);
236
+ const {
237
+ form: { canSave, onSave, onCancel },
238
+ readonly,
239
+ layout,
240
+ } = useFormContext(FORM_ACTIONS_NAME);
241
+
242
+ if (readonly || layout === 'static') {
243
+ return null;
244
+ }
245
+
246
+ // TODO(burdon): Currently onCancel is a no-op; implement "revert values".
247
+ // Deprecate FormSubmit ans use FormActions without Cancel button if no callback is supplied.
248
+
249
+ return (
250
+ <div role='none' className={mx('grid grid-flow-col auto-cols-fr gap-2 pbs-cardSpacingBlock', classNames)}>
251
+ {onCancel && (
252
+ <IconButton
253
+ icon='ph--x--regular'
254
+ iconEnd
255
+ label={t('cancel button label')}
256
+ onClick={onCancel}
257
+ data-testid='cancel-button'
258
+ />
259
+ )}
260
+ {onSave && (
261
+ <IconButton
262
+ type='submit'
263
+ variant='primary'
264
+ disabled={!canSave}
265
+ icon='ph--check--regular'
266
+ iconEnd
267
+ label={t('save button label')}
268
+ onClick={onSave}
269
+ data-testid='save-button'
113
270
  />
114
- {(onCancel || onSave) && !autoSave && !readonly && (
115
- <FormActions readonly={readonly} onCancel={onCancel} outerSpacing={outerSpacing} />
116
- )}
117
- </div>
118
- </FormProvider>
271
+ )}
272
+ </div>
273
+ );
274
+ };
275
+
276
+ FormActions.displayName = FORM_ACTIONS_NAME;
277
+
278
+ //
279
+ // Submit
280
+ //
281
+
282
+ const FORM_SUBMIT_NAME = 'Form.Submit';
283
+
284
+ type FormSubmitProps = ThemedClassName<Partial<Pick<IconButtonProps, 'icon' | 'label' | 'disabled'>>>;
285
+
286
+ const FormSubmit = ({ classNames, label, icon, disabled }: FormSubmitProps) => {
287
+ const { t } = useTranslation(translationKey);
288
+ const {
289
+ form: { canSave, onSave },
290
+ readonly,
291
+ layout,
292
+ } = useFormContext(FORM_SUBMIT_NAME);
293
+
294
+ if (readonly || layout === 'static') {
295
+ return null;
296
+ }
297
+
298
+ return (
299
+ <div role='none' className={mx('flex is-full pbs-cardSpacingBlock', classNames)}>
300
+ <IconButton
301
+ classNames='is-full'
302
+ type='submit'
303
+ variant='primary'
304
+ disabled={disabled ?? !canSave}
305
+ icon={icon ?? 'ph--check--regular'}
306
+ label={label ?? t('save button label')}
307
+ onClick={onSave}
308
+ data-testid='save-button'
309
+ />
310
+ </div>
119
311
  );
120
312
  };
313
+
314
+ FormSubmit.displayName = FORM_SUBMIT_NAME;
315
+
316
+ //
317
+ // Form
318
+ // https://www.radix-ui.com/primitives/docs/guides/composition
319
+ //
320
+
321
+ export const Form = {
322
+ Root: FormRoot,
323
+ Viewport: FormViewport,
324
+ Content: FormContent,
325
+ FieldSet: FormFieldSet,
326
+ Actions: FormActions,
327
+ Submit: FormSubmit,
328
+ Label: FormFieldLabel,
329
+ };
330
+
331
+ export { useFormContext, useFormValues, useFormFieldState };
332
+
333
+ export type {
334
+ FormRootProps,
335
+ FormViewportProps,
336
+ FormContentProps,
337
+ FormFieldSetProps,
338
+ FormActionsProps,
339
+ FormSubmitProps,
340
+ FormFieldLabelProps as LabelProps,
341
+ };