@wordpress/ui 0.10.0 → 0.11.1-next.v.202604091042.0
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/CHANGELOG.md +27 -0
- package/CONTRIBUTING.md +25 -0
- package/README.md +22 -2
- package/build/alert-dialog/context.cjs +6 -1
- package/build/alert-dialog/context.cjs.map +2 -2
- package/build/alert-dialog/popup.cjs +105 -33
- package/build/alert-dialog/popup.cjs.map +4 -4
- package/build/alert-dialog/root.cjs +106 -6
- package/build/alert-dialog/root.cjs.map +2 -2
- package/build/alert-dialog/trigger.cjs +4 -14
- package/build/alert-dialog/trigger.cjs.map +3 -3
- package/build/alert-dialog/types.cjs.map +1 -1
- package/build/button/button.cjs +16 -6
- package/build/button/button.cjs.map +3 -3
- package/build/card/content.cjs +3 -3
- package/build/card/content.cjs.map +1 -1
- package/build/card/full-bleed.cjs +3 -3
- package/build/card/full-bleed.cjs.map +1 -1
- package/build/card/header.cjs +3 -3
- package/build/card/header.cjs.map +1 -1
- package/build/card/root.cjs +3 -3
- package/build/card/root.cjs.map +1 -1
- package/build/card/title.cjs +3 -3
- package/build/card/title.cjs.map +1 -1
- package/build/collapsible-card/header.cjs +3 -3
- package/build/collapsible-card/header.cjs.map +2 -2
- package/build/empty-state/title.cjs.map +2 -2
- package/build/form/primitives/field/description.cjs +17 -4
- package/build/form/primitives/field/description.cjs.map +3 -3
- package/build/form/primitives/field/details.cjs +3 -3
- package/build/form/primitives/field/details.cjs.map +2 -2
- package/build/form/primitives/field/label.cjs +3 -3
- package/build/form/primitives/field/label.cjs.map +2 -2
- package/build/form/primitives/fieldset/description.cjs +20 -4
- package/build/form/primitives/fieldset/description.cjs.map +3 -3
- package/build/form/primitives/fieldset/details.cjs +3 -3
- package/build/form/primitives/fieldset/details.cjs.map +2 -2
- package/build/form/primitives/fieldset/legend.cjs +3 -3
- package/build/form/primitives/fieldset/legend.cjs.map +2 -2
- package/build/form/primitives/input/input.cjs +23 -7
- package/build/form/primitives/input/input.cjs.map +3 -3
- package/build/form/primitives/input-layout/input-layout.cjs +10 -0
- package/build/form/primitives/input-layout/input-layout.cjs.map +3 -3
- package/build/form/primitives/select/trigger.cjs +3 -3
- package/build/form/primitives/select/trigger.cjs.map +2 -2
- package/build/form/primitives/textarea/textarea.cjs +20 -1
- package/build/form/primitives/textarea/textarea.cjs.map +3 -3
- package/build/index.cjs +3 -0
- package/build/index.cjs.map +2 -2
- package/build/link/link.cjs +16 -6
- package/build/link/link.cjs.map +3 -3
- package/build/popover/arrow.cjs +94 -0
- package/build/popover/arrow.cjs.map +7 -0
- package/build/popover/close.cjs +45 -0
- package/build/popover/close.cjs.map +7 -0
- package/build/popover/context.cjs +76 -0
- package/build/popover/context.cjs.map +7 -0
- package/build/popover/description.cjs +70 -0
- package/build/popover/description.cjs.map +7 -0
- package/build/popover/index.cjs +49 -0
- package/build/popover/index.cjs.map +7 -0
- package/build/popover/popup.cjs +138 -0
- package/build/popover/popup.cjs.map +7 -0
- package/build/popover/root.cjs +35 -0
- package/build/popover/root.cjs.map +7 -0
- package/build/popover/title.cjs +56 -0
- package/build/popover/title.cjs.map +7 -0
- package/build/popover/trigger.cjs +38 -0
- package/build/popover/trigger.cjs.map +7 -0
- package/build/popover/types.cjs +19 -0
- package/build/popover/types.cjs.map +7 -0
- package/build/text/text.cjs +20 -5
- package/build/text/text.cjs.map +3 -3
- package/build/utils/use-deprioritized-initial-focus.cjs.map +2 -2
- package/build-module/alert-dialog/context.mjs +6 -1
- package/build-module/alert-dialog/context.mjs.map +2 -2
- package/build-module/alert-dialog/popup.mjs +107 -33
- package/build-module/alert-dialog/popup.mjs.map +4 -4
- package/build-module/alert-dialog/root.mjs +113 -7
- package/build-module/alert-dialog/root.mjs.map +2 -2
- package/build-module/alert-dialog/trigger.mjs +4 -4
- package/build-module/alert-dialog/trigger.mjs.map +3 -3
- package/build-module/button/button.mjs +16 -6
- package/build-module/button/button.mjs.map +3 -3
- package/build-module/card/content.mjs +3 -3
- package/build-module/card/content.mjs.map +1 -1
- package/build-module/card/full-bleed.mjs +3 -3
- package/build-module/card/full-bleed.mjs.map +1 -1
- package/build-module/card/header.mjs +3 -3
- package/build-module/card/header.mjs.map +1 -1
- package/build-module/card/root.mjs +3 -3
- package/build-module/card/root.mjs.map +1 -1
- package/build-module/card/title.mjs +3 -3
- package/build-module/card/title.mjs.map +1 -1
- package/build-module/collapsible-card/header.mjs +3 -3
- package/build-module/collapsible-card/header.mjs.map +2 -2
- package/build-module/empty-state/title.mjs.map +2 -2
- package/build-module/form/primitives/field/description.mjs +17 -4
- package/build-module/form/primitives/field/description.mjs.map +3 -3
- package/build-module/form/primitives/field/details.mjs +3 -3
- package/build-module/form/primitives/field/details.mjs.map +2 -2
- package/build-module/form/primitives/field/label.mjs +3 -3
- package/build-module/form/primitives/field/label.mjs.map +2 -2
- package/build-module/form/primitives/fieldset/description.mjs +20 -4
- package/build-module/form/primitives/fieldset/description.mjs.map +3 -3
- package/build-module/form/primitives/fieldset/details.mjs +3 -3
- package/build-module/form/primitives/fieldset/details.mjs.map +2 -2
- package/build-module/form/primitives/fieldset/legend.mjs +3 -3
- package/build-module/form/primitives/fieldset/legend.mjs.map +2 -2
- package/build-module/form/primitives/input/input.mjs +23 -7
- package/build-module/form/primitives/input/input.mjs.map +3 -3
- package/build-module/form/primitives/input-layout/input-layout.mjs +10 -0
- package/build-module/form/primitives/input-layout/input-layout.mjs.map +3 -3
- package/build-module/form/primitives/select/trigger.mjs +3 -3
- package/build-module/form/primitives/select/trigger.mjs.map +2 -2
- package/build-module/form/primitives/textarea/textarea.mjs +20 -1
- package/build-module/form/primitives/textarea/textarea.mjs.map +3 -3
- package/build-module/index.mjs +2 -0
- package/build-module/index.mjs.map +2 -2
- package/build-module/link/link.mjs +16 -6
- package/build-module/link/link.mjs.map +3 -3
- package/build-module/popover/arrow.mjs +59 -0
- package/build-module/popover/arrow.mjs.map +7 -0
- package/build-module/popover/close.mjs +20 -0
- package/build-module/popover/close.mjs.map +7 -0
- package/build-module/popover/context.mjs +57 -0
- package/build-module/popover/context.mjs.map +7 -0
- package/build-module/popover/description.mjs +35 -0
- package/build-module/popover/description.mjs.map +7 -0
- package/build-module/popover/index.mjs +18 -0
- package/build-module/popover/index.mjs.map +7 -0
- package/build-module/popover/popup.mjs +105 -0
- package/build-module/popover/popup.mjs.map +7 -0
- package/build-module/popover/root.mjs +10 -0
- package/build-module/popover/root.mjs.map +7 -0
- package/build-module/popover/title.mjs +31 -0
- package/build-module/popover/title.mjs.map +7 -0
- package/build-module/popover/trigger.mjs +13 -0
- package/build-module/popover/trigger.mjs.map +7 -0
- package/build-module/popover/types.mjs +1 -0
- package/build-module/popover/types.mjs.map +7 -0
- package/build-module/text/text.mjs +20 -5
- package/build-module/text/text.mjs.map +3 -3
- package/build-module/utils/use-deprioritized-initial-focus.mjs.map +2 -2
- package/build-types/alert-dialog/context.d.ts +6 -3
- package/build-types/alert-dialog/context.d.ts.map +1 -1
- package/build-types/alert-dialog/popup.d.ts.map +1 -1
- package/build-types/alert-dialog/root.d.ts +2 -8
- package/build-types/alert-dialog/root.d.ts.map +1 -1
- package/build-types/alert-dialog/stories/index.story.d.ts +18 -6
- package/build-types/alert-dialog/stories/index.story.d.ts.map +1 -1
- package/build-types/alert-dialog/trigger.d.ts +2 -1
- package/build-types/alert-dialog/trigger.d.ts.map +1 -1
- package/build-types/alert-dialog/types.d.ts +57 -26
- package/build-types/alert-dialog/types.d.ts.map +1 -1
- package/build-types/button/button.d.ts.map +1 -1
- package/build-types/card/stories/index.story.d.ts.map +1 -1
- package/build-types/empty-state/title.d.ts.map +1 -1
- package/build-types/form/primitives/field/description.d.ts.map +1 -1
- package/build-types/form/primitives/fieldset/description.d.ts.map +1 -1
- package/build-types/form/primitives/input/input.d.ts.map +1 -1
- package/build-types/form/primitives/input-layout/input-layout.d.ts.map +1 -1
- package/build-types/form/primitives/textarea/textarea.d.ts.map +1 -1
- package/build-types/form/stories/shared.d.ts.map +1 -1
- package/build-types/index.d.ts +1 -0
- package/build-types/index.d.ts.map +1 -1
- package/build-types/link/link.d.ts.map +1 -1
- package/build-types/popover/arrow.d.ts +10 -0
- package/build-types/popover/arrow.d.ts.map +1 -0
- package/build-types/popover/close.d.ts +11 -0
- package/build-types/popover/close.d.ts.map +1 -0
- package/build-types/popover/context.d.ts +22 -0
- package/build-types/popover/context.d.ts.map +1 -0
- package/build-types/popover/description.d.ts +10 -0
- package/build-types/popover/description.d.ts.map +1 -0
- package/build-types/popover/index.d.ts +9 -0
- package/build-types/popover/index.d.ts.map +1 -0
- package/build-types/popover/popup.d.ts +11 -0
- package/build-types/popover/popup.d.ts.map +1 -0
- package/build-types/popover/root.d.ts +37 -0
- package/build-types/popover/root.d.ts.map +1 -0
- package/build-types/popover/stories/index.story.d.ts +211 -0
- package/build-types/popover/stories/index.story.d.ts.map +1 -0
- package/build-types/popover/stories/utils.d.ts +25 -0
- package/build-types/popover/stories/utils.d.ts.map +1 -0
- package/build-types/popover/test/index.test.d.ts +2 -0
- package/build-types/popover/test/index.test.d.ts.map +1 -0
- package/build-types/popover/title.d.ts +20 -0
- package/build-types/popover/title.d.ts.map +1 -0
- package/build-types/popover/trigger.d.ts +10 -0
- package/build-types/popover/trigger.d.ts.map +1 -0
- package/build-types/popover/types.d.ts +83 -0
- package/build-types/popover/types.d.ts.map +1 -0
- package/build-types/text/stories/index.story.d.ts +4 -0
- package/build-types/text/stories/index.story.d.ts.map +1 -1
- package/build-types/text/text.d.ts.map +1 -1
- package/build-types/utils/use-deprioritized-initial-focus.d.ts +6 -5
- package/build-types/utils/use-deprioritized-initial-focus.d.ts.map +1 -1
- package/package.json +11 -11
- package/src/alert-dialog/context.tsx +12 -4
- package/src/alert-dialog/popup.tsx +91 -33
- package/src/alert-dialog/root.tsx +191 -13
- package/src/alert-dialog/stories/index.story.tsx +116 -65
- package/src/alert-dialog/style.module.css +11 -0
- package/src/alert-dialog/test/index.test.tsx +1265 -347
- package/src/alert-dialog/trigger.tsx +2 -2
- package/src/alert-dialog/types.ts +59 -28
- package/src/button/button.tsx +2 -0
- package/src/button/style.module.css +4 -0
- package/src/card/stories/index.story.tsx +0 -1
- package/src/card/style.module.css +1 -1
- package/src/card/test/index.test.tsx +0 -1
- package/src/empty-state/title.tsx +0 -1
- package/src/form/primitives/field/description.tsx +6 -1
- package/src/form/primitives/fieldset/description.tsx +9 -1
- package/src/form/primitives/input/input.tsx +6 -1
- package/src/form/primitives/input/style.module.css +4 -0
- package/src/form/primitives/input-layout/input-layout.tsx +2 -0
- package/src/form/primitives/textarea/textarea.tsx +10 -1
- package/src/form/stories/shared.tsx +4 -2
- package/src/index.ts +1 -0
- package/src/link/link.tsx +2 -0
- package/src/link/style.module.css +10 -0
- package/src/popover/arrow.tsx +49 -0
- package/src/popover/close.tsx +24 -0
- package/src/popover/context.tsx +100 -0
- package/src/popover/description.tsx +34 -0
- package/src/popover/index.ts +9 -0
- package/src/popover/popup.tsx +106 -0
- package/src/popover/root.tsx +41 -0
- package/src/popover/stories/index.story.tsx +1315 -0
- package/src/popover/stories/utils.tsx +91 -0
- package/src/popover/style.module.css +64 -0
- package/src/popover/test/index.test.tsx +727 -0
- package/src/popover/title.tsx +50 -0
- package/src/popover/trigger.tsx +17 -0
- package/src/popover/types.ts +113 -0
- package/src/text/stories/index.story.tsx +4 -2
- package/src/text/style.module.css +26 -0
- package/src/text/test/index.test.tsx +1 -4
- package/src/text/text.tsx +8 -1
- package/src/utils/css/field.module.css +4 -1
- package/src/utils/css/focus.module.css +7 -5
- package/src/utils/css/global-css-defense.module.css +117 -0
- package/src/utils/use-deprioritized-initial-focus.ts +5 -4
|
@@ -1,56 +1,114 @@
|
|
|
1
|
+
import { AlertDialog as _AlertDialog } from '@base-ui/react/alert-dialog';
|
|
2
|
+
import clsx from 'clsx';
|
|
1
3
|
import { forwardRef, useContext } from '@wordpress/element';
|
|
2
4
|
import { __ } from '@wordpress/i18n';
|
|
5
|
+
import {
|
|
6
|
+
type ThemeProvider as ThemeProviderType,
|
|
7
|
+
privateApis as themePrivateApis,
|
|
8
|
+
} from '@wordpress/theme';
|
|
9
|
+
|
|
3
10
|
import { Button } from '../button';
|
|
4
|
-
import
|
|
11
|
+
import dialogStyles from '../dialog/style.module.css';
|
|
12
|
+
import { unlock } from '../lock-unlock';
|
|
13
|
+
import { Stack } from '../stack';
|
|
14
|
+
import { Text } from '../text';
|
|
5
15
|
import { AlertDialogContext } from './context';
|
|
6
|
-
import
|
|
16
|
+
import alertDialogStyles from './style.module.css';
|
|
7
17
|
import type { PopupProps } from './types';
|
|
8
18
|
|
|
19
|
+
const ThemeProvider: typeof ThemeProviderType =
|
|
20
|
+
unlock( themePrivateApis ).ThemeProvider;
|
|
21
|
+
|
|
9
22
|
const Popup = forwardRef< HTMLDivElement, PopupProps >(
|
|
10
23
|
function AlertDialogPopup(
|
|
11
24
|
{
|
|
25
|
+
className,
|
|
26
|
+
intent = 'default',
|
|
12
27
|
title,
|
|
28
|
+
description,
|
|
13
29
|
children,
|
|
14
|
-
onConfirm,
|
|
15
30
|
confirmButtonText = __( 'OK' ),
|
|
16
31
|
cancelButtonText = __( 'Cancel' ),
|
|
17
|
-
|
|
32
|
+
...props
|
|
18
33
|
},
|
|
19
34
|
ref
|
|
20
35
|
) {
|
|
21
|
-
const {
|
|
36
|
+
const { phase, showSpinner, errorMessage, confirm } =
|
|
37
|
+
useContext( AlertDialogContext );
|
|
22
38
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
39
|
+
const confirmClassName =
|
|
40
|
+
intent === 'irreversible'
|
|
41
|
+
? alertDialogStyles[ 'irreversible-action' ]
|
|
42
|
+
: undefined;
|
|
43
|
+
|
|
44
|
+
const buttonsDisabled = phase !== 'idle' || undefined;
|
|
27
45
|
|
|
28
46
|
return (
|
|
29
|
-
<
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
{
|
|
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 }
|
|
49
58
|
>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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>
|
|
54
112
|
);
|
|
55
113
|
}
|
|
56
114
|
);
|
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
import { AlertDialog as _AlertDialog } from '@base-ui/react/alert-dialog';
|
|
2
|
-
import {
|
|
2
|
+
import { speak } from '@wordpress/a11y';
|
|
3
|
+
import {
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from '@wordpress/element';
|
|
10
|
+
|
|
3
11
|
import { AlertDialogContext } from './context';
|
|
12
|
+
import type { Phase } from './context';
|
|
4
13
|
import type { RootProps } from './types';
|
|
5
14
|
|
|
15
|
+
function isThenable( value: unknown ): value is PromiseLike< unknown > {
|
|
16
|
+
return (
|
|
17
|
+
value !== null &&
|
|
18
|
+
value !== undefined &&
|
|
19
|
+
typeof ( value as PromiseLike< unknown > ).then === 'function'
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
6
23
|
/**
|
|
7
24
|
* A dialog that requires a user response to proceed.
|
|
8
25
|
*
|
|
@@ -11,32 +28,193 @@ import type { RootProps } from './types';
|
|
|
11
28
|
* The `AlertDialog.Trigger` is optional — the dialog can also be controlled
|
|
12
29
|
* via `open` / `onOpenChange` props.
|
|
13
30
|
*
|
|
14
|
-
* ## Use cases
|
|
15
|
-
*
|
|
16
|
-
* - **Default intent**: Standard confirmation dialog for reversible actions.
|
|
17
|
-
* - **Irreversible intent**: Confirmation dialog for irreversible actions that
|
|
18
|
-
* cannot be undone. The confirm button uses error/danger coloring.
|
|
19
|
-
*
|
|
20
31
|
* For use cases outside the standard confirm/cancel pattern, use the lower-level
|
|
21
32
|
* `Dialog` component directly.
|
|
22
33
|
*
|
|
23
|
-
* See the [Destructive Actions guidelines](
|
|
34
|
+
* See the [Destructive Actions guidelines](https://wordpress.github.io/gutenberg/?path=/docs/design-system-patterns-destructive-actions--docs)
|
|
24
35
|
* for more details on when to use each pattern.
|
|
25
36
|
*/
|
|
26
37
|
function Root( {
|
|
27
|
-
intent = 'default',
|
|
28
38
|
children,
|
|
29
|
-
open,
|
|
39
|
+
open: openProp,
|
|
30
40
|
onOpenChange,
|
|
31
41
|
defaultOpen,
|
|
42
|
+
onConfirm,
|
|
32
43
|
}: RootProps ) {
|
|
33
|
-
const
|
|
44
|
+
const [ internalOpen, setInternalOpen ] = useState( defaultOpen ?? false );
|
|
45
|
+
|
|
46
|
+
// Internal state machine for the confirm-and-close lifecycle.
|
|
47
|
+
//
|
|
48
|
+
// Phase transitions:
|
|
49
|
+
//
|
|
50
|
+
// idle ──> pending ──> closing ──> idle
|
|
51
|
+
// (confirm (success, (animation
|
|
52
|
+
// clicked) close) complete)
|
|
53
|
+
//
|
|
54
|
+
// idle ──> pending ──> idle
|
|
55
|
+
// (confirm (error, or
|
|
56
|
+
// clicked) {close:false})
|
|
57
|
+
//
|
|
58
|
+
// idle ──> closing ──> idle
|
|
59
|
+
// (cancel/ (animation
|
|
60
|
+
// escape) complete)
|
|
61
|
+
//
|
|
62
|
+
// `showSpinner` tracks whether the confirm button shows a loading
|
|
63
|
+
// indicator. It is orthogonal to `phase`:
|
|
64
|
+
//
|
|
65
|
+
// Scenario | pending | closing
|
|
66
|
+
// --------------------------+---------+---------
|
|
67
|
+
// Sync onConfirm | false | false
|
|
68
|
+
// Async onConfirm (success) | true | true
|
|
69
|
+
// Async onConfirm (error) | true | n/a (-> idle)
|
|
70
|
+
// Cancel / Escape | n/a | false
|
|
71
|
+
//
|
|
72
|
+
// Buttons are disabled whenever phase !== 'idle'.
|
|
73
|
+
// Dismiss (Escape / Cancel) is blocked during 'pending'.
|
|
74
|
+
const [ phase, setPhase ] = useState< Phase >( 'idle' );
|
|
75
|
+
const [ showSpinner, setShowSpinner ] = useState( false );
|
|
76
|
+
const [ errorMessage, setErrorMessage ] = useState< string >();
|
|
77
|
+
|
|
78
|
+
const actionsRef = useRef< _AlertDialog.Root.Actions | null >( null );
|
|
79
|
+
|
|
80
|
+
const onConfirmRef = useRef( onConfirm );
|
|
81
|
+
onConfirmRef.current = onConfirm;
|
|
82
|
+
|
|
83
|
+
// Ref keeps phase accessible synchronously from callbacks that may
|
|
84
|
+
// run between a setState call and the subsequent React re-render.
|
|
85
|
+
const phaseRef = useRef( phase );
|
|
86
|
+
phaseRef.current = phase;
|
|
87
|
+
|
|
88
|
+
// Generation counter — safety net for the edge case where the component
|
|
89
|
+
// unmounts while an async confirm is in flight. Also incremented when
|
|
90
|
+
// the dialog finishes closing, so a stale promise settling after a
|
|
91
|
+
// dismiss+reopen cycle is silently discarded.
|
|
92
|
+
const confirmIdRef = useRef( 0 );
|
|
93
|
+
|
|
94
|
+
const effectiveOpen = openProp ?? internalOpen;
|
|
95
|
+
|
|
96
|
+
// Safety net: if the consumer keeps `open={true}` after a confirm
|
|
97
|
+
// (i.e. does not react to `onOpenChange`), the phase would be stuck
|
|
98
|
+
// at 'closing'. Detect the contradiction and reset to idle.
|
|
99
|
+
useEffect( () => {
|
|
100
|
+
if ( effectiveOpen && phase === 'closing' ) {
|
|
101
|
+
phaseRef.current = 'idle';
|
|
102
|
+
setPhase( 'idle' );
|
|
103
|
+
setShowSpinner( false );
|
|
104
|
+
}
|
|
105
|
+
}, [ effectiveOpen, phase ] );
|
|
106
|
+
|
|
107
|
+
const handleOpenChange = useCallback(
|
|
108
|
+
(
|
|
109
|
+
nextOpen: boolean,
|
|
110
|
+
eventDetails: _AlertDialog.Root.ChangeEventDetails
|
|
111
|
+
) => {
|
|
112
|
+
// Block dismiss while a confirm action is pending.
|
|
113
|
+
if ( ! nextOpen && phaseRef.current === 'pending' ) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if ( ! nextOpen && phaseRef.current === 'idle' ) {
|
|
118
|
+
phaseRef.current = 'closing';
|
|
119
|
+
setPhase( 'closing' );
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
setInternalOpen( nextOpen );
|
|
123
|
+
onOpenChange?.( nextOpen, eventDetails );
|
|
124
|
+
},
|
|
125
|
+
[ onOpenChange ]
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const confirm = useCallback( async () => {
|
|
129
|
+
if ( phaseRef.current !== 'idle' ) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
phaseRef.current = 'pending';
|
|
134
|
+
setPhase( 'pending' );
|
|
135
|
+
setErrorMessage( undefined );
|
|
136
|
+
|
|
137
|
+
const id = ++confirmIdRef.current;
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const rawResult = onConfirmRef.current?.();
|
|
141
|
+
|
|
142
|
+
// Show spinner only for async handlers (Promises).
|
|
143
|
+
// Sync handlers resolve in the same tick — no spinner needed.
|
|
144
|
+
if ( isThenable( rawResult ) ) {
|
|
145
|
+
setShowSpinner( true );
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const result = await Promise.resolve( rawResult );
|
|
149
|
+
|
|
150
|
+
// Discard if the component unmounted or the dialog was
|
|
151
|
+
// dismissed and reopened while the promise was in flight.
|
|
152
|
+
if ( confirmIdRef.current !== id ) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// An error message implies the dialog should stay open.
|
|
157
|
+
if ( result?.error ) {
|
|
158
|
+
phaseRef.current = 'idle';
|
|
159
|
+
setPhase( 'idle' );
|
|
160
|
+
setShowSpinner( false );
|
|
161
|
+
setErrorMessage( result.error );
|
|
162
|
+
speak( result.error, 'assertive' );
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const shouldClose = result?.close !== false;
|
|
167
|
+
|
|
168
|
+
if ( shouldClose ) {
|
|
169
|
+
phaseRef.current = 'closing';
|
|
170
|
+
setPhase( 'closing' );
|
|
171
|
+
actionsRef.current?.close();
|
|
172
|
+
} else {
|
|
173
|
+
phaseRef.current = 'idle';
|
|
174
|
+
setPhase( 'idle' );
|
|
175
|
+
setShowSpinner( false );
|
|
176
|
+
}
|
|
177
|
+
} catch ( error ) {
|
|
178
|
+
if ( confirmIdRef.current !== id ) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
phaseRef.current = 'idle';
|
|
182
|
+
setPhase( 'idle' );
|
|
183
|
+
setShowSpinner( false );
|
|
184
|
+
// eslint-disable-next-line no-console
|
|
185
|
+
console.error( error );
|
|
186
|
+
}
|
|
187
|
+
}, [] );
|
|
188
|
+
|
|
189
|
+
const handleOpenChangeComplete = useCallback( ( open: boolean ) => {
|
|
190
|
+
if ( ! open ) {
|
|
191
|
+
// Invalidate any in-flight async so a stale promise settling
|
|
192
|
+
// after dismiss+reopen doesn't close the new session.
|
|
193
|
+
confirmIdRef.current++;
|
|
194
|
+
phaseRef.current = 'idle';
|
|
195
|
+
setPhase( 'idle' );
|
|
196
|
+
setShowSpinner( false );
|
|
197
|
+
setErrorMessage( undefined );
|
|
198
|
+
}
|
|
199
|
+
}, [] );
|
|
200
|
+
|
|
201
|
+
const contextValue = useMemo(
|
|
202
|
+
() => ( {
|
|
203
|
+
phase,
|
|
204
|
+
showSpinner,
|
|
205
|
+
errorMessage,
|
|
206
|
+
confirm,
|
|
207
|
+
} ),
|
|
208
|
+
[ phase, showSpinner, errorMessage, confirm ]
|
|
209
|
+
);
|
|
34
210
|
|
|
35
211
|
return (
|
|
36
212
|
<_AlertDialog.Root
|
|
37
|
-
open={
|
|
38
|
-
onOpenChange={ onOpenChange }
|
|
213
|
+
open={ effectiveOpen }
|
|
39
214
|
defaultOpen={ defaultOpen }
|
|
215
|
+
onOpenChange={ handleOpenChange }
|
|
216
|
+
onOpenChangeComplete={ handleOpenChangeComplete }
|
|
217
|
+
actionsRef={ actionsRef }
|
|
40
218
|
>
|
|
41
219
|
<AlertDialogContext.Provider value={ contextValue }>
|
|
42
220
|
{ children }
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Menu } from '@base-ui/react/menu';
|
|
2
|
-
import { useState } from '@wordpress/element';
|
|
2
|
+
import { useId, useState } from '@wordpress/element';
|
|
3
3
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
4
4
|
import { action } from 'storybook/actions';
|
|
5
5
|
import { fn } from 'storybook/test';
|
|
6
6
|
|
|
7
|
-
import { AlertDialog } from '../..';
|
|
7
|
+
import { AlertDialog, Text } from '../..';
|
|
8
8
|
|
|
9
9
|
const meta: Meta< typeof AlertDialog.Root > = {
|
|
10
10
|
title: 'Design System/Components/AlertDialog',
|
|
@@ -14,6 +14,7 @@ const meta: Meta< typeof AlertDialog.Root > = {
|
|
|
14
14
|
'AlertDialog.Popup': AlertDialog.Popup,
|
|
15
15
|
},
|
|
16
16
|
argTypes: {
|
|
17
|
+
onConfirm: { action: fn() },
|
|
17
18
|
onOpenChange: { action: fn() },
|
|
18
19
|
},
|
|
19
20
|
};
|
|
@@ -33,10 +34,8 @@ export const Default: Story = {
|
|
|
33
34
|
<AlertDialog.Trigger>Move to trash</AlertDialog.Trigger>
|
|
34
35
|
<AlertDialog.Popup
|
|
35
36
|
title="Move to trash?"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
This post will be moved to trash. You can restore it later.
|
|
39
|
-
</AlertDialog.Popup>
|
|
37
|
+
description="This post will be moved to trash. You can restore it later."
|
|
38
|
+
/>
|
|
40
39
|
</>
|
|
41
40
|
),
|
|
42
41
|
},
|
|
@@ -48,38 +47,64 @@ export const Default: Story = {
|
|
|
48
47
|
*/
|
|
49
48
|
export const Irreversible: Story = {
|
|
50
49
|
args: {
|
|
51
|
-
intent: 'irreversible',
|
|
52
50
|
children: (
|
|
53
51
|
<>
|
|
54
52
|
<AlertDialog.Trigger>Delete permanently</AlertDialog.Trigger>
|
|
55
53
|
<AlertDialog.Popup
|
|
54
|
+
intent="irreversible"
|
|
56
55
|
title="Delete permanently?"
|
|
57
|
-
|
|
56
|
+
description="This action cannot be undone. All data will be lost."
|
|
58
57
|
confirmButtonText="Delete permanently"
|
|
59
|
-
|
|
60
|
-
This action cannot be undone. All data will be lost.
|
|
61
|
-
</AlertDialog.Popup>
|
|
58
|
+
/>
|
|
62
59
|
</>
|
|
63
60
|
),
|
|
64
61
|
},
|
|
65
62
|
};
|
|
66
63
|
|
|
67
64
|
/**
|
|
68
|
-
* Example with custom button
|
|
65
|
+
* Example with custom button labels for both confirm and cancel buttons.
|
|
69
66
|
*/
|
|
70
|
-
export const
|
|
67
|
+
export const CustomLabels: Story = {
|
|
71
68
|
args: {
|
|
72
69
|
children: (
|
|
73
70
|
<>
|
|
74
71
|
<AlertDialog.Trigger>Send feedback</AlertDialog.Trigger>
|
|
75
72
|
<AlertDialog.Popup
|
|
76
73
|
title="Send feedback?"
|
|
77
|
-
|
|
74
|
+
description="Your feedback helps us improve. Would you like to send it now?"
|
|
78
75
|
confirmButtonText="Send feedback"
|
|
79
76
|
cancelButtonText="Not now"
|
|
77
|
+
/>
|
|
78
|
+
</>
|
|
79
|
+
),
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Use `children` to render custom content between the description and the
|
|
85
|
+
* action buttons. The `description` should be self-contained for
|
|
86
|
+
* accessibility (`aria-describedby`); `children` adds supplementary detail.
|
|
87
|
+
*/
|
|
88
|
+
export const WithCustomContent: Story = {
|
|
89
|
+
args: {
|
|
90
|
+
children: (
|
|
91
|
+
<>
|
|
92
|
+
<AlertDialog.Trigger>Remove pages</AlertDialog.Trigger>
|
|
93
|
+
<AlertDialog.Popup
|
|
94
|
+
title="Remove 3 pages?"
|
|
95
|
+
description="These pages will be moved to trash."
|
|
96
|
+
confirmButtonText="Delete pages"
|
|
80
97
|
>
|
|
81
|
-
|
|
82
|
-
|
|
98
|
+
<ul
|
|
99
|
+
style={ {
|
|
100
|
+
margin: 0,
|
|
101
|
+
paddingInlineStart: 'var(--wpds-dimension-gap-lg)',
|
|
102
|
+
} }
|
|
103
|
+
>
|
|
104
|
+
<Text render={ <li /> }>About us</Text>
|
|
105
|
+
<Text render={ <li /> }>Contact</Text>
|
|
106
|
+
<Text render={ <li /> }>Privacy policy</Text>
|
|
107
|
+
</ul>
|
|
83
108
|
</AlertDialog.Popup>
|
|
84
109
|
</>
|
|
85
110
|
),
|
|
@@ -117,10 +142,7 @@ const menuItemStyles: React.CSSProperties = {
|
|
|
117
142
|
* component (not ready yet).
|
|
118
143
|
*/
|
|
119
144
|
export const MenuTrigger: Story = {
|
|
120
|
-
|
|
121
|
-
intent: 'irreversible',
|
|
122
|
-
},
|
|
123
|
-
render: ( args ) => {
|
|
145
|
+
render: () => {
|
|
124
146
|
const [ menuOpen, setMenuOpen ] = useState( false );
|
|
125
147
|
return (
|
|
126
148
|
<>
|
|
@@ -132,7 +154,12 @@ export const MenuTrigger: Story = {
|
|
|
132
154
|
<Menu.Item style={ menuItemStyles }>
|
|
133
155
|
Edit
|
|
134
156
|
</Menu.Item>
|
|
135
|
-
<AlertDialog.Root
|
|
157
|
+
<AlertDialog.Root
|
|
158
|
+
onConfirm={ () => {
|
|
159
|
+
setMenuOpen( false );
|
|
160
|
+
action( 'onConfirm' )();
|
|
161
|
+
} }
|
|
162
|
+
>
|
|
136
163
|
<Menu.Item
|
|
137
164
|
render={
|
|
138
165
|
<AlertDialog.Trigger
|
|
@@ -147,16 +174,11 @@ export const MenuTrigger: Story = {
|
|
|
147
174
|
>
|
|
148
175
|
Delete...
|
|
149
176
|
<AlertDialog.Popup
|
|
177
|
+
intent="irreversible"
|
|
150
178
|
title="Delete permanently?"
|
|
151
|
-
|
|
152
|
-
setMenuOpen( false );
|
|
153
|
-
action( 'onConfirm' )();
|
|
154
|
-
} }
|
|
179
|
+
description="This action cannot be undone. All data will be lost."
|
|
155
180
|
confirmButtonText="Delete permanently"
|
|
156
|
-
|
|
157
|
-
This action cannot be undone. All
|
|
158
|
-
data will be lost.
|
|
159
|
-
</AlertDialog.Popup>
|
|
181
|
+
/>
|
|
160
182
|
</Menu.Item>
|
|
161
183
|
</AlertDialog.Root>
|
|
162
184
|
</Menu.Popup>
|
|
@@ -168,56 +190,88 @@ export const MenuTrigger: Story = {
|
|
|
168
190
|
},
|
|
169
191
|
};
|
|
170
192
|
|
|
193
|
+
function sleep( ms: number ) {
|
|
194
|
+
return new Promise< void >( ( resolve ) => setTimeout( resolve, ms ) );
|
|
195
|
+
}
|
|
196
|
+
|
|
171
197
|
/**
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
198
|
+
* Async confirm flow. The consumer returns a promise from `onConfirm`.
|
|
199
|
+
* The dialog automatically manages the pending state: buttons are disabled
|
|
200
|
+
* and a spinner appears on the confirm button. Toggle between success and
|
|
201
|
+
* failure to test both outcomes.
|
|
202
|
+
*
|
|
203
|
+
* On failure, the consumer catches the error and returns
|
|
204
|
+
* `{ close: false, error: '...' }`. The component displays the message
|
|
205
|
+
* below the action buttons and announces it to screen readers. The error
|
|
206
|
+
* is automatically cleared on the next confirm attempt or when the dialog
|
|
207
|
+
* reopens.
|
|
176
208
|
*/
|
|
177
209
|
export const AsyncConfirm: Story = {
|
|
178
210
|
render: function AsyncConfirm( args ) {
|
|
179
|
-
const [
|
|
180
|
-
const
|
|
211
|
+
const [ shouldFail, setShouldFail ] = useState( false );
|
|
212
|
+
const successId = useId();
|
|
213
|
+
const failureId = useId();
|
|
181
214
|
|
|
182
215
|
return (
|
|
183
216
|
<>
|
|
184
|
-
<
|
|
185
|
-
|
|
186
|
-
|
|
217
|
+
<fieldset>
|
|
218
|
+
<legend>Async task outcome</legend>
|
|
219
|
+
<label htmlFor={ successId }>
|
|
220
|
+
<input
|
|
221
|
+
id={ successId }
|
|
222
|
+
type="radio"
|
|
223
|
+
name="async-outcome"
|
|
224
|
+
checked={ ! shouldFail }
|
|
225
|
+
onChange={ () => setShouldFail( false ) }
|
|
226
|
+
/>
|
|
227
|
+
Success (closes dialog)
|
|
228
|
+
</label>
|
|
229
|
+
<label
|
|
230
|
+
htmlFor={ failureId }
|
|
231
|
+
style={ { marginInlineStart: 12 } }
|
|
232
|
+
>
|
|
233
|
+
<input
|
|
234
|
+
id={ failureId }
|
|
235
|
+
type="radio"
|
|
236
|
+
name="async-outcome"
|
|
237
|
+
checked={ shouldFail }
|
|
238
|
+
onChange={ () => setShouldFail( true ) }
|
|
239
|
+
/>
|
|
240
|
+
Failure (dialog stays open, shows error)
|
|
241
|
+
</label>
|
|
242
|
+
</fieldset>
|
|
243
|
+
<br />
|
|
187
244
|
<AlertDialog.Root
|
|
188
245
|
{ ...args }
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
246
|
+
onConfirm={ async () => {
|
|
247
|
+
action( 'onConfirm' )();
|
|
248
|
+
try {
|
|
249
|
+
await sleep( 2000 );
|
|
250
|
+
if ( shouldFail ) {
|
|
251
|
+
throw new Error( 'Task failed' );
|
|
252
|
+
}
|
|
253
|
+
} catch {
|
|
254
|
+
return {
|
|
255
|
+
close: false,
|
|
256
|
+
error: 'Something went wrong. Please try again.',
|
|
257
|
+
};
|
|
193
258
|
}
|
|
194
|
-
|
|
259
|
+
return undefined;
|
|
195
260
|
} }
|
|
196
261
|
>
|
|
262
|
+
<AlertDialog.Trigger>
|
|
263
|
+
Delete permanently
|
|
264
|
+
</AlertDialog.Trigger>
|
|
197
265
|
<AlertDialog.Popup
|
|
266
|
+
intent="irreversible"
|
|
198
267
|
title="Delete permanently?"
|
|
199
|
-
|
|
200
|
-
onConfirm={ () => {
|
|
201
|
-
action( 'onConfirm' )();
|
|
202
|
-
setIsLoading( true );
|
|
203
|
-
new Promise< void >( ( resolve ) =>
|
|
204
|
-
setTimeout( resolve, 2000 )
|
|
205
|
-
).then( () => {
|
|
206
|
-
setIsLoading( false );
|
|
207
|
-
setIsOpen( false );
|
|
208
|
-
} );
|
|
209
|
-
} }
|
|
268
|
+
description="This action cannot be undone. All data will be lost."
|
|
210
269
|
confirmButtonText="Delete permanently"
|
|
211
|
-
|
|
212
|
-
This action cannot be undone. All data will be lost.
|
|
213
|
-
</AlertDialog.Popup>
|
|
270
|
+
/>
|
|
214
271
|
</AlertDialog.Root>
|
|
215
272
|
</>
|
|
216
273
|
);
|
|
217
274
|
},
|
|
218
|
-
args: {
|
|
219
|
-
intent: 'irreversible',
|
|
220
|
-
},
|
|
221
275
|
};
|
|
222
276
|
|
|
223
277
|
/**
|
|
@@ -242,11 +296,8 @@ export const Controlled: Story = {
|
|
|
242
296
|
>
|
|
243
297
|
<AlertDialog.Popup
|
|
244
298
|
title="Move to trash?"
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
This post will be moved to trash. You can restore it
|
|
248
|
-
later.
|
|
249
|
-
</AlertDialog.Popup>
|
|
299
|
+
description="This post will be moved to trash. You can restore it later."
|
|
300
|
+
/>
|
|
250
301
|
</AlertDialog.Root>
|
|
251
302
|
</>
|
|
252
303
|
);
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
@layer wp-ui-utilities, wp-ui-components, wp-ui-compositions, wp-ui-overrides;
|
|
2
2
|
|
|
3
|
+
@layer wp-ui-components {
|
|
4
|
+
.header {
|
|
5
|
+
margin-bottom: var(--wpds-dimension-gap-lg);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.error-message {
|
|
9
|
+
align-self: flex-end;
|
|
10
|
+
color: var(--wpds-color-fg-content-error);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
3
14
|
@layer wp-ui-compositions {
|
|
4
15
|
.irreversible-action {
|
|
5
16
|
--wp-ui-button-background-color: var(--wpds-color-bg-interactive-error-strong);
|