Skip to content

Commit 18cbe34

Browse files
authored
Merge pull request #1926 from AtCoder-NoviSteps/#1925
♻️ Update docs and refactoring (#1925)
2 parents b9d54fe + c31cf68 commit 18cbe34

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,12 @@ 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+
// Reference to the element that triggered the dropdown.
8+
lastTriggerElement: null as HTMLElement | null,
9+
// Bounce timer for resizing event.
10+
resizeTimeout: undefined as ReturnType<typeof setTimeout> | undefined,
11+
};
912

1013
/**
1114
* A Svelte action that manages dropdown behavior for an HTML element.
@@ -56,6 +59,8 @@ export function handleDropdownBehavior(
5659
};
5760
}
5861

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

6166
// Close the dropdown on scroll.
@@ -79,9 +84,9 @@ export function handleDropdownBehavior(
7984

8085
// Recalculate the dropdown position on resize.
8186
const handleWindowResize = () => {
82-
clearTimeout(resizeTimeout);
87+
clearTimeout(dropdownContext.resizeTimeout);
8388

84-
resizeTimeout = setTimeout(() => {
89+
dropdownContext.resizeTimeout = setTimeout(() => {
8590
recalculateDropdownPosition({
8691
updatePosition: options.updatePosition || (() => {}),
8792
dropdownIsOpen: options.isOpen,
@@ -119,7 +124,7 @@ export function handleDropdownBehavior(
119124
window.removeEventListener('click', handleWindowClick);
120125
window.removeEventListener('resize', handleWindowResize);
121126

122-
clearTimeout(resizeTimeout);
127+
clearTimeout(dropdownContext.resizeTimeout);
123128
unsubscribe();
124129
}
125130
},
@@ -140,13 +145,14 @@ export function calculateDropdownPosition(event: MouseEvent): {
140145
y: number;
141146
isInBottomHalf: boolean;
142147
} {
143-
lastTriggerElement = event.currentTarget as HTMLElement;
144-
const rect = (lastTriggerElement as HTMLElement).getBoundingClientRect();
148+
dropdownContext.lastTriggerElement = event.currentTarget as HTMLElement;
149+
const rect = dropdownContext.lastTriggerElement.getBoundingClientRect();
145150
const { x, y } = preventDropdownOverflowWhenNearViewportEdge(rect.right, rect.bottom);
146151

147152
return {
148153
x: x,
149154
y: y,
155+
// Returns true if the element is in the bottom half of the viewport.
150156
isInBottomHalf: rect.top > window.innerHeight / 2,
151157
};
152158
}
@@ -168,17 +174,17 @@ export function calculateDropdownPosition(event: MouseEvent): {
168174
*
169175
* The dropdown will be positioned at the bottom-right corner of the trigger element.
170176
* 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.
177+
* in the bottom half of the screen, suggesting the dropdown should expand upward.
172178
*/
173179
export function recalculateDropdownPosition(options: {
174180
updatePosition: (x: number, y: number, isInBottomHalf: boolean) => void;
175181
dropdownIsOpen: boolean;
176182
}): void {
177-
if (!browser || !lastTriggerElement || !options.dropdownIsOpen) {
183+
if (!browser || !dropdownContext.lastTriggerElement || !options.dropdownIsOpen) {
178184
return;
179185
}
180186

181-
const rect = lastTriggerElement.getBoundingClientRect();
187+
const rect = dropdownContext.lastTriggerElement.getBoundingClientRect();
182188
const { x, y } = preventDropdownOverflowWhenNearViewportEdge(rect.right, rect.bottom);
183189
options.updatePosition(x, y, rect.top > window.innerHeight / 2);
184190
}
@@ -201,9 +207,16 @@ function preventDropdownOverflowWhenNearViewportEdge(
201207
): { x: number; y: number } {
202208
const margin = 10; // minimal margin from viewport edge
203209

210+
if (x < margin) {
211+
x = margin;
212+
}
204213
if (x > window.innerWidth - margin) {
205214
x = window.innerWidth - margin;
206215
}
216+
217+
if (y < margin) {
218+
y = margin;
219+
}
207220
if (y > window.innerHeight - margin) {
208221
y = window.innerHeight - margin;
209222
}
@@ -241,7 +254,7 @@ export function toggleDropdown(
241254
event.stopPropagation();
242255

243256
// Save the last trigger element for position calculations.
244-
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)