@mantine-vue/notifications 3.1.0 → 3.3.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/esm/index.mjs CHANGED
@@ -104,7 +104,9 @@ function getAutoClose(autoClose, notificationAutoClose) {
104
104
  }
105
105
  //#endregion
106
106
  //#region src/components/NotificationContainer/NotificationContainer.vue?vue&type=script&setup=true&lang.ts
107
+ var _hoisted_1 = ["role", "inert"];
107
108
  var SCROLL_DISMISS_RESET_TIMEOUT = 120;
109
+ var MAX_VISIBLE_STACK_DEPTH = 4;
108
110
  //#endregion
109
111
  //#region src/components/NotificationContainer/NotificationContainer.vue
110
112
  var NotificationContainer_default = /* @__PURE__ */ defineComponent({
@@ -117,12 +119,22 @@ var NotificationContainer_default = /* @__PURE__ */ defineComponent({
117
119
  transitionDuration: {},
118
120
  allowDragDismiss: { type: Boolean },
119
121
  allowScrollDismiss: { type: Boolean },
120
- paused: { type: Boolean }
122
+ paused: { type: Boolean },
123
+ renderNotification: { type: [Function, null] },
124
+ layout: {},
125
+ stackIndex: {},
126
+ stackSize: {},
127
+ stackPosition: {},
128
+ stackExpanded: { type: Boolean },
129
+ stackExpandedOffset: {},
130
+ transitionState: {}
121
131
  },
122
132
  emits: [
123
133
  "hide",
124
134
  "hoverStart",
125
- "hoverEnd"
135
+ "hoverEnd",
136
+ "expandRequest",
137
+ "heightChange"
126
138
  ],
127
139
  setup(__props, { emit: __emit }) {
128
140
  const props = __props;
@@ -135,11 +147,14 @@ var NotificationContainer_default = /* @__PURE__ */ defineComponent({
135
147
  const active = ref(false);
136
148
  const scrollDismissActive = ref(false);
137
149
  const hovered = ref(false);
150
+ const focused = ref(false);
151
+ const contributedActive = ref(false);
138
152
  const startX = ref(0);
139
153
  const startTime = ref(0);
140
154
  let autoCloseTimeout = -1;
141
155
  let hideTimeout = -1;
142
156
  let scrollDismissTimeout = -1;
157
+ let resizeObserver;
143
158
  const autoCloseDuration = computed(() => getAutoClose(props.autoClose, props.data.autoClose));
144
159
  const isCloseDisabled = computed(() => props.data.allowClose === false);
145
160
  function normalizeStyle(style) {
@@ -157,7 +172,10 @@ var NotificationContainer_default = /* @__PURE__ */ defineComponent({
157
172
  "withCloseButton",
158
173
  "onOpen",
159
174
  "onClose",
160
- "id"
175
+ "id",
176
+ "renderNotification",
177
+ "priority",
178
+ "__sequence"
161
179
  ]);
162
180
  return Object.fromEntries(Object.entries(props.data).filter(([key]) => !omittedKeys.has(key)).map(([key, value]) => [key, cloneRenderable(value)]));
163
181
  }
@@ -167,6 +185,7 @@ var NotificationContainer_default = /* @__PURE__ */ defineComponent({
167
185
  return value;
168
186
  }
169
187
  const renderMessage = () => cloneRenderable(typeof props.data.message === "function" ? props.data.message() : props.data.message);
188
+ const renderCustomNotification = () => cloneRenderable(props.renderNotification?.(props.data));
170
189
  const notificationStyle = computed(() => {
171
190
  const baseStyle = {
172
191
  ...normalizeStyle(attrs.style),
@@ -175,19 +194,44 @@ var NotificationContainer_default = /* @__PURE__ */ defineComponent({
175
194
  const baseOpacity = typeof baseStyle.opacity === "number" ? baseStyle.opacity : 1;
176
195
  const swipeOpacity = dismissed.value ? 0 : 1 - Math.min(Math.abs(offset.value) / 200, 1) * .6;
177
196
  const transitionDuration = baseStyle.transitionDuration ?? `${props.transitionDuration}ms, ${props.transitionDuration}ms, ${props.transitionDuration}ms`;
197
+ const isStackedLayout = props.layout === "stacked";
198
+ const isStacked = isStackedLayout && (props.stackIndex ?? 0) > 0;
199
+ const isCollapsed = isStacked && !props.stackExpanded;
200
+ const isExiting = props.transitionState === "exiting" || props.transitionState === "exited";
201
+ const stackDirection = props.stackPosition?.startsWith("top") ? 1 : -1;
202
+ const stackDepth = Math.min(props.stackIndex ?? 0, MAX_VISIBLE_STACK_DEPTH);
203
+ const collapsedOffset = isCollapsed ? stackDepth * 10 * stackDirection : 0;
204
+ const staggerDelay = isStackedLayout ? (props.stackIndex ?? 0) * 30 : 0;
205
+ const isDragging = active.value || scrollDismissActive.value;
206
+ let transform = "var(--notifications-state-transform) translate3d(var(--notifications-swipe-offset), 0, 0)";
207
+ if (isStackedLayout) {
208
+ if (isExiting) transform = `translateY(${props.stackExpanded ? props.stackExpandedOffset ?? 0 : collapsedOffset}px) ${transform}`;
209
+ else if (props.stackExpanded) transform = `translateY(${props.stackExpandedOffset ?? 0}px) translate3d(var(--notifications-swipe-offset), 0, 0)`;
210
+ else if (isStacked) transform = `scale(${1 - stackDepth * .03}) translateY(${collapsedOffset}px)`;
211
+ }
178
212
  return {
179
213
  ...baseStyle,
214
+ ...isStackedLayout ? { maxHeight: void 0 } : {},
180
215
  "--notifications-state-transform": typeof baseStyle.transform === "string" ? baseStyle.transform : "translateX(0)",
181
216
  "--notifications-state-opacity": String(baseOpacity),
182
217
  "--notifications-swipe-offset": `${offset.value}px`,
183
218
  "--notifications-swipe-opacity": String(swipeOpacity),
184
- transform: "var(--notifications-state-transform) translate3d(var(--notifications-swipe-offset), 0, 0)",
219
+ transform,
185
220
  opacity: "calc(var(--notifications-state-opacity) * var(--notifications-swipe-opacity))",
186
- transitionDuration: active.value || scrollDismissActive.value ? "0ms, 0ms, 0ms" : transitionDuration,
221
+ transitionDuration: isDragging ? "0ms, 0ms, 0ms" : transitionDuration,
187
222
  cursor: "default",
188
- touchAction: "pan-y"
223
+ touchAction: "pan-y",
224
+ ...isStackedLayout ? {
225
+ gridArea: "1 / 1",
226
+ zIndex: (props.stackSize ?? 5) - (props.stackIndex ?? 0),
227
+ pointerEvents: isCollapsed ? "none" : void 0,
228
+ alignSelf: stackDirection === 1 ? "start" : "end",
229
+ transition: isDragging ? "none" : `transform ${props.transitionDuration}ms cubic-bezier(.51,.3,0,1.21), opacity ${props.transitionDuration}ms ease`,
230
+ transitionDelay: isDragging || isExiting ? "0ms" : `${staggerDelay}ms`
231
+ } : {}
189
232
  };
190
233
  });
234
+ const isCollapsed = computed(() => props.layout === "stacked" && (props.stackIndex ?? 0) > 0 && !props.stackExpanded);
191
235
  const cancelAutoClose = () => window.clearTimeout(autoCloseTimeout);
192
236
  const cancelHide = () => window.clearTimeout(hideTimeout);
193
237
  const cancelScrollDismissReset = () => window.clearTimeout(scrollDismissTimeout);
@@ -198,7 +242,7 @@ var NotificationContainer_default = /* @__PURE__ */ defineComponent({
198
242
  cancelScrollDismissReset();
199
243
  }
200
244
  function handleAutoClose() {
201
- if (typeof window === "undefined" || dismissed.value || active.value || props.paused || hovered.value || typeof autoCloseDuration.value !== "number") return;
245
+ if (typeof window === "undefined" || dismissed.value || active.value || props.paused || hovered.value || focused.value || typeof autoCloseDuration.value !== "number") return;
202
246
  cancelAutoClose();
203
247
  autoCloseTimeout = window.setTimeout(handleHide, autoCloseDuration.value);
204
248
  }
@@ -288,7 +332,34 @@ var NotificationContainer_default = /* @__PURE__ */ defineComponent({
288
332
  function handleMouseEnter() {
289
333
  hovered.value = true;
290
334
  cancelAutoClose();
291
- emit("hoverStart");
335
+ syncActiveContribution();
336
+ }
337
+ function syncActiveContribution() {
338
+ const isActive = hovered.value || focused.value;
339
+ if (isActive && !contributedActive.value) {
340
+ contributedActive.value = true;
341
+ emit("hoverStart");
342
+ } else if (!isActive && contributedActive.value) {
343
+ contributedActive.value = false;
344
+ emit("hoverEnd");
345
+ }
346
+ }
347
+ function handleFocusIn() {
348
+ if (props.layout !== "stacked") return;
349
+ focused.value = true;
350
+ cancelAutoClose();
351
+ syncActiveContribution();
352
+ }
353
+ function handleFocusOut(event) {
354
+ if (props.layout !== "stacked") return;
355
+ const next = event.relatedTarget;
356
+ if (next && notificationRef.value?.contains(next)) return;
357
+ focused.value = false;
358
+ if (!scrollDismissActive.value) handleAutoClose();
359
+ syncActiveContribution();
360
+ }
361
+ function handlePointerDownCapture(event) {
362
+ if (props.layout === "stacked" && (event.pointerType === "touch" || event.pointerType === "pen")) emit("expandRequest");
292
363
  }
293
364
  function handleMouseLeave() {
294
365
  hovered.value = false;
@@ -296,7 +367,7 @@ var NotificationContainer_default = /* @__PURE__ */ defineComponent({
296
367
  resetSwipe();
297
368
  handleAutoClose();
298
369
  }
299
- emit("hoverEnd");
370
+ syncActiveContribution();
300
371
  }
301
372
  watch(scrollDismissActive, (enabled, _, onCleanup) => {
302
373
  if (!enabled) return;
@@ -316,35 +387,69 @@ var NotificationContainer_default = /* @__PURE__ */ defineComponent({
316
387
  props.data.onOpen?.(props.data);
317
388
  notificationRef.value?.addEventListener("wheel", handleWheel, { passive: false });
318
389
  window.addEventListener("resize", handleResize);
390
+ if (notificationRef.value) {
391
+ emit("heightChange", notificationRef.value.offsetHeight);
392
+ if (typeof ResizeObserver !== "undefined") {
393
+ resizeObserver = new ResizeObserver(() => {
394
+ if (notificationRef.value) emit("heightChange", notificationRef.value.offsetHeight);
395
+ });
396
+ resizeObserver.observe(notificationRef.value);
397
+ }
398
+ }
319
399
  });
320
400
  function handleResize() {
321
401
  if (dismissed.value) offset.value = getExitOffset(dismissDirection.value);
322
402
  }
323
403
  onBeforeUnmount(() => {
324
- if (hovered.value) emit("hoverEnd");
404
+ hovered.value = false;
405
+ focused.value = false;
406
+ syncActiveContribution();
325
407
  cancelAutoClose();
326
408
  cancelHide();
327
409
  cancelScrollDismissReset();
328
410
  cancelPointerDrag(false);
329
411
  notificationRef.value?.removeEventListener("wheel", handleWheel);
330
412
  window.removeEventListener("resize", handleResize);
413
+ resizeObserver?.disconnect();
331
414
  });
415
+ function setNotificationRef(node) {
416
+ notificationRef.value = node?.$el ?? node ?? null;
417
+ }
332
418
  return (_ctx, _cache) => {
333
- return openBlock(), createBlock(unref(Notification), mergeProps({
419
+ return props.renderNotification ? (openBlock(), createElementBlock("div", mergeProps({ key: 0 }, unref(attrs), {
420
+ ref: setNotificationRef,
421
+ style: notificationStyle.value,
422
+ role: unref(attrs).role || "alert",
423
+ inert: isCollapsed.value || void 0,
424
+ onMouseenter: handleMouseEnter,
425
+ onMouseleave: handleMouseLeave,
426
+ onFocusin: handleFocusIn,
427
+ onFocusout: handleFocusOut,
428
+ onPointerdownCapture: handlePointerDownCapture,
429
+ onPointerdown: handlePointerDown
430
+ }), [(openBlock(), createBlock(resolveDynamicComponent(renderCustomNotification)))], 16, _hoisted_1)) : (openBlock(), createBlock(unref(Notification), mergeProps({ key: 1 }, {
334
431
  ...unref(attrs),
335
432
  ...getNotificationProps()
336
433
  }, {
337
- ref: (node) => notificationRef.value = node?.$el ?? node,
434
+ ref: setNotificationRef,
338
435
  style: notificationStyle.value,
436
+ inert: isCollapsed.value || void 0,
339
437
  "with-close-button": isCloseDisabled.value ? false : props.data.withCloseButton,
340
438
  onClose: handleHide,
341
439
  onMouseenter: handleMouseEnter,
342
440
  onMouseleave: handleMouseLeave,
441
+ onFocusin: handleFocusIn,
442
+ onFocusout: handleFocusOut,
443
+ onPointerdownCapture: handlePointerDownCapture,
343
444
  onPointerdown: handlePointerDown
344
445
  }), {
345
446
  default: withCtx(() => [(openBlock(), createBlock(resolveDynamicComponent(renderMessage)))]),
346
447
  _: 1
347
- }, 16, ["style", "with-close-button"]);
448
+ }, 16, [
449
+ "style",
450
+ "inert",
451
+ "with-close-button"
452
+ ]));
348
453
  };
349
454
  }
350
455
  });
@@ -416,8 +521,8 @@ function getNotificationStateStyles({ state, maxHeight, position, transitionDura
416
521
  };
417
522
  }
418
523
  var Notifications_module_default = {
419
- root: "_root_1izcx_1",
420
- notification: "_notification_1izcx_40"
524
+ root: "_root_1hrsl_1",
525
+ notification: "_notification_1hrsl_40"
421
526
  };
422
527
  //#endregion
423
528
  //#region src/components/Notifications/Notifications.vue?vue&type=script&setup=true&lang.ts
@@ -457,6 +562,8 @@ var Notifications = Object.assign(/* @__PURE__ */ defineComponent({
457
562
  default: true
458
563
  },
459
564
  pauseResetOnHover: { default: "all" },
565
+ renderNotification: {},
566
+ layout: { default: "default" },
460
567
  classNames: {},
461
568
  styles: {},
462
569
  vars: {},
@@ -468,10 +575,18 @@ var Notifications = Object.assign(/* @__PURE__ */ defineComponent({
468
575
  const props = useProps("Notifications", null, rawProps);
469
576
  const theme = useMantineTheme();
470
577
  const shouldReduceMotion = useReducedMotion();
471
- const hoveredCount = ref(0);
578
+ const STACK_EXPANDED_GAP = 16;
579
+ const FALLBACK_NOTIFICATION_HEIGHT = 80;
580
+ const hoveredCounts = ref({});
581
+ const hoveredGroups = ref({});
582
+ const expandedPositions = ref({});
583
+ const pinnedPosition = ref(null);
584
+ const notificationHeights = ref({});
585
+ const collapseTimeouts = {};
472
586
  const data = useNotifications(props.store);
473
587
  const duration = computed(() => theme.value.respectReducedMotion && shouldReduceMotion.value ? 1 : props.transitionDuration);
474
588
  const grouped = computed(() => getGroupedNotifications(data.value.notifications, props.position));
589
+ const hoveredCount = computed(() => Object.values(hoveredCounts.value).reduce((sum, value) => sum + (value ?? 0), 0) + Object.values(hoveredGroups.value).filter(Boolean).length);
475
590
  const getStyles = useStyles({
476
591
  name: "Notifications",
477
592
  classes: Notifications_module_default,
@@ -517,47 +632,164 @@ var Notifications = Object.assign(/* @__PURE__ */ defineComponent({
517
632
  delete data.style;
518
633
  return data;
519
634
  }
635
+ function isStackExpanded(position) {
636
+ return props.layout === "stacked" && Boolean(expandedPositions.value[position]);
637
+ }
638
+ function orderedGroup(position) {
639
+ const items = grouped.value[position];
640
+ if (props.layout !== "stacked") return items;
641
+ return [...items].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0) || (b.__sequence ?? 0) - (a.__sequence ?? 0));
642
+ }
643
+ function expandedOffset(position, index) {
644
+ if (!isStackExpanded(position)) return 0;
645
+ const direction = position.startsWith("top") ? 1 : -1;
646
+ return orderedGroup(position).slice(0, index).reduce((sum, item) => sum + (notificationHeights.value[item.id] ?? FALLBACK_NOTIFICATION_HEIGHT) + STACK_EXPANDED_GAP, 0) * direction;
647
+ }
648
+ function setExpanded(position, expanded) {
649
+ expandedPositions.value = {
650
+ ...expandedPositions.value,
651
+ [position]: expanded
652
+ };
653
+ }
654
+ function scheduleCollapse(position) {
655
+ window.clearTimeout(collapseTimeouts[position]);
656
+ if ((hoveredCounts.value[position] ?? 0) > 0 || hoveredGroups.value[position] || pinnedPosition.value === position) return;
657
+ collapseTimeouts[position] = window.setTimeout(() => setExpanded(position, false), 200);
658
+ }
659
+ function handleHoverStart(position) {
660
+ hoveredCounts.value = {
661
+ ...hoveredCounts.value,
662
+ [position]: (hoveredCounts.value[position] ?? 0) + 1
663
+ };
664
+ if (props.layout === "stacked") setExpanded(position, true);
665
+ }
666
+ function handleHoverEnd(position) {
667
+ hoveredCounts.value = {
668
+ ...hoveredCounts.value,
669
+ [position]: Math.max(0, (hoveredCounts.value[position] ?? 0) - 1)
670
+ };
671
+ if (props.layout === "stacked") scheduleCollapse(position);
672
+ }
673
+ function handleGroupHover(position, hovered) {
674
+ if (props.layout !== "stacked") return;
675
+ hoveredGroups.value = {
676
+ ...hoveredGroups.value,
677
+ [position]: hovered
678
+ };
679
+ if (hovered) {
680
+ window.clearTimeout(collapseTimeouts[position]);
681
+ setExpanded(position, true);
682
+ } else scheduleCollapse(position);
683
+ }
684
+ function handleExpandRequest(position) {
685
+ pinnedPosition.value = position;
686
+ setExpanded(position, true);
687
+ }
688
+ function handleDocumentPointerDown(event) {
689
+ if (!pinnedPosition.value) return;
690
+ if (!event.target?.closest(`[data-notifications-position="${pinnedPosition.value}"]`)) {
691
+ const previous = pinnedPosition.value;
692
+ pinnedPosition.value = null;
693
+ scheduleCollapse(previous);
694
+ }
695
+ }
696
+ watch(() => props.layout, (layout) => {
697
+ if (layout !== "stacked") {
698
+ pinnedPosition.value = null;
699
+ hoveredGroups.value = {};
700
+ expandedPositions.value = {};
701
+ }
702
+ });
703
+ watch(grouped, (groups) => {
704
+ if (pinnedPosition.value && groups[pinnedPosition.value].length === 0) pinnedPosition.value = null;
705
+ });
706
+ onMounted(() => document.addEventListener("pointerdown", handleDocumentPointerDown));
707
+ onBeforeUnmount(() => {
708
+ document.removeEventListener("pointerdown", handleDocumentPointerDown);
709
+ positions.forEach((position) => window.clearTimeout(collapseTimeouts[position]));
710
+ });
520
711
  return (_ctx, _cache) => {
521
712
  return openBlock(), createBlock(unref(OptionalPortal), mergeProps(unref(props).portalProps, { "within-portal": unref(props).withinPortal }), {
522
713
  default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(unref(positions), (position) => {
523
714
  return openBlock(), createBlock(unref(Box), mergeProps({ key: position }, { ref_for: true }, {
524
715
  ...unref(attrs),
525
716
  ...unref(getStyles)("root")
526
- }, { "data-position": position }), {
717
+ }, {
718
+ "data-position": position,
719
+ "data-notifications-position": position,
720
+ "data-layout": unref(props).layout
721
+ }), {
527
722
  default: withCtx(() => [createVNode(TransitionGroup, {
723
+ tag: "div",
528
724
  css: false,
725
+ onMouseenter: ($event) => handleGroupHover(position, true),
726
+ onMouseleave: ($event) => handleGroupHover(position, false),
727
+ onFocusin: ($event) => handleGroupHover(position, true),
728
+ onFocusout: ($event) => handleGroupHover(position, false),
529
729
  onBeforeEnter: (element) => applyTransitionStyles(element, "exited", position),
530
730
  onEnter: (element, done) => enter(element, done, position),
531
731
  onLeave: (element, done) => leave(element, done, position)
532
732
  }, {
533
- default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(grouped.value[position], (notification) => {
733
+ default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(orderedGroup(position), (notification, stackIndex) => {
534
734
  return openBlock(), createBlock(NotificationContainer_default, mergeProps({ key: notification.id }, { ref_for: true }, unref(getStyles)("notification", { style: notification.style }), {
535
735
  data: notificationData(notification),
536
736
  "auto-close": unref(props).autoClose,
537
737
  "transition-duration": duration.value,
538
738
  "allow-drag-dismiss": unref(props).allowDragDismiss,
539
739
  "allow-scroll-dismiss": unref(props).allowScrollDismiss,
540
- paused: unref(props).pauseResetOnHover === "all" && hoveredCount.value > 0,
740
+ paused: unref(props).pauseResetOnHover === "all" && hoveredCount.value > 0 || unref(props).layout === "stacked" && !isStackExpanded(position) && stackIndex > 0,
741
+ "render-notification": "renderNotification" in notification ? notification.renderNotification : unref(props).renderNotification,
742
+ layout: unref(props).layout,
743
+ "stack-index": stackIndex,
744
+ "stack-size": orderedGroup(position).length,
745
+ "stack-position": position,
746
+ "stack-expanded": isStackExpanded(position),
747
+ "stack-expanded-offset": expandedOffset(position, stackIndex),
748
+ "transition-state": "entered",
541
749
  onHide: _cache[0] || (_cache[0] = (id) => unref(hideNotification)(id, unref(props).store)),
542
- onHoverStart: _cache[1] || (_cache[1] = ($event) => hoveredCount.value += 1),
543
- onHoverEnd: _cache[2] || (_cache[2] = ($event) => hoveredCount.value = Math.max(0, hoveredCount.value - 1))
750
+ onHoverStart: ($event) => handleHoverStart(position),
751
+ onHoverEnd: ($event) => handleHoverEnd(position),
752
+ onExpandRequest: ($event) => handleExpandRequest(position),
753
+ onHeightChange: (height) => notificationHeights.value = {
754
+ ...notificationHeights.value,
755
+ [notification.id]: height
756
+ }
544
757
  }), null, 16, [
545
758
  "data",
546
759
  "auto-close",
547
760
  "transition-duration",
548
761
  "allow-drag-dismiss",
549
762
  "allow-scroll-dismiss",
550
- "paused"
763
+ "paused",
764
+ "render-notification",
765
+ "layout",
766
+ "stack-index",
767
+ "stack-size",
768
+ "stack-position",
769
+ "stack-expanded",
770
+ "stack-expanded-offset",
771
+ "onHoverStart",
772
+ "onHoverEnd",
773
+ "onExpandRequest",
774
+ "onHeightChange"
551
775
  ]);
552
776
  }), 128))]),
553
777
  _: 2
554
778
  }, 1032, [
779
+ "onMouseenter",
780
+ "onMouseleave",
781
+ "onFocusin",
782
+ "onFocusout",
555
783
  "onBeforeEnter",
556
784
  "onEnter",
557
785
  "onLeave"
558
786
  ])]),
559
787
  _: 2
560
- }, 1040, ["data-position"]);
788
+ }, 1040, [
789
+ "data-position",
790
+ "data-notifications-position",
791
+ "data-layout"
792
+ ]);
561
793
  }), 128))]),
562
794
  _: 1
563
795
  }, 16, ["within-portal"]);