beez-ui 2.6.0 → 2.6.1

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 CHANGED
@@ -4,6 +4,15 @@ Todos los cambios relevantes de beez-ui se documentan en este archivo con el for
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.6.1] - 2026-10-16
8
+
9
+ ### Fixed
10
+
11
+ - Los botones de un toast de `ThemedToaster` (acción, cancelar o cerrar) responden mientras hay un `Dialog`, `AlertDialog` o `Sheet` abierto, y presionarlos ya no cierra el `Dialog` o el `Sheet`. También se alcanzan con el teclado mediante el atajo de Sonner (Alt+T), que ya no queda bloqueado por la trampa de foco del modal; si el control enfocado cierra su toast, el foco vuelve al modal en lugar de perderse.
12
+ - Presionar Escape en un menú, select o diálogo abierto desde el `Sidebar` en mobile cierra solo esa capa y deja el sidebar abierto.
13
+ - Abrir el `Sidebar` en mobile en una ventana de escritorio angosta ya no desplaza la página de costado cuando desaparece la barra de scroll.
14
+ - El contenido de `PopoverContent` más alto que el espacio disponible en pantalla se desplaza dentro del panel en lugar de quedar cortado fuera del viewport.
15
+
7
16
  ## [2.6.0] - 2026-10-15
8
17
 
9
18
  ### Added
@@ -11,6 +11,7 @@ import { Dialog as DialogPrimitive } from "radix-ui";
11
11
  import { MotionSlot } from "../motion/motion-slot.js";
12
12
  import { MotionPresence, MotionOpenProvider, useMotionOpenState } from "../motion/presence.js";
13
13
  import { cn } from "../lib/utils.js";
14
+ import { keepOpenOnToastInteraction } from "../lib/toast-layer.js";
14
15
  import { Button } from "./button.js";
15
16
  import { ControlButton } from "./control-button.js";
16
17
  import { XIcon } from "lucide-react";
