@multiplatform.one/frappe-ui 6.3.0 → 6.4.1

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 (74) hide show
  1. package/dist/cjs/FrappeDashboard.cjs +12 -2
  2. package/dist/cjs/FrappeDashboard.native.js +12 -2
  3. package/dist/cjs/FrappeDashboard.native.js.map +1 -1
  4. package/dist/cjs/FrappeLikedBy.cjs +2 -1
  5. package/dist/cjs/FrappeLikedBy.native.js +3 -1
  6. package/dist/cjs/FrappeLikedBy.native.js.map +1 -1
  7. package/dist/cjs/FrappeListView.cjs +10 -1
  8. package/dist/cjs/FrappeListView.native.js +10 -1
  9. package/dist/cjs/FrappeListView.native.js.map +1 -1
  10. package/dist/cjs/fieldRegistry.cjs +1 -1
  11. package/dist/cjs/fieldRegistry.native.js +1 -1
  12. package/dist/cjs/fieldRegistry.native.js.map +1 -1
  13. package/dist/cjs/fields/MarkdownEditor/index.cjs +33 -108
  14. package/dist/cjs/fields/MarkdownEditor/index.native.js +3 -3
  15. package/dist/cjs/fields/MarkdownEditor/index.native.js.map +1 -1
  16. package/dist/esm/FrappeDashboard.mjs +13 -3
  17. package/dist/esm/FrappeDashboard.mjs.map +1 -1
  18. package/dist/esm/FrappeDashboard.native.js +13 -3
  19. package/dist/esm/FrappeDashboard.native.js.map +1 -1
  20. package/dist/esm/FrappeLikedBy.mjs +3 -2
  21. package/dist/esm/FrappeLikedBy.mjs.map +1 -1
  22. package/dist/esm/FrappeLikedBy.native.js +4 -2
  23. package/dist/esm/FrappeLikedBy.native.js.map +1 -1
  24. package/dist/esm/FrappeListView.mjs +11 -2
  25. package/dist/esm/FrappeListView.mjs.map +1 -1
  26. package/dist/esm/FrappeListView.native.js +11 -2
  27. package/dist/esm/FrappeListView.native.js.map +1 -1
  28. package/dist/esm/fieldRegistry.mjs +1 -1
  29. package/dist/esm/fieldRegistry.mjs.map +1 -1
  30. package/dist/esm/fieldRegistry.native.js +1 -1
  31. package/dist/esm/fieldRegistry.native.js.map +1 -1
  32. package/dist/esm/fields/MarkdownEditor/index.mjs +36 -100
  33. package/dist/esm/fields/MarkdownEditor/index.mjs.map +1 -1
  34. package/dist/esm/fields/MarkdownEditor/index.native.js.map +1 -1
  35. package/dist/jsx/FrappeDashboard.mjs +13 -3
  36. package/dist/jsx/FrappeDashboard.mjs.map +1 -1
  37. package/dist/jsx/FrappeDashboard.native.js +12 -2
  38. package/dist/jsx/FrappeDashboard.native.js.map +1 -1
  39. package/dist/jsx/FrappeLikedBy.mjs +3 -2
  40. package/dist/jsx/FrappeLikedBy.mjs.map +1 -1
  41. package/dist/jsx/FrappeLikedBy.native.js +3 -1
  42. package/dist/jsx/FrappeLikedBy.native.js.map +1 -1
  43. package/dist/jsx/FrappeListView.mjs +11 -2
  44. package/dist/jsx/FrappeListView.mjs.map +1 -1
  45. package/dist/jsx/FrappeListView.native.js +10 -1
  46. package/dist/jsx/FrappeListView.native.js.map +1 -1
  47. package/dist/jsx/fieldRegistry.mjs +1 -1
  48. package/dist/jsx/fieldRegistry.mjs.map +1 -1
  49. package/dist/jsx/fieldRegistry.native.js +1 -1
  50. package/dist/jsx/fieldRegistry.native.js.map +1 -1
  51. package/dist/jsx/fields/MarkdownEditor/index.mjs +36 -100
  52. package/dist/jsx/fields/MarkdownEditor/index.mjs.map +1 -1
  53. package/dist/jsx/fields/MarkdownEditor/index.native.js +3 -3
  54. package/dist/jsx/fields/MarkdownEditor/index.native.js.map +1 -1
  55. package/package.json +22 -15
  56. package/src/FrappeDashboard.stories.tsx +3 -1
  57. package/src/FrappeDashboard.tsx +21 -1
  58. package/src/FrappeLikedBy.tsx +13 -2
  59. package/src/FrappeListView.tsx +21 -1
  60. package/src/FrappeTable.stories.tsx +91 -12
  61. package/src/fieldRegistry.ts +1 -1
  62. package/src/fields/Date/index.tsx +47 -3
  63. package/src/fields/MarkdownEditor/MarkdownEditor.spec.tsx +56 -32
  64. package/src/fields/MarkdownEditor/MarkdownEditor.stories.tsx +5 -5
  65. package/src/fields/MarkdownEditor/index.native.tsx +292 -0
  66. package/src/fields/MarkdownEditor/index.tsx +64 -139
  67. package/types/FrappeDashboard.d.ts.map +1 -1
  68. package/types/FrappeLikedBy.d.ts.map +1 -1
  69. package/types/FrappeListView.d.ts.map +1 -1
  70. package/types/fieldRegistry.d.ts.map +1 -1
  71. package/types/fields/MarkdownEditor/index.d.ts +11 -5
  72. package/types/fields/MarkdownEditor/index.d.ts.map +1 -1
  73. package/types/fields/MarkdownEditor/index.native.d.ts +47 -0
  74. package/types/fields/MarkdownEditor/index.native.d.ts.map +1 -0
