varsel 0.5.2 → 0.6.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/dist/VarselItem.svelte +825 -735
- package/dist/VarselItem.svelte.d.ts +1 -0
- package/dist/VarselItem.svelte.d.ts.map +1 -1
- package/dist/VarselManager.svelte +477 -344
- package/dist/VarselManager.svelte.d.ts +1 -1
- package/dist/VarselManager.svelte.d.ts.map +1 -1
- package/dist/VarselToaster.svelte +29 -23
- package/dist/VarselToaster.svelte.d.ts +2 -2
- package/dist/VarselToaster.svelte.d.ts.map +1 -1
- package/dist/core/accessibility.d.ts +14 -0
- package/dist/core/accessibility.d.ts.map +1 -1
- package/dist/core/accessibility.js +64 -0
- package/dist/core/swipe.d.ts +6 -0
- package/dist/core/swipe.d.ts.map +1 -1
- package/dist/core/swipe.js +6 -0
- package/dist/core/toast-factory.d.ts.map +1 -1
- package/dist/core/toast-factory.js +38 -11
- package/dist/core/toast-state.d.ts +8 -4
- package/dist/core/toast-state.d.ts.map +1 -1
- package/dist/core/toast-state.js +19 -5
- package/dist/core/toaster-instances.d.ts +1 -0
- package/dist/core/toaster-instances.d.ts.map +1 -1
- package/dist/core/toaster-instances.js +9 -2
- package/dist/core/types.d.ts +42 -9
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/variants.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/internals.d.ts +4 -4
- package/dist/internals.d.ts.map +1 -1
- package/dist/internals.js +3 -3
- package/dist/styles.css +49 -49
- package/dist/variant-icons.d.ts +25 -51
- package/dist/variant-icons.d.ts.map +1 -1
- package/dist/variant-icons.js +27 -17
- package/package.json +2 -2
|
@@ -1,380 +1,508 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import VarselItem from "./VarselItem.svelte";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
);
|
|
56
|
-
let heldToasts = $state<Record<ToastPosition, Set<string>>>(
|
|
57
|
-
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
let
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
let
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
let
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
let
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
next
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
for (const position of Object.keys(grouped) as ToastPosition[]) {
|
|
114
|
-
const list = grouped[position];
|
|
115
|
-
const activeToasts = list.filter(
|
|
116
|
-
(toast) => !toast.isLeaving && !toast.shouldClose,
|
|
117
|
-
);
|
|
118
|
-
const activeIndexMap = new Map<string, number>();
|
|
119
|
-
activeToasts.forEach((toast, activeIndex) => {
|
|
120
|
-
activeIndexMap.set(toast.id, activeIndex);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
positioned[position] = list.map((toast, orderIndex) => {
|
|
124
|
-
let stackIndex =
|
|
125
|
-
activeIndexMap.get(toast.id) ?? previousStackIndex[toast.id];
|
|
126
|
-
if (stackIndex == null || Number.isNaN(stackIndex)) {
|
|
127
|
-
stackIndex = orderIndex;
|
|
128
|
-
}
|
|
2
|
+
/**
|
|
3
|
+
* @component
|
|
4
|
+
* @description
|
|
5
|
+
* Internal component responsible for grouping toasts by position and calculating
|
|
6
|
+
* their stacking offsets (both collapsed and expanded).
|
|
7
|
+
* It handles the "hover to expand" logic and manages the lifecycle of toast groups.
|
|
8
|
+
*/
|
|
9
|
+
import VarselItem from "./VarselItem.svelte";
|
|
10
|
+
import {
|
|
11
|
+
ANIMATION_CONFIG,
|
|
12
|
+
FOCUSABLE_SELECTORS,
|
|
13
|
+
focusManager,
|
|
14
|
+
type PositionedToast,
|
|
15
|
+
type ToastData,
|
|
16
|
+
type ToastPosition,
|
|
17
|
+
} from "./internals";
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
toasts = [],
|
|
21
|
+
onRemove,
|
|
22
|
+
expandedGap = ANIMATION_CONFIG.EXPANDED_GAP,
|
|
23
|
+
position: defaultPosition = "bottom-center",
|
|
24
|
+
visibleToasts = 3,
|
|
25
|
+
expand = true,
|
|
26
|
+
duration = 5000,
|
|
27
|
+
closeButton = true,
|
|
28
|
+
pauseOnHover = true,
|
|
29
|
+
offset = undefined,
|
|
30
|
+
dir = "auto",
|
|
31
|
+
}: {
|
|
32
|
+
toasts?: ToastData[];
|
|
33
|
+
onRemove: (id: string) => void;
|
|
34
|
+
expandedGap?: number;
|
|
35
|
+
position?: ToastPosition;
|
|
36
|
+
visibleToasts?: number;
|
|
37
|
+
expand?: boolean;
|
|
38
|
+
duration?: number;
|
|
39
|
+
closeButton?: boolean;
|
|
40
|
+
pauseOnHover?: boolean;
|
|
41
|
+
offset?: number | string;
|
|
42
|
+
dir?: "ltr" | "rtl" | "auto";
|
|
43
|
+
} = $props();
|
|
44
|
+
|
|
45
|
+
const createPositionMap = <T,>(value: () => T): Record<ToastPosition, T> => ({
|
|
46
|
+
"top-left": value(),
|
|
47
|
+
"top-center": value(),
|
|
48
|
+
"top-right": value(),
|
|
49
|
+
"bottom-left": value(),
|
|
50
|
+
"bottom-center": value(),
|
|
51
|
+
"bottom-right": value(),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
let heights = $state<Record<string, number>>({});
|
|
55
|
+
let hovered = $state<Record<ToastPosition, boolean>>(createPositionMap(() => false));
|
|
56
|
+
let heldToasts = $state<Record<ToastPosition, Set<string>>>(
|
|
57
|
+
createPositionMap(() => new Set<string>()),
|
|
58
|
+
);
|
|
59
|
+
let isWindowFocused = $state(
|
|
60
|
+
typeof document === "undefined" ? true : document.visibilityState !== "hidden",
|
|
61
|
+
);
|
|
62
|
+
let isToastRegionClaimed = $state(false);
|
|
63
|
+
|
|
64
|
+
// Non-reactive internal state for "previous" values (mimicking legacy behavior)
|
|
65
|
+
let previousStackIndex: Record<string, number> = {};
|
|
66
|
+
let previousCollapsedOffsets: Record<string, number> = {};
|
|
67
|
+
let previousExpandedOffsets: Record<string, number> = {};
|
|
68
|
+
|
|
69
|
+
let toastsByPosition = $state<Record<ToastPosition, PositionedToast[]>>(
|
|
70
|
+
createPositionMap<PositionedToast[]>(() => []),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
let collapsedOffsetData = $state<{
|
|
74
|
+
byPosition: Record<ToastPosition, number[]>;
|
|
75
|
+
byId: Record<string, number>;
|
|
76
|
+
}>({ byPosition: createPositionMap<number[]>(() => []), byId: {} });
|
|
77
|
+
|
|
78
|
+
let expandedOffsetData = $state<{
|
|
79
|
+
byPosition: Record<ToastPosition, number[]>;
|
|
80
|
+
byId: Record<string, number>;
|
|
81
|
+
}>({ byPosition: createPositionMap<number[]>(() => []), byId: {} });
|
|
82
|
+
|
|
83
|
+
let positionEntries = $derived(
|
|
84
|
+
Object.entries(toastsByPosition) as [ToastPosition, PositionedToast[]][],
|
|
85
|
+
);
|
|
86
|
+
let latestPositionEntries = $derived(positionEntries);
|
|
87
|
+
let latestHovered = $derived(hovered);
|
|
88
|
+
|
|
89
|
+
const updateHoldState = (
|
|
90
|
+
position: ToastPosition,
|
|
91
|
+
toastId: string,
|
|
92
|
+
isHolding: boolean,
|
|
93
|
+
) => {
|
|
94
|
+
const current = heldToasts[position] ?? new Set<string>();
|
|
95
|
+
const next = new Set(current);
|
|
96
|
+
if (isHolding) {
|
|
97
|
+
next.add(toastId);
|
|
98
|
+
} else {
|
|
99
|
+
next.delete(toastId);
|
|
100
|
+
}
|
|
101
|
+
if (next.size !== current.size) {
|
|
102
|
+
heldToasts = { ...heldToasts, [position]: next };
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Calculate toastsByPosition based on toasts
|
|
107
|
+
$effect(() => {
|
|
108
|
+
const grouped = createPositionMap<ToastData[]>(() => []);
|
|
109
|
+
for (const toast of toasts) {
|
|
110
|
+
const pos = toast.position || defaultPosition;
|
|
111
|
+
grouped[pos].push(toast);
|
|
112
|
+
}
|
|
129
113
|
|
|
130
|
-
|
|
114
|
+
const nextStackIndices: Record<string, number> = {};
|
|
115
|
+
const positioned = createPositionMap<PositionedToast[]>(() => []);
|
|
116
|
+
|
|
117
|
+
for (const position of Object.keys(grouped) as ToastPosition[]) {
|
|
118
|
+
const list = grouped[position];
|
|
119
|
+
const activeToasts = list.filter(
|
|
120
|
+
(toast) => !toast.isLeaving && !toast.shouldClose,
|
|
121
|
+
);
|
|
122
|
+
const activeIndexMap = new Map<string, number>();
|
|
123
|
+
activeToasts.forEach((toast, activeIndex) => {
|
|
124
|
+
activeIndexMap.set(toast.id, activeIndex);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
positioned[position] = list.map((toast, orderIndex) => {
|
|
128
|
+
let stackIndex = activeIndexMap.get(toast.id) ?? previousStackIndex[toast.id];
|
|
129
|
+
if (stackIndex == null || Number.isNaN(stackIndex)) {
|
|
130
|
+
stackIndex = orderIndex;
|
|
131
|
+
}
|
|
131
132
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
previousStackIndex = nextStackIndices;
|
|
143
|
-
toastsByPosition = positioned;
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
// Update hovered state based on empty groups
|
|
147
|
-
$effect(() => {
|
|
148
|
-
const next = { ...hovered };
|
|
149
|
-
let changed = false;
|
|
150
|
-
for (const pos of Object.keys(hovered) as ToastPosition[]) {
|
|
151
|
-
const hasToast = (toastsByPosition[pos]?.length ?? 0) > 0;
|
|
152
|
-
if (!hasToast && next[pos]) {
|
|
153
|
-
next[pos] = false;
|
|
154
|
-
changed = true;
|
|
133
|
+
nextStackIndices[toast.id] = stackIndex;
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
...toast,
|
|
137
|
+
position: position,
|
|
138
|
+
index: stackIndex,
|
|
139
|
+
renderIndex: orderIndex,
|
|
140
|
+
total: list.length,
|
|
141
|
+
};
|
|
142
|
+
}) as PositionedToast[];
|
|
155
143
|
}
|
|
156
|
-
}
|
|
157
|
-
if (changed) {
|
|
158
|
-
hovered = next;
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
// Calculate collapsedOffsetData
|
|
163
|
-
$effect(() => {
|
|
164
|
-
const byPosition = createPositionMap<number[]>(() => []);
|
|
165
|
-
const byId: Record<string, number> = {};
|
|
166
|
-
|
|
167
|
-
for (const [pos, group] of positionEntries) {
|
|
168
|
-
const isTopPosition = pos.startsWith("top-");
|
|
169
|
-
const activeToasts = group.filter((toast) => !toast.shouldClose);
|
|
170
|
-
const offsetsForActive: number[] = [];
|
|
171
|
-
|
|
172
|
-
for (let i = 0; i < activeToasts.length; i++) {
|
|
173
|
-
if (i === 0) {
|
|
174
|
-
offsetsForActive.push(0);
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
144
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
145
|
+
previousStackIndex = nextStackIndices;
|
|
146
|
+
toastsByPosition = positioned;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Update hovered state based on empty groups
|
|
150
|
+
$effect(() => {
|
|
151
|
+
const next = { ...hovered };
|
|
152
|
+
let changed = false;
|
|
153
|
+
for (const pos of Object.keys(hovered) as ToastPosition[]) {
|
|
154
|
+
const hasToast = (toastsByPosition[pos]?.length ?? 0) > 0;
|
|
155
|
+
if (!hasToast && next[pos]) {
|
|
156
|
+
next[pos] = false;
|
|
157
|
+
changed = true;
|
|
184
158
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
159
|
+
}
|
|
160
|
+
if (changed) {
|
|
161
|
+
hovered = next;
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
$effect(() => {
|
|
166
|
+
if (toasts.length === 0 && isToastRegionClaimed) {
|
|
167
|
+
isToastRegionClaimed = false;
|
|
168
|
+
focusManager.releaseClaim();
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Calculate collapsedOffsetData
|
|
173
|
+
$effect(() => {
|
|
174
|
+
const byPosition = createPositionMap<number[]>(() => []);
|
|
175
|
+
const byId: Record<string, number> = {};
|
|
176
|
+
|
|
177
|
+
for (const [pos, group] of positionEntries) {
|
|
178
|
+
const isTopPosition = pos.startsWith("top-");
|
|
179
|
+
const activeToasts = group.filter((toast) => !toast.shouldClose);
|
|
180
|
+
const offsetsForActive: number[] = [];
|
|
181
|
+
|
|
182
|
+
for (let i = 0; i < activeToasts.length; i++) {
|
|
183
|
+
if (i === 0) {
|
|
184
|
+
offsetsForActive.push(0);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const prevToast = activeToasts[i - 1];
|
|
189
|
+
const currentToast = activeToasts[i];
|
|
190
|
+
const prevOffset = offsetsForActive[i - 1] ?? 0;
|
|
191
|
+
if (!prevToast || !currentToast) {
|
|
192
|
+
offsetsForActive.push(prevOffset);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const prevHeight = heights[prevToast.id];
|
|
196
|
+
const currentHeight = heights[currentToast.id];
|
|
197
|
+
const fallbackOffset =
|
|
198
|
+
prevOffset + (isTopPosition ? 1 : -1) * ANIMATION_CONFIG.STACK_OFFSET;
|
|
199
|
+
|
|
200
|
+
if (
|
|
201
|
+
prevHeight == null ||
|
|
202
|
+
currentHeight == null ||
|
|
203
|
+
Number.isNaN(prevHeight) ||
|
|
204
|
+
Number.isNaN(currentHeight)
|
|
205
|
+
) {
|
|
206
|
+
offsetsForActive.push(fallbackOffset);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (isTopPosition) {
|
|
211
|
+
offsetsForActive.push(
|
|
212
|
+
prevOffset + (prevHeight - currentHeight + ANIMATION_CONFIG.STACK_OFFSET),
|
|
213
|
+
);
|
|
214
|
+
} else {
|
|
215
|
+
offsetsForActive.push(
|
|
216
|
+
prevOffset + (currentHeight - prevHeight - ANIMATION_CONFIG.STACK_OFFSET),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
198
219
|
}
|
|
199
220
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
);
|
|
205
|
-
} else {
|
|
206
|
-
offsetsForActive.push(
|
|
207
|
-
prevOffset +
|
|
208
|
-
(currentHeight - prevHeight - ANIMATION_CONFIG.STACK_OFFSET),
|
|
209
|
-
);
|
|
221
|
+
for (let i = 0; i < activeToasts.length; i++) {
|
|
222
|
+
const toast = activeToasts[i];
|
|
223
|
+
if (!toast) continue;
|
|
224
|
+
byId[toast.id] = offsetsForActive[i] ?? 0;
|
|
210
225
|
}
|
|
211
|
-
}
|
|
212
226
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
227
|
+
for (const toast of group) {
|
|
228
|
+
if (byId[toast.id] != null) continue;
|
|
229
|
+
const previousOffset = previousCollapsedOffsets[toast.id];
|
|
230
|
+
if (typeof previousOffset === "number") {
|
|
231
|
+
byId[toast.id] = previousOffset;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
218
234
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
byId[toast.id] = previousOffset;
|
|
224
|
-
continue;
|
|
235
|
+
const defaultOffset = isTopPosition
|
|
236
|
+
? toast.index * ANIMATION_CONFIG.STACK_OFFSET
|
|
237
|
+
: -(toast.index * ANIMATION_CONFIG.STACK_OFFSET);
|
|
238
|
+
byId[toast.id] = defaultOffset;
|
|
225
239
|
}
|
|
226
240
|
|
|
227
|
-
|
|
228
|
-
? toast.index * ANIMATION_CONFIG.STACK_OFFSET
|
|
229
|
-
: -(toast.index * ANIMATION_CONFIG.STACK_OFFSET);
|
|
230
|
-
byId[toast.id] = defaultOffset;
|
|
241
|
+
byPosition[pos] = group.map((toast) => byId[toast.id] ?? 0);
|
|
231
242
|
}
|
|
232
243
|
|
|
233
|
-
|
|
234
|
-
|
|
244
|
+
previousCollapsedOffsets = byId;
|
|
245
|
+
collapsedOffsetData = { byPosition, byId };
|
|
246
|
+
});
|
|
235
247
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
248
|
+
// Calculate expandedOffsetData
|
|
249
|
+
$effect(() => {
|
|
250
|
+
const byPosition = createPositionMap<number[]>(() => []);
|
|
251
|
+
const byId: Record<string, number> = {};
|
|
239
252
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
253
|
+
for (const [pos, group] of positionEntries) {
|
|
254
|
+
const offsets: number[] = [];
|
|
255
|
+
const activeToasts = group.filter((toast) => !toast.shouldClose);
|
|
256
|
+
let acc = 0;
|
|
244
257
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
258
|
+
for (let i = 0; i < activeToasts.length; i++) {
|
|
259
|
+
if (i === 0) {
|
|
260
|
+
offsets.push(0);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const prevToast = activeToasts[i - 1];
|
|
264
|
+
const prevHeight = prevToast ? (heights[prevToast.id] ?? 0) : 0;
|
|
265
|
+
acc += prevHeight + expandedGap;
|
|
266
|
+
offsets.push(acc);
|
|
267
|
+
}
|
|
249
268
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
269
|
+
for (let i = 0; i < activeToasts.length; i++) {
|
|
270
|
+
const toast = activeToasts[i];
|
|
271
|
+
if (!toast) continue;
|
|
272
|
+
byId[toast.id] = offsets[i] ?? 0;
|
|
254
273
|
}
|
|
255
|
-
const prevToast = activeToasts[i - 1];
|
|
256
|
-
const prevHeight = prevToast ? (heights[prevToast.id] ?? 0) : 0;
|
|
257
|
-
acc += prevHeight + expandedGap;
|
|
258
|
-
offsets.push(acc);
|
|
259
|
-
}
|
|
260
274
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (!toast) continue;
|
|
264
|
-
byId[toast.id] = offsets[i] ?? 0;
|
|
265
|
-
}
|
|
275
|
+
for (const toast of group) {
|
|
276
|
+
if (byId[toast.id] != null) continue;
|
|
266
277
|
|
|
267
|
-
|
|
268
|
-
|
|
278
|
+
const previousOffset = previousExpandedOffsets[toast.id];
|
|
279
|
+
if (typeof previousOffset === "number") {
|
|
280
|
+
byId[toast.id] = previousOffset;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
269
283
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
284
|
+
let fallback = 0;
|
|
285
|
+
for (const candidate of group) {
|
|
286
|
+
if (candidate.id === toast.id) break;
|
|
287
|
+
const height = heights[candidate.id] ?? 0;
|
|
288
|
+
fallback += height + expandedGap;
|
|
289
|
+
}
|
|
290
|
+
byId[toast.id] = fallback;
|
|
274
291
|
}
|
|
275
292
|
|
|
276
|
-
|
|
277
|
-
for (const candidate of group) {
|
|
278
|
-
if (candidate.id === toast.id) break;
|
|
279
|
-
const height = heights[candidate.id] ?? 0;
|
|
280
|
-
fallback += height + expandedGap;
|
|
281
|
-
}
|
|
282
|
-
byId[toast.id] = fallback;
|
|
293
|
+
byPosition[pos] = group.map((toast) => byId[toast.id] ?? 0);
|
|
283
294
|
}
|
|
284
295
|
|
|
285
|
-
|
|
286
|
-
|
|
296
|
+
previousExpandedOffsets = byId;
|
|
297
|
+
expandedOffsetData = { byPosition, byId };
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
$effect(() => {
|
|
301
|
+
let hoverFrameId: number | null = null;
|
|
302
|
+
let pointerX = 0;
|
|
303
|
+
let pointerY = 0;
|
|
304
|
+
|
|
305
|
+
const updateHoverState = (x: number, y: number) => {
|
|
306
|
+
if (latestPositionEntries.length === 0) return;
|
|
307
|
+
const next: Record<ToastPosition, boolean> = {
|
|
308
|
+
...latestHovered,
|
|
309
|
+
};
|
|
310
|
+
for (const [pos, group] of latestPositionEntries) {
|
|
311
|
+
let top = Number.POSITIVE_INFINITY;
|
|
312
|
+
let left = Number.POSITIVE_INFINITY;
|
|
313
|
+
let right = Number.NEGATIVE_INFINITY;
|
|
314
|
+
let bottom = Number.NEGATIVE_INFINITY;
|
|
315
|
+
let any = false;
|
|
316
|
+
for (const t of group) {
|
|
317
|
+
if (t.index >= visibleToasts) continue;
|
|
318
|
+
const el = document.querySelector(
|
|
319
|
+
`[data-toast-id="${t.id}"]`,
|
|
320
|
+
) as HTMLElement | null;
|
|
321
|
+
if (!el) continue;
|
|
322
|
+
const rect = el.getBoundingClientRect();
|
|
323
|
+
top = Math.min(top, rect.top);
|
|
324
|
+
left = Math.min(left, rect.left);
|
|
325
|
+
right = Math.max(right, rect.right);
|
|
326
|
+
bottom = Math.max(bottom, rect.bottom);
|
|
327
|
+
any = true;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (!any) {
|
|
331
|
+
next[pos] = false;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
287
334
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
335
|
+
const inside = x >= left && x <= right && y >= top && y <= bottom;
|
|
336
|
+
next[pos] = inside;
|
|
337
|
+
}
|
|
291
338
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
339
|
+
const changed = (Object.keys(next) as ToastPosition[]).some(
|
|
340
|
+
(key) => next[key] !== hovered[key],
|
|
341
|
+
);
|
|
342
|
+
if (changed) {
|
|
343
|
+
hovered = next;
|
|
344
|
+
}
|
|
298
345
|
};
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if (!el) continue;
|
|
311
|
-
const rect = el.getBoundingClientRect();
|
|
312
|
-
top = Math.min(top, rect.top);
|
|
313
|
-
left = Math.min(left, rect.left);
|
|
314
|
-
right = Math.max(right, rect.right);
|
|
315
|
-
bottom = Math.max(bottom, rect.bottom);
|
|
316
|
-
any = true;
|
|
346
|
+
|
|
347
|
+
const flushHoverUpdate = () => {
|
|
348
|
+
hoverFrameId = null;
|
|
349
|
+
updateHoverState(pointerX, pointerY);
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const handleMouseMove = (event: MouseEvent) => {
|
|
353
|
+
pointerX = event.clientX;
|
|
354
|
+
pointerY = event.clientY;
|
|
355
|
+
if (hoverFrameId == null) {
|
|
356
|
+
hoverFrameId = requestAnimationFrame(flushHoverUpdate);
|
|
317
357
|
}
|
|
358
|
+
};
|
|
318
359
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
360
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
361
|
+
if (event.key === "F6") {
|
|
362
|
+
if (latestPositionEntries.length === 0) return;
|
|
363
|
+
let target: HTMLElement | null = null;
|
|
364
|
+
for (const [, group] of latestPositionEntries) {
|
|
365
|
+
const candidate = group.find(
|
|
366
|
+
(t) => !t.shouldClose && !t.isLeaving && t.index < visibleToasts,
|
|
367
|
+
);
|
|
368
|
+
if (!candidate) continue;
|
|
369
|
+
const el = document.querySelector(
|
|
370
|
+
`[data-toast-id="${candidate.id}"]`,
|
|
371
|
+
) as HTMLElement | null;
|
|
372
|
+
if (el) {
|
|
373
|
+
target = el;
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (!target) return;
|
|
378
|
+
if (document.activeElement && target.contains(document.activeElement)) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
event.preventDefault();
|
|
382
|
+
focusManager.savePrevFocus();
|
|
383
|
+
isToastRegionClaimed = true;
|
|
384
|
+
const focusable = target.querySelector<HTMLElement>(FOCUSABLE_SELECTORS);
|
|
385
|
+
(focusable ?? target).focus({ preventScroll: true });
|
|
386
|
+
return;
|
|
322
387
|
}
|
|
323
388
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
389
|
+
if (event.key !== "Escape") return;
|
|
390
|
+
for (const [, group] of latestPositionEntries) {
|
|
391
|
+
const latestToast = group?.[0];
|
|
392
|
+
if (!latestToast) continue;
|
|
393
|
+
const container = document.querySelector(
|
|
394
|
+
`[data-toast-id="${latestToast.id}"]`,
|
|
395
|
+
) as HTMLElement | null;
|
|
396
|
+
if (!container) continue;
|
|
397
|
+
const active = document.activeElement as HTMLElement | null;
|
|
398
|
+
if (active && container.contains(active)) {
|
|
399
|
+
const closeBtn = container.querySelector(
|
|
400
|
+
'[aria-label="Close toast"]',
|
|
401
|
+
) as HTMLButtonElement | null;
|
|
402
|
+
if (closeBtn) {
|
|
403
|
+
event.preventDefault();
|
|
404
|
+
closeBtn.click();
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
};
|
|
327
409
|
|
|
328
|
-
const changed = (Object.keys(next) as ToastPosition[]).some(
|
|
329
|
-
(key) => next[key] !== hovered[key],
|
|
330
|
-
);
|
|
331
|
-
if (changed) {
|
|
332
|
-
hovered = next;
|
|
333
|
-
}
|
|
334
|
-
};
|
|
335
410
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
411
|
+
const handleWindowBlur = () => {
|
|
412
|
+
isWindowFocused = false;
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const handleWindowFocus = () => {
|
|
416
|
+
isWindowFocused = true;
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const handleVisibilityChange = () => {
|
|
420
|
+
isWindowFocused = document.visibilityState !== "hidden";
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const handleDocumentPointerDown = (event: PointerEvent) => {
|
|
424
|
+
if (event.pointerType !== "touch") return;
|
|
425
|
+
if (latestPositionEntries.length === 0) return;
|
|
426
|
+
const target = event.target as Element | null;
|
|
427
|
+
if (target && target.closest("[data-toast-id]")) return;
|
|
428
|
+
|
|
429
|
+
let changed = false;
|
|
430
|
+
const next = { ...hovered };
|
|
431
|
+
for (const pos of Object.keys(hovered) as ToastPosition[]) {
|
|
432
|
+
if (next[pos]) {
|
|
433
|
+
next[pos] = false;
|
|
434
|
+
changed = true;
|
|
353
435
|
}
|
|
354
436
|
}
|
|
355
|
-
|
|
437
|
+
if (changed) {
|
|
438
|
+
hovered = next;
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
document.addEventListener("mousemove", handleMouseMove);
|
|
443
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
444
|
+
window.addEventListener("blur", handleWindowBlur);
|
|
445
|
+
window.addEventListener("focus", handleWindowFocus);
|
|
446
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
447
|
+
document.addEventListener("pointerdown", handleDocumentPointerDown, true);
|
|
448
|
+
return () => {
|
|
449
|
+
if (hoverFrameId != null) {
|
|
450
|
+
cancelAnimationFrame(hoverFrameId);
|
|
451
|
+
hoverFrameId = null;
|
|
452
|
+
}
|
|
453
|
+
document.removeEventListener("mousemove", handleMouseMove);
|
|
454
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
455
|
+
window.removeEventListener("blur", handleWindowBlur);
|
|
456
|
+
window.removeEventListener("focus", handleWindowFocus);
|
|
457
|
+
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
458
|
+
document.removeEventListener("pointerdown", handleDocumentPointerDown, true);
|
|
459
|
+
};
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
const handleHeightChange = (id: string, height: number) => {
|
|
463
|
+
if (heights[id] === height) return;
|
|
464
|
+
heights = { ...heights, [id]: height };
|
|
356
465
|
};
|
|
357
466
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
document.removeEventListener("keydown", handleKeyDown);
|
|
467
|
+
const handleFocusGuard = () => {
|
|
468
|
+
if (!isToastRegionClaimed) return;
|
|
469
|
+
isToastRegionClaimed = false;
|
|
470
|
+
focusManager.restoreFocusToPrevElement();
|
|
363
471
|
};
|
|
364
|
-
});
|
|
365
472
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
473
|
+
let highPriorityToasts = $derived(
|
|
474
|
+
toasts.filter(
|
|
475
|
+
(toast) =>
|
|
476
|
+
toast.priority === "high" &&
|
|
477
|
+
!toast.shouldClose &&
|
|
478
|
+
!toast.isLeaving &&
|
|
479
|
+
(toast.title || toast.description),
|
|
480
|
+
),
|
|
481
|
+
);
|
|
370
482
|
</script>
|
|
371
483
|
|
|
484
|
+
{#if highPriorityToasts.length > 0}
|
|
485
|
+
<div class="vs-sr-only" role="region" aria-label="High priority notifications">
|
|
486
|
+
{#each highPriorityToasts as toast (toast.id)}
|
|
487
|
+
<div role="alert" aria-atomic="true">
|
|
488
|
+
{#if toast.title}<span>{toast.title}</span>{/if}
|
|
489
|
+
{#if toast.title && toast.description}<span>: </span>{/if}
|
|
490
|
+
{#if toast.description}<span>{toast.description}</span>{/if}
|
|
491
|
+
</div>
|
|
492
|
+
{/each}
|
|
493
|
+
</div>
|
|
494
|
+
{/if}
|
|
495
|
+
|
|
372
496
|
{#if toasts.length > 0}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
{
|
|
497
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
498
|
+
<div
|
|
499
|
+
class="vs-sr-only"
|
|
500
|
+
role="presentation"
|
|
501
|
+
tabindex={isToastRegionClaimed ? 0 : -1}
|
|
502
|
+
onfocus={handleFocusGuard}
|
|
503
|
+
></div>
|
|
504
|
+
<div class="pointer-events-none fixed inset-0 z-50" {dir}>
|
|
505
|
+
{#each positionEntries as [position, positionToasts] (position)}
|
|
378
506
|
{@const pos = position}
|
|
379
507
|
{@const expandedOffsets = expandedOffsetData.byPosition[pos]}
|
|
380
508
|
{@const collapsedOffsets = collapsedOffsetData.byPosition[pos]}
|
|
@@ -388,20 +516,18 @@ const handleHeightChange = (id: string, height: number) => {
|
|
|
388
516
|
visibleStackLimit,
|
|
389
517
|
)}
|
|
390
518
|
{@const lastVisibleToastId = activeToasts[maxVisibleStackIndex]?.id}
|
|
391
|
-
{@const lastVisibleRenderIndex =
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
: -1}
|
|
519
|
+
{@const lastVisibleRenderIndex =
|
|
520
|
+
lastVisibleToastId != null
|
|
521
|
+
? positionToasts.findIndex((candidate) => candidate.id === lastVisibleToastId)
|
|
522
|
+
: -1}
|
|
396
523
|
{@const sharedHiddenCollapsedOffset =
|
|
397
524
|
lastVisibleRenderIndex >= 0
|
|
398
525
|
? collapsedOffsets?.[lastVisibleRenderIndex]
|
|
399
526
|
: undefined}
|
|
400
527
|
{#each positionToasts as toast, idx (toast.id)}
|
|
401
|
-
{@const toastIsHidden =
|
|
402
|
-
toast.index >= visibleToasts}
|
|
528
|
+
{@const toastIsHidden = toast.index >= visibleToasts}
|
|
403
529
|
{@const hiddenCollapsedOffset = toastIsHidden
|
|
404
|
-
? sharedHiddenCollapsedOffset ?? collapsedOffsets?.[idx]
|
|
530
|
+
? (sharedHiddenCollapsedOffset ?? collapsedOffsets?.[idx])
|
|
405
531
|
: collapsedOffsets?.[idx]}
|
|
406
532
|
{@const collapsedOffsetValue = collapsedOffsets?.[idx]}
|
|
407
533
|
<VarselItem
|
|
@@ -414,18 +540,25 @@ const handleHeightChange = (id: string, height: number) => {
|
|
|
414
540
|
onGroupHoverEnter={() => {
|
|
415
541
|
hovered = { ...hovered, [pos]: true };
|
|
416
542
|
}}
|
|
417
|
-
onGroupHoldChange={(holding) =>
|
|
418
|
-
updateHoldState(pos, toast.id, holding)}
|
|
543
|
+
onGroupHoldChange={(holding) => updateHoldState(pos, toast.id, holding)}
|
|
419
544
|
collapsedOffset={collapsedOffsetValue}
|
|
420
|
-
|
|
545
|
+
{hiddenCollapsedOffset}
|
|
421
546
|
defaultDuration={duration}
|
|
422
547
|
defaultShowClose={closeButton}
|
|
423
548
|
{pauseOnHover}
|
|
424
549
|
{offset}
|
|
425
550
|
{expand}
|
|
426
551
|
{visibleToasts}
|
|
552
|
+
{isWindowFocused}
|
|
427
553
|
/>
|
|
428
554
|
{/each}
|
|
429
555
|
{/each}
|
|
430
556
|
</div>
|
|
431
|
-
|
|
557
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
558
|
+
<div
|
|
559
|
+
class="vs-sr-only"
|
|
560
|
+
role="presentation"
|
|
561
|
+
tabindex={isToastRegionClaimed ? 0 : -1}
|
|
562
|
+
onfocus={handleFocusGuard}
|
|
563
|
+
></div>
|
|
564
|
+
{/if}
|