@@ -3,9 +3,12 @@ import { browser } from '$app/environment';
33
44const 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 */
173179export 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