varsel 0.5.4 → 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 +473 -360
- package/dist/VarselManager.svelte.d.ts +1 -1
- package/dist/VarselManager.svelte.d.ts.map +1 -1
- package/dist/VarselToaster.svelte +22 -22
- 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 +15 -9
- 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.map +1 -1
- package/dist/core/toaster-instances.js +1 -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 +1 -2
|
@@ -1,400 +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
|
+
}
|
|
334
|
+
|
|
335
|
+
const inside = x >= left && x <= right && y >= top && y <= bottom;
|
|
336
|
+
next[pos] = inside;
|
|
337
|
+
}
|
|
287
338
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
339
|
+
const changed = (Object.keys(next) as ToastPosition[]).some(
|
|
340
|
+
(key) => next[key] !== hovered[key],
|
|
341
|
+
);
|
|
342
|
+
if (changed) {
|
|
343
|
+
hovered = next;
|
|
344
|
+
}
|
|
345
|
+
};
|
|
291
346
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
347
|
+
const flushHoverUpdate = () => {
|
|
348
|
+
hoverFrameId = null;
|
|
349
|
+
updateHoverState(pointerX, pointerY);
|
|
350
|
+
};
|
|
296
351
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
352
|
+
const handleMouseMove = (event: MouseEvent) => {
|
|
353
|
+
pointerX = event.clientX;
|
|
354
|
+
pointerY = event.clientY;
|
|
355
|
+
if (hoverFrameId == null) {
|
|
356
|
+
hoverFrameId = requestAnimationFrame(flushHoverUpdate);
|
|
357
|
+
}
|
|
301
358
|
};
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
359
|
+
|
|
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;
|
|
320
387
|
}
|
|
321
388
|
|
|
322
|
-
if (
|
|
323
|
-
|
|
324
|
-
|
|
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
|
+
}
|
|
325
407
|
}
|
|
408
|
+
};
|
|
326
409
|
|
|
327
|
-
const inside = x >= left && x <= right && y >= top && y <= bottom;
|
|
328
|
-
next[pos] = inside;
|
|
329
|
-
}
|
|
330
410
|
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
if (changed) {
|
|
335
|
-
hovered = next;
|
|
336
|
-
}
|
|
337
|
-
};
|
|
411
|
+
const handleWindowBlur = () => {
|
|
412
|
+
isWindowFocused = false;
|
|
413
|
+
};
|
|
338
414
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
};
|
|
415
|
+
const handleWindowFocus = () => {
|
|
416
|
+
isWindowFocused = true;
|
|
417
|
+
};
|
|
343
418
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if (hoverFrameId == null) {
|
|
348
|
-
hoverFrameId = requestAnimationFrame(flushHoverUpdate);
|
|
349
|
-
}
|
|
350
|
-
};
|
|
419
|
+
const handleVisibilityChange = () => {
|
|
420
|
+
isWindowFocused = document.visibilityState !== "hidden";
|
|
421
|
+
};
|
|
351
422
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const
|
|
356
|
-
if (
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
'[aria-label="Close toast"]',
|
|
365
|
-
) as HTMLButtonElement | null;
|
|
366
|
-
if (closeBtn) {
|
|
367
|
-
event.preventDefault();
|
|
368
|
-
closeBtn.click();
|
|
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;
|
|
369
435
|
}
|
|
370
436
|
}
|
|
371
|
-
|
|
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 };
|
|
372
465
|
};
|
|
373
466
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
cancelAnimationFrame(hoverFrameId);
|
|
379
|
-
hoverFrameId = null;
|
|
380
|
-
}
|
|
381
|
-
document.removeEventListener("mousemove", handleMouseMove);
|
|
382
|
-
document.removeEventListener("keydown", handleKeyDown);
|
|
467
|
+
const handleFocusGuard = () => {
|
|
468
|
+
if (!isToastRegionClaimed) return;
|
|
469
|
+
isToastRegionClaimed = false;
|
|
470
|
+
focusManager.restoreFocusToPrevElement();
|
|
383
471
|
};
|
|
384
|
-
});
|
|
385
472
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
+
);
|
|
390
482
|
</script>
|
|
391
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
|
+
|
|
392
496
|
{#if toasts.length > 0}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
{
|
|
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)}
|
|
398
506
|
{@const pos = position}
|
|
399
507
|
{@const expandedOffsets = expandedOffsetData.byPosition[pos]}
|
|
400
508
|
{@const collapsedOffsets = collapsedOffsetData.byPosition[pos]}
|
|
@@ -408,20 +516,18 @@ const handleHeightChange = (id: string, height: number) => {
|
|
|
408
516
|
visibleStackLimit,
|
|
409
517
|
)}
|
|
410
518
|
{@const lastVisibleToastId = activeToasts[maxVisibleStackIndex]?.id}
|
|
411
|
-
{@const lastVisibleRenderIndex =
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
: -1}
|
|
519
|
+
{@const lastVisibleRenderIndex =
|
|
520
|
+
lastVisibleToastId != null
|
|
521
|
+
? positionToasts.findIndex((candidate) => candidate.id === lastVisibleToastId)
|
|
522
|
+
: -1}
|
|
416
523
|
{@const sharedHiddenCollapsedOffset =
|
|
417
524
|
lastVisibleRenderIndex >= 0
|
|
418
525
|
? collapsedOffsets?.[lastVisibleRenderIndex]
|
|
419
526
|
: undefined}
|
|
420
527
|
{#each positionToasts as toast, idx (toast.id)}
|
|
421
|
-
{@const toastIsHidden =
|
|
422
|
-
toast.index >= visibleToasts}
|
|
528
|
+
{@const toastIsHidden = toast.index >= visibleToasts}
|
|
423
529
|
{@const hiddenCollapsedOffset = toastIsHidden
|
|
424
|
-
? sharedHiddenCollapsedOffset ?? collapsedOffsets?.[idx]
|
|
530
|
+
? (sharedHiddenCollapsedOffset ?? collapsedOffsets?.[idx])
|
|
425
531
|
: collapsedOffsets?.[idx]}
|
|
426
532
|
{@const collapsedOffsetValue = collapsedOffsets?.[idx]}
|
|
427
533
|
<VarselItem
|
|
@@ -434,18 +540,25 @@ const handleHeightChange = (id: string, height: number) => {
|
|
|
434
540
|
onGroupHoverEnter={() => {
|
|
435
541
|
hovered = { ...hovered, [pos]: true };
|
|
436
542
|
}}
|
|
437
|
-
onGroupHoldChange={(holding) =>
|
|
438
|
-
updateHoldState(pos, toast.id, holding)}
|
|
543
|
+
onGroupHoldChange={(holding) => updateHoldState(pos, toast.id, holding)}
|
|
439
544
|
collapsedOffset={collapsedOffsetValue}
|
|
440
|
-
|
|
545
|
+
{hiddenCollapsedOffset}
|
|
441
546
|
defaultDuration={duration}
|
|
442
547
|
defaultShowClose={closeButton}
|
|
443
548
|
{pauseOnHover}
|
|
444
549
|
{offset}
|
|
445
550
|
{expand}
|
|
446
551
|
{visibleToasts}
|
|
552
|
+
{isWindowFocused}
|
|
447
553
|
/>
|
|
448
554
|
{/each}
|
|
449
555
|
{/each}
|
|
450
556
|
</div>
|
|
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>
|
|
451
564
|
{/if}
|