@@ -74,10 +74,14 @@ export interface DialogContextValue {
7474 * the one way out an alert must not offer.
7575 */
7676 role : DialogRole ;
77+ /** Whether `Card.Header` renders its dismiss. From `showCloseButton` on the root. */
78+ showCloseButton : boolean ;
7779}
7880
7981export const DialogContext = React . createContext < DialogContextValue | null > ( null ) ;
8082
83+ const ShowCloseButtonContext = React . createContext ( true ) ;
84+
8185/**
8286 * In a dialog, as opposed to standalone. A surface that can be rendered either way — `Card`,
8387 * `Profile` — reads this to take the dialog's dismiss and its overlay treatment, without knowing
@@ -136,32 +140,55 @@ export interface DialogPopupProps extends MosaicComponentProps<'div'> {
136140type DialogRootBaseProps < Payload > = Omit < HeadlessDialogProps < Payload > , 'role' | 'closedBy' > ;
137141
138142/**
139- * `role` is the whole dismissal API: it decides how the dialog announces itself AND what closes it,
140- * with no prop to contradict either.
143+ * `role` decides how the dialog announces itself; `shouldCloseOnInteractOutside` decides whether
144+ * an outside press closes it; `showCloseButton` decides whether the surface renders its dismiss.
145+ * Escape always closes.
141146 *
142147 * - `alertdialog` announces as an interruption rather than as a surface the user navigated to, and
143- * cannot be dismissed by an outside press. A dialog asking a question it needs an answer to must
144- * not be answerable by clicking next to it. Escape still closes, which is the keyboard's
145- * equivalent of the cancel button that is always present;
146- * - `dialog` dismisses on an outside press as well.
148+ * never closes on an outside press. A dialog asking a question it needs an answer to must not be
149+ * answerable by clicking next to it. Escape is the keyboard's equivalent of the cancel button
150+ * that is always present;
151+ * - `dialog` closes on an outside press unless `shouldCloseOnInteractOutside` is `false`, for a
152+ * task an accidental press would throw away, such as entering a verification code.
147153 *
148154 * It says nothing about the variant: an alert is a `Card` like every other dialog.
149155 */
150- export type DialogRootProps < Payload = unknown > = DialogRootBaseProps < Payload > & {
151- /** The popup's ARIA role, and with it what dismisses the dialog. @default 'dialog' */
152- role ?: DialogRole ;
153- } ;
156+ export type DialogRootProps < Payload = unknown > = DialogRootBaseProps < Payload > &
157+ (
158+ | {
159+ /** The popup's ARIA role. @default 'dialog' */
160+ role ?: 'dialog' ;
161+ /** Whether an outside press closes the dialog. @default true */
162+ shouldCloseOnInteractOutside ?: boolean ;
163+ /** Whether the surface renders its dismiss. @default true */
164+ showCloseButton ?: boolean ;
165+ }
166+ | {
167+ role : 'alertdialog' ;
168+ shouldCloseOnInteractOutside ?: false ;
169+ /** Whether the surface renders its dismiss, for a step past the question. @default false */
170+ showCloseButton ?: boolean ;
171+ }
172+ ) ;
154173
155174/** Owns the open state and the decisions — role, dismissal — every part reads. */
156- function Root < Payload = unknown > ( { role = 'dialog' , children, ...rest } : DialogRootProps < Payload > ) {
175+ function Root < Payload = unknown > ( {
176+ role = 'dialog' ,
177+ shouldCloseOnInteractOutside = role === 'dialog' ,
178+ showCloseButton = role === 'dialog' ,
179+ children,
180+ ...rest
181+ } : DialogRootProps < Payload > ) {
157182 return (
158- < Primitive . Root < Payload >
159- { ...rest }
160- role = { role }
161- closedBy = { role === 'alertdialog' ? 'closerequest' : 'any' }
162- >
163- { children }
164- </ Primitive . Root >
183+ < ShowCloseButtonContext . Provider value = { showCloseButton } >
184+ < Primitive . Root < Payload >
185+ { ...rest }
186+ role = { role }
187+ closedBy = { shouldCloseOnInteractOutside ? 'any' : 'closerequest' }
188+ >
189+ { children }
190+ </ Primitive . Root >
191+ </ ShowCloseButtonContext . Provider >
165192 ) ;
166193}
167194
@@ -191,8 +218,8 @@ const Close = React.forwardRef<HTMLButtonElement, DialogCloseProps>(function Dia
191218} ) ;
192219
193220/**
194- * Warns when the corner dismiss is rendered inside an alert dialog, where a corner X is a way out
195- * without answering the question.
221+ * Warns when the corner dismiss is rendered inside an alert dialog that has not opted in with
222+ * `showCloseButton`, where a corner X is a way out without answering the question.
196223 */
197224function useCloseButtonWarning ( isAlert : boolean ) {
198225 React . useEffect ( ( ) => {
@@ -219,9 +246,10 @@ const CloseButton = React.forwardRef<HTMLButtonElement, DialogCloseButtonProps>(
219246 ref ,
220247) {
221248 const surface = React . useContext ( DialogContext ) ;
249+ const showCloseButton = React . useContext ( ShowCloseButtonContext ) ;
222250 const { role } = useHeadlessDialogContext ( ) ;
223251 const variant = surface ?. variant ?? 'card' ;
224- useCloseButtonWarning ( role === 'alertdialog' ) ;
252+ useCloseButtonWarning ( role === 'alertdialog' && ! showCloseButton ) ;
225253 return (
226254 < span { ...stylex . props ( styles . closeButton , closeInsets [ variant ] ) } >
227255 < Primitive . Close
@@ -362,15 +390,16 @@ const Popup = React.forwardRef<HTMLDivElement, DialogPopupProps>(function Dialog
362390 // The headless flag, not the stack check below — the variant rule is about opening a dialog inside
363391 // ANY dialog, which is broader than the card-on-card case the stacking styles cover.
364392 const { role, isStacked : isNestedInDialog , labelId, descriptionId } = useHeadlessDialogContext ( ) ;
393+ const showCloseButton = React . useContext ( ShowCloseButtonContext ) ;
365394 const isAlert = role === 'alertdialog' ;
366395 // A profile has its own compact-band treatment and takes no placement; the warning says so.
367396 const compactPlacement : DialogCompactPlacement = variant === 'profile' ? 'center' : ( compactPlacementProp ?? 'sheet' ) ;
368397 useCompactPlacementWarning ( variant , compactPlacementProp ) ;
369398 useNestedVariantWarning ( isNestedInDialog , variant ) ;
370399
371400 const surface = React . useMemo (
372- ( ) => ( { labelId, descriptionId, variant, role } ) ,
373- [ labelId , descriptionId , variant , role ] ,
401+ ( ) => ( { labelId, descriptionId, variant, role, showCloseButton } ) ,
402+ [ labelId , descriptionId , variant , role , showCloseButton ] ,
374403 ) ;
375404 // Observed through state rather than a plain ref, because the warnings have to re-run when the
376405 // node arrives and a ref mutation does not re-render.
0 commit comments