@@ -1,11 +1,13 @@
1
1
  /**
2
- * MarkdownEditorField contract specs: canonical onChange emission (LC-14),
3
- * readOnly/disabled gating, table-cell chromeless branch, skeleton branch,
4
- * and registry resolution for the "Markdown Editor" fieldtype.
2
+ * MarkdownEditorField contract specs: rich editor in markdown capabilities
3
+ * (LC-52 MARKDOWN-EXPRESSIBLE-EDITING no highlight/underline/color
4
+ * controls), canonical onChange emitting markdown (LC-14), readOnly/disabled
5
+ * gating, table-cell chromeless branch, skeleton branch, and registry
6
+ * resolution for the "Markdown Editor" fieldtype.
5
7
  */
6
8
  import { TableCellContext } from "@multiplatform.one/forms";
7
9
  import { renderWithProviders } from "@multiplatform.one/test-utils";
8
- import { act, cleanup, fireEvent } from "@testing-library/react";
10
+ import { act, cleanup, fireEvent, waitFor } from "@testing-library/react";
9
11
  import type React from "react";
10
12
  import { afterEach, describe, expect, it, vi } from "vitest";
11
13
  import { getFieldComponent, type FieldRenderProps } from "../../fieldRegistry";
@@ -13,63 +15,85 @@ import { MarkdownEditorField } from ".";
13
15
 
14
16
  afterEach(cleanup);
15
17
 
