Skip to content

feat(ui): dialog close confirmation - #9439

Open
maxyinger wants to merge 2 commits into
max/mosaic-alert-dialogfrom
max/dialog-close-confirmation
Open

feat(ui): dialog close confirmation#9439
maxyinger wants to merge 2 commits into
max/mosaic-alert-dialogfrom
max/dialog-close-confirmation

Conversation

@maxyinger

@maxyinger maxyinger commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

https://swingset-git-max-dialog-close-confirmation.clerkstage.dev/components/alert-dialog -> Confirm a discard

Description

  • Adds createConfirmHandle() — promise-based, so a confirmation reads as if (await confirm.show({…}))
  • Adds <AlertDialog.Confirm> — rendered inside the dialog it guards, not app-global
  • Adds useConfirmedClose() — wraps the dialog's own onOpenChange
    • covers Escape, outside press, Dialog.CloseButton, Dialog.Close, and the close render prop
    • guarded dialog must be controlled
  • Headless: handle.open(payload) — programmatic counterpart of a trigger's payload
const confirm = React.useMemo(() => createConfirmHandle(), []);

const onOpenChange = useConfirmedClose({
  handle: confirm,
  when: () => value !== '',
  onOpenChange: setOpen,
  confirm: { title: 'Discard changes?', description: '…', actionLabel: 'Discard', destructive: true },
});

<Dialog open={open} onOpenChange={onOpenChange} closedBy='closerequest'>
  {/* … */}
  <AlertDialog.Confirm handle={confirm} finalFocus={inputRef} />
</Dialog>

Preview

Swingset link tbd

Keep editing Discard
https://github.com/user-attachments/assets/a9d64cac-d677-49f2-b161-4b8f1abbbba3 https://github.com/user-attachments/assets/8c7e375c-fd43-411f-a19e-5f0f9448dcac

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview Aug 14, 2026 8:43pm
swingset Error Error Aug 14, 2026 8:43pm

Request Review

@changeset-bot

changeset-bot Bot commented Aug 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4e4cf8a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added the ui label Aug 13, 2026
@maxyinger
maxyinger force-pushed the max/dialog-close-confirmation branch from df92efe to a848c54 Compare August 13, 2026 17:35
maxyinger added a commit that referenced this pull request Aug 13, 2026
Let an explicit `handle.open(payload)` win over the stale `activeTriggerId`
lookup, keep the payload published through the exit transition so a
`handle.close()` no longer blanks children-as-function content, and settle
an in-flight question `false` when `<AlertDialog.Confirm>` unmounts rather
than poisoning the handle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@maxyinger
maxyinger force-pushed the max/dialog-close-confirmation branch from a848c54 to 01220eb Compare August 13, 2026 19:59
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-08-14T20:46:01.489Z

Summary

Metric Count
Packages analyzed 19
Packages with changes 0
🔴 Breaking changes 0
🟡 Non-breaking changes 0
🟢 Additions 0

No API Changes Detected

All packages have stable APIs with no detected changes.


Report generated by Break Check

Last ran on 4e4cf8a.

@pkg-pr-new

pkg-pr-new Bot commented Aug 13, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@9439

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@9439

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@9439

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@9439

@clerk/electron

npm i https://pkg.pr.new/@clerk/electron@9439

@clerk/electron-passkeys

npm i https://pkg.pr.new/@clerk/electron-passkeys@9439

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@9439

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@9439

@clerk/expo-google-signin

npm i https://pkg.pr.new/@clerk/expo-google-signin@9439

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@9439

@clerk/express

npm i https://pkg.pr.new/@clerk/express@9439

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@9439

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@9439

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@9439

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@9439

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@9439

@clerk/react

npm i https://pkg.pr.new/@clerk/react@9439

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@9439

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@9439

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@9439

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@9439

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@9439

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@9439

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@9439

commit: e08c483

maxyinger added a commit that referenced this pull request Aug 13, 2026
Let an explicit `handle.open(payload)` win over the stale `activeTriggerId`
lookup, keep the payload published through the exit transition so a
`handle.close()` no longer blanks children-as-function content, and settle
an in-flight question `false` when `<AlertDialog.Confirm>` unmounts rather
than poisoning the handle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@maxyinger
maxyinger force-pushed the max/dialog-close-confirmation branch from 01220eb to 4027545 Compare August 13, 2026 22:24
@maxyinger
maxyinger force-pushed the max/dialog-close-confirmation branch from 4027545 to 248ec82 Compare August 13, 2026 23:08
@maxyinger
maxyinger force-pushed the max/dialog-close-confirmation branch from 248ec82 to e08c483 Compare August 14, 2026 15:01
maxyinger and others added 2 commits August 14, 2026 14:37
A dialog holding unsaved work should ask before discarding it. Three pieces,
each with one job: `createConfirmHandle()` links a question to its answer,
`useConfirmedClose` guards the close path, and `<AlertDialog.Confirm>` is the
dialog, rendered inside the one it guards so the two share a floating tree —
escape ordering, the stacking styles and the refcounted scroll lock all read
that tree, and a globally mounted confirmation would break every one of them.

`show()` returns a promise resolving to the answer, so a confirmation reads as
`if (await confirm.show({…}))` rather than as a pair of state variables and a
callback. Calling it while one is already showing returns the IN-FLIGHT promise
instead of opening a second: holding Escape against a guarded dialog would
otherwise stack a confirmation per keypress.

The veto is the absence of a commit. `useConfirmedClose` wraps the consumer's
own `onOpenChange`, so it covers every close the dialog owns — Escape, outside
press, `Dialog.CloseButton`, `Dialog.Close`, and the `close` the wrapper hands
its children all funnel through it. A button wired to the consumer's own
`setOpen(false)` never reaches the dialog and so bypasses the question; that is
inherent, and both the hook's JSDoc and the docs page say so.

Two ordering details that are load-bearing. The action settles `true` before
closing, and `settle` is a no-op once a question is answered, so the close that
follows cannot overwrite the answer with `false`. And the hook reads `when` and
`onOpenChange` through a ref, so the callback identity is stable across the
keystrokes of the very form whose dirtiness `when` reports on.

Headless gains `handle.open(payload)` — the programmatic counterpart of a
trigger's payload, which is how the confirmation's own text reaches it. The
root holds it in a ref as well as in state: the registry lookup that runs once
the dialog is open resolves a trigger-less open to `undefined`, and would
otherwise blank the dialog a commit after it was filled.
Let an explicit `handle.open(payload)` win over the stale `activeTriggerId`
lookup, keep the payload published through the exit transition so a
`handle.close()` no longer blanks children-as-function content, and settle
an in-flight question `false` when `<AlertDialog.Confirm>` unmounts rather
than poisoning the handle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant