@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.
@@ -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 = false,
56
+ dismissible = true,
43
57
  cardStyle,
44
58
  testID
45
59
  }) {
46
- // Confirm resolves + closes SYNCHRONOUSLY, in this order, and must NOT go
47
- // through the Dialog's auto-close (`shouldCloseOnPress: false`). In
48
- // controlled mode `close()` fires `onClose` FIRST, and the `confirm()` host
49
- // wires `onClose` to resolve the promise as *cancelled* (`false`). Running
50
- // `onConfirm` (resolve `true`, which removes the queue entry) before
51
- // `onClose` (now a no-op for that entry) is what makes the confirm button
52
- // resolve `true`.
53
- const handleConfirm = (0, _react.useCallback)(() => {
54
- onConfirm?.();
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
- }, [onConfirm, onClose]);
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: handleConfirm,
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()`, which in
65
- // controlled mode fires `onClose` (the host resolves `false`). `onCancel`
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
- open: visible,
75
- onClose: onClose,
119
+ control: control,
120
+ onClose: handleClose,
76
121
  placement: "center"
77
- // Alert dialogs are blocking by default the backdrop / Escape only
78
- // dismiss when the caller opts in via `dismissible`.
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","handleConfirm","useCallback","actions","label","onPress","color","shouldCloseOnPress","push","jsx","open","placement","dismissOnBackdrop","style","memo","displayName"],"sourceRoot":"../../../src","sources":["alert-dialog/AlertDialog.tsx"],"mappings":"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","ignoreList":[]}
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 = false,
51
+ dismissible = true,
38
52
  cardStyle,
39
53
  testID
40
54
  }) {
41
- // Confirm resolves + closes SYNCHRONOUSLY, in this order, and must NOT go
42
- // through the Dialog's auto-close (`shouldCloseOnPress: false`). In
43
- // controlled mode `close()` fires `onClose` FIRST, and the `confirm()` host
44
- // wires `onClose` to resolve the promise as *cancelled* (`false`). Running
45
- // `onConfirm` (resolve `true`, which removes the queue entry) before
46
- // `onClose` (now a no-op for that entry) is what makes the confirm button
47
- // resolve `true`.
48
- const handleConfirm = useCallback(() => {
49
- onConfirm?.();
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
- }, [onConfirm, onClose]);
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: handleConfirm,
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()`, which in
60
- // controlled mode fires `onClose` (the host resolves `false`). `onCancel`
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
- open: visible,
70
- onClose: onClose,
114
+ control: control,
115
+ onClose: handleClose,
71
116
  placement: "center"
72
- // Alert dialogs are blocking by default the backdrop / Escape only
73
- // dismiss when the caller opts in via `dismissible`.
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","handleConfirm","actions","label","onPress","color","shouldCloseOnPress","push","open","placement","dismissOnBackdrop","style","displayName"],"sourceRoot":"../../../src","sources":["alert-dialog/AlertDialog.tsx"],"mappings":";;AAAA,OAAOA,KAAK,IAAIC,IAAI,EAAEC,WAAW,QAAQ,OAAO;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAOjD;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,KAAK;IACnBC,SAAS;IACTC;EACgB,CAAC,EAAE;IACnB;IACA;IACA;IACA;IACA;IACA;IACA;IACA,MAAMC,aAAa,GAAGpB,WAAW,CAAC,MAAM;MACtCa,SAAS,GAAG,CAAC;MACbL,OAAO,CAAC,CAAC;IACX,CAAC,EAAE,CAACK,SAAS,EAAEL,OAAO,CAAC,CAAC;IAExB,MAAMa,OAAuB,GAAG,CAC9B;MACEC,KAAK,EAAEX,YAAY;MACnBY,OAAO,EAAEH,aAAa;MACtBI,KAAK,EAAET,WAAW,GAAG,aAAa,GAAG,SAAS;MAC9CU,kBAAkB,EAAE;IACtB,CAAC,CACF;IACD,IAAI,CAACT,UAAU,EAAE;MACf;MACA;MACA;MACAK,OAAO,CAACK,IAAI,CAAC;QACXJ,KAAK,EAAEV,WAAW;QAClBW,OAAO,EAAET,QAAQ;QACjBU,KAAK,EAAE;MACT,CAAC,CAAC;IACJ;IAEA,oBACEtB,IAAA,CAACE,MAAM;MACLuB,IAAI,EAAEpB,OAAQ;MACdC,OAAO,EAAEA,OAAQ;MACjBoB,SAAS,EAAC;MACV;MACA;MAAA;MACAC,iBAAiB,EAAEZ,WAAY;MAC/BR,KAAK,EAAEA,KAAM;MACbC,WAAW,EAAEA,WAAY;MACzBW,OAAO,EAAEA,OAAQ;MACjBC,KAAK,EAAEb,KAAM;MACbqB,KAAK,EAAEZ,SAAU;MACjBC,MAAM,EAAEA;IAAO,CAChB,CAAC;EAEN,CAAC;EAED,MAAMb,WAAW,gBAAGP,IAAI,CAACM,oBAAoB,CAAC;EAC9CC,WAAW,CAACyB,WAAW,GAAG,aAAa;EACvC,OAAOzB,WAAW;AACpB","ignoreList":[]}
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,KAA4B,MAAM,OAAO,CAAC;AAEjD,OAAO,KAAK,EAAgB,WAAW,EAAE,MAAM,WAAW,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEhD,KAAK,eAAe,GAAG,KAAK,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC;AAExD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe,kLAepD,gBAAgB,8CAqDpB;AAED,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC"}
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 `false` — alert dialogs are
29
- * usually blocking until answered.
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;;;OAGG;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,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB"}
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,KAA4B,MAAM,OAAO,CAAC;AAEjD,OAAO,KAAK,EAAgB,WAAW,EAAE,MAAM,WAAW,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEhD,KAAK,eAAe,GAAG,KAAK,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC;AAExD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe,kLAepD,gBAAgB,8CAqDpB;AAED,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC"}
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 `false` — alert dialogs are
29
- * usually blocking until answered.
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;;;OAGG;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,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/bloom",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
4
4
  "description": "Bloom UI — Oxy ecosystem component library for React Native + Expo + Web",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -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 = false,
54
+ dismissible = true,
41
55
  cardStyle,
42
56
  testID,
43
57
  }: AlertDialogProps) {
44
- // Confirm resolves + closes SYNCHRONOUSLY, in this order, and must NOT go
45
- // through the Dialog's auto-close (`shouldCloseOnPress: false`). In
46
- // controlled mode `close()` fires `onClose` FIRST, and the `confirm()` host
47
- // wires `onClose` to resolve the promise as *cancelled* (`false`). Running
48
- // `onConfirm` (resolve `true`, which removes the queue entry) before
49
- // `onClose` (now a no-op for that entry) is what makes the confirm button
50
- // resolve `true`.
51
- const handleConfirm = useCallback(() => {
52
- onConfirm?.();
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
- }, [onConfirm, onClose]);
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: handleConfirm,
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()`, which in
66
- // controlled mode fires `onClose` (the host resolves `false`). `onCancel`
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
- open={visible}
78
- onClose={onClose}
121
+ control={control}
122
+ onClose={handleClose}
79
123
  placement="center"
80
- // Alert dialogs are blocking by default the backdrop / Escape only
81
- // dismiss when the caller opts in via `dismissible`.
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 `false` — alert dialogs are
31
- * usually blocking until answered.
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
  }