@dxos/react-ui-form 0.8.4-main.b97322e → 0.8.4-main.bcb3aa67d6

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