Skip to content

Commit 74430f3

Browse files
ciampoaduth
andauthored
ui/AlertDialog: better async confirm APIs, fully use base ui's AlertDialog (#76937)
* AlertDialog: Refactor to use Base UI primitives and internal state machine Made-with: Cursor * AlertDialog: Rewrite tests for state-machine-based API Made-with: Cursor * AlertDialog: Update stories and CHANGELOG for new API Made-with: Cursor --- Co-authored-by: ciampo <mciampini@git.wordpress.org> Co-authored-by: aduth <aduth@git.wordpress.org>
1 parent b0a4da8 commit 74430f3

File tree

9 files changed

+1756
-492
lines changed

9 files changed

+1756
-492
lines changed

packages/ui/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@
55
### Breaking Changes
66

77
- `Text`: Apply `margin: 0`, removing user-agent margins when the component renders as block-level elements (for example `p` or `h1``h6` via `render` prop) ([#76970](https://github.com/WordPress/gutenberg/pull/76970)).
8+
- `AlertDialog`: Revise component API ([#76937](https://github.com/WordPress/gutenberg/pull/76937)):
9+
- `AlertDialog.Root`: moved `intent` prop to `AlertDialog.Popup`;
10+
- `AlertDialog.Popup`: moved `onConfirm` prop to `AlertDialog.Root` (now optional; supports async handlers, `{ close: false }` to keep the dialog open, and `{ error: '...' }` to display a built-in error message);
11+
- `AlertDialog.Popup`: removed `loading` prop (async flows are now handled internally via `Promise`-returning `onConfirm`);
12+
- `AlertDialog.Popup`: made `children` optional in favor of a new `description` prop, which describes the alert dialog semantically.
13+
14+
### Internal
15+
16+
- `AlertDialog`: Rewrite internals to use Base UI's `AlertDialog` primitives directly instead of `Dialog` wrappers. Introduces an internal state machine for async confirm flows ([#76937](https://github.com/WordPress/gutenberg/pull/76937)).
817

918
### Bug Fixes
1019

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
import { createContext } from '@wordpress/element';
2-
import type { RootProps } from './types';
32

4-
type Intent = NonNullable< RootProps[ 'intent' ] >;
3+
type Phase = 'idle' | 'pending' | 'closing';
54

65
interface AlertDialogContextValue {
7-
intent: Intent;
6+
phase: Phase;
7+
showSpinner: boolean;
8+
errorMessage?: string;
9+
confirm: () => Promise< void >;
810
}
911

12+
const noop = async () => {};
13+
1014
const AlertDialogContext = createContext< AlertDialogContextValue >( {
11-
intent: 'default',
15+
phase: 'idle',
16+
showSpinner: false,
17+
errorMessage: undefined,
18+
confirm: noop,
1219
} );
1320

1421
export { AlertDialogContext };
22+
export type { Phase };

packages/ui/src/alert-dialog/popup.tsx

Lines changed: 91 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,114 @@
1+
import { AlertDialog as _AlertDialog } from '@base-ui/react/alert-dialog';
2+
import clsx from 'clsx';
13
import { forwardRef, useContext } from '@wordpress/element';
24
import { __ } from '@wordpress/i18n';
5+
import {
6+
type ThemeProvider as ThemeProviderType,
7+
privateApis as themePrivateApis,
8+
} from '@wordpress/theme';
9+
310
import { Button } from '../button';
4-
import * as Dialog from '../dialog';
11+
import dialogStyles from '../dialog/style.module.css';
12+
import { unlock } from '../lock-unlock';
13+
import { Stack } from '../stack';
14+
import { Text } from '../text';
515
import { AlertDialogContext } from './context';
6-
import styles from './style.module.css';
16+
import alertDialogStyles from './style.module.css';
717
import type { PopupProps } from './types';
818

19+
const ThemeProvider: typeof ThemeProviderType =
20+
unlock( themePrivateApis ).ThemeProvider;
21+
922
const Popup = forwardRef< HTMLDivElement, PopupProps >(
1023
function AlertDialogPopup(
1124
{
25+
className,
26+
intent = 'default',
1227
title,
28+
description,
1329
children,
14-
onConfirm,
1530
confirmButtonText = __( 'OK' ),
1631
cancelButtonText = __( 'Cancel' ),
17-
loading,
32+
...props
1833
},
1934
ref
2035
) {
21-
const { intent } = useContext( AlertDialogContext );
36+
const { phase, showSpinner, errorMessage, confirm } =
37+
useContext( AlertDialogContext );
2238

23-
// When `loading` is provided, the consumer controls when the dialog
24-
// closes (async flow). Use a plain Button so clicking confirm doesn't
25-
// auto-close — the consumer sets `open={false}` after their operation.
26-
const ConfirmButton = loading !== undefined ? Button : Dialog.Action;
39+
const confirmClassName =
40+
intent === 'irreversible'
41+
? alertDialogStyles[ 'irreversible-action' ]
42+
: undefined;
43+
44+
const buttonsDisabled = phase !== 'idle' || undefined;
2745

2846
return (
29-
<Dialog.Popup ref={ ref }>
30-
<Dialog.Header>
31-
<Dialog.Title>{ title }</Dialog.Title>
32-
</Dialog.Header>
33-
{ children }
34-
<Dialog.Footer>
35-
<Dialog.Action
36-
variant="minimal"
37-
disabled={ loading || undefined }
38-
>
39-
{ cancelButtonText }
40-
</Dialog.Action>
41-
<ConfirmButton
42-
className={
43-
intent === 'irreversible'
44-
? styles[ 'irreversible-action' ]
45-
: undefined
46-
}
47-
onClick={ onConfirm }
48-
loading={ loading }
47+
<_AlertDialog.Portal>
48+
<_AlertDialog.Backdrop className={ dialogStyles.backdrop } />
49+
<ThemeProvider>
50+
<_AlertDialog.Popup
51+
ref={ ref }
52+
className={ clsx(
53+
dialogStyles.popup,
54+
className,
55+
dialogStyles[ 'is-medium' ]
56+
) }
57+
{ ...props }
4958
>
50-
{ confirmButtonText }
51-
</ConfirmButton>
52-
</Dialog.Footer>
53-
</Dialog.Popup>
59+
<Stack
60+
direction="column"
61+
gap="sm"
62+
className={ alertDialogStyles.header }
63+
>
64+
<Text
65+
variant="heading-xl"
66+
render={ <_AlertDialog.Title /> }
67+
className={ dialogStyles.title }
68+
>
69+
{ title }
70+
</Text>
71+
{ description && (
72+
<Text
73+
variant="body-md"
74+
render={ <_AlertDialog.Description /> }
75+
>
76+
{ description }
77+
</Text>
78+
) }
79+
</Stack>
80+
{ children }
81+
<Stack direction="column" gap="md">
82+
<div className={ dialogStyles.footer }>
83+
<_AlertDialog.Close
84+
render={ <Button variant="minimal" /> }
85+
disabled={ buttonsDisabled }
86+
>
87+
{ cancelButtonText }
88+
</_AlertDialog.Close>
89+
<Button
90+
className={ confirmClassName }
91+
onClick={ confirm }
92+
loading={ showSpinner || undefined }
93+
disabled={ buttonsDisabled }
94+
>
95+
{ confirmButtonText }
96+
</Button>
97+
</div>
98+
{ errorMessage && (
99+
<Text
100+
variant="body-sm"
101+
className={
102+
alertDialogStyles[ 'error-message' ]
103+
}
104+
>
105+
{ errorMessage }
106+
</Text>
107+
) }
108+
</Stack>
109+
</_AlertDialog.Popup>
110+
</ThemeProvider>
111+
</_AlertDialog.Portal>
54112
);
55113
}
56114
);

0 commit comments

Comments
 (0)