@axinom/mosaic-ui 0.74.0 → 0.74.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 (35) hide show
  1. package/dist/cjs/components/FormStation/FormStation.d.ts +17 -1
  2. package/dist/cjs/components/FormStation/FormStation.d.ts.map +1 -1
  3. package/dist/cjs/components/FormStation/FormStationHeader/FormStationHeader.d.ts +2 -0
  4. package/dist/cjs/components/FormStation/FormStationHeader/FormStationHeader.d.ts.map +1 -1
  5. package/dist/cjs/components/FormStation/ModalForm/ModalForm.d.ts +41 -0
  6. package/dist/cjs/components/FormStation/ModalForm/ModalForm.d.ts.map +1 -0
  7. package/dist/cjs/components/FormStation/ModalForm/useModalForm.d.ts +19 -0
  8. package/dist/cjs/components/FormStation/ModalForm/useModalForm.d.ts.map +1 -0
  9. package/dist/cjs/components/FormStation/index.d.ts +2 -0
  10. package/dist/cjs/components/FormStation/index.d.ts.map +1 -1
  11. package/dist/cjs/index.js +4 -4
  12. package/dist/cjs/index.js.map +1 -1
  13. package/dist/esm/components/FormStation/FormStation.d.ts +17 -1
  14. package/dist/esm/components/FormStation/FormStation.d.ts.map +1 -1
  15. package/dist/esm/components/FormStation/FormStationHeader/FormStationHeader.d.ts +2 -0
  16. package/dist/esm/components/FormStation/FormStationHeader/FormStationHeader.d.ts.map +1 -1
  17. package/dist/esm/components/FormStation/ModalForm/ModalForm.d.ts +41 -0
  18. package/dist/esm/components/FormStation/ModalForm/ModalForm.d.ts.map +1 -0
  19. package/dist/esm/components/FormStation/ModalForm/useModalForm.d.ts +19 -0
  20. package/dist/esm/components/FormStation/ModalForm/useModalForm.d.ts.map +1 -0
  21. package/dist/esm/components/FormStation/index.d.ts +2 -0
  22. package/dist/esm/components/FormStation/index.d.ts.map +1 -1
  23. package/dist/esm/index.js +1 -1
  24. package/dist/esm/index.js.map +1 -1
  25. package/package.json +2 -2
  26. package/src/components/FormStation/FormStation.tsx +22 -1
  27. package/src/components/FormStation/FormStationHeader/FormStationHeader.tsx +34 -2
  28. package/src/components/FormStation/ModalForm/ModalForm.module.scss +18 -0
  29. package/src/components/FormStation/ModalForm/ModalForm.spec.tsx +290 -0
  30. package/src/components/FormStation/ModalForm/ModalForm.stories.tsx +191 -0
  31. package/src/components/FormStation/ModalForm/ModalForm.tsx +80 -0
  32. package/src/components/FormStation/ModalForm/useModalForm.tsx +30 -0
  33. package/src/components/FormStation/index.ts +5 -0
  34. package/src/components/Modal/Modal.module.scss +4 -3
  35. package/src/styles/variables.scss +4 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axinom/mosaic-ui",
3
- "version": "0.74.0",
3
+ "version": "0.74.1",
4
4
  "engines": {
5
5
  "node": ">=20.19.0"
6
6
  },
@@ -112,5 +112,5 @@
112
112
  "publishConfig": {
113
113
  "access": "public"
114
114
  },
115
- "gitHead": "8be80fd805ce6806abff3a5dbbe84ead9af9aea2"
115
+ "gitHead": "4e25b38a8dac4aaada88ec0ca96c94bfcb12b19c"
116
116
  }
@@ -67,6 +67,22 @@ export interface FormStationProps<
67
67
  validationSchema?: OptionalObjectSchema<ObjectSchemaDefinition<TValues>>;
68
68
  /** URL to navigate to when the form is reset (usually on a create station) */
69
69
  cancelNavigationUrl?: string;
70
+ /**
71
+ * Called when the header's 'Cancel' action was selected and the form was
72
+ * reset. Takes precedence over `cancelNavigationUrl`.
73
+ */
74
+ onCancel?: () => void;
75
+ /**
76
+ * Called once the header's save action saved the form successfully. Setting
77
+ * it also makes that action submit the form directly, instead of saving on
78
+ * navigation.
79
+ */
80
+ onSaveCompleted?: () => void;
81
+ /**
82
+ * If set to true, a dirty form is saved when the user navigates away, and
83
+ * navigation is cancelled while the form is invalid. (default: true)
84
+ */
85
+ saveOnNavigate?: boolean;
70
86
  /** Component which contains additional information */
