@oxyhq/bloom 0.31.0 → 0.32.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/lib/commonjs/alert-dialog/AlertDialog.js +69 -21
- package/lib/commonjs/alert-dialog/AlertDialog.js.map +1 -1
- package/lib/module/alert-dialog/AlertDialog.js +70 -22
- package/lib/module/alert-dialog/AlertDialog.js.map +1 -1
- package/lib/typescript/commonjs/alert-dialog/AlertDialog.d.ts +13 -0
- package/lib/typescript/commonjs/alert-dialog/AlertDialog.d.ts.map +1 -1
- package/lib/typescript/commonjs/alert-dialog/types.d.ts +8 -2
- package/lib/typescript/commonjs/alert-dialog/types.d.ts.map +1 -1
- package/lib/typescript/module/alert-dialog/AlertDialog.d.ts +13 -0
- package/lib/typescript/module/alert-dialog/AlertDialog.d.ts.map +1 -1
- package/lib/typescript/module/alert-dialog/types.d.ts +8 -2
- package/lib/typescript/module/alert-dialog/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/AlertDialog.test.tsx +72 -0
- package/src/alert-dialog/AlertDialog.tsx +68 -21
- package/src/alert-dialog/types.ts +8 -2
|
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.createAlertDialog = createAlertDialog;
|
|
7
7
|
var _react = _interopRequireWildcard(require("react"));
|
|
8
|
+
var _context = require("../dialog/context.js");
|
|
8
9
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
9
10
|
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
10
11
|
/**
|
|
@@ -24,6 +25,19 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
|
|
|
24
25
|
* imperative `confirm()` helper + `<AlertDialogHost />` to trigger a confirm
|
|
25
26
|
* from an event handler without owning visible state.
|
|
26
27
|
*
|
|
28
|
+
* Control mode — IMPERATIVE, not controlled. `AlertDialog` keeps its public
|
|
29
|
+
* *controlled* API (`visible` boolean + `onClose`) but internally bridges it
|
|
30
|
+
* onto the Dialog's imperative `useDialogControl()` handle, so `confirm()`
|
|
31
|
+
* drives `<Dialog>` through the EXACT uncontrolled/imperative internals
|
|
32
|
+
* Mention's `ConfirmPrompt` uses. That is the code path that actually plays the
|
|
33
|
+
* exit animation on dismiss: imperative `close()` sets `isClosing`, runs the
|
|
34
|
+
* exit animation, and fires `onClose` only once it settles. The controlled path
|
|
35
|
+
* instead fires `onClose` synchronously — and because `<AlertDialogHost />`
|
|
36
|
+
* resolves + drops the queue entry inside `onClose`, the surface would then
|
|
37
|
+
* unmount instantly, skipping the exit animation entirely. Bridging to
|
|
38
|
+
* imperative mode eliminates that fork so `confirm()` and Mention's
|
|
39
|
+
* `ConfirmPrompt` are pixel/timing identical.
|
|
40
|
+
*
|
|
27
41
|
* The centered card uses the Dialog default `maxWidth` (480px) for full visual
|
|
28
42
|
* parity with the shared confirm surface.
|
|
29
43
|
*/
|
|
@@ -39,31 +53,62 @@ function createAlertDialog(Dialog) {
|
|
|
39
53
|
onCancel,
|
|
40
54
|
destructive = false,
|
|
41
55
|
hideCancel = false,
|
|
42
|
-
dismissible =
|
|
56
|
+
dismissible = true,
|
|
43
57
|
cardStyle,
|
|
44
58
|
testID
|
|
45
59
|
}) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
// `
|
|
51
|
-
// `
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
const control = (0, _context.useDialogControl)();
|
|
61
|
+
|
|
62
|
+
// Bridge the public *controlled* `visible` prop onto the Dialog's
|
|
63
|
+
// imperative open/close. Opening on mount straight from an effect (no
|
|
64
|
+
// `setTimeout`) mirrors bloom's own `AutoMountedDialog` (which backs
|
|
65
|
+
// `alert()`) — the canonical fresh-mount imperative-open pattern:
|
|
66
|
+
// `<AlertDialogHost />` mounts a FRESH `AlertDialog` per confirm
|
|
67
|
+
// (`key={id}`) with the final content already committed, so there is no
|
|
68
|
+
// stale-content window to defer past and the entry animation curve stays
|
|
69
|
+
// identical to Mention's. `control` is referentially stable (memoised on
|
|
70
|
+
// its id), so this effect only re-runs when `visible` actually flips. When
|
|
71
|
+
// a direct consumer flips `visible` to `false`, we imperatively `close()`
|
|
72
|
+
// so the exit animation still plays.
|
|
73
|
+
const closingFromPropRef = (0, _react.useRef)(false);
|
|
74
|
+
(0, _react.useEffect)(() => {
|
|
75
|
+
if (visible) {
|
|
76
|
+
control.open();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
closingFromPropRef.current = true;
|
|
80
|
+
control.close();
|
|
81
|
+
}, [visible, control]);
|
|
82
|
+
|
|
83
|
+
// The Dialog fires `onClose` after the exit animation settles (imperative
|
|
84
|
+
// mode). Forward ONLY user-initiated dismissals (backdrop / Escape / cancel)
|
|
85
|
+
// to the consumer — a consumer-initiated close (they flipped `visible` to
|
|
86
|
+
// `false`) must not re-enter `onClose`, preserving the previous controlled
|
|
87
|
+
// semantics where a programmatic close does not fire `onClose`.
|
|
88
|
+
const handleClose = (0, _react.useCallback)(() => {
|
|
89
|
+
if (closingFromPropRef.current) {
|
|
90
|
+
closingFromPropRef.current = false;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
55
93
|
onClose();
|
|
56
|
-
}, [
|
|
94
|
+
}, [onClose]);
|
|
95
|
+
|
|
96
|
+
// Confirm/cancel map onto the Dialog's declarative action row. Both keep the
|
|
97
|
+
// default `shouldCloseOnPress` (true): the button runs the exit animation
|
|
98
|
+
// FIRST, then invokes `onPress` as a close-completion callback — so
|
|
99
|
+
// `onConfirm` resolves the `confirm()` promise as the surface animates out,
|
|
100
|
+
// exactly like Mention. `onConfirm` (resolve `true`, drains the entry) runs
|
|
101
|
+
// just BEFORE the Dialog's own post-close `onClose` (→ `handleClose` →
|
|
102
|
+
// resolve `false`, now a no-op for the drained entry), which is what makes
|
|
103
|
+
// the confirm button win the double-resolution guard.
|
|
57
104
|
const actions = [{
|
|
58
105
|
label: confirmLabel,
|
|
59
|
-
onPress:
|
|
60
|
-
color: destructive ? 'destructive' : 'default'
|
|
61
|
-
shouldCloseOnPress: false
|
|
106
|
+
onPress: onConfirm,
|
|
107
|
+
color: destructive ? 'destructive' : 'default'
|
|
62
108
|
}];
|
|
63
109
|
if (!hideCancel) {
|
|
64
|
-
// The `cancel` color auto-dismisses via the Dialog's `close()
|
|
65
|
-
//
|
|
66
|
-
// runs after the surface finishes closing.
|
|
110
|
+
// The `cancel` color auto-dismisses via the Dialog's `close()` (exit
|
|
111
|
+
// animation), then runs `onCancel` as a close-completion callback.
|
|
67
112
|
actions.push({
|
|
68
113
|
label: cancelLabel,
|
|
69
114
|
onPress: onCancel,
|
|
@@ -71,11 +116,14 @@ function createAlertDialog(Dialog) {
|
|
|
71
116
|
});
|
|
72
117
|
}
|
|
73
118
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(Dialog, {
|
|
74
|
-
|
|
75
|
-
onClose:
|
|
119
|
+
control: control,
|
|
120
|
+
onClose: handleClose,
|
|
76
121
|
placement: "center"
|
|
77
|
-
// Alert dialogs
|
|
78
|
-
//
|
|
122
|
+
// Alert dialogs dismiss on backdrop / Escape by default (matching the
|
|
123
|
+
// shared Dialog default and Mention's `ConfirmPrompt`). A backdrop /
|
|
124
|
+
// Escape dismissal can only ever resolve the confirm as cancelled
|
|
125
|
+
// (`false`), never the destructive action — callers that need a truly
|
|
126
|
+
// blocking confirm opt out with `dismissible={false}`.
|
|
79
127
|
,
|
|
80
128
|
dismissOnBackdrop: dismissible,
|
|
81
129
|
title: title,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["_react","_interopRequireWildcard","require","_jsxRuntime","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","default","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","createAlertDialog","Dialog","AlertDialogComponent","AlertDialog","visible","onClose","title","description","confirmLabel","cancelLabel","onConfirm","onCancel","destructive","hideCancel","dismissible","cardStyle","testID","
|
|
1
|
+
{"version":3,"names":["_react","_interopRequireWildcard","require","_context","_jsxRuntime","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","default","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","createAlertDialog","Dialog","AlertDialogComponent","AlertDialog","visible","onClose","title","description","confirmLabel","cancelLabel","onConfirm","onCancel","destructive","hideCancel","dismissible","cardStyle","testID","control","useDialogControl","closingFromPropRef","useRef","useEffect","open","current","close","handleClose","useCallback","actions","label","onPress","color","push","jsx","placement","dismissOnBackdrop","style","memo","displayName"],"sourceRoot":"../../../src","sources":["alert-dialog/AlertDialog.tsx"],"mappings":"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","ignoreList":[]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import React, { memo, useCallback } from 'react';
|
|
3
|
+
import React, { memo, useCallback, useEffect, useRef } from 'react';
|
|
4
|
+
import { useDialogControl } from "../dialog/context.js";
|
|
4
5
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
5
6
|
/**
|
|
6
7
|
* Build the `AlertDialog` component bound to a platform `Dialog`.
|
|
@@ -19,6 +20,19 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
19
20
|
* imperative `confirm()` helper + `<AlertDialogHost />` to trigger a confirm
|
|
20
21
|
* from an event handler without owning visible state.
|
|
21
22
|
*
|
|
23
|
+
* Control mode — IMPERATIVE, not controlled. `AlertDialog` keeps its public
|
|
24
|
+
* *controlled* API (`visible` boolean + `onClose`) but internally bridges it
|
|
25
|
+
* onto the Dialog's imperative `useDialogControl()` handle, so `confirm()`
|
|
26
|
+
* drives `<Dialog>` through the EXACT uncontrolled/imperative internals
|
|
27
|
+
* Mention's `ConfirmPrompt` uses. That is the code path that actually plays the
|
|
28
|
+
* exit animation on dismiss: imperative `close()` sets `isClosing`, runs the
|
|
29
|
+
* exit animation, and fires `onClose` only once it settles. The controlled path
|
|
30
|
+
* instead fires `onClose` synchronously — and because `<AlertDialogHost />`
|
|
31
|
+
* resolves + drops the queue entry inside `onClose`, the surface would then
|
|
32
|
+
* unmount instantly, skipping the exit animation entirely. Bridging to
|
|
33
|
+
* imperative mode eliminates that fork so `confirm()` and Mention's
|
|
34
|
+
* `ConfirmPrompt` are pixel/timing identical.
|
|
35
|
+
*
|
|
22
36
|
* The centered card uses the Dialog default `maxWidth` (480px) for full visual
|
|
23
37
|
* parity with the shared confirm surface.
|
|
24
38
|
*/
|
|
@@ -34,31 +48,62 @@ export function createAlertDialog(Dialog) {
|
|
|
34
48
|
onCancel,
|
|
35
49
|
destructive = false,
|
|
36
50
|
hideCancel = false,
|
|
37
|
-
dismissible =
|
|
51
|
+
dismissible = true,
|
|
38
52
|
cardStyle,
|
|
39
53
|
testID
|
|
40
54
|
}) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
// `
|
|
46
|
-
// `
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
const control = useDialogControl();
|
|
56
|
+
|
|
57
|
+
// Bridge the public *controlled* `visible` prop onto the Dialog's
|
|
58
|
+
// imperative open/close. Opening on mount straight from an effect (no
|
|
59
|
+
// `setTimeout`) mirrors bloom's own `AutoMountedDialog` (which backs
|
|
60
|
+
// `alert()`) — the canonical fresh-mount imperative-open pattern:
|
|
61
|
+
// `<AlertDialogHost />` mounts a FRESH `AlertDialog` per confirm
|
|
62
|
+
// (`key={id}`) with the final content already committed, so there is no
|
|
63
|
+
// stale-content window to defer past and the entry animation curve stays
|
|
64
|
+
// identical to Mention's. `control` is referentially stable (memoised on
|
|
65
|
+
// its id), so this effect only re-runs when `visible` actually flips. When
|
|
66
|
+
// a direct consumer flips `visible` to `false`, we imperatively `close()`
|
|
67
|
+
// so the exit animation still plays.
|
|
68
|
+
const closingFromPropRef = useRef(false);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (visible) {
|
|
71
|
+
control.open();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
closingFromPropRef.current = true;
|
|
75
|
+
control.close();
|
|
76
|
+
}, [visible, control]);
|
|
77
|
+
|
|
78
|
+
// The Dialog fires `onClose` after the exit animation settles (imperative
|
|
79
|
+
// mode). Forward ONLY user-initiated dismissals (backdrop / Escape / cancel)
|
|
80
|
+
// to the consumer — a consumer-initiated close (they flipped `visible` to
|
|
81
|
+
// `false`) must not re-enter `onClose`, preserving the previous controlled
|
|
82
|
+
// semantics where a programmatic close does not fire `onClose`.
|
|
83
|
+
const handleClose = useCallback(() => {
|
|
84
|
+
if (closingFromPropRef.current) {
|
|
85
|
+
closingFromPropRef.current = false;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
50
88
|
onClose();
|
|
51
|
-
}, [
|
|
89
|
+
}, [onClose]);
|
|
90
|
+
|
|
91
|
+
// Confirm/cancel map onto the Dialog's declarative action row. Both keep the
|
|
92
|
+
// default `shouldCloseOnPress` (true): the button runs the exit animation
|
|
93
|
+
// FIRST, then invokes `onPress` as a close-completion callback — so
|
|
94
|
+
// `onConfirm` resolves the `confirm()` promise as the surface animates out,
|
|
95
|
+
// exactly like Mention. `onConfirm` (resolve `true`, drains the entry) runs
|
|
96
|
+
// just BEFORE the Dialog's own post-close `onClose` (→ `handleClose` →
|
|
97
|
+
// resolve `false`, now a no-op for the drained entry), which is what makes
|
|
98
|
+
// the confirm button win the double-resolution guard.
|
|
52
99
|
const actions = [{
|
|
53
100
|
label: confirmLabel,
|
|
54
|
-
onPress:
|
|
55
|
-
color: destructive ? 'destructive' : 'default'
|
|
56
|
-
shouldCloseOnPress: false
|
|
101
|
+
onPress: onConfirm,
|
|
102
|
+
color: destructive ? 'destructive' : 'default'
|
|
57
103
|
}];
|
|
58
104
|
if (!hideCancel) {
|
|
59
|
-
// The `cancel` color auto-dismisses via the Dialog's `close()
|
|
60
|
-
//
|
|
61
|
-
// runs after the surface finishes closing.
|
|
105
|
+
// The `cancel` color auto-dismisses via the Dialog's `close()` (exit
|
|
106
|
+
// animation), then runs `onCancel` as a close-completion callback.
|
|
62
107
|
actions.push({
|
|
63
108
|
label: cancelLabel,
|
|
64
109
|
onPress: onCancel,
|
|
@@ -66,11 +111,14 @@ export function createAlertDialog(Dialog) {
|
|
|
66
111
|
});
|
|
67
112
|
}
|
|
68
113
|
return /*#__PURE__*/_jsx(Dialog, {
|
|
69
|
-
|
|
70
|
-
onClose:
|
|
114
|
+
control: control,
|
|
115
|
+
onClose: handleClose,
|
|
71
116
|
placement: "center"
|
|
72
|
-
// Alert dialogs
|
|
73
|
-
//
|
|
117
|
+
// Alert dialogs dismiss on backdrop / Escape by default (matching the
|
|
118
|
+
// shared Dialog default and Mention's `ConfirmPrompt`). A backdrop /
|
|
119
|
+
// Escape dismissal can only ever resolve the confirm as cancelled
|
|
120
|
+
// (`false`), never the destructive action — callers that need a truly
|
|
121
|
+
// blocking confirm opt out with `dismissible={false}`.
|
|
74
122
|
,
|
|
75
123
|
dismissOnBackdrop: dismissible,
|
|
76
124
|
title: title,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["React","memo","useCallback","jsx","_jsx","createAlertDialog","Dialog","AlertDialogComponent","AlertDialog","visible","onClose","title","description","confirmLabel","cancelLabel","onConfirm","onCancel","destructive","hideCancel","dismissible","cardStyle","testID","
|
|
1
|
+
{"version":3,"names":["React","memo","useCallback","useEffect","useRef","useDialogControl","jsx","_jsx","createAlertDialog","Dialog","AlertDialogComponent","AlertDialog","visible","onClose","title","description","confirmLabel","cancelLabel","onConfirm","onCancel","destructive","hideCancel","dismissible","cardStyle","testID","control","closingFromPropRef","open","current","close","handleClose","actions","label","onPress","color","push","placement","dismissOnBackdrop","style","displayName"],"sourceRoot":"../../../src","sources":["alert-dialog/AlertDialog.tsx"],"mappings":";;AAAA,OAAOA,KAAK,IAAIC,IAAI,EAAEC,WAAW,EAAEC,SAAS,EAAEC,MAAM,QAAQ,OAAO;AAGnE,SAASC,gBAAgB,QAAQ,sBAAmB;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAKrD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,iBAAiBA,CAACC,MAAuB,EAAE;EACzD,MAAMC,oBAAoB,GAAG,SAASC,WAAWA,CAAC;IAChDC,OAAO;IACPC,OAAO;IACPC,KAAK;IACLC,WAAW;IACXC,YAAY,GAAG,SAAS;IACxBC,WAAW,GAAG,QAAQ;IACtBC,SAAS;IACTC,QAAQ;IACRC,WAAW,GAAG,KAAK;IACnBC,UAAU,GAAG,KAAK;IAClBC,WAAW,GAAG,IAAI;IAClBC,SAAS;IACTC;EACgB,CAAC,EAAE;IACnB,MAAMC,OAAO,GAAGpB,gBAAgB,CAAC,CAAC;;IAElC;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA,MAAMqB,kBAAkB,GAAGtB,MAAM,CAAC,KAAK,CAAC;IACxCD,SAAS,CAAC,MAAM;MACd,IAAIS,OAAO,EAAE;QACXa,OAAO,CAACE,IAAI,CAAC,CAAC;QACd;MACF;MACAD,kBAAkB,CAACE,OAAO,GAAG,IAAI;MACjCH,OAAO,CAACI,KAAK,CAAC,CAAC;IACjB,CAAC,EAAE,CAACjB,OAAO,EAAEa,OAAO,CAAC,CAAC;;IAEtB;IACA;IACA;IACA;IACA;IACA,MAAMK,WAAW,GAAG5B,WAAW,CAAC,MAAM;MACpC,IAAIwB,kBAAkB,CAACE,OAAO,EAAE;QAC9BF,kBAAkB,CAACE,OAAO,GAAG,KAAK;QAClC;MACF;MACAf,OAAO,CAAC,CAAC;IACX,CAAC,EAAE,CAACA,OAAO,CAAC,CAAC;;IAEb;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA,MAAMkB,OAAuB,GAAG,CAC9B;MACEC,KAAK,EAAEhB,YAAY;MACnBiB,OAAO,EAAEf,SAAS;MAClBgB,KAAK,EAAEd,WAAW,GAAG,aAAa,GAAG;IACvC,CAAC,CACF;IACD,IAAI,CAACC,UAAU,EAAE;MACf;MACA;MACAU,OAAO,CAACI,IAAI,CAAC;QACXH,KAAK,EAAEf,WAAW;QAClBgB,OAAO,EAAEd,QAAQ;QACjBe,KAAK,EAAE;MACT,CAAC,CAAC;IACJ;IAEA,oBACE3B,IAAA,CAACE,MAAM;MACLgB,OAAO,EAAEA,OAAQ;MACjBZ,OAAO,EAAEiB,WAAY;MACrBM,SAAS,EAAC;MACV;MACA;MACA;MACA;MACA;MAAA;MACAC,iBAAiB,EAAEf,WAAY;MAC/BR,KAAK,EAAEA,KAAM;MACbC,WAAW,EAAEA,WAAY;MACzBgB,OAAO,EAAEA,OAAQ;MACjBC,KAAK,EAAElB,KAAM;MACbwB,KAAK,EAAEf,SAAU;MACjBC,MAAM,EAAEA;IAAO,CAChB,CAAC;EAEN,CAAC;EAED,MAAMb,WAAW,gBAAGV,IAAI,CAACS,oBAAoB,CAAC;EAC9CC,WAAW,CAAC4B,WAAW,GAAG,aAAa;EACvC,OAAO5B,WAAW;AACpB","ignoreList":[]}
|
|
@@ -19,6 +19,19 @@ type DialogComponent = React.ComponentType<DialogProps>;
|
|
|
19
19
|
* imperative `confirm()` helper + `<AlertDialogHost />` to trigger a confirm
|
|
20
20
|
* from an event handler without owning visible state.
|
|
21
21
|
*
|
|
22
|
+
* Control mode — IMPERATIVE, not controlled. `AlertDialog` keeps its public
|
|
23
|
+
* *controlled* API (`visible` boolean + `onClose`) but internally bridges it
|
|
24
|
+
* onto the Dialog's imperative `useDialogControl()` handle, so `confirm()`
|
|
25
|
+
* drives `<Dialog>` through the EXACT uncontrolled/imperative internals
|
|
26
|
+
* Mention's `ConfirmPrompt` uses. That is the code path that actually plays the
|
|
27
|
+
* exit animation on dismiss: imperative `close()` sets `isClosing`, runs the
|
|
28
|
+
* exit animation, and fires `onClose` only once it settles. The controlled path
|
|
29
|
+
* instead fires `onClose` synchronously — and because `<AlertDialogHost />`
|
|
30
|
+
* resolves + drops the queue entry inside `onClose`, the surface would then
|
|
31
|
+
* unmount instantly, skipping the exit animation entirely. Bridging to
|
|
32
|
+
* imperative mode eliminates that fork so `confirm()` and Mention's
|
|
33
|
+
* `ConfirmPrompt` are pixel/timing identical.
|
|
34
|
+
*
|
|
22
35
|
* The centered card uses the Dialog default `maxWidth` (480px) for full visual
|
|
23
36
|
* parity with the shared confirm surface.
|
|
24
37
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AlertDialog.d.ts","sourceRoot":"","sources":["../../../../src/alert-dialog/AlertDialog.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"AlertDialog.d.ts","sourceRoot":"","sources":["../../../../src/alert-dialog/AlertDialog.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA+C,MAAM,OAAO,CAAC;AAEpE,OAAO,KAAK,EAAgB,WAAW,EAAE,MAAM,WAAW,CAAC;AAE3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEhD,KAAK,eAAe,GAAG,KAAK,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC;AAExD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe,kLAepD,gBAAgB,8CAsFpB;AAED,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC"}
|
|
@@ -25,8 +25,10 @@ export interface AlertDialogProps {
|
|
|
25
25
|
/** Render only the confirm button (no cancel). Defaults to `false`. */
|
|
26
26
|
hideCancel?: boolean;
|
|
27
27
|
/**
|
|
28
|
-
* Tap-outside / Escape dismisses. Defaults to `
|
|
29
|
-
*
|
|
28
|
+
* Tap-outside / Escape dismisses. Defaults to `true` — matching the shared
|
|
29
|
+
* `Dialog` default and Mention's `ConfirmPrompt`. A backdrop / Escape
|
|
30
|
+
* dismissal always resolves the confirm as cancelled (`false`), so it is
|
|
31
|
+
* safe by default; pass `false` for a truly blocking confirm.
|
|
30
32
|
*/
|
|
31
33
|
dismissible?: boolean;
|
|
32
34
|
cardStyle?: StyleProp<ViewStyle>;
|
|
@@ -39,6 +41,10 @@ export interface ConfirmOptions {
|
|
|
39
41
|
cancelLabel?: string;
|
|
40
42
|
destructive?: boolean;
|
|
41
43
|
hideCancel?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Tap-outside / Escape dismisses (resolving `false`). Defaults to `true`;
|
|
46
|
+
* pass `false` for a blocking confirm that must be answered by a button.
|
|
47
|
+
*/
|
|
42
48
|
dismissible?: boolean;
|
|
43
49
|
}
|
|
44
50
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/alert-dialog/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD,MAAM,MAAM,sBAAsB,GAAG,SAAS,GAAG,QAAQ,GAAG,aAAa,CAAC;AAE1E,MAAM,WAAW,gBAAgB;IAC/B,4DAA4D;IAC5D,OAAO,EAAE,OAAO,CAAC;IACjB,sEAAsE;IACtE,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,gBAAgB;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qDAAqD;IACrD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mDAAmD;IACnD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,uEAAuE;IACvE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/alert-dialog/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD,MAAM,MAAM,sBAAsB,GAAG,SAAS,GAAG,QAAQ,GAAG,aAAa,CAAC;AAE1E,MAAM,WAAW,gBAAgB;IAC/B,4DAA4D;IAC5D,OAAO,EAAE,OAAO,CAAC;IACjB,sEAAsE;IACtE,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,gBAAgB;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qDAAqD;IACrD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mDAAmD;IACnD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,uEAAuE;IACvE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB"}
|
|
@@ -19,6 +19,19 @@ type DialogComponent = React.ComponentType<DialogProps>;
|
|
|
19
19
|
* imperative `confirm()` helper + `<AlertDialogHost />` to trigger a confirm
|
|
20
20
|
* from an event handler without owning visible state.
|
|
21
21
|
*
|
|
22
|
+
* Control mode — IMPERATIVE, not controlled. `AlertDialog` keeps its public
|
|
23
|
+
* *controlled* API (`visible` boolean + `onClose`) but internally bridges it
|
|
24
|
+
* onto the Dialog's imperative `useDialogControl()` handle, so `confirm()`
|
|
25
|
+
* drives `<Dialog>` through the EXACT uncontrolled/imperative internals
|
|
26
|
+
* Mention's `ConfirmPrompt` uses. That is the code path that actually plays the
|
|
27
|
+
* exit animation on dismiss: imperative `close()` sets `isClosing`, runs the
|
|
28
|
+
* exit animation, and fires `onClose` only once it settles. The controlled path
|
|
29
|
+
* instead fires `onClose` synchronously — and because `<AlertDialogHost />`
|
|
30
|
+
* resolves + drops the queue entry inside `onClose`, the surface would then
|
|
31
|
+
* unmount instantly, skipping the exit animation entirely. Bridging to
|
|
32
|
+
* imperative mode eliminates that fork so `confirm()` and Mention's
|
|
33
|
+
* `ConfirmPrompt` are pixel/timing identical.
|
|
34
|
+
*
|
|
22
35
|
* The centered card uses the Dialog default `maxWidth` (480px) for full visual
|
|
23
36
|
* parity with the shared confirm surface.
|
|
24
37
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AlertDialog.d.ts","sourceRoot":"","sources":["../../../../src/alert-dialog/AlertDialog.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"AlertDialog.d.ts","sourceRoot":"","sources":["../../../../src/alert-dialog/AlertDialog.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA+C,MAAM,OAAO,CAAC;AAEpE,OAAO,KAAK,EAAgB,WAAW,EAAE,MAAM,WAAW,CAAC;AAE3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEhD,KAAK,eAAe,GAAG,KAAK,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC;AAExD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe,kLAepD,gBAAgB,8CAsFpB;AAED,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC"}
|
|
@@ -25,8 +25,10 @@ export interface AlertDialogProps {
|
|
|
25
25
|
/** Render only the confirm button (no cancel). Defaults to `false`. */
|
|
26
26
|
hideCancel?: boolean;
|
|
27
27
|
/**
|
|
28
|
-
* Tap-outside / Escape dismisses. Defaults to `
|
|
29
|
-
*
|
|
28
|
+
* Tap-outside / Escape dismisses. Defaults to `true` — matching the shared
|
|
29
|
+
* `Dialog` default and Mention's `ConfirmPrompt`. A backdrop / Escape
|
|
30
|
+
* dismissal always resolves the confirm as cancelled (`false`), so it is
|
|
31
|
+
* safe by default; pass `false` for a truly blocking confirm.
|
|
30
32
|
*/
|
|
31
33
|
dismissible?: boolean;
|
|
32
34
|
cardStyle?: StyleProp<ViewStyle>;
|
|
@@ -39,6 +41,10 @@ export interface ConfirmOptions {
|
|
|
39
41
|
cancelLabel?: string;
|
|
40
42
|
destructive?: boolean;
|
|
41
43
|
hideCancel?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Tap-outside / Escape dismisses (resolving `false`). Defaults to `true`;
|
|
46
|
+
* pass `false` for a blocking confirm that must be answered by a button.
|
|
47
|
+
*/
|
|
42
48
|
dismissible?: boolean;
|
|
43
49
|
}
|
|
44
50
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/alert-dialog/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD,MAAM,MAAM,sBAAsB,GAAG,SAAS,GAAG,QAAQ,GAAG,aAAa,CAAC;AAE1E,MAAM,WAAW,gBAAgB;IAC/B,4DAA4D;IAC5D,OAAO,EAAE,OAAO,CAAC;IACjB,sEAAsE;IACtE,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,gBAAgB;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qDAAqD;IACrD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mDAAmD;IACnD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,uEAAuE;IACvE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/alert-dialog/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD,MAAM,MAAM,sBAAsB,GAAG,SAAS,GAAG,QAAQ,GAAG,aAAa,CAAC;AAE1E,MAAM,WAAW,gBAAgB;IAC/B,4DAA4D;IAC5D,OAAO,EAAE,OAAO,CAAC;IACjB,sEAAsE;IACtE,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,gBAAgB;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qDAAqD;IACrD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mDAAmD;IACnD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,uEAAuE;IACvE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB"}
|
package/package.json
CHANGED
|
@@ -2,7 +2,10 @@ import React, { useState } from 'react';
|
|
|
2
2
|
import { act, fireEvent, render } from '@testing-library/react-native';
|
|
3
3
|
|
|
4
4
|
import { AlertDialog, AlertDialogHost, confirm } from '../alert-dialog';
|
|
5
|
+
import { createAlertDialog } from '../alert-dialog/AlertDialog';
|
|
6
|
+
import { createAlertDialogHost } from '../alert-dialog/AlertDialogHost';
|
|
5
7
|
import { getConfirmQueue, resolveConfirm } from '../alert-dialog/confirm-store';
|
|
8
|
+
import type { DialogProps } from '../dialog';
|
|
6
9
|
import { BloomThemeProvider } from '../theme/BloomThemeProvider';
|
|
7
10
|
|
|
8
11
|
function renderWithTheme(ui: React.ReactElement) {
|
|
@@ -209,4 +212,73 @@ describe('AlertDialog (built on Dialog)', () => {
|
|
|
209
212
|
await expect(result).resolves.toBe(true);
|
|
210
213
|
});
|
|
211
214
|
});
|
|
215
|
+
|
|
216
|
+
// Finding 1 (control-mode) + Finding 2 (backdrop-dismiss default). These pin
|
|
217
|
+
// the two behavioural fixes at the prop boundary between `AlertDialog` and the
|
|
218
|
+
// underlying `Dialog`, using a prop-capturing mock `Dialog` so the assertions
|
|
219
|
+
// are deterministic and platform-agnostic.
|
|
220
|
+
describe('control-mode bridge + dismiss defaults', () => {
|
|
221
|
+
function captureDialogProps() {
|
|
222
|
+
const captured: { props?: DialogProps } = {};
|
|
223
|
+
const MockDialog = (props: DialogProps) => {
|
|
224
|
+
captured.props = props;
|
|
225
|
+
return null;
|
|
226
|
+
};
|
|
227
|
+
return { captured, MockDialog };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
it('drives the underlying Dialog imperatively (control), never the controlled `open` prop', () => {
|
|
231
|
+
const { captured, MockDialog } = captureDialogProps();
|
|
232
|
+
const TestAlertDialog = createAlertDialog(MockDialog);
|
|
233
|
+
renderWithTheme(<TestAlertDialog visible onClose={() => {}} title="Delete?" />);
|
|
234
|
+
// The whole point of the fix: `confirm()` must take the Dialog's
|
|
235
|
+
// imperative/uncontrolled branch (the one that plays the exit animation),
|
|
236
|
+
// so an imperative `control` handle is passed and the controlled `open`
|
|
237
|
+
// boolean — which would take the other branch — is never set.
|
|
238
|
+
expect(captured.props?.control).toBeDefined();
|
|
239
|
+
expect(captured.props?.open).toBeUndefined();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('defaults dismissOnBackdrop to true (backdrop / Escape dismiss by default)', () => {
|
|
243
|
+
const { captured, MockDialog } = captureDialogProps();
|
|
244
|
+
const TestAlertDialog = createAlertDialog(MockDialog);
|
|
245
|
+
renderWithTheme(<TestAlertDialog visible onClose={() => {}} title="Delete?" />);
|
|
246
|
+
// Matches the shared Dialog default + Mention's ConfirmPrompt.
|
|
247
|
+
expect(captured.props?.dismissOnBackdrop).toBe(true);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('still supports an opt-in blocking confirm via dismissible={false}', () => {
|
|
251
|
+
const { captured, MockDialog } = captureDialogProps();
|
|
252
|
+
const TestAlertDialog = createAlertDialog(MockDialog);
|
|
253
|
+
renderWithTheme(
|
|
254
|
+
<TestAlertDialog visible onClose={() => {}} title="Delete?" dismissible={false} />,
|
|
255
|
+
);
|
|
256
|
+
expect(captured.props?.dismissOnBackdrop).toBe(false);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('resolves false when dismissed via backdrop / Escape (Dialog onClose) with no button press', async () => {
|
|
260
|
+
// Backdrop tap and Escape both route through the Dialog's `onClose`. Now
|
|
261
|
+
// that dismissOnBackdrop defaults true, that path is reachable by default
|
|
262
|
+
// and must resolve the confirm as cancelled (false) and drain the queue —
|
|
263
|
+
// never trigger the (potentially destructive) confirm action.
|
|
264
|
+
const captured: { onClose?: () => void } = {};
|
|
265
|
+
const MockDialog = (props: DialogProps) => {
|
|
266
|
+
captured.onClose = props.onClose;
|
|
267
|
+
return null;
|
|
268
|
+
};
|
|
269
|
+
const Host = createAlertDialogHost(createAlertDialog(MockDialog));
|
|
270
|
+
renderWithTheme(<Host />);
|
|
271
|
+
let result!: Promise<boolean>;
|
|
272
|
+
act(() => {
|
|
273
|
+
result = confirm({ title: 'Discard unsaved changes?' });
|
|
274
|
+
});
|
|
275
|
+
act(() => {
|
|
276
|
+
// Simulate the backdrop / Escape dismissal the real Dialog fires after
|
|
277
|
+
// its exit animation settles.
|
|
278
|
+
captured.onClose?.();
|
|
279
|
+
});
|
|
280
|
+
await expect(result).resolves.toBe(false);
|
|
281
|
+
expect(getConfirmQueue()).toHaveLength(0);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
212
284
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import React, { memo, useCallback } from 'react';
|
|
1
|
+
import React, { memo, useCallback, useEffect, useRef } from 'react';
|
|
2
2
|
|
|
3
3
|
import type { DialogAction, DialogProps } from '../dialog';
|
|
4
|
+
import { useDialogControl } from '../dialog/context';
|
|
4
5
|
import type { AlertDialogProps } from './types';
|
|
5
6
|
|
|
6
7
|
type DialogComponent = React.ComponentType<DialogProps>;
|
|
@@ -22,6 +23,19 @@ type DialogComponent = React.ComponentType<DialogProps>;
|
|
|
22
23
|
* imperative `confirm()` helper + `<AlertDialogHost />` to trigger a confirm
|
|
23
24
|
* from an event handler without owning visible state.
|
|
24
25
|
*
|
|
26
|
+
* Control mode — IMPERATIVE, not controlled. `AlertDialog` keeps its public
|
|
27
|
+
* *controlled* API (`visible` boolean + `onClose`) but internally bridges it
|
|
28
|
+
* onto the Dialog's imperative `useDialogControl()` handle, so `confirm()`
|
|
29
|
+
* drives `<Dialog>` through the EXACT uncontrolled/imperative internals
|
|
30
|
+
* Mention's `ConfirmPrompt` uses. That is the code path that actually plays the
|
|
31
|
+
* exit animation on dismiss: imperative `close()` sets `isClosing`, runs the
|
|
32
|
+
* exit animation, and fires `onClose` only once it settles. The controlled path
|
|
33
|
+
* instead fires `onClose` synchronously — and because `<AlertDialogHost />`
|
|
34
|
+
* resolves + drops the queue entry inside `onClose`, the surface would then
|
|
35
|
+
* unmount instantly, skipping the exit animation entirely. Bridging to
|
|
36
|
+
* imperative mode eliminates that fork so `confirm()` and Mention's
|
|
37
|
+
* `ConfirmPrompt` are pixel/timing identical.
|
|
38
|
+
*
|
|
25
39
|
* The centered card uses the Dialog default `maxWidth` (480px) for full visual
|
|
26
40
|
* parity with the shared confirm surface.
|
|
27
41
|
*/
|
|
@@ -37,34 +51,64 @@ export function createAlertDialog(Dialog: DialogComponent) {
|
|
|
37
51
|
onCancel,
|
|
38
52
|
destructive = false,
|
|
39
53
|
hideCancel = false,
|
|
40
|
-
dismissible =
|
|
54
|
+
dismissible = true,
|
|
41
55
|
cardStyle,
|
|
42
56
|
testID,
|
|
43
57
|
}: AlertDialogProps) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
// `
|
|
49
|
-
// `
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
const control = useDialogControl();
|
|
59
|
+
|
|
60
|
+
// Bridge the public *controlled* `visible` prop onto the Dialog's
|
|
61
|
+
// imperative open/close. Opening on mount straight from an effect (no
|
|
62
|
+
// `setTimeout`) mirrors bloom's own `AutoMountedDialog` (which backs
|
|
63
|
+
// `alert()`) — the canonical fresh-mount imperative-open pattern:
|
|
64
|
+
// `<AlertDialogHost />` mounts a FRESH `AlertDialog` per confirm
|
|
65
|
+
// (`key={id}`) with the final content already committed, so there is no
|
|
66
|
+
// stale-content window to defer past and the entry animation curve stays
|
|
67
|
+
// identical to Mention's. `control` is referentially stable (memoised on
|
|
68
|
+
// its id), so this effect only re-runs when `visible` actually flips. When
|
|
69
|
+
// a direct consumer flips `visible` to `false`, we imperatively `close()`
|
|
70
|
+
// so the exit animation still plays.
|
|
71
|
+
const closingFromPropRef = useRef(false);
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (visible) {
|
|
74
|
+
control.open();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
closingFromPropRef.current = true;
|
|
78
|
+
control.close();
|
|
79
|
+
}, [visible, control]);
|
|
80
|
+
|
|
81
|
+
// The Dialog fires `onClose` after the exit animation settles (imperative
|
|
82
|
+
// mode). Forward ONLY user-initiated dismissals (backdrop / Escape / cancel)
|
|
83
|
+
// to the consumer — a consumer-initiated close (they flipped `visible` to
|
|
84
|
+
// `false`) must not re-enter `onClose`, preserving the previous controlled
|
|
85
|
+
// semantics where a programmatic close does not fire `onClose`.
|
|
86
|
+
const handleClose = useCallback(() => {
|
|
87
|
+
if (closingFromPropRef.current) {
|
|
88
|
+
closingFromPropRef.current = false;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
53
91
|
onClose();
|
|
54
|
-
}, [
|
|
92
|
+
}, [onClose]);
|
|
55
93
|
|
|
94
|
+
// Confirm/cancel map onto the Dialog's declarative action row. Both keep the
|
|
95
|
+
// default `shouldCloseOnPress` (true): the button runs the exit animation
|
|
96
|
+
// FIRST, then invokes `onPress` as a close-completion callback — so
|
|
97
|
+
// `onConfirm` resolves the `confirm()` promise as the surface animates out,
|
|
98
|
+
// exactly like Mention. `onConfirm` (resolve `true`, drains the entry) runs
|
|
99
|
+
// just BEFORE the Dialog's own post-close `onClose` (→ `handleClose` →
|
|
100
|
+
// resolve `false`, now a no-op for the drained entry), which is what makes
|
|
101
|
+
// the confirm button win the double-resolution guard.
|
|
56
102
|
const actions: DialogAction[] = [
|
|
57
103
|
{
|
|
58
104
|
label: confirmLabel,
|
|
59
|
-
onPress:
|
|
105
|
+
onPress: onConfirm,
|
|
60
106
|
color: destructive ? 'destructive' : 'default',
|
|
61
|
-
shouldCloseOnPress: false,
|
|
62
107
|
},
|
|
63
108
|
];
|
|
64
109
|
if (!hideCancel) {
|
|
65
|
-
// The `cancel` color auto-dismisses via the Dialog's `close()
|
|
66
|
-
//
|
|
67
|
-
// runs after the surface finishes closing.
|
|
110
|
+
// The `cancel` color auto-dismisses via the Dialog's `close()` (exit
|
|
111
|
+
// animation), then runs `onCancel` as a close-completion callback.
|
|
68
112
|
actions.push({
|
|
69
113
|
label: cancelLabel,
|
|
70
114
|
onPress: onCancel,
|
|
@@ -74,11 +118,14 @@ export function createAlertDialog(Dialog: DialogComponent) {
|
|
|
74
118
|
|
|
75
119
|
return (
|
|
76
120
|
<Dialog
|
|
77
|
-
|
|
78
|
-
onClose={
|
|
121
|
+
control={control}
|
|
122
|
+
onClose={handleClose}
|
|
79
123
|
placement="center"
|
|
80
|
-
// Alert dialogs
|
|
81
|
-
//
|
|
124
|
+
// Alert dialogs dismiss on backdrop / Escape by default (matching the
|
|
125
|
+
// shared Dialog default and Mention's `ConfirmPrompt`). A backdrop /
|
|
126
|
+
// Escape dismissal can only ever resolve the confirm as cancelled
|
|
127
|
+
// (`false`), never the destructive action — callers that need a truly
|
|
128
|
+
// blocking confirm opt out with `dismissible={false}`.
|
|
82
129
|
dismissOnBackdrop={dismissible}
|
|
83
130
|
title={title}
|
|
84
131
|
description={description}
|
|
@@ -27,8 +27,10 @@ export interface AlertDialogProps {
|
|
|
27
27
|
/** Render only the confirm button (no cancel). Defaults to `false`. */
|
|
28
28
|
hideCancel?: boolean;
|
|
29
29
|
/**
|
|
30
|
-
* Tap-outside / Escape dismisses. Defaults to `
|
|
31
|
-
*
|
|
30
|
+
* Tap-outside / Escape dismisses. Defaults to `true` — matching the shared
|
|
31
|
+
* `Dialog` default and Mention's `ConfirmPrompt`. A backdrop / Escape
|
|
32
|
+
* dismissal always resolves the confirm as cancelled (`false`), so it is
|
|
33
|
+
* safe by default; pass `false` for a truly blocking confirm.
|
|
32
34
|
*/
|
|
33
35
|
dismissible?: boolean;
|
|
34
36
|
cardStyle?: StyleProp<ViewStyle>;
|
|
@@ -42,5 +44,9 @@ export interface ConfirmOptions {
|
|
|
42
44
|
cancelLabel?: string;
|
|
43
45
|
destructive?: boolean;
|
|
44
46
|
hideCancel?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Tap-outside / Escape dismisses (resolving `false`). Defaults to `true`;
|
|
49
|
+
* pass `false` for a blocking confirm that must be answered by a button.
|
|
50
|
+
*/
|
|
45
51
|
dismissible?: boolean;
|
|
46
52
|
}
|