@@ -3,9 +3,11 @@ 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+ 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 */
173178export 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