@@ -210,7 +211,7 @@ function DialogViewportCloseButton() {
210
211
  * @returns The styled primitive with its original interaction contract.
211
212
  */
212
213
  function DialogContent(t0) {
213
- const $ = _c(26);
214
+ const $ = _c(29);
214
215
  let children;
215
216
  let className;
216
217
  let props;
@@ -256,17 +257,25 @@ function DialogContent(t0) {
256
257
  t7 = $[9];
257
258
  }
258
259
  let t8;
259
- if ($[10] !== closeButtonPlacement || $[11] !== showCloseButton) {
260
- t8 = showCloseButton && closeButtonPlacement === "viewport" && /* @__PURE__ */ _jsx(DialogViewportCloseButton, {});
261
- $[10] = closeButtonPlacement;
262
- $[11] = showCloseButton;
263
- $[12] = t8;
260
+ if ($[10] !== props.onInteractOutside) {
261
+ t8 = keepOpenOnToastInteraction(props.onInteractOutside);
262
+ $[10] = props.onInteractOutside;
263
+ $[11] = t8;
264
264
  } else {
265
- t8 = $[12];
265
+ t8 = $[11];
266
266
  }
267
267
  let t9;
268
- if ($[13] !== closeButtonPlacement || $[14] !== showCloseButton) {
269
- t9 = showCloseButton && closeButtonPlacement === "content" && /* @__PURE__ */ _jsx(DialogPrimitive.Close, {
268
+ if ($[12] !== closeButtonPlacement || $[13] !== showCloseButton) {
269
+ t9 = showCloseButton && closeButtonPlacement === "viewport" && /* @__PURE__ */ _jsx(DialogViewportCloseButton, {});
270
+ $[12] = closeButtonPlacement;
271
+ $[13] = showCloseButton;
272
+ $[14] = t9;
273
+ } else {
274
+ t9 = $[14];
275
+ }
276
+ let t10;
277
+ if ($[15] !== closeButtonPlacement || $[16] !== showCloseButton) {
278
+ t10 = showCloseButton && closeButtonPlacement === "content" && /* @__PURE__ */ _jsx(DialogPrimitive.Close, {
270
279
  "data-slot": "dialog-close",
271
280
  asChild: true,
272
281
  children: /* @__PURE__ */ _jsx(MotionSlot, {
@@ -282,15 +291,15 @@ function DialogContent(t0) {
282
291
  })
283
292
  })
284
293
  });
285
- $[13] = closeButtonPlacement;
286
- $[14] = showCloseButton;
287
- $[15] = t9;
294
+ $[15] = closeButtonPlacement;
295
+ $[16] = showCloseButton;
296
+ $[17] = t10;
288
297
  } else {
289
- t9 = $[15];
298
+ t10 = $[17];
290
299
  }
291
- let t10;
292
- if ($[16] !== children || $[17] !== props || $[18] !== t5 || $[19] !== t7 || $[20] !== t8 || $[21] !== t9) {
293
- t10 = /* @__PURE__ */ _jsxs(React.Fragment, { children: [t4, /* @__PURE__ */ _jsx(MotionSlot, {
300
+ let t11;
301
+ if ($[18] !== children || $[19] !== props || $[20] !== t10 || $[21] !== t5 || $[22] !== t7 || $[23] !== t8 || $[24] !== t9) {
302
+ t11 = /* @__PURE__ */ _jsxs(React.Fragment, { children: [t4, /* @__PURE__ */ _jsx(MotionSlot, {
294
303
  kind: "center-morph",
295
304
  children: /* @__PURE__ */ _jsxs(DialogPrimitive.Content, {
296
305
  forceMount: true,
@@ -298,40 +307,42 @@ function DialogContent(t0) {
298
307
  "data-close-button-placement": t5,
299
308
  className: t7,
300
309
  ...props,
310
+ onInteractOutside: t8,
301
311
  children: [
302
312
  children,
303
- t8,
304
- t9
313
+ t9,
314
+ t10
305
315
  ]
306
316
  })
307
317
  })] });
308
- $[16] = children;
309
- $[17] = props;
310
- $[18] = t5;
311
- $[19] = t7;
312
- $[20] = t8;
313
- $[21] = t9;
314
- $[22] = t10;
318
+ $[18] = children;
319
+ $[19] = props;
320
+ $[20] = t10;
321
+ $[21] = t5;
322
+ $[22] = t7;
323
+ $[23] = t8;
324
+ $[24] = t9;
325
+ $[25] = t11;
315
326
  } else {
316
- t10 = $[22];
327
+ t11 = $[25];
317
328
  }
318
- let t11;
319
- if ($[23] !== props.forceMount || $[24] !== t10) {
320
- t11 = /* @__PURE__ */ _jsx(DialogPortal, {
329
+ let t12;
330
+ if ($[26] !== props.forceMount || $[27] !== t11) {
331
+ t12 = /* @__PURE__ */ _jsx(DialogPortal, {
321
332
  forceMount: true,
322
333
  children: /* @__PURE__ */ _jsx(MotionPresence, {
323
334
  scope: "dialog",
324
335
  forceMount: t3,
325
- children: t10
336
+ children: t11
326
337
  })
327
338
  });
328
- $[23] = props.forceMount;
329
- $[24] = t10;
330
- $[25] = t11;
339
+ $[26] = props.forceMount;
340
+ $[27] = t11;
341
+ $[28] = t12;
331
342
  } else {
332
- t11 = $[25];
343
+ t12 = $[28];
333
344
  }
334
- return t11;
345
+ return t12;
335
346
  }
336
347
  /**
337
348
  * Stacks the title and description. While the panel shows its own close button, the header keeps Arc's gap to it so
@@ -11,6 +11,7 @@ declare function PopoverTrigger({ ...props }: React.ComponentProps<typeof Popove
11
11
  declare function PopoverClose({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Close>): React.JSX.Element;
12
12
  /**
13
13
  * Renders Arc's panel: raised surface, hairline border, panel radius and floating shadow, kept inside the viewport.
14
+ * Content taller than the room left on the placed side scrolls inside the panel instead of running off screen.
14
15
  * @param props - Radix content properties. `align` defaults to `center`, `sideOffset` to 6px and `collisionPadding`
15
16
  * to 10px.
16
17
  * @returns The portaled panel.
@@ -106,6 +106,7 @@ function resolvePopoverMotion() {
106
106
  }
107
107
  /**
108
108
  * Renders Arc's panel: raised surface, hairline border, panel radius and floating shadow, kept inside the viewport.
109
+ * Content taller than the room left on the placed side scrolls inside the panel instead of running off screen.
109
110
  * @param props - Radix content properties. `align` defaults to `center`, `sideOffset` to 6px and `collisionPadding`
110
111
  * to 10px.
111
112
  * @returns The portaled panel.
@@ -138,7 +139,7 @@ function PopoverContent(t0) {
138
139
  const t4 = props.forceMount;
139
140
  let t5;
140
141
  if ($[6] !== className) {
141
- t5 = cn("z-50 flex w-72 max-w-[calc(100vw-20px)] origin-(--radix-popover-content-transform-origin) flex-col gap-2.5 rounded-(--radius-panel) border border-border bg-surface-raised p-(--space-4) text-sm leading-(--leading-body) text-foreground shadow-(--shadow-floating) outline-hidden", className);
142
+ t5 = cn("z-50 flex max-h-(--radix-popover-content-available-height) w-72 max-w-[calc(100vw-20px)] origin-(--radix-popover-content-transform-origin) flex-col gap-2.5 overflow-y-auto overscroll-contain *:shrink-0 rounded-(--radius-panel) border border-border bg-surface-raised p-(--space-4) text-sm leading-(--leading-body) text-foreground shadow-(--shadow-floating) outline-hidden", className);
142
143
  $[6] = className;
143
144
  $[7] = t5;
144
145
  } else {
@@ -5,6 +5,7 @@ import { Dialog as SheetPrimitive } from "radix-ui";
5
5
  import { MotionSlot } from "../motion/motion-slot.js";
6
6
  import { MotionPresence, MotionOpenProvider, useMotionOpenState } from "../motion/presence.js";
7
7
  import { cn } from "../lib/utils.js";
8
+ import { keepOpenOnToastInteraction } from "../lib/toast-layer.js";
8
9
  import { ControlButton } from "./control-button.js";
9
10
  import { XIcon } from "lucide-react";
10
11
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
@@ -160,7 +161,7 @@ function SheetOverlay(t0) {
160
161
  * @returns The styled primitive with its original interaction contract.
161
162
  */
162
163
  function SheetContent(t0) {
163
- const $ = _c(20);
164
+ const $ = _c(23);
164
165
  let children;
165
166
  let className;
166
167
  let props;
@@ -203,8 +204,16 @@ function SheetContent(t0) {
203
204
  t5 = $[8];
204
205
  }
205
206
  let t6;
206
- if ($[9] !== showCloseButton) {
207
- t6 = showCloseButton && /* @__PURE__ */ _jsx(SheetPrimitive.Close, {
207
+ if ($[9] !== props.onInteractOutside) {
208
+ t6 = keepOpenOnToastInteraction(props.onInteractOutside);
209
+ $[9] = props.onInteractOutside;
210
+ $[10] = t6;
211
+ } else {
212
+ t6 = $[10];
213
+ }
214
+ let t7;
215
+ if ($[11] !== showCloseButton) {
216
+ t7 = showCloseButton && /* @__PURE__ */ _jsx(SheetPrimitive.Close, {
208
217
  "data-slot": "sheet-close",
209
218
  asChild: true,
210
219
  children: /* @__PURE__ */ _jsx(MotionSlot, {
@@ -220,14 +229,14 @@ function SheetContent(t0) {
220
229
  })
221
230
  })
222
231
  });
223
- $[9] = showCloseButton;
224
- $[10] = t6;
232
+ $[11] = showCloseButton;
233
+ $[12] = t7;
225
234
  } else {
226
- t6 = $[10];
235
+ t7 = $[12];
227
236
  }
228
- let t7;
229
- if ($[11] !== children || $[12] !== props || $[13] !== side || $[14] !== t5 || $[15] !== t6) {
230
- t7 = /* @__PURE__ */ _jsxs(React.Fragment, { children: [t4, /* @__PURE__ */ _jsx(MotionSlot, {
237
+ let t8;
238
+ if ($[13] !== children || $[14] !== props || $[15] !== side || $[16] !== t5 || $[17] !== t6 || $[18] !== t7) {
239
+ t8 = /* @__PURE__ */ _jsxs(React.Fragment, { children: [t4, /* @__PURE__ */ _jsx(MotionSlot, {
231
240
  kind: "sheet",
232
241
  children: /* @__PURE__ */ _jsxs(SheetPrimitive.Content, {
233
242
  forceMount: true,
@@ -235,35 +244,37 @@ function SheetContent(t0) {
235
244
  "data-side": side,
236
245
  className: t5,
237
246
  ...props,
238
- children: [children, t6]
247
+ onInteractOutside: t6,
248
+ children: [children, t7]
239
249
  })
240
250
  })] });
241
- $[11] = children;
242
- $[12] = props;
243
- $[13] = side;
244
- $[14] = t5;
245
- $[15] = t6;
246
- $[16] = t7;
251
+ $[13] = children;
252
+ $[14] = props;
253
+ $[15] = side;
254
+ $[16] = t5;
255
+ $[17] = t6;
256
+ $[18] = t7;
257
+ $[19] = t8;
247
258
  } else {
248
- t7 = $[16];
259
+ t8 = $[19];
249
260
  }
250
- let t8;
251
- if ($[17] !== props.forceMount || $[18] !== t7) {
252
- t8 = /* @__PURE__ */ _jsx(SheetPortal, {
261
+ let t9;
262
+ if ($[20] !== props.forceMount || $[21] !== t8) {
263
+ t9 = /* @__PURE__ */ _jsx(SheetPortal, {
253
264
  forceMount: true,
254
265
  children: /* @__PURE__ */ _jsx(MotionPresence, {
255
266
  scope: "sheet",
256
267
  forceMount: t3,
257
- children: t7
268
+ children: t8
258
269
  })
259
270
  });
260
- $[17] = props.forceMount;
261
- $[18] = t7;
262
- $[19] = t8;
271
+ $[20] = props.forceMount;
272
+ $[21] = t8;
273
+ $[22] = t9;
263
274
  } else {
264
- t8 = $[19];
275
+ t9 = $[22];
265
276
  }
266
- return t8;
277
+ return t9;
267
278
  }
268
279
  function SheetHeader(t0) {
269
280
  const $ = _c(8);
@@ -404,15 +404,22 @@ function MobileSidebar(t0) {
404
404
  const previousBodyStyles = {
405
405
  left: body.style.left,
406
406
  overflow: body.style.overflow,
407
+ paddingRight: body.style.paddingRight,
407
408
  position: body.style.position,
408
409
  right: body.style.right,
409
410
  top: body.style.top
410
411
  };
412
+ const clientWidthBeforeLock = document.documentElement.clientWidth;
413
+ const bodyPaddingRight = Number.parseFloat(getComputedStyle(body).paddingRight) || 0;
411
414
  body.style.position = "fixed";
412
415
  body.style.top = `-${scrollY}px`;
413
416
  body.style.left = "0";
414
417
  body.style.right = "0";
415
418
  body.style.overflow = "hidden";
419
+ const scrollbarWidth = document.documentElement.clientWidth - clientWidthBeforeLock;
420
+ if (scrollbarWidth > 0) {
421
+ body.style.paddingRight = `${bodyPaddingRight + scrollbarWidth}px`;
422
+ }
416
423
  const restoreTriggerFocus = () => triggerRef.current?.focus({ preventScroll: true });
417
424
  const focusFrame = requestAnimationFrame(() => {
418
425
  const firstFocusable = panelRef.current?.querySelector(FOCUSABLE_SELECTOR);
@@ -425,6 +432,7 @@ function MobileSidebar(t0) {
425
432
  body.style.left = previousBodyStyles.left;
426
433
  body.style.right = previousBodyStyles.right;
427
434
  body.style.overflow = previousBodyStyles.overflow;
435
+ body.style.paddingRight = previousBodyStyles.paddingRight;
428
436
  window.scrollTo(0, scrollY);
429
437
  restoreTriggerFocus();
430
438
  };
@@ -447,6 +455,9 @@ function MobileSidebar(t0) {
447
455
  let t5;
448
456
  if ($[7] !== setOpenMobile) {
449
457
  t5 = (event) => {
458
+ if (event.defaultPrevented || !panelRef.current?.contains(event.target)) {
459
+ return;
460
+ }
450
461
  if (event.key === "Escape") {
451
462
  event.preventDefault();
452
463
  setOpenMobile(false);
@@ -1,4 +1,8 @@
1
1
  import { type ToasterProps } from "sonner";
2
- /** Renders shared notification styling using the consumer's resolved theme. */
3
- declare const ThemedToaster: ({ theme, ...props }: ToasterProps) => import("react").JSX.Element;
2
+ /**
3
+ * Renders shared notification styling using the consumer's resolved theme. Toasts take pointer input even while a
4
+ * modal overlay (which disables pointer events on the page) is open, and Sonner's hotkey moves keyboard focus into
5
+ * them past the modal's focus trap, so their actions stay usable from a dialog.
6
+ */
7
+ declare const ThemedToaster: ({ theme, className, ...props }: ToasterProps) => import("react").JSX.Element;
4
8
  export { ThemedToaster };
@@ -1,104 +1,134 @@
1
1
  "use client";
2
2
  import { c as _c } from "react/compiler-runtime";
3
3
  import { MotionSlot } from "../motion/motion-slot.js";
4
+ import { cn } from "../lib/utils.js";
5
+ import { letFocusReachToastLayer } from "../lib/toast-layer.js";
6
+ import { useEffect } from "react";
4
7
  import { Toaster as Sonner } from "sonner";
5
8
  import { useTheme } from "next-themes";
6
9
  import { CircleCheck, Info, TriangleAlert, OctagonAlert, Loader } from "lucide-react";
7
10
  import { jsx as _jsx } from "react/jsx-runtime";
8
- /** Renders shared notification styling using the consumer's resolved theme. */
11
+ /**
12
+ * Renders shared notification styling using the consumer's resolved theme. Toasts take pointer input even while a
13
+ * modal overlay (which disables pointer events on the page) is open, and Sonner's hotkey moves keyboard focus into
14
+ * them past the modal's focus trap, so their actions stay usable from a dialog.
15
+ */
9
16
  const ThemedToaster = (t0) => {
10
- const $ = _c(12);
17
+ const $ = _c(17);
18
+ let className;
11
19
  let props;
12
20
  let theme;
13
21
  if ($[0] !== t0) {
14
- ({theme, ...props} = t0);
22
+ ({theme, className, ...props} = t0);
15
23
  $[0] = t0;
16
- $[1] = props;
17
- $[2] = theme;
24
+ $[1] = className;
25
+ $[2] = props;
26
+ $[3] = theme;
18
27
  } else {
19
- props = $[1];
20
- theme = $[2];
28
+ className = $[1];
29
+ props = $[2];
30
+ theme = $[3];
21
31
  }
22
32
  const { resolvedTheme } = useTheme();
23
33
  const activeTheme = theme ?? (resolvedTheme === "dark" ? "dark" : "light");
24
34
  let t1;
25
- if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
26
- t1 = /* @__PURE__ */ _jsx(MotionSlot, {
35
+ if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
36
+ t1 = [];
37
+ $[4] = t1;
38
+ } else {
39
+ t1 = $[4];
40
+ }
41
+ useEffect(_temp, t1);
42
+ let t2;
43
+ if ($[5] !== className) {
44
+ t2 = cn("toaster group pointer-events-auto", className);
45
+ $[5] = className;
46
+ $[6] = t2;
47
+ } else {
48
+ t2 = $[6];
49
+ }
50
+ let t3;
51
+ if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
52
+ t3 = /* @__PURE__ */ _jsx(MotionSlot, {
27
53
  kind: "selection",
28
54
  children: /* @__PURE__ */ _jsx(CircleCheck, { className: "size-4" })
29
55
  });
30
- $[3] = t1;
56
+ $[7] = t3;
31
57
  } else {
32
- t1 = $[3];
58
+ t3 = $[7];
33
59
  }
34
- let t2;
35
- if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
36
- t2 = /* @__PURE__ */ _jsx(MotionSlot, {
60
+ let t4;
61
+ if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
62
+ t4 = /* @__PURE__ */ _jsx(MotionSlot, {
37
63
  kind: "selection",
38
64
  children: /* @__PURE__ */ _jsx(Info, { className: "size-4" })
39
65
  });
40
- $[4] = t2;
66
+ $[8] = t4;
41
67
  } else {
42
- t2 = $[4];
68
+ t4 = $[8];
43
69
  }
44
- let t3;
45
- if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
46
- t3 = /* @__PURE__ */ _jsx(MotionSlot, {
70
+ let t5;
71
+ if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
72
+ t5 = /* @__PURE__ */ _jsx(MotionSlot, {
47
73
  kind: "selection",
48
74
  children: /* @__PURE__ */ _jsx(TriangleAlert, { className: "size-4" })
49
75
  });
50
- $[5] = t3;
76
+ $[9] = t5;
51
77
  } else {
52
- t3 = $[5];
78
+ t5 = $[9];
53
79
  }
54
- let t4;
55
- let t5;
56
- if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
57
- t4 = {
58
- success: t1,
59
- info: t2,
60
- warning: t3,
80
+ let t6;
81
+ let t7;
82
+ if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
83
+ t6 = {
84
+ success: t3,
85
+ info: t4,
86
+ warning: t5,
61
87
  error: /* @__PURE__ */ _jsx(MotionSlot, {
62
88
  kind: "selection",
63
89
  children: /* @__PURE__ */ _jsx(OctagonAlert, { className: "size-4" })
64
90
  }),
65
91
  loading: /* @__PURE__ */ _jsx(Loader, { className: "size-4 animate-spin" })
66
92
  };
67
- t5 = {
93
+ t7 = {
68
94
  "--normal-bg": "var(--popover)",
69
95
  "--normal-text": "var(--popover-foreground)",
70
96
  "--normal-border": "var(--border)",
71
97
  "--border-radius": "var(--radius)"
72
98
  };
73
- $[6] = t4;
74
- $[7] = t5;
99
+ $[10] = t6;
100
+ $[11] = t7;
75
101
  } else {
76
- t4 = $[6];
77
- t5 = $[7];
102
+ t6 = $[10];
103
+ t7 = $[11];
78
104
  }
79
- let t6;
80
- if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
81
- t6 = { classNames: { toast: "cn-toast" } };
82
- $[8] = t6;
105
+ let t8;
106
+ if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
107
+ t8 = { classNames: { toast: "cn-toast" } };
108
+ $[12] = t8;
83
109
  } else {
84
- t6 = $[8];
110
+ t8 = $[12];
85
111
  }
86
- let t7;
87
- if ($[9] !== activeTheme || $[10] !== props) {
88
- t7 = /* @__PURE__ */ _jsx(Sonner, {
112
+ let t9;
113
+ if ($[13] !== activeTheme || $[14] !== props || $[15] !== t2) {
114
+ t9 = /* @__PURE__ */ _jsx(Sonner, {
89
115
  theme: activeTheme,
90
- className: "toaster group",
91
- icons: t4,
92
- style: t5,
93
- toastOptions: t6,
116
+ className: t2,
117
+ icons: t6,
118
+ style: t7,
119
+ toastOptions: t8,
94
120
  ...props
95
121
  });
96
- $[9] = activeTheme;
97
- $[10] = props;
98
- $[11] = t7;
122
+ $[13] = activeTheme;
123
+ $[14] = props;
124
+ $[15] = t2;
125
+ $[16] = t9;
99
126
  } else {
100
- t7 = $[11];
127
+ t9 = $[16];
101
128
  }
102
- return t7;
129
+ return t9;
103
130
  };
104
131
  export { ThemedToaster };
132
+ function _temp() {
133
+ return letFocusReachToastLayer(document);
134
+ }
@@ -0,0 +1,3 @@
1
+ /** Contract with the toast layer rendered by `ThemedToaster` (Sonner). */
2
+ /** Selector of the list Sonner renders for each toast position, the outermost element of the toast layer. */
3
+ export declare const TOAST_LAYER_SELECTOR = "[data-sonner-toaster]";
@@ -0,0 +1,3 @@
1
+ /** Contract with the toast layer rendered by `ThemedToaster` (Sonner). */
2
+ /** Selector of the list Sonner renders for each toast position, the outermost element of the toast layer. */
3
+ export const TOAST_LAYER_SELECTOR = "[data-sonner-toaster]";
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Tells whether an event target belongs to the toast layer.
3
+ * @param target - Target of the outside interaction.
4
+ * @returns `true` when the target is a toast or one of its controls.
5
+ */
6
+ export declare function isToastLayerTarget(target: EventTarget | null): boolean;
7
+ /**
8
+ * Lets keyboard focus enter the toast layer while a modal traps focus. Radix's focus trap listens for `focusin` and
9
+ * `focusout` on the document and pulls focus back into the modal whenever it lands outside, so Sonner's hotkey could
10
+ * never move focus to a toast's controls. Focus changes that land on a toast stop at the body, after React has run
11
+ * its own handlers and before the document-level trap sees them; every other focus change still reaches the trap.
12
+ * Because the trap never sees focus leave, it cannot pull it back either: when the focused toast control is removed
13
+ * (an action or close button dismisses its toast), focus returns to the element it came from instead of the body.
14
+ * @param ownerDocument - Document whose body hosts the app and the modal portals.
15
+ * @returns Removes the listeners.
16
+ */
17
+ export declare function letFocusReachToastLayer(ownerDocument: Document): () => void;
18
+ /**
19
+ * Wraps a modal's `onInteractOutside` so presses on toasts never dismiss it. The consumer's handler still runs first
20
+ * and can keep preventing dismissal for its own reasons.
21
+ * @param handler - The consumer's `onInteractOutside`, if any.
22
+ * @returns The handler to give the Radix content.
23
+ */
24
+ export declare function keepOpenOnToastInteraction<OutsideEvent extends Event>(handler?: (event: OutsideEvent) => void): (event: OutsideEvent) => void;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Keeps toasts usable while a modal overlay is open. Toasts sit above every overlay, but a modal `Dialog` or `Sheet`
3
+ * treats a press on them as a press outside and closes, so a toast's action or close button would dismiss the form
4
+ * that raised it instead of running.
5
+ */
6
+ import { TOAST_LAYER_SELECTOR } from "../constants/toast.js";
7
+ /**
8
+ * Tells whether an event target belongs to the toast layer.
9
+ * @param target - Target of the outside interaction.
10
+ * @returns `true` when the target is a toast or one of its controls.
11
+ */
12
+ export function isToastLayerTarget(target) {
13
+ return target instanceof Element && target.closest(TOAST_LAYER_SELECTOR) !== null;
14
+ }
15
+ /**
16
+ * Lets keyboard focus enter the toast layer while a modal traps focus. Radix's focus trap listens for `focusin` and
17
+ * `focusout` on the document and pulls focus back into the modal whenever it lands outside, so Sonner's hotkey could
18
+ * never move focus to a toast's controls. Focus changes that land on a toast stop at the body, after React has run
19
+ * its own handlers and before the document-level trap sees them; every other focus change still reaches the trap.
20
+ * Because the trap never sees focus leave, it cannot pull it back either: when the focused toast control is removed
21
+ * (an action or close button dismisses its toast), focus returns to the element it came from instead of the body.
22
+ * @param ownerDocument - Document whose body hosts the app and the modal portals.
23
+ * @returns Removes the listeners.
24
+ */
25
+ export function letFocusReachToastLayer(ownerDocument) {
26
+ const { body } = ownerDocument;
27
+ let focusOrigin = null;
28
+ let focusedToastElement = null;
29
+ const restoreFocusAfterRemoval = new MutationObserver(() => {
30
+ if (focusedToastElement?.isConnected !== false) return;
31
+ const activeElement = ownerDocument.activeElement;
32
+ if (activeElement === null || activeElement === body) focusOrigin?.focus({ preventScroll: true });
33
+ stopTrackingToastFocus();
34
+ });
35
+ const stopTrackingToastFocus = () => {
36
+ restoreFocusAfterRemoval.disconnect();
37
+ focusOrigin = null;
38
+ focusedToastElement = null;
39
+ };
40
+ const stopFocusIntoToast = (event) => {
41
+ const focusedElement = event.type === "focusin" ? event.target : event.relatedTarget;
42
+ if (!isToastLayerTarget(focusedElement)) {
43
+ if (event.type === "focusin") stopTrackingToastFocus();
44
+ return;
45
+ }
46
+ event.stopPropagation();
47
+ if (event.type !== "focusin") return;
48
+ if (focusedToastElement === null) {
49
+ focusOrigin = event.relatedTarget instanceof HTMLElement ? event.relatedTarget : null;
50
+ restoreFocusAfterRemoval.observe(body, {
51
+ childList: true,
52
+ subtree: true
53
+ });
54
+ }
55
+ focusedToastElement = focusedElement;
56
+ };
57
+ body.addEventListener("focusin", stopFocusIntoToast);
58
+ body.addEventListener("focusout", stopFocusIntoToast);
59
+ return () => {
60
+ stopTrackingToastFocus();
61
+ body.removeEventListener("focusin", stopFocusIntoToast);
62
+ body.removeEventListener("focusout", stopFocusIntoToast);
63
+ };
64
+ }
65
+ /**
66
+ * Wraps a modal's `onInteractOutside` so presses on toasts never dismiss it. The consumer's handler still runs first
67
+ * and can keep preventing dismissal for its own reasons.
68
+ * @param handler - The consumer's `onInteractOutside`, if any.
69
+ * @returns The handler to give the Radix content.
70
+ */
71
+ export function keepOpenOnToastInteraction(handler) {
72
+ return (event) => {
73
+ handler?.(event);
74
+ if (isToastLayerTarget(event.target)) event.preventDefault();
75
+ };
76
+ }