18
+ const getProseMirror = (container: HTMLElement) =>
19
+ container.querySelector<HTMLElement>('[contenteditable="true"]');
20
+
16
21
  describe("MarkdownEditorField (standalone)", () => {
17
- it("emits the markdown source through canonical onChange", async () => {
18
- const onChange = vi.fn();
22
+ it("renders the rich text editor (WYSIWYG), not a source textarea", async () => {
19
23
  const result = renderWithProviders(
20
- <MarkdownEditorField label="Notes" value="" onChange={onChange} />,
24
+ <MarkdownEditorField label="Notes" value="# Hello" onChange={() => {}} />,
21
25
  );
22
- const textarea = result.getTextArea();
23
- expect(textarea).toBeTruthy();
24
- await act(async () => {
25
- if (textarea) fireEvent.change(textarea, { target: { value: "# Hello" } });
26
- });
27
- expect(onChange).toHaveBeenCalledWith("# Hello");
26
+ await waitFor(() => expect(getProseMirror(result.container)).toBeTruthy());
27
+ expect(result.getTextArea()).toBeNull();
28
+ // markdown value parsed into rich nodes
29
+ expect(result.container.querySelector("h1")?.textContent).toBe("Hello");
28
30
  });
29
31
 
30
- it("readOnly gates onChange (no emission even if the DOM event fires)", async () => {
32
+ it("markdown capabilities: no underline/highlight/color toolbar controls (LC-52)", async () => {
33
+ const result = renderWithProviders(
34
+ <MarkdownEditorField label="Notes" value="hello" onChange={() => {}} />,
35
+ );
36
+ await waitFor(() => expect(result.queryByLabelText("Bold")).toBeTruthy());
37
+ expect(result.queryByLabelText("Underline")).toBeNull();
38
+ expect(result.queryByLabelText("Text Color")).toBeNull();
39
+ expect(result.queryByLabelText("Highlight Color")).toBeNull();
40
+ // markdown-expressible controls stay
41
+ expect(result.queryByLabelText("Italic")).toBeTruthy();
42
+ expect(result.queryByLabelText("Blockquote")).toBeTruthy();
43
+ });
44
+
45
+ it("emits markdown (not HTML) through canonical onChange", async () => {
31
46
  const onChange = vi.fn();
32
47
  const result = renderWithProviders(
33
- <TableCellContext.Provider value={{ inTableCell: true, editable: true }}>
34
- <MarkdownEditorField label="Notes" value="original" readOnly onChange={onChange} />
35
- </TableCellContext.Provider>,
48
+ <MarkdownEditorField label="Notes" value="hello" onChange={onChange} />,
36
49
  );
37
- const textarea = result.getTextArea();
38
- expect(textarea).toBeTruthy();
50
+ await waitFor(() => expect(result.queryByLabelText("Blockquote")).toBeTruthy());
39
51
  await act(async () => {
40
- if (textarea) fireEvent.change(textarea, { target: { value: "mutated" } });
52
+ fireEvent.click(result.getByLabelText("Blockquote"));
41
53
  });
42
- expect(onChange).not.toHaveBeenCalled();
54
+ await waitFor(() => expect(onChange).toHaveBeenCalled());
55
+ const emitted = onChange.mock.calls.at(-1)?.[0] as string;
56
+ expect(emitted).toContain("> hello");
57
+ expect(emitted).not.toContain("<blockquote>");
43
58
  });
44
59
 
45
- it("disabled gates onChange", async () => {
60
+ it("readOnly renders a non-editable surface without emitting changes", async () => {
46
61
  const onChange = vi.fn();
47
62
  const result = renderWithProviders(
48
- <TableCellContext.Provider value={{ inTableCell: true, editable: true }}>
49
- <MarkdownEditorField label="Notes" value="original" disabled onChange={onChange} />
50
- </TableCellContext.Provider>,
63
+ <MarkdownEditorField label="Notes" value="original" readOnly onChange={onChange} />,
51
64
  );
52
- const textarea = result.getTextArea();
53
- await act(async () => {
54
- if (textarea) fireEvent.change(textarea, { target: { value: "mutated" } });
55
- });
65
+ await waitFor(() =>
66
+ expect(result.container.querySelector('[contenteditable="false"]')).toBeTruthy(),
67
+ );
68
+ expect(getProseMirror(result.container)).toBeNull();
56
69
  expect(onChange).not.toHaveBeenCalled();
57
70
  });
58
71
 
59
- it("renders chromeless (bare source pane, no Write/Preview tabs) in table cells", () => {
72
+ it("disabled renders a non-editable surface", async () => {
73
+ const result = renderWithProviders(
74
+ <MarkdownEditorField label="Notes" value="original" disabled onChange={() => {}} />,
75
+ );
76
+ await waitFor(() =>
77
+ expect(result.container.querySelector('[contenteditable="false"]')).toBeTruthy(),
78
+ );
79
+ expect(getProseMirror(result.container)).toBeNull();
80
+ });
81
+
82
+ it("renders chromeless (no fixed formatting toolbar) in table cells", async () => {
60
83
  const result = renderWithProviders(
61
84
  <TableCellContext.Provider value={{ inTableCell: true, editable: true }}>
62
85
  <MarkdownEditorField label="Notes" value="# cell" onChange={() => {}} />
63
86
  </TableCellContext.Provider>,
64
87
  );
65
- expect(result.getTextArea()).toBeTruthy();
66
- expect(result.queryByText("Preview")).toBeNull();
88
+ await waitFor(() => expect(getProseMirror(result.container)).toBeTruthy());
89
+ expect(result.queryByLabelText("Formatting toolbar")).toBeNull();
67
90
  });
68
91
 
69
92
  it("skeleton branch renders no editable surface", () => {
70
93
  const result = renderWithProviders(
71
94
  <MarkdownEditorField label="Notes" skeleton value="# hidden" onChange={() => {}} />,
72
95
  );
96
+ expect(getProseMirror(result.container)).toBeNull();
73
97
  expect(result.getTextArea()).toBeNull();
74
98
  });
75
99
  });
@@ -9,14 +9,14 @@ const FIXTURE = `# Release notes
9
9
 
10
10
  Dedicated **Markdown Editor** field for the Frappe \`Markdown Editor\` fieldtype.
11
11
 
12
- - Live preview through the house renderer
13
- - Side-by-side above the md breakpoint, tabs below it
12
+ - WYSIWYG editing via the canonical rich text editor
13
+ - Constrained to what ~~HTML~~ *markdown* can express (LC-52)
14
14
  - Full field contract: label / hint / error / readOnly / skeleton
15
15
 
16
- > Write on the left, read on the right.
16
+ > Serializes to and from a GFM markdown string.
17
17
 
18
18
  \`\`\`python
19
- print("fenced code renders too")
19
+ print("fenced code works too")
20
20
  \`\`\`
21
21
  `;
22
22
 
@@ -28,7 +28,7 @@ export default {
28
28
  docs: {
29
29
  description: {
30
30
  component:
31
- "Frappe `Markdown Editor` fieldtype: markdown source pane + live preview (house @multiplatform.one/markdown renderer). Side-by-side md container width, Write/Preview tabs below it.",
31
+ "Frappe `Markdown Editor` fieldtype: the canonical rich text editor in markdown capabilities (LC-52) WYSIWYG constrained to GFM-expressible features (no highlight/underline/color/alignment), serializing to and from a markdown string. Native falls back to markdown source editing.",
32
32
  },
33
33
  },
34
34
  },
@@ -0,0 +1,292 @@
1
+ /**
2
+ * MarkdownEditorField (NATIVE fallback) — markdown source pane with a live
3
+ * rendered preview (house `@multiplatform.one/markdown` renderer).
4
+ *
5
+ * The web tier renders the canonical rich text editor constrained to
6
+ * markdown capabilities (LC-52 MARKDOWN-EXPRESSIBLE-EDITING). On native the
7
+ * 10Tap WebView cannot express that constraint honestly tonight (extension
8
+ * gating + markdown serialization live in the web bundle), so this tier
9
+ * keeps plain markdown source editing — documented fallback, not a parallel
10
+ * WYSIWYG implementation.
11
+ *
12
+ * Full field contract: three-branch render (skeleton → standalone →
13
+ * form-integrated), canonical `onChange(value)` (LC-14), FieldLayout slot
14
+ * order, chromeless inside table cells (useIsInTableCell).
15
+ */
16
+
17
+ import {
18
+ Field,
19
+ FieldLayout,
20
+ Skeleton,
21
+ TextArea,
22
+ formCommonColors,
23
+ getFieldError,
24
+ mergeFieldHandler,
25
+ useFormField,
26
+ useIsInTableCell,
27
+ useResolvedValidators,
28
+ type AnyFormApi,
29
+ } from "@multiplatform.one/forms";
30
+ import { useEffect, useState, type ComponentType, type ReactNode } from "react";
31
+ import { useTranslation } from "react-i18next";
32
+ import type { FontSizeTokens, LabelProps, SizeTokens } from "tamagui";
33
+ import { Paragraph, YStack } from "tamagui";
34
+ import { useResolvedKnobs } from "@multiplatform.one/theme";
35
+ import { EditorPreviewPanes, PreviewSurface } from "../shared/EditorPreviewPanes";
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Preview (lazy house Markdown renderer)
39
+ // ---------------------------------------------------------------------------
40
+
41
+ type MarkdownComponent = ComponentType<{ children?: string }>;
42
+
43
+ // Module-level cache so the renderer loads once per session, not per field.
44
+ let markdownRendererCache: MarkdownComponent | null = null;
45
+
46
+ /**
47
+ * The house Markdown renderer loads on demand the first time a preview pane
48
+ * renders (same optional-dependency pattern as the components shiki
49
+ * Highlighter): `@multiplatform.one/markdown` reaches consumers transitively
50
+ * via `@multiplatform.one/rich-text`, and a failed import degrades to a
51
+ * plain-text preview instead of crashing the field.
52
+ */
53
+ function useMarkdownRenderer(enabled: boolean): MarkdownComponent | null {
54
+ const [renderer, setRenderer] = useState<MarkdownComponent | null>(markdownRendererCache);
55
+
56
+ useEffect(() => {
57
+ if (!enabled || renderer) return;
58
+ let cancelled = false;
59
+ import("@multiplatform.one/markdown")
60
+ .then((mod) => {
61
+ markdownRendererCache = mod.Markdown as MarkdownComponent;
62
+ if (!cancelled) setRenderer(() => markdownRendererCache);
63
+ })
64
+ .catch(() => {
65
+ // Renderer unavailable — the plain-text fallback below stays up.
66
+ });
67
+ return () => {
68
+ cancelled = true;
69
+ };
70
+ }, [enabled, renderer]);
71
+
72
+ return renderer;
73
+ }
74
+
75
+ interface MarkdownPreviewProps {
76
+ value: string;
77
+ compact?: boolean;
78
+ }
79
+
80
+ function MarkdownPreview({ value, compact }: MarkdownPreviewProps) {
81
+ const { t } = useTranslation();
82
+ const { knobProps } = useResolvedKnobs({ compact });
83
+ const Renderer = useMarkdownRenderer(!!value);
84
+ // Content type scale follows the size knob (same idiom as CodeBlock).
85
+ const textSize = knobProps.sizeToken as FontSizeTokens;
86
+
87
+ return (
88
+ <PreviewSurface compact={compact}>
89
+ {!value ? (
90
+ <Paragraph color={formCommonColors.muted} size={textSize}>
91
+ {t("Nothing to preview")}
92
+ </Paragraph>
93
+ ) : Renderer ? (
94
+ <Renderer>{value}</Renderer>
95
+ ) : (
96
+ // Loading / renderer unavailable: honest plain-text fallback.
97
+ <Paragraph size={textSize} whiteSpace="pre-wrap">
98
+ {value}
99
+ </Paragraph>
100
+ )}
101
+ </PreviewSurface>
102
+ );
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // MarkdownEditorField
107
+ // ---------------------------------------------------------------------------
108
+
109
+ export interface MarkdownEditorFieldProps {
110
+ name?: string;
111
+ form?: AnyFormApi;
112
+ validators?: Record<string, unknown>;
113
+ label?: ReactNode;
114
+ labelProps?: Omit<LabelProps, "htmlFor" | "ref">;
115
+ helperText?: string;
116
+ error?: string | boolean;
117
+ required?: boolean;
118
+ disabled?: boolean;
119
+ readOnly?: boolean;
120
+ skeleton?: boolean;
121
+ compact?: boolean;
122
+ size?: SizeTokens;
123
+ id?: string;
124
+ value?: string;
125
+ defaultValue?: string;
126
+ /** Canonical change contract (LC-14): emits the markdown source string. */
127
+ onChange?: (value: string) => void;
128
+ onBlur?: (...args: any[]) => void;
129
+ placeholder?: string;
130
+ /** Minimum visible source rows (default 6). */
131
+ minRows?: number;
132
+ /** Maximum source rows before the textarea scrolls (default 16). */
133
+ maxRows?: number;
134
+ "aria-label"?: string;
135
+ }
136
+
137
+ export function MarkdownEditorField({
138
+ name,
139
+ form: formProp,
140
+ validators,
141
+ label,
142
+ labelProps,
143
+ helperText,
144
+ error,
145
+ required,
146
+ disabled,
147
+ readOnly,
148
+ skeleton,
149
+ compact,
150
+ size,
151
+ id: idProp,
152
+ value,
153
+ defaultValue,
154
+ onChange,
155
+ onBlur,
156
+ placeholder,
157
+ minRows = 6,
158
+ maxRows = 16,
159
+ "aria-label": ariaLabel,
160
+ }: MarkdownEditorFieldProps) {
161
+ const { t } = useTranslation();
162
+ const { resolvedForm, knobProps, id } = useFormField({ form: formProp, id: idProp, compact });
163
+ const resolvedValidators = useResolvedValidators(required, validators, label, name);
164
+ const inTableCell = useIsInTableCell();
165
+
166
+ // Standalone/uncontrolled mirror so the live preview always has the current
167
+ // source (the form branch reads field state instead).
168
+ const [internalValue, setInternalValue] = useState(value ?? defaultValue ?? "");
169
+ useEffect(() => {
170
+ if (value !== undefined) setInternalValue(value);
171
+ }, [value]);
172
+
173
+ const accessibleName =
174
+ ariaLabel ?? (typeof label === "string" ? label : undefined) ?? placeholder;
175
+
176
+ if (skeleton) {
177
+ return (
178
+ <YStack {...knobProps.gap} width="100%">
179
+ {label && <Skeleton variant="text" width="30%" height={14} />}
180
+ <Skeleton variant="rounded" width="100%" height={compact ? 120 : 200} />
181
+ </YStack>
182
+ );
183
+ }
184
+
185
+ const renderSource = (
186
+ textValue: string,
187
+ handleChange?: (text: string) => void,
188
+ hasError?: boolean,
189
+ ) => (
190
+ <TextArea
191
+ id={id}
192
+ value={textValue}
193
+ onChangeText={(text) => {
194
+ if (disabled || readOnly) return;
195
+ handleChange?.(text);
196
+ onChange?.(text);
197
+ }}
198
+ placeholder={placeholder}
199
+ disabled={disabled}
200
+ readOnly={readOnly}
201
+ required={required}
202
+ error={hasError ? true : undefined}
203
+ compact={compact}
204
+ size={size}
205
+ minRows={minRows}
206
+ maxRows={maxRows}
207
+ textAreaProps={{
208
+ fontFamily: "$mono",
209
+ autoCapitalize: "none",
210
+ autoCorrect: false,
211
+ spellCheck: false,
212
+ ...(accessibleName ? { "aria-label": accessibleName } : {}),
213
+ }}
214
+ />
215
+ );
216
+
217
+ // Table cells get the chromeless source pane only (no tabs/preview chrome);
218
+ // InputParts inside the forms TextArea detects the cell context itself.
219
+ const renderControl = (
220
+ textValue: string,
221
+ handleChange?: (text: string) => void,
222
+ hasError?: boolean,
223
+ ) => {
224
+ if (inTableCell) return renderSource(textValue, handleChange, hasError);
225
+ return (
226
+ <EditorPreviewPanes
227
+ editor={renderSource(textValue, handleChange, hasError)}
228
+ preview={<MarkdownPreview value={textValue} compact={compact} />}
229
+ editTabLabel={t("Write")}
230
+ previewTabLabel={t("Preview")}
231
+ compact={compact}
232
+ defaultPreview={!!readOnly}
233
+ />
234
+ );
235
+ };
236
+
237
+ if (!resolvedForm || !name) {
238
+ if (inTableCell) return renderControl(internalValue, setInternalValue, !!error);
239
+ return (
240
+ <FieldLayout
241
+ id={id}
242
+ label={label}
243
+ labelProps={labelProps}
244
+ error={error}
245
+ helperText={helperText}
246
+ required={required}
247
+ disabled={disabled}
248
+ size={size}
249
+ knobProps={knobProps}
250
+ onBlur={onBlur}
251
+ aria-label={ariaLabel}
252
+ placeholder={placeholder}
253
+ >
254
+ {renderControl(internalValue, setInternalValue, !!error)}
255
+ </FieldLayout>
256
+ );
257
+ }
258
+
259
+ return (
260
+ <Field
261
+ form={resolvedForm}
262
+ name={name}
263
+ defaultValue={defaultValue}
264
+ validators={resolvedValidators}
265
+ >
266
+ {(field) => {
267
+ const resolvedError = getFieldError(field, error);
268
+ const fieldValue = typeof field.state.value === "string" ? field.state.value : "";
269
+ const handleChange = (text: string) => field.handleChange(text as any);
270
+ if (inTableCell) return renderControl(fieldValue, handleChange, !!resolvedError);
271
+ return (
272
+ <FieldLayout
273
+ id={id}
274
+ label={label}
275
+ labelProps={labelProps}
276
+ error={resolvedError}
277
+ helperText={helperText}
278
+ required={required}
279
+ disabled={disabled}
280
+ size={size}
281
+ knobProps={knobProps}
282
+ onBlur={mergeFieldHandler(field, "handleBlur", onBlur)}
283
+ aria-label={ariaLabel}
284
+ placeholder={placeholder}
285
+ >
286
+ {renderControl(fieldValue, handleChange, !!resolvedError)}
287
+ </FieldLayout>
288
+ );
289
+ }}
290
+ </Field>
291
+ );
292
+ }