Skip to content

Commit bbd4a79

Browse files
committed
feat(ui): demo a vetoed close in the nested dialog story
Makes the panel -> prompt -> prompt case reachable: typing into "add email address" and then trying to close it stacks a confirmation instead. Every close request routes through the controlled `onOpenChange`, so declining to commit there covers Escape, the corner X and Cancel at once. Hand-rolled, and meant to be replaced by the AlertDialog and close confirmation work rather than kept.
1 parent 0f1b3db commit bbd4a79

2 files changed

Lines changed: 76 additions & 14 deletions

File tree

packages/swingset/src/stories/dialog.component.mdx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,11 @@ prompt.
365365
This is the nested case, not a stack: the prompt paints its own scrim over the panel, and the panel
366366
neither dims nor recedes.
367367

368+
Type into **Add email address** and then try to close it — Escape, the corner X, or Cancel — and a
369+
confirmation stacks on top instead, making the panel → prompt → prompt case reachable. The veto is
370+
a controlled `open` whose `onOpenChange` declines to commit; every close request routes through it,
371+
so one check covers all of them.
372+
368373
Stack a prompt on a prompt and the relationship changes — the shape a close confirmation
369374
takes:
370375

packages/swingset/src/stories/dialog.component.stories.tsx

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -107,49 +107,105 @@ const sectionHeader = {
107107
justifyContent: 'space-between',
108108
} as const;
109109

110-
/** A `prompt` dialog opened from inside the `panel` — the shape the account profile uses. */
110+
/**
111+
* A `prompt` dialog opened from inside the `panel` — the shape the account profile uses.
112+
*
113+
* With `confirmDiscard`, closing it while the field holds anything opens a confirmation stacked on
114+
* top rather than closing: `panel -> prompt -> prompt`, and the veto is nothing more than a
115+
* controlled `open` whose `onOpenChange` declines to commit. Hand-rolled here on purpose — it is
116+
* what the `AlertDialog` and close-confirmation work is meant to replace.
117+
*/
111118
function AddValueDialog({
112119
trigger,
113120
title,
114121
description,
115122
placeholder,
116123
confirmLabel = 'Continue',
117124
confirmColor,
125+
confirmDiscard = false,
118126
}: {
119127
trigger: (props: RenderProps) => React.ReactElement;
120128
title: string;
121129
description: string;
122130
placeholder: string;
123131
confirmLabel?: string;
124132
confirmColor?: 'negative';
133+
confirmDiscard?: boolean;
125134
}) {
135+
const [open, setOpen] = React.useState(false);
136+
const [discardOpen, setDiscardOpen] = React.useState(false);
137+
const [value, setValue] = React.useState('');
138+
139+
const dismiss = () => {
140+
setValue('');
141+
setOpen(false);
142+
};
143+
126144
return (
127145
<Dialog
128146
trigger={trigger}
129147
closedBy='closerequest'
148+
open={open}
149+
onOpenChange={next => {
150+
// The veto. Every close request lands here — Escape, the corner X, `Dialog.Close` — so
151+
// declining to commit covers all of them at once. A footer button wired to a bare
152+
// `setOpen(false)` would go around it, which is the argument for `Dialog.Close`.
153+
if (!next && confirmDiscard && value.trim() !== '') {
154+
setDiscardOpen(true);
155+
return;
156+
}
157+
if (!next) {
158+
setValue('');
159+
}
160+
setOpen(next);
161+
}}
130162
>
131-
{({ close }) => (
132-
<>
133-
<Dialog.CloseButton />
134-
<Dialog.Title render={<Heading size='sm' />}>{title}</Dialog.Title>
135-
<Dialog.Description render={<Text />}>{description}</Dialog.Description>
136-
<Input placeholder={placeholder} />
163+
<Dialog.CloseButton />
164+
<Dialog.Title render={<Heading size='sm' />}>{title}</Dialog.Title>
165+
<Dialog.Description render={<Text />}>{description}</Dialog.Description>
166+
<Input
167+
placeholder={placeholder}
168+
value={value}
169+
onChange={event => setValue(event.target.value)}
170+
/>
171+
<div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'flex-end' }}>
172+
<Dialog.Close render={<Button variant='outline' />}>Cancel</Dialog.Close>
173+
<Button
174+
color={confirmColor}
175+
onClick={dismiss}
176+
>
177+
{confirmLabel}
178+
</Button>
179+
</div>
180+
{confirmDiscard ? (
181+
<Dialog
182+
open={discardOpen}
183+
onOpenChange={setDiscardOpen}
184+
closedBy='closerequest'
185+
>
186+
<Dialog.Title render={<Heading size='sm' />}>Discard changes?</Dialog.Title>
187+
<Dialog.Description render={<Text />}>
188+
You have not finished adding this address. It will not be saved.
189+
</Dialog.Description>
137190
<div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'flex-end' }}>
138191
<Button
139192
variant='outline'
140-
onClick={close}
193+
onClick={() => setDiscardOpen(false)}
141194
>
142-
Cancel
195+
Keep editing
143196
</Button>
144197
<Button
145-
color={confirmColor}
146-
onClick={close}
198+
color='negative'
199+
onClick={() => {
200+
setDiscardOpen(false);
201+
dismiss();
202+
}}
147203
>
148-
{confirmLabel}
204+
Discard
149205
</Button>
150206
</div>
151-
</>
152-
)}
207+
</Dialog>
208+
) : null}
153209
</Dialog>
154210
);
155211
}
@@ -173,6 +229,7 @@ export function Nested() {
173229
title='Add email address'
174230
description="We'll send a verification code to this address."
175231
placeholder='you@example.com'
232+
confirmDiscard
176233
/>
177234
</div>
178235
<Item.Group>

0 commit comments

Comments
 (0)