annotate design_system/src/components/notifications.js @ 253:fdf3816959cb

[ui] Add Sonner-like notification stack Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Tue, 04 Aug 2026 12:21:19 -0700
parents 7a7581f040e8
children 2b6e732087ff
Ignore whitespace changes - Everywhere: Within whitespace: At end of lines:
rev   line source
252
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
1 const MAX_VISIBLE = 3;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
2 const MAX_QUEUED = 20;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
3 const MAX_RECORDS = MAX_VISIBLE + MAX_QUEUED;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
4 const DEFAULT_DURATION_MS = 5000;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
5 const MIN_DURATION_MS = 250;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
6 const MAX_DURATION_MS = 120000;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
7 const TONES = new Set(["info", "success", "warning", "error"]);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
8 const ANNOUNCEMENTS = new Set(["none", "polite", "assertive"]);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
9
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
10 function isShortString(value, maximum, allowEmpty = false) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
11 return typeof value === "string" &&
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
12 value.length <= maximum &&
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
13 (allowEmpty || value.trim().length > 0);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
14 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
15
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
16 export function validateNotification(value) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
17 if (!value ||
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
18 value.version !== 1 ||
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
19 !isShortString(value.id, 128) ||
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
20 !TONES.has(value.tone) ||
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
21 !isShortString(value.message, 500) ||
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
22 !ANNOUNCEMENTS.has(value.announcement)) return null;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
23 if (value.description !== undefined &&
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
24 !isShortString(value.description, 1000, true)) return null;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
25 if (value.persistent !== undefined &&
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
26 typeof value.persistent !== "boolean") return null;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
27 if (value.durationMs !== undefined &&
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
28 (!Number.isFinite(value.durationMs) ||
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
29 value.durationMs < MIN_DURATION_MS ||
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
30 value.durationMs > MAX_DURATION_MS)) return null;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
31
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
32 let action;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
33 if (value.action !== undefined) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
34 if (!value.action ||
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
35 !isShortString(value.action.token, 256) ||
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
36 !isShortString(value.action.label, 120)) return null;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
37 action = Object.freeze({
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
38 token: value.action.token,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
39 label: value.action.label,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
40 });
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
41 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
42
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
43 return Object.freeze({
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
44 version: 1,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
45 id: value.id,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
46 tone: value.tone,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
47 message: value.message,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
48 description: value.description || "",
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
49 announcement: value.announcement,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
50 durationMs: value.durationMs ?? DEFAULT_DURATION_MS,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
51 persistent: value.persistent === true,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
52 action,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
53 });
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
54 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
55
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
56 export function validateNotificationDismiss(value) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
57 if (!value ||
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
58 value.version !== 1 ||
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
59 !isShortString(value.id, 128)) return null;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
60 return value.id;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
61 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
62
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
63 function makeRecord(notification, announced = false) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
64 return {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
65 ...notification,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
66 announced,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
67 pauseReasons: new Set(),
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
68 remainingMs: notification.durationMs,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
69 startedAt: 0,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
70 timer: null,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
71 view: null,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
72 };
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
73 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
74
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
75 export class ZenNotifications extends HTMLElement {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
76 static version = 1;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
77
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
78 #records = new Map();
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
79 #announcementQueues = {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
80 polite: [],
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
81 assertive: [],
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
82 };
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
83 #announcementTimers = {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
84 polite: null,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
85 assertive: null,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
86 };
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
87 #announcementRunning = {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
88 polite: false,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
89 assertive: false,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
90 };
253
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
91 #exitTimers = new Map();
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
92 #exitingElements = new Set();
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
93 #layoutFrame = null;
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
94 #resizeObserver = null;
252
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
95
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
96 constructor() {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
97 super();
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
98 this.handleNotify = event => {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
99 if (!(event instanceof CustomEvent) ||
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
100 !event.bubbles ||
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
101 event.composed) return;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
102 event.stopPropagation();
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
103 this.notify(event.detail);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
104 };
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
105 this.handleDismiss = event => {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
106 if (!(event instanceof CustomEvent) ||
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
107 !event.bubbles ||
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
108 event.composed) return;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
109 event.stopPropagation();
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
110 const id = validateNotificationDismiss(event.detail);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
111 if (id) this.dismiss(id, "programmatic");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
112 };
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
113 this.handleVisibility = () => {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
114 for (const record of this.#records.values()) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
115 if (document.hidden) this.pause(record, "hidden");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
116 else this.resume(record, "hidden");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
117 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
118 };
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
119 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
120
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
121 connectedCallback() {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
122 if (!this.stack) this.createInternalElements();
253
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
123 if (!this.#resizeObserver &&
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
124 typeof ResizeObserver === "function") {
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
125 this.#resizeObserver = new ResizeObserver(() => {
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
126 this.requestLayout();
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
127 });
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
128 }
252
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
129 this.addEventListener("zen-notify", this.handleNotify);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
130 this.addEventListener(
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
131 "zen-dismiss-notification",
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
132 this.handleDismiss,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
133 );
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
134 document.addEventListener(
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
135 "visibilitychange",
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
136 this.handleVisibility,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
137 );
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
138 this.render();
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
139 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
140
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
141 disconnectedCallback() {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
142 this.removeEventListener("zen-notify", this.handleNotify);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
143 this.removeEventListener(
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
144 "zen-dismiss-notification",
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
145 this.handleDismiss,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
146 );
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
147 document.removeEventListener(
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
148 "visibilitychange",
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
149 this.handleVisibility,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
150 );
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
151 for (const record of this.#records.values()) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
152 this.clearTimer(record);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
153 }
253
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
154 cancelAnimationFrame(this.#layoutFrame);
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
155 this.#layoutFrame = null;
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
156 this.#resizeObserver?.disconnect();
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
157 for (const timer of this.#exitTimers.values()) {
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
158 clearTimeout(timer);
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
159 }
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
160 this.#exitTimers.clear();
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
161 this.#exitingElements.clear();
252
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
162 this.#records.clear();
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
163 for (const priority of ["polite", "assertive"]) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
164 clearTimeout(this.#announcementTimers[priority]);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
165 this.#announcementTimers[priority] = null;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
166 this.#announcementQueues[priority].length = 0;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
167 this.#announcementRunning[priority] = false;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
168 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
169 for (const element of this.querySelectorAll(
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
170 ":scope > [data-zen-notifications-internal]",
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
171 )) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
172 element.remove();
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
173 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
174 this.stack = null;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
175 this.politeRegion = null;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
176 this.assertiveRegion = null;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
177 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
178
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
179 createInternalElements() {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
180 const label = this.getAttribute("aria-label") || "Notifications";
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
181 this.stack = document.createElement("section");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
182 this.stack.dataset.zenNotificationStack = "";
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
183 this.stack.dataset.zenNotificationsInternal = "";
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
184 this.stack.setAttribute("role", "region");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
185 this.stack.setAttribute("aria-label", label);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
186
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
187 this.politeRegion = document.createElement("span");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
188 this.politeRegion.dataset.zenLive = "polite";
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
189 this.politeRegion.dataset.zenNotificationsInternal = "";
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
190 this.politeRegion.className = "zen-visually-hidden";
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
191 this.politeRegion.setAttribute("aria-live", "polite");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
192 this.politeRegion.setAttribute("aria-atomic", "true");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
193
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
194 this.assertiveRegion = document.createElement("span");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
195 this.assertiveRegion.dataset.zenLive = "assertive";
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
196 this.assertiveRegion.dataset.zenNotificationsInternal = "";
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
197 this.assertiveRegion.className = "zen-visually-hidden";
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
198 this.assertiveRegion.setAttribute("aria-live", "assertive");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
199 this.assertiveRegion.setAttribute("aria-atomic", "true");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
200
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
201 this.append(
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
202 this.stack,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
203 this.politeRegion,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
204 this.assertiveRegion,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
205 );
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
206 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
207
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
208 get size() {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
209 return this.#records.size;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
210 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
211
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
212 get visibleCount() {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
213 return Math.min(this.#records.size, MAX_VISIBLE);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
214 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
215
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
216 notify(value) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
217 const notification = validateNotification(value);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
218 if (!notification) return false;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
219
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
220 const previous = this.#records.get(notification.id);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
221 if (!previous && this.#records.size >= MAX_RECORDS) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
222 const oldestFinite = [...this.#records.values()].find(
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
223 record => !record.persistent,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
224 );
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
225 if (!oldestFinite) return false;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
226 this.removeRecord(oldestFinite.id, "overflow", false);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
227 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
228
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
229 let record;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
230 if (previous) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
231 this.clearTimer(previous);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
232 Object.assign(previous, notification);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
233 previous.remainingMs = notification.durationMs;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
234 this.#records.delete(notification.id);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
235 record = previous;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
236 } else {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
237 record = makeRecord(notification);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
238 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
239 this.#records.set(notification.id, record);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
240 this.render();
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
241 return true;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
242 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
243
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
244 dismiss(id, reason = "dismissed") {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
245 if (!isShortString(id, 128) || !this.#records.has(id)) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
246 return false;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
247 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
248 this.removeRecord(id, reason, true);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
249 return true;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
250 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
251
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
252 removeRecord(id, reason, render) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
253 const record = this.#records.get(id);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
254 if (!record) return;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
255 this.clearTimer(record);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
256 this.#records.delete(id);
253
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
257 const article = record.view?.article;
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
258 if (article && render && article.isConnected) {
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
259 article.dataset.removing = "true";
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
260 this.#exitingElements.add(article);
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
261 const exitTimer = setTimeout(() => {
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
262 this.#resizeObserver?.unobserve(article);
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
263 article.remove();
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
264 this.#exitingElements.delete(article);
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
265 this.#exitTimers.delete(article);
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
266 this.requestLayout();
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
267 }, 180);
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
268 this.#exitTimers.set(article, exitTimer);
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
269 } else if (article) {
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
270 this.#resizeObserver?.unobserve(article);
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
271 article.remove();
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
272 }
252
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
273 record.view = null;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
274 this.dispatchEvent(new CustomEvent("zen-notification-removed", {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
275 bubbles: true,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
276 composed: false,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
277 detail: Object.freeze({
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
278 version: 1,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
279 id,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
280 reason,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
281 }),
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
282 }));
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
283 if (render) this.render();
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
284 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
285
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
286 clearTimer(record) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
287 if (!record.timer) return;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
288 clearTimeout(record.timer);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
289 record.remainingMs = Math.max(
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
290 0,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
291 record.remainingMs - (performance.now() - record.startedAt),
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
292 );
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
293 record.timer = null;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
294 record.startedAt = 0;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
295 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
296
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
297 pause(record, reason) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
298 if (record.persistent || record.pauseReasons.has(reason)) return;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
299 record.pauseReasons.add(reason);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
300 this.clearTimer(record);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
301 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
302
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
303 resume(record, reason) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
304 if (record.persistent) return;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
305 record.pauseReasons.delete(reason);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
306 this.schedule(record);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
307 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
308
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
309 schedule(record) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
310 if (record.persistent ||
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
311 record.timer ||
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
312 record.pauseReasons.size > 0 ||
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
313 !this.#records.has(record.id)) return;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
314 if (record.remainingMs <= 0) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
315 this.dismiss(record.id, "timeout");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
316 return;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
317 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
318 record.startedAt = performance.now();
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
319 record.timer = setTimeout(() => {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
320 record.timer = null;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
321 record.remainingMs = 0;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
322 this.dismiss(record.id, "timeout");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
323 }, record.remainingMs);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
324 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
325
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
326 announce(record) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
327 if (record.announced) return;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
328 record.announced = true;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
329 if (record.announcement === "none") return;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
330 this.#announcementQueues[record.announcement].push({
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
331 id: record.id,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
332 message: record.message,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
333 });
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
334 this.runAnnouncementQueue(record.announcement);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
335 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
336
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
337 runAnnouncementQueue(priority) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
338 if (this.#announcementRunning[priority]) return;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
339 const next = this.#announcementQueues[priority].shift();
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
340 if (!next) return;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
341 this.#announcementRunning[priority] = true;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
342 const region = priority === "assertive"
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
343 ? this.assertiveRegion
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
344 : this.politeRegion;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
345 region.textContent = "";
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
346 this.#announcementTimers[priority] = setTimeout(() => {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
347 if (this.isConnected && this.#records.has(next.id)) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
348 region.textContent = next.message;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
349 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
350 this.#announcementTimers[priority] = setTimeout(() => {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
351 this.#announcementTimers[priority] = null;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
352 this.#announcementRunning[priority] = false;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
353 this.runAnnouncementQueue(priority);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
354 }, 120);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
355 }, 16);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
356 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
357
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
358 createNotificationElement(record) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
359 if (!record.view) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
360 const article = document.createElement("article");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
361 article.dataset.zenNotificationId = record.id;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
362
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
363 const tone = document.createElement("span");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
364 tone.className = "zen-notification-tone";
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
365 tone.setAttribute("aria-hidden", "true");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
366
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
367 const content = document.createElement("div");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
368 content.className = "zen-notification-content";
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
369 const message = document.createElement("p");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
370 message.className = "zen-notification-message";
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
371 content.append(message);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
372
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
373 const dismiss = document.createElement("button");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
374 dismiss.type = "button";
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
375 dismiss.className = "zen-notification-dismiss";
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
376 dismiss.textContent = "×";
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
377 dismiss.addEventListener("click", () => {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
378 this.dismiss(record.id, "dismissed");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
379 });
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
380
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
381 article.addEventListener("pointerenter", () => {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
382 this.pause(record, "hover");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
383 });
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
384 article.addEventListener("pointerleave", () => {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
385 this.resume(record, "hover");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
386 });
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
387 article.addEventListener("focusin", () => {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
388 this.pause(record, "focus");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
389 });
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
390 article.addEventListener("focusout", event => {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
391 if (!article.contains(event.relatedTarget)) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
392 this.resume(record, "focus");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
393 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
394 });
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
395
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
396 article.append(tone, content, dismiss);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
397 record.view = {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
398 action: null,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
399 article,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
400 content,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
401 description: null,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
402 dismiss,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
403 message,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
404 tone,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
405 };
253
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
406 this.#resizeObserver?.observe(article);
252
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
407 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
408
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
409 const view = record.view;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
410 view.article.dataset.tone = record.tone;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
411 view.tone.textContent = {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
412 info: "i",
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
413 success: "✓",
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
414 warning: "!",
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
415 error: "×",
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
416 }[record.tone];
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
417 view.message.textContent = record.message;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
418 view.dismiss.setAttribute(
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
419 "aria-label",
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
420 this.getAttribute("dismiss-label") || "Dismiss notification",
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
421 );
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
422
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
423 if (record.description) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
424 if (!view.description) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
425 view.description = document.createElement("p");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
426 view.description.className = "zen-notification-description";
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
427 view.content.insertBefore(
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
428 view.description,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
429 view.action,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
430 );
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
431 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
432 view.description.textContent = record.description;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
433 } else if (view.description) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
434 view.description.remove();
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
435 view.description = null;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
436 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
437
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
438 if (record.action) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
439 if (!view.action) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
440 view.action = document.createElement("button");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
441 view.action.type = "button";
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
442 view.action.className = "zen-notification-action";
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
443 view.action.addEventListener("click", () => {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
444 this.dispatchEvent(new CustomEvent("zen-notification-action", {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
445 bubbles: true,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
446 composed: false,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
447 detail: Object.freeze({
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
448 version: 1,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
449 id: record.id,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
450 token: record.action.token,
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
451 }),
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
452 }));
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
453 });
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
454 view.content.append(view.action);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
455 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
456 view.action.textContent = record.action.label;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
457 } else if (view.action) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
458 view.action.remove();
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
459 view.action = null;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
460 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
461 return view.article;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
462 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
463
253
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
464 requestLayout() {
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
465 cancelAnimationFrame(this.#layoutFrame);
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
466 this.#layoutFrame = requestAnimationFrame(() => {
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
467 this.#layoutFrame = null;
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
468 this.layoutStack();
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
469 });
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
470 }
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
471
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
472 layoutStack() {
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
473 if (!this.stack) return;
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
474 const visible = [...this.#records.values()]
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
475 .slice(-MAX_VISIBLE)
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
476 .filter(record => record.view?.article.isConnected);
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
477 const styles = getComputedStyle(this.stack);
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
478 const configuredGap = Number.parseFloat(
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
479 styles.getPropertyValue("--zen-notification-gap"),
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
480 );
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
481 const gap = Number.isFinite(configuredGap)
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
482 ? configuredGap
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
483 : 12;
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
484 let expandedHeight = 0;
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
485 let newestHeight = 0;
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
486
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
487 for (let index = visible.length - 1; index >= 0; index--) {
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
488 const record = visible[index];
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
489 const article = record.view.article;
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
490 const depth = visible.length - 1 - index;
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
491 const height = article.offsetHeight;
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
492 if (depth === 0) newestHeight = height;
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
493 article.style.setProperty(
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
494 "--zen-notification-depth",
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
495 String(depth),
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
496 );
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
497 article.style.setProperty(
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
498 "--zen-notification-collapsed-offset",
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
499 `${-depth * 10}px`,
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
500 );
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
501 article.style.setProperty(
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
502 "--zen-notification-collapsed-opacity",
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
503 String(Math.max(0.55, 1 - depth * 0.18)),
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
504 );
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
505 article.dataset.depth = String(depth);
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
506 article.style.setProperty(
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
507 "--zen-notification-offset",
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
508 `${-expandedHeight}px`,
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
509 );
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
510 article.style.setProperty(
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
511 "--zen-notification-scale",
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
512 String(Math.max(0.88, 1 - depth * 0.055)),
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
513 );
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
514 article.style.zIndex = String(visible.length - depth);
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
515 expandedHeight += height + gap;
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
516 }
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
517
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
518 this.stack.dataset.count = String(visible.length);
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
519 this.stack.style.setProperty(
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
520 "--zen-notification-collapsed-height",
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
521 `${newestHeight + Math.max(0, visible.length - 1) * 10}px`,
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
522 );
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
523 this.stack.style.setProperty(
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
524 "--zen-notification-expanded-height",
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
525 `${Math.max(0, expandedHeight - gap)}px`,
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
526 );
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
527 }
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
528
252
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
529 render() {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
530 if (!this.stack) return;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
531 const records = [...this.#records.values()];
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
532 const visible = records.slice(-MAX_VISIBLE);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
533 const visibleIds = new Set(visible.map(record => record.id));
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
534
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
535 for (const record of records) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
536 if (visibleIds.has(record.id)) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
537 this.resume(record, "queued");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
538 } else {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
539 this.pause(record, "queued");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
540 this.resume(record, "hover");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
541 this.resume(record, "focus");
253
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
542 if (record.view?.article) {
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
543 this.#resizeObserver?.unobserve(record.view.article);
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
544 record.view.article.remove();
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
545 }
252
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
546 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
547 if (document.hidden) this.pause(record, "hidden");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
548 else this.resume(record, "hidden");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
549 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
550
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
551 const elements = visible.map(record =>
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
552 this.createNotificationElement(record)
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
553 );
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
554 for (let index = 0; index < elements.length; index++) {
253
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
555 const current = [...this.stack.children]
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
556 .filter(element => !this.#exitingElements.has(element))[index];
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
557 if (current !== elements[index]) {
252
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
558 this.stack.insertBefore(
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
559 elements[index],
253
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
560 current || null,
252
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
561 );
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
562 }
253
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
563 this.#resizeObserver?.observe(elements[index]);
252
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
564 }
253
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
565 const desired = new Set(elements);
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
566 for (const element of [...this.stack.children]) {
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
567 if (!desired.has(element) &&
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
568 !this.#exitingElements.has(element)) {
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
569 element.remove();
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
570 }
252
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
571 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
572
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
573 for (const record of visible) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
574 const element = record.view.article;
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
575 if (element.matches(":hover")) this.pause(record, "hover");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
576 else this.resume(record, "hover");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
577 if (element.contains(document.activeElement)) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
578 this.pause(record, "focus");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
579 } else this.resume(record, "focus");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
580 this.announce(record);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
581 this.schedule(record);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
582 }
253
fdf3816959cb [ui] Add Sonner-like notification stack
MrJuneJune <me@mrjunejune.com>
parents: 252
diff changeset
583 this.requestLayout();
252
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
584 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
585 }
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
586
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
587 const existing = customElements.get("zen-notifications");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
588 if (!existing) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
589 customElements.define("zen-notifications", ZenNotifications);
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
590 } else if (existing.version !== ZenNotifications.version) {
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
591 throw new Error("Incompatible zen-notifications component");
7a7581f040e8 [ui] Add scoped notification component
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
592 }