71
87
  infoPanel?: JSX.Element;
72
88
  /** Optional flag for removing the padding around child components. */
@@ -101,6 +117,9 @@ export const FormStation = <TValues extends Data, TSubmitResponse = unknown>({
101
117
  actions,
102
118
  actionsWidth,
103
119
  cancelNavigationUrl,
120
+ onCancel,
121
+ onSaveCompleted,
122
+ saveOnNavigate = true,
104
123
  initialData,
105
124
  saveData,
106
125
  validationSchema,
@@ -191,13 +210,15 @@ export const FormStation = <TValues extends Data, TSubmitResponse = unknown>({
191
210
  defaultTitle={defaultTitle}
192
211
  subtitle={subtitle}
193
212
  cancelNavigationUrl={cancelNavigationUrl}
213
+ onCancel={onCancel}
214
+ onSaveCompleted={onSaveCompleted}
194
215
  className={classes.header}
195
216
  setTabTitle={setTabTitle}
196
217
  showSaveHeaderAction={showSaveHeaderAction}
197
218
  saveHeaderActionConfig={saveHeaderActionConfig}
198
219
  setValidationError={setValidationError}
199
220
  />
200
- {!bulkEditContext && (
221
+ {saveOnNavigate && !bulkEditContext && (
201
222
  <SaveOnNavigate
202
223
  isSubmitting={isFormSubmitting}
203
224
  onNavigationCancelled={setValidationError}
@@ -21,6 +21,8 @@ export const FormStationHeader: React.FC<
21
21
  titleProperty?: string;
22
22
  defaultTitle?: string;
23
23
  cancelNavigationUrl?: string;
24
+ onCancel?: () => void;
25
+ onSaveCompleted?: () => void;
24
26
  setTabTitle?: boolean;
25
27
  showSaveHeaderAction?: boolean;
26
28
  saveHeaderActionConfig?: Pick<PageHeaderJsActionProps, 'label' | 'icon'>;
@@ -31,6 +33,8 @@ export const FormStationHeader: React.FC<
31
33
  defaultTitle,
32
34
  subtitle,
33
35
  cancelNavigationUrl,
36
+ onCancel,
37
+ onSaveCompleted,
34
38
  className,
35
39
  setTabTitle,
36
40
  showSaveHeaderAction,
@@ -71,7 +75,23 @@ export const FormStationHeader: React.FC<
71
75
  kind: 'action',
72
76
  actionType: PageHeaderActionType.Context,
73
77
  onClick: async () => {
74
- if (quickEditContext?.isQuickEditMode) {
78
+ if (onSaveCompleted) {
79
+ // No navigation happens, so `SaveOnNavigate` cannot do the save.
80
+ const errors = await validateForm();
81
+ if (Object.keys(errors).length > 0) {
82
+ setValidationError();
83
+ return;
84
+ }
85
+
86
+ try {
87
+ await submitForm();
88
+ } catch {
89
+ // The station error is already set by the submit handler.
90
+ return;
91
+ }
92
+
93
+ onSaveCompleted();
94
+ } else if (quickEditContext?.isQuickEditMode) {
75
95
  quickEditContext.refresh();
76
96
  } else if (bulkEditContext?.isBulkEditMode) {
77
97
  const errors = await validateForm();
@@ -111,7 +131,17 @@ export const FormStationHeader: React.FC<
111
131
  });
112
132
  }
113
133
 
114
- if (cancelNavigationUrl) {
134
+ if (onCancel) {
135
+ actionItems.push({
136
+ label: 'Cancel',
137
+ icon: IconName.X,
138
+ kind: 'action',
139
+ onClick: () => {
140
+ resetForm();
141
+ onCancel();
142
+ },
143
+ });
144
+ } else if (cancelNavigationUrl) {
115
145
  actionItems.push({
116
146
  label: 'Cancel',
117
147
  icon: IconName.X,
@@ -130,6 +160,8 @@ export const FormStationHeader: React.FC<
130
160
  cancelNavigationUrl,
131
161
  dirty,
132
162
  history,
163
+ onCancel,
164
+ onSaveCompleted,
133
165
  quickEditContext,
134
166
  resetForm,
135
167
  saveHeaderActionConfig.icon,
@@ -0,0 +1,18 @@
1
+ @import '../../../styles/common.scss';
2
+
3
+ .container {
4
+ @include boxSizing;
5
+
6
+ display: grid;
7
+ grid-template-rows: minmax(0, 1fr);
8
+
9
+ // The modal sizes itself to its content, so the form claims its own width.
10
+ width: calc(100vw - #{$modal-padding-side} - #{$modal-padding-side});
11
+ }
12
+
13
+ // FormStation sizes itself to the viewport minus the app header, which is the
14
+ // wrong box inside a modal. Doubled class to outrank it regardless of
15
+ // stylesheet order.
16
+ .station.station {
17
+ height: calc(100vh - #{$modal-padding-top} - #{$modal-padding-bottom});
18
+ }
@@ -0,0 +1,290 @@
1
+ import { fireEvent, render, screen } from '@testing-library/react';
2
+ import { useFormikContext } from 'formik';
3
+ import React, { useEffect } from 'react';
4
+ import { act } from 'react-dom/test-utils';
5
+ import { Link, MemoryRouter, Route } from 'react-router-dom';
6
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
7
+ import * as Yup from 'yup';
8
+ import { noop } from '../../../helpers/utils';
9
+ import { initializeUi } from '../../../initialize';
10
+ import { TextButton } from '../../Buttons';
11
+ import type { ObjectSchemaDefinition } from '../FormStation.models';
12
+ import { ModalForm } from './ModalForm';
13
+ import { useModalForm } from './useModalForm';
14
+
15
+ vi.mock('../../../initialize');
16
+
17
+ interface FormValues {
18
+ something: string;
19
+ }
20
+
21
+ const initialData = {
22
+ loading: false,
23
+ data: { something: 'initial' },
24
+ };
25
+
26
+ /** Makes the form dirty, so the header shows its 'Save' action. */
27
+ const ChangeValue: React.FC = () => {
28
+ const context = useFormikContext();
29
+ useEffect(() => {
30
+ context.setFieldValue('something', 'changed');
31
+ // eslint-disable-next-line react-hooks/exhaustive-deps
32
+ }, []);
33
+ return null;
34
+ };
35
+
36
+ /** Waits for Formik's asynchronous state updates to settle. */
37
+ const settle = async (): Promise<void> => {
38
+ await act(async () => {
39
+ await new Promise((resolve) => setTimeout(resolve, 0));
40
+ });
41
+ };
42
+
43
+ const headerActions = (): HTMLElement[] =>
44
+ Array.from(
45
+ document.querySelectorAll<HTMLElement>(
46
+ '[data-test-id="page-header-actions"] [data-test-id="action"]',
47
+ ),
48
+ );
49
+
50
+ describe('ModalForm', () => {
51
+ beforeEach(() => {
52
+ vi.clearAllMocks();
53
+
54
+ initializeUi({
55
+ showNotification: () => -1,
56
+ addIndicator: () => -1,
57
+ removeIndicator: noop,
58
+ on: () => noop,
59
+ setTitle: noop,
60
+ setSaveIndicator: noop,
61
+ });
62
+ });
63
+
64
+ const renderModalForm = (
65
+ props: Partial<React.ComponentProps<typeof ModalForm<FormValues>>> = {},
66
+ ): { closeModal: () => void; saveData: () => void } => {
67
+ const closeModal = vi.fn();
68
+ const saveData = vi.fn();
69
+
70
+ render(
71
+ <MemoryRouter>
72
+ <ModalForm<FormValues>
73
+ title="Modal Form"
74
+ closeModal={closeModal}
75
+ initialData={initialData}
76
+ saveData={saveData}
77
+ {...props}
78
+ >
79
+ <ChangeValue />
80
+ </ModalForm>
81
+ </MemoryRouter>,
82
+ );
83
+
84
+ return { closeModal, saveData };
85
+ };
86
+
87
+ it('shows the given title', async () => {
88
+ renderModalForm();
89
+ await settle();
90
+
91
+ expect(screen.getByTestId('page-header-title')).toHaveTextContent(
92
+ 'Modal Form',
93
+ );
94
+ });
95
+
96
+ it('closes the modal without saving when cancelled', async () => {
97
+ const { closeModal, saveData } = renderModalForm();
98
+ await settle();
99
+
100
+ // 'Undo Changes', 'Save' and 'Cancel'
101
+ const actions = headerActions();
102
+ expect(actions).toHaveLength(3);
103
+
104
+ fireEvent.click(actions[2]);
105
+ await settle();
106
+
107
+ expect(saveData).not.toHaveBeenCalled();
108
+ expect(closeModal).toHaveBeenCalledTimes(1);
109
+ });
110
+
111
+ it('saves the data and closes the modal when saved', async () => {
112
+ const { closeModal, saveData } = renderModalForm();
113
+ await settle();
114
+
115
+ fireEvent.click(headerActions()[1]);
116
+ await settle();
117
+
118
+ expect(saveData).toHaveBeenCalledTimes(1);
119
+ expect(saveData).toHaveBeenCalledWith(
120
+ { something: 'changed' },
121
+ initialData,
122
+ expect.anything(),
123
+ );
124
+ expect(closeModal).toHaveBeenCalledTimes(1);
125
+ });
126
+
127
+ it('keeps the modal open and shows the error when saving fails', async () => {
128
+ const { closeModal } = renderModalForm({
129
+ saveData: () => {
130
+ throw { title: 'Saving failed' };
131
+ },
132
+ });
133
+ await settle();
134
+
135
+ fireEvent.click(headerActions()[1]);
136
+ await settle();
137
+
138
+ expect(closeModal).not.toHaveBeenCalled();
139
+ expect(screen.getByText('Saving failed')).toBeInTheDocument();
140
+ });
141
+
142
+ it('keeps the modal open and does not save an invalid form', async () => {
143
+ const { closeModal, saveData } = renderModalForm({
144
+ validationSchema: Yup.object<ObjectSchemaDefinition<FormValues>>({
145
+ something: Yup.string().max(3, 'Too long'),
146
+ }),
147
+ });
148
+ await settle();
149
+
150
+ fireEvent.click(headerActions()[1]);
151
+ await settle();
152
+
153
+ expect(saveData).not.toHaveBeenCalled();
154
+ expect(closeModal).not.toHaveBeenCalled();
155
+ expect(
156
+ screen.getByText('Please fix the errors in the form to proceed.'),
157
+ ).toBeInTheDocument();
158
+ });
159
+
160
+ it('does not show a save action while the form is pristine', async () => {
161
+ render(
162
+ <MemoryRouter>
163
+ <ModalForm<FormValues>
164
+ title="Modal Form"
165
+ closeModal={noop}
166
+ initialData={initialData}
167
+ saveData={noop}
168
+ />
169
+ </MemoryRouter>,
170
+ );
171
+ await settle();
172
+
173
+ // Only 'Cancel' - the others appear once the form is dirty.
174
+ expect(headerActions()).toHaveLength(1);
175
+ });
176
+
177
+ it('discards the changes instead of saving them when navigating away', async () => {
178
+ let path = '';
179
+ const closeModal = vi.fn();
180
+ const saveData = vi.fn();
181
+
182
+ render(
183
+ <MemoryRouter>
184
+ <ModalForm<FormValues>
185
+ title="Modal Form"
186
+ closeModal={closeModal}
187
+ initialData={initialData}
188
+ saveData={saveData}
189
+ >
190
+ <ChangeValue />
191
+ </ModalForm>
192
+ <Link to="/somewhere-else">Go</Link>
193
+ <Route
194
+ path="*"
195
+ render={({ location }) => {
196
+ path = location.pathname;
197
+ return null;
198
+ }}
199
+ />
200
+ </MemoryRouter>,
201
+ );
202
+ await settle();
203
+
204
+ fireEvent.click(screen.getByText('Go'));
205
+ await settle();
206
+
207
+ expect(saveData).not.toHaveBeenCalled();
208
+ expect(path).toBe('/somewhere-else');
209
+ });
210
+
211
+ it('does not block navigation while the form is invalid', async () => {
212
+ let path = '';
213
+
214
+ render(
215
+ <MemoryRouter>
216
+ <ModalForm<FormValues>
217
+ title="Modal Form"
218
+ closeModal={noop}
219
+ initialData={initialData}
220
+ saveData={noop}
221
+ validationSchema={Yup.object<ObjectSchemaDefinition<FormValues>>({
222
+ something: Yup.string().max(3, 'Too long'),
223
+ })}
224
+ >
225
+ <ChangeValue />
226
+ </ModalForm>
227
+ <Link to="/somewhere-else">Go</Link>
228
+ <Route
229
+ path="*"
230
+ render={({ location }) => {
231
+ path = location.pathname;
232
+ return null;
233
+ }}
234
+ />
235
+ </MemoryRouter>,
236
+ );
237
+ await settle();
238
+
239
+ fireEvent.click(screen.getByText('Go'));
240
+ await settle();
241
+
242
+ expect(path).toBe('/somewhere-else');
243
+ });
244
+
245
+ describe('useModalForm', () => {
246
+ const Host: React.FC = () => {
247
+ const { ModalWrapper, openModal } = useModalForm<FormValues>({
248
+ title: 'Modal Form',
249
+ initialData,
250
+ saveData: noop,
251
+ children: <ChangeValue />,
252
+ });
253
+
254
+ return (
255
+ <>
256
+ <TextButton text="Open" onButtonClicked={openModal} />
257
+ <ModalWrapper />
258
+ </>
259
+ );
260
+ };
261
+
262
+ it('opens the form in a modal and closes it again when cancelled', async () => {
263
+ render(
264
+ <MemoryRouter>
265
+ <Host />
266
+ </MemoryRouter>,
267
+ );
268
+
269
+ expect(screen.queryByTestId('modal')).not.toBeInTheDocument();
270
+
271
+ fireEvent.click(screen.getByText('Open'));
272
+ await settle();
273
+
274
+ expect(screen.getByTestId('modal')).toBeInTheDocument();
275
+ expect(screen.getByTestId('page-header-title')).toHaveTextContent(
276
+ 'Modal Form',
277
+ );
278
+
279
+ const actions = headerActions();
280
+ fireEvent.click(actions[actions.length - 1]);
281
+ await settle();
282
+
283
+ // The modal plays an exit animation before it unmounts.
284
+ fireEvent.animationEnd(screen.getByTestId('modal'));
285
+ await settle();
286
+
287
+ expect(screen.queryByTestId('modal')).not.toBeInTheDocument();
288
+ });
289
+ });
290
+ });
@@ -0,0 +1,191 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Field } from 'formik';
3
+ import React from 'react';
4
+ import { MemoryRouter } from 'react-router';
5
+ import { action } from 'storybook/actions';
6
+ import * as Yup from 'yup';
7
+ import { createGroups } from '../../../helpers/storybook';
8
+ import { TextButton } from '../../Buttons';
9
+ import {
10
+ CustomTagsField,
11
+ SelectField,
12
+ SingleLineTextField,
13
+ TagsField,
14
+ } from '../../FormElements';
15
+ import { useModal } from '../../Modal';
16
+ import type { ObjectSchemaDefinition } from '../FormStation.models';
17
+ import { ModalForm } from './ModalForm';
18
+ import { useModalForm } from './useModalForm';
19
+
20
+ interface MovieValues {
21
+ title?: string;
22
+ genres?: string[];
23
+ ratings?: string;
24
+ shortDescription?: string;
25
+ cast?: string[];
26
+ }
27
+
28
+ type ModalFormStoryType = typeof ModalForm<MovieValues>;
29
+
30
+ const groups = createGroups({
31
+ Content: [],
32
+ Behavior: [],
33
+ Styling: [],
34
+ });
35
+
36
+ const meta: Meta<ModalFormStoryType> = {
37
+ title: 'Primary Components/Station Controls/FormStation/ModalForm',
38
+ component: ModalForm,
39
+ argTypes: {
40
+ ...groups,
41
+ closeModal: {
42
+ control: false,
43
+ },
44
+ },
45
+ decorators: [
46
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
47
+ (Story) => (
48
+ <MemoryRouter>
49
+ <Story />
50
+ </MemoryRouter>
51
+ ),
52
+ ],
53
+ };
54
+ export default meta;
55
+
56
+ const formFields = (
57
+ <>
58
+ <Field name="title" label="Title" as={SingleLineTextField} />
59
+ <Field
60
+ name="genres"
61
+ label="Genre(s)"
62
+ tagsOptions={['Crime', 'Drama', 'Thriller']}
63
+ as={TagsField}
64
+ />
65
+ <Field
66
+ name="ratings"
67
+ label="Age Rating"
68
+ options={[
69
+ { value: 'PG', label: 'Parental Guidance Suggested (PG)' },
70
+ { value: 'PG-13', label: 'Parents Strongly Cautioned (PG-13)' },
71
+ { value: 'R', label: 'Restricted (R)' },
72
+ ]}
73
+ as={SelectField}
74
+ />
75
+ <Field
76
+ name="shortDescription"
77
+ label="Short Description"
78
+ placeholder="Enter a short description..."
79
+ as={SingleLineTextField}
80
+ />
81
+ <Field name="cast" label="Cast" as={CustomTagsField} />
82
+ </>
83
+ );
84
+
85
+ const defaultArgs = {
86
+ title: 'Edit Movie',
87
+ subtitle: 'Movies',
88
+ saveData: (...args: unknown[]): Promise<void> => {
89
+ action('saveData')(args);
90
+ return new Promise<void>((resolve) => setTimeout(resolve, 500));
91
+ },
92
+ validationSchema: Yup.object<ObjectSchemaDefinition<MovieValues>>({
93
+ title: Yup.string().required().max(25).label('Title'),
94
+ shortDescription: Yup.string().required().label('Short Description'),
95
+ }),
96
+ initialData: {
97
+ loading: false,
98
+ data: {
99
+ title: 'The Silent Sea',
100
+ genres: ['Drama'],
101
+ ratings: 'PG-13',
102
+ shortDescription: 'A crew searches an abandoned lunar research station.',
103
+ cast: ['Bae Doona'],
104
+ },
105
+ },
106
+ children: formFields,
107
+ };
108
+
109
+ /** The form hosted in a modal by the `useModal` hook. */
110
+ export const Default: StoryObj<ModalFormStoryType> = {
111
+ args: defaultArgs,
112
+ render: (args) =>
113
+ React.createElement(() => {
114
+ const { ModalWrapper, openModal } = useModal(({ closeModal }) => (
115
+ <ModalForm<MovieValues> {...args} closeModal={closeModal} />
116
+ ));
117
+
118
+ return (
119
+ <div>
120
+ <TextButton text="Edit Movie" onButtonClicked={openModal} />
121
+ <ModalWrapper />
122
+ </div>
123
+ );
124
+ }),
125
+ };
126
+
127
+ /** The same form, set up with the `useModalForm` hook instead. */
128
+ export const WithUseModalForm: StoryObj<ModalFormStoryType> = {
129
+ args: defaultArgs,
130
+ render: (args) =>
131
+ React.createElement(() => {
132
+ const { ModalWrapper, openModal } = useModalForm<MovieValues>(args);
133
+
134
+ return (
135
+ <div>
136
+ <TextButton text="Edit Movie" onButtonClicked={openModal} />
137
+ <ModalWrapper />
138
+ </div>
139
+ );
140
+ }),
141
+ };
142
+
143
+ /** A failing save keeps the modal open and shows the error on the form. */
144
+ export const SaveFailing: StoryObj<ModalFormStoryType> = {
145
+ args: {
146
+ ...defaultArgs,
147
+ saveData: () => {
148
+ throw {
149
+ title: 'Unable to save the movie.',
150
+ body: 'The service did not respond. Please try again later.',
151
+ };
152
+ },
153
+ },
154
+ render: (args) =>
155
+ React.createElement(() => {
156
+ const { ModalWrapper, openModal } = useModalForm<MovieValues>(args);
157
+
158
+ return (
159
+ <div>
160
+ <TextButton text="Edit Movie" onButtonClicked={openModal} />
161
+ <ModalWrapper />
162
+ </div>
163
+ );
164
+ }),
165
+ };
166
+
167
+ /** A narrower form, requested with the `width` prop. */
168
+ export const CustomWidth: StoryObj<ModalFormStoryType> = {
169
+ args: {
170
+ ...defaultArgs,
171
+ title: 'Add Genre',
172
+ subtitle: undefined,
173
+ width: '800px',
174
+ validationSchema: Yup.object<ObjectSchemaDefinition<MovieValues>>({
175
+ title: Yup.string().required().max(25).label('Title'),
176
+ }),
177
+ initialData: { loading: false, data: { title: '' } },
178
+ children: <Field name="title" label="Title" as={SingleLineTextField} />,
179
+ },
180
+ render: (args) =>
181
+ React.createElement(() => {
182
+ const { ModalWrapper, openModal } = useModalForm<MovieValues>(args);
183
+
184
+ return (
185
+ <div>
186
+ <TextButton text="Add Genre" onButtonClicked={openModal} />
187
+ <ModalWrapper />
188
+ </div>
189
+ );
190
+ }),
191
+ };