Skip to content

Commit d094cc7

Browse files
committed
🐛 Avoid multiple activation of dropdowns (#1907)
1 parent 3719f91 commit d094cc7

File tree

1 file changed

+89
-11
lines changed

1 file changed

+89
-11
lines changed

src/lib/components/SubmissionStatus/UpdatingDropdown.svelte

Lines changed: 89 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,18 @@
2525
2626
<UpdatingDropdown bind:this={updatingDropdown} {taskResult} {isLoggedIn} {onupdate} />
2727
-->
28+
<script module lang="ts">
29+
import { writable } from 'svelte/store';
30+
31+
// Avoid multiple activation of dropdowns.
32+
const activeDropdownId = writable<string | null>(null);
33+
</script>
34+
2835
<script lang="ts">
2936
import { getStores } from '$app/stores';
3037
import { enhance } from '$app/forms';
38+
import { onMount } from 'svelte';
39+
import { browser } from '$app/environment';
3140
3241
import { Dropdown, DropdownUl, DropdownLi, uiHelpers } from 'svelte-5-ui-lib';
3342
import Check from 'lucide-svelte/icons/check';
@@ -56,35 +65,53 @@
5665
let dropdownStatus = $state(false);
5766
let closeDropdown = dropdown.close;
5867
59-
let dropdownX = $state(0);
60-
let dropdownY = $state(0);
61-
let isLowerHalfInScreen = $state(false);
68+
let dropdownPosition = $state({ x: 0, y: 0, isLower: false });
69+
70+
const componentId = Math.random().toString(36).substring(2);
71+
72+
onMount(() => {
73+
const unsubscribe = activeDropdownId.subscribe((id) => {
74+
if (id && id !== componentId && dropdownStatus) {
75+
closeDropdown();
76+
}
77+
});
78+
79+
return unsubscribe;
80+
});
6281
6382
$effect(() => {
6483
activeUrl = $page.url.pathname;
6584
dropdownStatus = dropdown.isOpen;
6685
6786
if (dropdownStatus) {
68-
document.documentElement.style.setProperty('--dropdown-x', `${dropdownX}px`);
69-
document.documentElement.style.setProperty('--dropdown-y', `${dropdownY}px`);
87+
document.documentElement.style.setProperty('--dropdown-x', `${dropdownPosition.x}px`);
88+
document.documentElement.style.setProperty('--dropdown-y', `${dropdownPosition.y}px`);
7089
}
7190
});
7291
7392
export function toggle(event?: MouseEvent): void {
7493
if (event) {
94+
event.stopPropagation();
7595
getDropdownPosition(event);
7696
}
7797
98+
activeDropdownId.set(componentId);
7899
dropdown.toggle();
100+
101+
// Note: Wait until the next event cycle before accepting a click event.
102+
if (!dropdownStatus) {
103+
setTimeout(() => {}, 0);
104+
}
79105
}
80106
81107
function getDropdownPosition(event: MouseEvent): void {
82108
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
83109
84-
dropdownX = rect.right;
85-
dropdownY = rect.bottom;
86-
87-
isLowerHalfInScreen = rect.top > window.innerHeight / 2;
110+
dropdownPosition = {
111+
x: rect.right,
112+
y: rect.bottom,
113+
isLower: rect.top > window.innerHeight / 2,
114+
};
88115
}
89116
90117
function getDropdownClasses(isLower: boolean): string {
@@ -208,14 +235,65 @@
208235
};
209236
return option;
210237
});
238+
239+
function manageDropdown(node: HTMLElement) {
240+
if (!browser) {
241+
return {
242+
destroy: () => {},
243+
};
244+
}
245+
246+
let justOpened = false;
247+
248+
const handleScroll = () => {
249+
if (dropdownStatus) {
250+
closeDropdown();
251+
}
252+
};
253+
254+
const handleWindowClick = (event: MouseEvent) => {
255+
if (justOpened) {
256+
justOpened = false;
257+
return;
258+
}
259+
260+
if (dropdownStatus && !node.contains(event.target as Node)) {
261+
closeDropdown();
262+
}
263+
};
264+
265+
window.addEventListener('scroll', handleScroll, { passive: true });
266+
window.addEventListener('click', handleWindowClick);
267+
268+
return {
269+
update(newStatus: boolean) {
270+
if (newStatus && !dropdownStatus) {
271+
justOpened = true;
272+
273+
setTimeout(() => {
274+
justOpened = false;
275+
}, 0);
276+
}
277+
278+
dropdownStatus = newStatus;
279+
},
280+
281+
destroy() {
282+
if (browser) {
283+
window.removeEventListener('scroll', handleScroll);
284+
window.removeEventListener('click', handleWindowClick);
285+
}
286+
},
287+
};
288+
}
211289
</script>
212290

213-
<div class="fixed inset-0 pointer-events-none z-50 w-full h-full">
291+
<div class="fixed inset-0 pointer-events-none z-50 w-full h-full" use:manageDropdown>
214292
<Dropdown
215293
{activeUrl}
216294
{dropdownStatus}
217295
{closeDropdown}
218-
class={getDropdownClasses(isLowerHalfInScreen)}
296+
class={getDropdownClasses(dropdownPosition.isLower)}
219297
>
220298
<DropdownUl class="border rounded-lg shadow">
221299
{#if isLoggedIn}

0 commit comments

Comments
 (0)