Skip to content

Commit 72b1fc6

Browse files
committed
♻️ Update docs and refactoring (#1925)
1 parent b9d54fe commit 72b1fc6

File tree

1 file changed

+27
-12
lines changed

1 file changed

+27
-12
lines changed

src/lib/actions/handle_dropdown.ts

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import { browser } from '$app/environment';
33

44
const activeDropdownId: Writable<string | null> = writable<string | null>(null);
55

6-
let lastTriggerElement: HTMLElement | null = null;
7-
// Bounce timer for resizing event.
8-
let resizeTimeout: ReturnType<typeof setTimeout>;
6+
const dropdownContext = {
7+
lastTriggerElement: null as HTMLElement | null,
8+
// Bounce timer for resizing event.
9+
resizeTimeout: undefined as ReturnType<typeof setTimeout> | undefined,
10+
};
911

1012
/**
1113
* A Svelte action that manages dropdown behavior for an HTML element.
@@ -56,6 +58,8 @@ export function handleDropdownBehavior(
5658
};
5759
}
5860

61+
// Flag to prevent the dropdown from closing immediately after opening
62+
// when a click event propagates to the window.
5963
let ignoreNextClick = false;
6064

6165
// Close the dropdown on scroll.
@@ -79,9 +83,9 @@ export function handleDropdownBehavior(
7983

8084
// Recalculate the dropdown position on resize.
8185
const handleWindowResize = () => {
82-
clearTimeout(resizeTimeout);
86+
clearTimeout(dropdownContext.resizeTimeout);
8387

84-
resizeTimeout = setTimeout(() => {
88+
dropdownContext.resizeTimeout = setTimeout(() => {
8589
recalculateDropdownPosition({
8690
updatePosition: options.updatePosition || (() => {}),
8791
dropdownIsOpen: options.isOpen,
@@ -119,7 +123,7 @@ export function handleDropdownBehavior(
119123
window.removeEventListener('click', handleWindowClick);
120124
window.removeEventListener('resize', handleWindowResize);
121125

122-
clearTimeout(resizeTimeout);
126+
clearTimeout(dropdownContext.resizeTimeout);
123127
unsubscribe();
124128
}
125129
},
@@ -140,13 +144,14 @@ export function calculateDropdownPosition(event: MouseEvent): {
140144
y: number;
141145
isInBottomHalf: boolean;
142146
} {
143-
lastTriggerElement = event.currentTarget as HTMLElement;
144-
const rect = (lastTriggerElement as HTMLElement).getBoundingClientRect();
147+
dropdownContext.lastTriggerElement = event.currentTarget as HTMLElement;
148+
const rect = dropdownContext.lastTriggerElement.getBoundingClientRect();
145149
const { x, y } = preventDropdownOverflowWhenNearViewportEdge(rect.right, rect.bottom);
146150

147151
return {
148152
x: x,
149153
y: y,
154+
// Returns true if the element is in the bottom half of the viewport.
150155
isInBottomHalf: rect.top > window.innerHeight / 2,
151156
};
152157
}
@@ -168,17 +173,17 @@ export function calculateDropdownPosition(event: MouseEvent): {
168173
*
169174
* The dropdown will be positioned at the bottom-right corner of the trigger element.
170175
* The `isInBottomHalf` parameter passed to `updatePosition` will be true if the trigger is
171-
* in the top half of the screen, suggesting the dropdown should expand downward.
176+
* in the bottom half of the screen, suggesting the dropdown should expand upward.
172177
*/
173178
export function recalculateDropdownPosition(options: {
174179
updatePosition: (x: number, y: number, isInBottomHalf: boolean) => void;
175180
dropdownIsOpen: boolean;
176181
}): void {
177-
if (!browser || !lastTriggerElement || !options.dropdownIsOpen) {
182+
if (!browser || !dropdownContext.lastTriggerElement || !options.dropdownIsOpen) {
178183
return;
179184
}
180185

181-
const rect = lastTriggerElement.getBoundingClientRect();
186+
const rect = dropdownContext.lastTriggerElement.getBoundingClientRect();
182187
const { x, y } = preventDropdownOverflowWhenNearViewportEdge(rect.right, rect.bottom);
183188
options.updatePosition(x, y, rect.top > window.innerHeight / 2);
184189
}
@@ -201,9 +206,16 @@ function preventDropdownOverflowWhenNearViewportEdge(
201206
): { x: number; y: number } {
202207
const margin = 10; // minimal margin from viewport edge
203208

209+
if (x < margin) {
210+
x = margin;
211+
}
204212
if (x > window.innerWidth - margin) {
205213
x = window.innerWidth - margin;
206214
}
215+
216+
if (y < margin) {
217+
y = margin;
218+
}
207219
if (y > window.innerHeight - margin) {
208220
y = window.innerHeight - margin;
209221
}
@@ -241,7 +253,8 @@ export function toggleDropdown(
241253
event.stopPropagation();
242254

243255
// Save the last trigger element for position calculations.
244-
lastTriggerElement = event.currentTarget as HTMLElement;
256+
// lastTriggerElement = event.currentTarget as HTMLElement;
257+
dropdownContext.lastTriggerElement = event.currentTarget as HTMLElement;
245258

246259
if (options.getPosition) {
247260
options.getPosition(event);
@@ -250,6 +263,8 @@ export function toggleDropdown(
250263

251264
activeDropdownId.set(options.dropdownId);
252265

266+
// Small delay to ensure DOM updates and event propagation is complete
267+
// before toggling the dropdown
253268
setTimeout(() => {
254269
options.toggle();
255270
}, 10);

0 commit comments

Comments
 (0)