@dxos/react-ui-form 0.8.4-main.1da679c → 0.8.4-main.21d9917

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