@zeno-lib/forms 0.6.0 → 0.7.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.
- package/dist/create-zeno-form.d.mts +19 -4
- package/dist/create-zeno-form.mjs +29 -6
- package/dist/form-element.d.mts +16 -3
- package/dist/form-element.mjs +38 -18
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +2 -1
- package/dist/lib/date-value.d.mts +24 -0
- package/dist/lib/date-value.mjs +37 -0
- package/dist/lib/default-props.d.mts +7 -0
- package/dist/lib/default-props.mjs +13 -0
- package/dist/lib/empty-value.mjs +25 -1
- package/dist/lib/form-disabled.d.mts +28 -0
- package/dist/lib/form-disabled.mjs +54 -0
- package/dist/lib/item-options.d.mts +45 -0
- package/dist/lib/item-options.mjs +70 -0
- package/dist/lib/schema-required.mjs +17 -0
- package/dist/lib/use-field-control.d.mts +7 -0
- package/dist/lib/use-field-control.mjs +4 -1
- package/package.json +1 -1
- package/src/create-form.tsx +34 -29
- package/src/create-zeno-form.tsx +124 -51
- package/src/date-picker-field.test.tsx +108 -0
- package/src/default-props.test-d.ts +66 -0
- package/src/default-props.test.tsx +165 -0
- package/src/fields/checkbox-field.tsx +5 -0
- package/src/fields/checkbox-group-field.tsx +6 -2
- package/src/fields/combobox-field.tsx +107 -40
- package/src/fields/date-picker-field.tsx +34 -11
- package/src/fields/field-shell.tsx +8 -1
- package/src/fields/input-field.tsx +2 -0
- package/src/fields/multi-select-field.tsx +100 -42
- package/src/fields/otp-field.tsx +8 -2
- package/src/fields/radio-group-field.tsx +9 -1
- package/src/fields/reset-button.tsx +4 -0
- package/src/fields/select-field.tsx +9 -1
- package/src/fields/slider-field.tsx +9 -1
- package/src/fields/submit-button.tsx +3 -1
- package/src/fields/switch-field.tsx +5 -0
- package/src/fields/textarea-field.tsx +2 -0
- package/src/form-dialog.test.tsx +255 -4
- package/src/form-dialog.tsx +187 -51
- package/src/form-disabled.test.tsx +329 -0
- package/src/form-element.tsx +64 -29
- package/src/grouped-options.test-d.ts +59 -0
- package/src/grouped-options.test.tsx +251 -0
- package/src/index.ts +1 -0
- package/src/lib/date-value.test.ts +14 -0
- package/src/lib/date-value.ts +70 -0
- package/src/lib/default-props.ts +19 -0
- package/src/lib/empty-value.test.ts +19 -0
- package/src/lib/empty-value.ts +45 -1
- package/src/lib/form-disabled.ts +95 -0
- package/src/lib/item-options.test.ts +11 -0
- package/src/lib/item-options.ts +165 -0
- package/src/lib/schema-required.test.ts +9 -0
- package/src/lib/schema-required.ts +33 -0
- package/src/lib/use-field-control.ts +11 -1
- package/src/with-form.test-d.ts +76 -0
- package/src/with-form.test.tsx +60 -0
package/src/form-dialog.tsx
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import { FormProvider } from "@zeno-lib/forms"
|
|
4
|
+
import { withDefaultProps } from "@zeno-lib/forms/lib/default-props"
|
|
4
5
|
import {
|
|
5
6
|
focusFirstInvalid,
|
|
6
7
|
restoreFocus,
|
|
7
8
|
} from "@zeno-lib/forms/lib/focus-first-invalid"
|
|
9
|
+
import { useIsFormDisabled } from "@zeno-lib/forms/lib/form-disabled"
|
|
8
10
|
import { reportSubmitError } from "@zeno-lib/forms/lib/submit-error"
|
|
9
11
|
import {
|
|
10
12
|
type FormDialogController,
|
|
@@ -14,10 +16,13 @@ import {
|
|
|
14
16
|
import { useUnsavedChangesWarning } from "@zeno-lib/forms/lib/use-unsaved-changes-warning"
|
|
15
17
|
import { type AnyFormApi, useSelector } from "@zeno-lib/forms/tanstack"
|
|
16
18
|
import {
|
|
19
|
+
type ComponentType,
|
|
20
|
+
createContext,
|
|
17
21
|
type FormEvent,
|
|
18
22
|
type KeyboardEvent,
|
|
19
23
|
type ReactElement,
|
|
20
24
|
type ReactNode,
|
|
25
|
+
useContext,
|
|
21
26
|
useId,
|
|
22
27
|
useLayoutEffect,
|
|
23
28
|
useRef,
|
|
@@ -45,7 +50,7 @@ import {
|
|
|
45
50
|
DialogTrigger,
|
|
46
51
|
} from "@/components/ui/dialog"
|
|
47
52
|
import { Spinner } from "@/components/ui/spinner"
|
|
48
|
-
import { FormError } from "./fields/form-error"
|
|
53
|
+
import { FormError, type FormErrorProps } from "./fields/form-error"
|
|
49
54
|
|
|
50
55
|
type DiscardPromptText = {
|
|
51
56
|
title?: ReactNode
|
|
@@ -54,6 +59,10 @@ type DiscardPromptText = {
|
|
|
54
59
|
cancelLabel?: ReactNode
|
|
55
60
|
/** The Save action's label (`saveFromPrompt`). Defaults to `submitLabel`. */
|
|
56
61
|
saveLabel?: ReactNode
|
|
62
|
+
/** The title with `saveFromPrompt`. Defaults to `title`. */
|
|
63
|
+
saveTitle?: ReactNode
|
|
64
|
+
/** The description with `saveFromPrompt`. Defaults to `description`. */
|
|
65
|
+
saveDescription?: ReactNode
|
|
57
66
|
}
|
|
58
67
|
|
|
59
68
|
// The slice of the `useForm()` API the dialog uses. Structural, so any Zeno /
|
|
@@ -77,6 +86,11 @@ type DialogForm<TValues> = {
|
|
|
77
86
|
readonly state: DialogFormState
|
|
78
87
|
handleSubmit: () => unknown
|
|
79
88
|
reset: (values?: TValues) => void
|
|
89
|
+
/**
|
|
90
|
+
* The form's own `FormError` (`useForm` attaches it), rendered with the
|
|
91
|
+
* app's `defaultProps`. The dialog falls back to the kit's.
|
|
92
|
+
*/
|
|
93
|
+
FormError?: ComponentType<Pick<FormErrorProps, "submitErrorMessage">>
|
|
80
94
|
}
|
|
81
95
|
|
|
82
96
|
type FormDialogProps<TValues> = {
|
|
@@ -96,12 +110,16 @@ type FormDialogProps<TValues> = {
|
|
|
96
110
|
cancelLabel?: ReactNode
|
|
97
111
|
/** Extra footer content, rendered before Cancel / Save. */
|
|
98
112
|
footer?: ReactNode
|
|
99
|
-
/**
|
|
113
|
+
/**
|
|
114
|
+
* Close after a successful submit. Defaults to `true`. A close the user
|
|
115
|
+
* asked for while the submit ran goes through once it succeeds either way,
|
|
116
|
+
* unless they edited a field since.
|
|
117
|
+
*/
|
|
100
118
|
closeOnSubmit?: boolean
|
|
101
119
|
/**
|
|
102
|
-
* Shown above the footer when the submit handler throws. Defaults to
|
|
103
|
-
*
|
|
104
|
-
* was thrown.
|
|
120
|
+
* Shown above the footer when the submit handler throws. Defaults to the
|
|
121
|
+
* form's `FormError` default: your `defaultProps`, or
|
|
122
|
+
* `"Something went wrong. Try again."`. A function gets what was thrown.
|
|
105
123
|
*/
|
|
106
124
|
submitErrorMessage?: ReactNode | ((error: unknown) => ReactNode)
|
|
107
125
|
/**
|
|
@@ -113,12 +131,15 @@ type FormDialogProps<TValues> = {
|
|
|
113
131
|
discardPrompt?: DiscardPromptText
|
|
114
132
|
/**
|
|
115
133
|
* Offer Save in the discard prompt, next to Discard: it submits, and closes
|
|
116
|
-
* the dialog if the submit succeeds.
|
|
134
|
+
* the dialog if the submit succeeds. Not in a prompt that opens while the
|
|
135
|
+
* dialog is disabled. Defaults to `false`.
|
|
117
136
|
*/
|
|
118
137
|
saveFromPrompt?: boolean
|
|
119
138
|
/**
|
|
120
|
-
* Disable
|
|
121
|
-
*
|
|
139
|
+
* Disable every field, Save and ⌘/Ctrl+Enter, as `useForm({ disabled })`
|
|
140
|
+
* does. A `<fieldset disabled>` also disables native controls of your own.
|
|
141
|
+
* A discard prompt that opens while disabled doesn't offer Save. Defaults
|
|
142
|
+
* to `false`.
|
|
122
143
|
*/
|
|
123
144
|
disabled?: boolean
|
|
124
145
|
/** Class for the dialog popup (e.g. a wider `sm:max-w-lg`). */
|
|
@@ -127,6 +148,87 @@ type FormDialogProps<TValues> = {
|
|
|
127
148
|
formClassName?: string
|
|
128
149
|
}
|
|
129
150
|
|
|
151
|
+
/** The `FormDialog` props a `FormDialogProvider` can set for every dialog. */
|
|
152
|
+
type FormDialogDefaults = Omit<
|
|
153
|
+
FormDialogProps<unknown>,
|
|
154
|
+
| "children"
|
|
155
|
+
| "description"
|
|
156
|
+
| "dialog"
|
|
157
|
+
| "disabled"
|
|
158
|
+
| "footer"
|
|
159
|
+
| "form"
|
|
160
|
+
| "title"
|
|
161
|
+
| "trigger"
|
|
162
|
+
>
|
|
163
|
+
|
|
164
|
+
const FormDialogDefaultsContext = createContext<FormDialogDefaults | undefined>(
|
|
165
|
+
undefined
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
// `props` over `defaults`, with the discard prompt's copy merged key by key.
|
|
169
|
+
function withDialogDefaults<P extends FormDialogDefaults>(
|
|
170
|
+
defaults: FormDialogDefaults | undefined,
|
|
171
|
+
props: P
|
|
172
|
+
): P {
|
|
173
|
+
if (defaults === undefined) {
|
|
174
|
+
return props
|
|
175
|
+
}
|
|
176
|
+
const merged = withDefaultProps(defaults as Partial<P>, props)
|
|
177
|
+
// With `saveFromPrompt`, a `save*` key falls back to `title`, `description`
|
|
178
|
+
// or `submitLabel`. The nearest level wins: one that sets the fallback but
|
|
179
|
+
// not the `save*` key drops the `save*` key set further up.
|
|
180
|
+
const own = props.discardPrompt ?? {}
|
|
181
|
+
const inherited: DiscardPromptText = { ...defaults.discardPrompt }
|
|
182
|
+
if (props.submitLabel !== undefined) {
|
|
183
|
+
inherited.saveLabel = undefined
|
|
184
|
+
}
|
|
185
|
+
if (own.title !== undefined) {
|
|
186
|
+
inherited.saveTitle = undefined
|
|
187
|
+
}
|
|
188
|
+
if (own.description !== undefined) {
|
|
189
|
+
inherited.saveDescription = undefined
|
|
190
|
+
}
|
|
191
|
+
merged.discardPrompt = withDefaultProps(inherited, own)
|
|
192
|
+
return merged
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// The prompt's title and description for its mode.
|
|
196
|
+
function getPromptCopy(
|
|
197
|
+
text: DiscardPromptText | undefined,
|
|
198
|
+
saveFromPrompt: boolean
|
|
199
|
+
) {
|
|
200
|
+
if (saveFromPrompt) {
|
|
201
|
+
return {
|
|
202
|
+
description:
|
|
203
|
+
text?.saveDescription ??
|
|
204
|
+
text?.description ??
|
|
205
|
+
"Your unsaved changes will be lost unless you save them.",
|
|
206
|
+
title: text?.saveTitle ?? text?.title ?? "Save changes?",
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
description: text?.description ?? "Your unsaved changes will be lost.",
|
|
211
|
+
title: text?.title ?? "Discard changes?",
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Sets the default props of every `FormDialog` below it, such as its copy or
|
|
217
|
+
* `saveFromPrompt`. A prop passed to a dialog wins, and `discardPrompt` merges
|
|
218
|
+
* key by key. A nested provider merges over its parent the same way.
|
|
219
|
+
*/
|
|
220
|
+
function FormDialogProvider({
|
|
221
|
+
children,
|
|
222
|
+
...defaults
|
|
223
|
+
}: FormDialogDefaults & { children: ReactNode }) {
|
|
224
|
+
const parent = useContext(FormDialogDefaultsContext)
|
|
225
|
+
return (
|
|
226
|
+
<FormDialogDefaultsContext value={withDialogDefaults(parent, defaults)}>
|
|
227
|
+
{children}
|
|
228
|
+
</FormDialogDefaultsContext>
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
|
|
130
232
|
/**
|
|
131
233
|
* A dialog-hosted form: opens with fresh values and fields per session,
|
|
132
234
|
* submits from a footer button outside the `<form>` (via `form="id"`), shows a
|
|
@@ -135,30 +237,34 @@ type FormDialogProps<TValues> = {
|
|
|
135
237
|
* submit) above the footer, closes on success, and asks before discarding
|
|
136
238
|
* unsaved changes (Cancel, ×, Escape, outside press) or leaving the page.
|
|
137
239
|
*/
|
|
138
|
-
function FormDialog<TValues>({
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
240
|
+
function FormDialog<TValues>(props: FormDialogProps<TValues>) {
|
|
241
|
+
const {
|
|
242
|
+
cancelLabel = "Cancel",
|
|
243
|
+
children,
|
|
244
|
+
className,
|
|
245
|
+
closeOnSubmit = true,
|
|
246
|
+
description,
|
|
247
|
+
dialog,
|
|
248
|
+
disabled = false,
|
|
249
|
+
discardPrompt,
|
|
250
|
+
footer,
|
|
251
|
+
form,
|
|
252
|
+
formClassName,
|
|
253
|
+
guard = true,
|
|
254
|
+
saveFromPrompt = false,
|
|
255
|
+
submitErrorMessage,
|
|
256
|
+
submitLabel = "Save",
|
|
257
|
+
title,
|
|
258
|
+
trigger,
|
|
259
|
+
} = withDialogDefaults(useContext(FormDialogDefaultsContext), props)
|
|
157
260
|
const formId = useId()
|
|
261
|
+
// `useForm({ disabled })` locks the dialog as this prop does.
|
|
262
|
+
const isDisabled =
|
|
263
|
+
useIsFormDisabled(form as unknown as AnyFormApi) || disabled
|
|
158
264
|
const popupRef = useRef<HTMLDivElement>(null)
|
|
159
265
|
const formRef = useRef<HTMLFormElement>(null)
|
|
160
|
-
// Set when a
|
|
161
|
-
// to the first invalid field as it closes.
|
|
266
|
+
// Set when a submit fails while the discard prompt is open, so the prompt
|
|
267
|
+
// hands focus to the first invalid field as it closes.
|
|
162
268
|
const focusInvalidOnPromptClose = useRef<boolean>(false)
|
|
163
269
|
// The open prompt hides the dialog from assistive tech, so a form-level
|
|
164
270
|
// error that shows up meanwhile (a Save from the prompt, or a footer Save
|
|
@@ -166,7 +272,13 @@ function FormDialog<TValues>({
|
|
|
166
272
|
// the prompt has closed, which announces it.
|
|
167
273
|
const announceOnPromptClosed = useRef<boolean>(false)
|
|
168
274
|
const [formErrorKey, setFormErrorKey] = useState(0)
|
|
275
|
+
// Whether the discard prompt offers Save, set as it opens: a disabled
|
|
276
|
+
// dialog can't save. Kept while it's open, so the prompt doesn't change
|
|
277
|
+
// under the user; one disabled meanwhile only turns its Save off.
|
|
278
|
+
const [promptOffersSave, setPromptOffersSave] = useState(false)
|
|
169
279
|
const { close, defaultValues, focus, isOpen, open, session } = dialog
|
|
280
|
+
const DialogFormError = form.FormError ?? FormError
|
|
281
|
+
const promptCopy = getPromptCopy(discardPrompt, promptOffersSave)
|
|
170
282
|
// Read after a submit's await: the dialog may have closed, or reopened on
|
|
171
283
|
// another record, while a slow save was in flight.
|
|
172
284
|
const latest = useRef({ isOpen, session })
|
|
@@ -211,12 +323,18 @@ function FormDialog<TValues>({
|
|
|
211
323
|
}
|
|
212
324
|
}, [session])
|
|
213
325
|
|
|
326
|
+
// A held leave belongs to the session it was asked in. Drop it when that
|
|
327
|
+
// session ends some other way (`dialog.close()`, a submit that closes it,
|
|
328
|
+
// `dialog.open()` on another record), so the prompt never outlives it.
|
|
329
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: on every session change.
|
|
330
|
+
useLayoutEffect(() => cancelLeave, [isOpen, session])
|
|
331
|
+
|
|
214
332
|
// Every way to submit (footer Save, ⌘/Ctrl+Enter, the prompt's Save) checks
|
|
215
333
|
// this first. TanStack only refuses a second `handleSubmit` while one runs on
|
|
216
334
|
// the very first attempt, and `isOpen` is false through the exit animation,
|
|
217
335
|
// while the fields still hold the values just saved.
|
|
218
336
|
function canSubmit(): boolean {
|
|
219
|
-
return isOpen && !(
|
|
337
|
+
return isOpen && !(isDisabled || form.state.isSubmitting)
|
|
220
338
|
}
|
|
221
339
|
|
|
222
340
|
// Resolves whether the form saved. A submit handler that throws leaves
|
|
@@ -240,10 +358,25 @@ function FormDialog<TValues>({
|
|
|
240
358
|
}
|
|
241
359
|
const node = event.currentTarget
|
|
242
360
|
const focused = node.ownerDocument.activeElement
|
|
361
|
+
// The fields stay editable while the submit runs. TanStack replaces
|
|
362
|
+
// `values` on every edit, so the same object means none came after.
|
|
363
|
+
const submitted = form.state.values
|
|
243
364
|
const saved = await submit()
|
|
244
365
|
if (!latest.current.isOpen || latest.current.session !== session) {
|
|
245
366
|
return // the session this submit belongs to is over
|
|
246
367
|
}
|
|
368
|
+
if (isConfirmingRef.current) {
|
|
369
|
+
// The user asked to leave while the submit ran. If it saved every
|
|
370
|
+
// change, that leave goes through, `closeOnSubmit` or not. After a
|
|
371
|
+
// failure or a later edit, the prompt waits for the user's answer, and
|
|
372
|
+
// it traps focus, so it moves focus to an invalid field as it closes.
|
|
373
|
+
if (saved && form.state.values === submitted) {
|
|
374
|
+
confirmLeave()
|
|
375
|
+
} else {
|
|
376
|
+
focusInvalidOnPromptClose.current = !saved
|
|
377
|
+
}
|
|
378
|
+
return
|
|
379
|
+
}
|
|
247
380
|
if (saved) {
|
|
248
381
|
if (closeOnSubmit) {
|
|
249
382
|
// Close directly, past the guard: the changes are saved. The form is
|
|
@@ -290,6 +423,7 @@ function FormDialog<TValues>({
|
|
|
290
423
|
if (next) {
|
|
291
424
|
open()
|
|
292
425
|
} else {
|
|
426
|
+
setPromptOffersSave(saveFromPrompt && !isDisabled)
|
|
293
427
|
requestLeave(close)
|
|
294
428
|
}
|
|
295
429
|
}}
|
|
@@ -308,7 +442,7 @@ function FormDialog<TValues>({
|
|
|
308
442
|
initialFocus={() => {
|
|
309
443
|
// A disabled control can't take focus.
|
|
310
444
|
const target =
|
|
311
|
-
focus && !
|
|
445
|
+
focus && !isDisabled
|
|
312
446
|
? findFieldElement(popupRef.current, focus)
|
|
313
447
|
: null
|
|
314
448
|
return target ?? true
|
|
@@ -320,11 +454,11 @@ function FormDialog<TValues>({
|
|
|
320
454
|
<DialogTitle>{title}</DialogTitle>
|
|
321
455
|
{description && <DialogDescription>{description}</DialogDescription>}
|
|
322
456
|
</DialogHeader>
|
|
323
|
-
<FormProvider form={form}>
|
|
457
|
+
<FormProvider disabled={disabled} form={form}>
|
|
324
458
|
{/* Around the <form>, not inside it, so `formClassName` child
|
|
325
459
|
selectors (`space-y-4`) still reach the fields. `min-w-0` is
|
|
326
460
|
for a browser that ignores `contents` on a fieldset. */}
|
|
327
|
-
<fieldset className="contents min-w-0" disabled={
|
|
461
|
+
<fieldset className="contents min-w-0" disabled={isDisabled}>
|
|
328
462
|
{/* Keyed on the session so every open mounts fresh fields. The
|
|
329
463
|
popup stays mounted through its exit animation, so reopening
|
|
330
464
|
before it ends would keep each field's local state (and any
|
|
@@ -343,7 +477,7 @@ function FormDialog<TValues>({
|
|
|
343
477
|
{/* Next to the Save button that failed, and outside the <form>, so
|
|
344
478
|
it stays in view when the fields scroll and adds no child for
|
|
345
479
|
`formClassName` selectors to match. */}
|
|
346
|
-
<
|
|
480
|
+
<DialogFormError
|
|
347
481
|
key={formErrorKey}
|
|
348
482
|
submitErrorMessage={submitErrorMessage}
|
|
349
483
|
/>
|
|
@@ -354,7 +488,7 @@ function FormDialog<TValues>({
|
|
|
354
488
|
{cancelLabel}
|
|
355
489
|
</DialogClose>
|
|
356
490
|
<Button
|
|
357
|
-
disabled={
|
|
491
|
+
disabled={isDisabled || isSubmitting}
|
|
358
492
|
form={formId}
|
|
359
493
|
type="submit"
|
|
360
494
|
>
|
|
@@ -378,32 +512,29 @@ function FormDialog<TValues>({
|
|
|
378
512
|
setFormErrorKey((key) => key + 1)
|
|
379
513
|
}
|
|
380
514
|
}}
|
|
381
|
-
|
|
515
|
+
// Closes in the same render as the dialog.
|
|
516
|
+
open={isConfirming && isOpen}
|
|
382
517
|
>
|
|
383
518
|
<AlertDialogContent
|
|
384
519
|
// The prompt traps focus while open, so a failed Save's invalid
|
|
385
520
|
// field gets focus as the prompt closes, not from `submit`.
|
|
386
521
|
finalFocus={() => {
|
|
387
|
-
|
|
522
|
+
const focusInvalid = focusInvalidOnPromptClose.current
|
|
523
|
+
focusInvalidOnPromptClose.current = false
|
|
524
|
+
// After Discard, the closing dialog hands focus back itself.
|
|
525
|
+
if (!(focusInvalid && latest.current.isOpen)) {
|
|
388
526
|
return true
|
|
389
527
|
}
|
|
390
|
-
focusInvalidOnPromptClose.current = false
|
|
391
528
|
// Base UI focuses what this returns too; focusing here first
|
|
392
529
|
// finds a field that takes focus, skipping disabled or hidden ones.
|
|
393
530
|
return focusFirstInvalid(formRef.current) ?? true
|
|
394
531
|
}}
|
|
395
|
-
size={
|
|
532
|
+
size={promptOffersSave ? "default" : "sm"}
|
|
396
533
|
>
|
|
397
534
|
<AlertDialogHeader>
|
|
398
|
-
<AlertDialogTitle>
|
|
399
|
-
{discardPrompt?.title ??
|
|
400
|
-
(saveFromPrompt ? "Save changes?" : "Discard changes?")}
|
|
401
|
-
</AlertDialogTitle>
|
|
535
|
+
<AlertDialogTitle>{promptCopy.title}</AlertDialogTitle>
|
|
402
536
|
<AlertDialogDescription>
|
|
403
|
-
{
|
|
404
|
-
(saveFromPrompt
|
|
405
|
-
? "Your unsaved changes will be lost unless you save them."
|
|
406
|
-
: "Your unsaved changes will be lost.")}
|
|
537
|
+
{promptCopy.description}
|
|
407
538
|
</AlertDialogDescription>
|
|
408
539
|
</AlertDialogHeader>
|
|
409
540
|
<AlertDialogFooter>
|
|
@@ -417,9 +548,9 @@ function FormDialog<TValues>({
|
|
|
417
548
|
>
|
|
418
549
|
{discardPrompt?.confirmLabel ?? "Discard"}
|
|
419
550
|
</AlertDialogAction>
|
|
420
|
-
{
|
|
551
|
+
{promptOffersSave && (
|
|
421
552
|
<AlertDialogAction
|
|
422
|
-
disabled={
|
|
553
|
+
disabled={isDisabled || isSaving || isSubmitting}
|
|
423
554
|
onClick={saveAndLeave}
|
|
424
555
|
>
|
|
425
556
|
{isSaving && <Spinner />}
|
|
@@ -433,5 +564,10 @@ function FormDialog<TValues>({
|
|
|
433
564
|
)
|
|
434
565
|
}
|
|
435
566
|
|
|
436
|
-
export type {
|
|
437
|
-
|
|
567
|
+
export type {
|
|
568
|
+
DialogForm,
|
|
569
|
+
DiscardPromptText,
|
|
570
|
+
FormDialogDefaults,
|
|
571
|
+
FormDialogProps,
|
|
572
|
+
}
|
|
573
|
+
export { FormDialog, FormDialogProvider }
|