@@ -3,9 +3,12 @@ import { browser } from '$app/environment';
3
3
4
4
const activeDropdownId : Writable < string | null > = writable < string | null > ( null ) ;
5
5
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
+ } ;
9
12
10
13
/**
11
14
* A Svelte action that manages dropdown behavior for an HTML element.
@@ -56,6 +59,8 @@ export function handleDropdownBehavior(
56
59
} ;
57
60
}
58
61
62
+ // Flag to prevent the dropdown from closing immediately after opening
63
+ // when a click event propagates to the window.
59
64
let ignoreNextClick = false ;
60
65
61
66
// Close the dropdown on scroll.
@@ -79,9 +84,9 @@ export function handleDropdownBehavior(
79
84
80
85
// Recalculate the dropdown position on resize.
81
86
const handleWindowResize = ( ) => {
82
- clearTimeout ( resizeTimeout ) ;
87
+ clearTimeout ( dropdownContext . resizeTimeout ) ;
83
88
84
- resizeTimeout = setTimeout ( ( ) => {
89
+ dropdownContext . resizeTimeout = setTimeout ( ( ) => {
85
90
recalculateDropdownPosition ( {
86
91
updatePosition : options . updatePosition || ( ( ) => { } ) ,
87
92
dropdownIsOpen : options . isOpen ,
@@ -119,7 +124,7 @@ export function handleDropdownBehavior(
119
124
window . removeEventListener ( 'click' , handleWindowClick ) ;
120
125
window . removeEventListener ( 'resize' , handleWindowResize ) ;
121
126
122
- clearTimeout ( resizeTimeout ) ;
127
+ clearTimeout ( dropdownContext . resizeTimeout ) ;
123
128
unsubscribe ( ) ;
124
129
}
125
130
} ,
@@ -140,13 +145,14 @@ export function calculateDropdownPosition(event: MouseEvent): {
140
145
y : number ;
141
146
isInBottomHalf : boolean ;
142
147
} {
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 ( ) ;
145
150
const { x, y } = preventDropdownOverflowWhenNearViewportEdge ( rect . right , rect . bottom ) ;
146
151
147
152
return {
148
153
x : x ,
149
154
y : y ,
155
+ // Returns true if the element is in the bottom half of the viewport.
150
156
isInBottomHalf : rect . top > window . innerHeight / 2 ,
151
157
} ;
152
158
}
@@ -168,17 +174,17 @@ export function calculateDropdownPosition(event: MouseEvent): {
168
174
*
169
175
* The dropdown will be positioned at the bottom-right corner of the trigger element.
170
176
* 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 .
172
178
*/
173
179
export function recalculateDropdownPosition ( options : {
174
180
updatePosition : ( x : number , y : number , isInBottomHalf : boolean ) => void ;
175
181
dropdownIsOpen : boolean ;
176
182
} ) : void {
177
- if ( ! browser || ! lastTriggerElement || ! options . dropdownIsOpen ) {
183
+ if ( ! browser || ! dropdownContext . lastTriggerElement || ! options . dropdownIsOpen ) {
178
184
return ;
179
185
}
180
186
181
- const rect = lastTriggerElement . getBoundingClientRect ( ) ;
187
+ const rect = dropdownContext . lastTriggerElement . getBoundingClientRect ( ) ;
182
188
const { x, y } = preventDropdownOverflowWhenNearViewportEdge ( rect . right , rect . bottom ) ;
183
189
options . updatePosition ( x , y , rect . top > window . innerHeight / 2 ) ;
184
190
}
@@ -201,9 +207,16 @@ function preventDropdownOverflowWhenNearViewportEdge(
201
207
) : { x : number ; y : number } {
202
208
const margin = 10 ; // minimal margin from viewport edge
203
209
210
+ if ( x < margin ) {
211
+ x = margin ;
212
+ }
204
213
if ( x > window . innerWidth - margin ) {
205
214
x = window . innerWidth - margin ;
206
215
}
216
+
217
+ if ( y < margin ) {
218
+ y = margin ;
219
+ }
207
220
if ( y > window . innerHeight - margin ) {
208
221
y = window . innerHeight - margin ;
209
222
}
@@ -241,7 +254,7 @@ export function toggleDropdown(
241
254
event . stopPropagation ( ) ;
242
255
243
256
// Save the last trigger element for position calculations.
244
- lastTriggerElement = event . currentTarget as HTMLElement ;
257
+ dropdownContext . lastTriggerElement = event . currentTarget as HTMLElement ;
245
258
246
259
if ( options . getPosition ) {
247
260
options . getPosition ( event ) ;
@@ -250,6 +263,8 @@ export function toggleDropdown(
250
263
251
264
activeDropdownId . set ( options . dropdownId ) ;
252
265
266
+ // Small delay to ensure DOM updates and event propagation is complete
267
+ // before toggling the dropdown
253
268
setTimeout ( ( ) => {
254
269
options . toggle ( ) ;
255
270
} , 10 ) ;
0 commit comments