@@ -22,6 +22,13 @@ const VERTICAL_LAYOUT_ICON = `<svg viewBox="0 0 24 24">
2222 <rect x="2" y="12" width="20" height="10" stroke="currentColor" stroke-width="2" fill="none"/>
2323</svg>` ;
2424
25+ // Overflow dropdown icon (three horizontal dots / ellipsis menu)
26+ const OVERFLOW_ICON = `<svg viewBox="0 0 24 24" fill="currentColor">
27+ <circle cx="5" cy="12" r="2"/>
28+ <circle cx="12" cy="12" r="2"/>
29+ <circle cx="19" cy="12" r="2"/>
30+ </svg>` ;
31+
2532export interface PersistedJsonYasr extends YasrPersistentConfig {
2633 responseSummary : Parser . ResponseSummary ;
2734}
@@ -75,6 +82,10 @@ export class Tab extends EventEmitter {
7582 private yasrWrapperEl : HTMLDivElement | undefined ;
7683 private endpointSelect : EndpointSelect | undefined ;
7784 private endpointButtonsContainer : HTMLDivElement | undefined ;
85+ private endpointOverflowButton : HTMLButtonElement | undefined ;
86+ private endpointOverflowDropdown : HTMLDivElement | undefined ;
87+ private endpointButtonConfigs : Array < { endpoint : string ; label : string } > = [ ] ;
88+ private resizeObserver : ResizeObserver | undefined ;
7889 private settingsModal ?: TabSettingsModal ;
7990 private currentOrientation : "vertical" | "horizontal" ;
8091 private orientationToggleButton ?: HTMLButtonElement ;
@@ -387,13 +398,196 @@ export class Tab extends EventEmitter {
387398 }
388399
389400 this . refreshEndpointButtons ( ) ;
401+ this . initEndpointButtonsResizeObserver ( ) ;
402+ }
403+
404+ private initEndpointButtonsResizeObserver ( ) {
405+ if ( ! this . controlBarEl || ! this . endpointButtonsContainer ) return ;
406+
407+ // Clean up existing observer
408+ if ( this . resizeObserver ) {
409+ this . resizeObserver . disconnect ( ) ;
410+ }
411+
412+ // Create resize observer to detect when we need to show overflow
413+ this . resizeObserver = new ResizeObserver ( ( ) => {
414+ this . updateEndpointButtonsOverflow ( ) ;
415+ } ) ;
416+
417+ this . resizeObserver . observe ( this . controlBarEl ) ;
418+ }
419+
420+ private updateEndpointButtonsOverflow ( ) {
421+ if ( ! this . endpointButtonsContainer || ! this . controlBarEl ) return ;
422+
423+ // Get all actual endpoint buttons (not the overflow button)
424+ const buttons = Array . from (
425+ this . endpointButtonsContainer . querySelectorAll ( ".endpointButton:not(.endpointOverflowBtn)" ) ,
426+ ) as HTMLButtonElement [ ] ;
427+
428+ if ( buttons . length === 0 ) {
429+ this . hideOverflowButton ( ) ;
430+ return ;
431+ }
432+
433+ // Get the container's available width
434+ const containerRect = this . controlBarEl . getBoundingClientRect ( ) ;
435+ const containerWidth = containerRect . width ;
436+
437+ // Calculate the space used by other elements (endpoint select, settings buttons, etc.)
438+ const endpointButtonsRect = this . endpointButtonsContainer . getBoundingClientRect ( ) ;
439+ const buttonsContainerLeft = endpointButtonsRect . left - containerRect . left ;
440+
441+ // Estimate available space for endpoint buttons (leave some margin for overflow button)
442+ const overflowButtonWidth = 40 ; // Approximate width of overflow button
443+ const availableWidth = containerWidth - buttonsContainerLeft - overflowButtonWidth - 20 ; // 20px margin
444+
445+ // Make all buttons temporarily visible to measure
446+ buttons . forEach ( ( btn ) => btn . classList . remove ( "endpointButtonHidden" ) ) ;
447+
448+ // Check if buttons overflow
449+ let totalWidth = 0 ;
450+ let overflowIndex = - 1 ;
451+
452+ for ( let i = 0 ; i < buttons . length ; i ++ ) {
453+ const btn = buttons [ i ] ;
454+ const btnWidth = btn . offsetWidth + 4 ; // Include gap
455+ totalWidth += btnWidth ;
456+
457+ if ( totalWidth > availableWidth && overflowIndex === - 1 ) {
458+ overflowIndex = i ;
459+ }
460+ }
461+
462+ if ( overflowIndex === - 1 ) {
463+ // All buttons fit, hide overflow button
464+ this . hideOverflowButton ( ) ;
465+ buttons . forEach ( ( btn ) => btn . classList . remove ( "endpointButtonHidden" ) ) ;
466+ } else {
467+ // Some buttons need to go into overflow
468+ buttons . forEach ( ( btn , index ) => {
469+ if ( index >= overflowIndex ) {
470+ btn . classList . add ( "endpointButtonHidden" ) ;
471+ } else {
472+ btn . classList . remove ( "endpointButtonHidden" ) ;
473+ }
474+ } ) ;
475+ this . showOverflowButton ( overflowIndex ) ;
476+ }
477+ }
478+
479+ private showOverflowButton ( overflowStartIndex : number ) {
480+ if ( ! this . endpointButtonsContainer ) return ;
481+
482+ // Create overflow button if it doesn't exist
483+ if ( ! this . endpointOverflowButton ) {
484+ this . endpointOverflowButton = document . createElement ( "button" ) ;
485+ addClass ( this . endpointOverflowButton , "endpointOverflowBtn" ) ;
486+ this . endpointOverflowButton . innerHTML = OVERFLOW_ICON ;
487+ this . endpointOverflowButton . title = "More endpoints" ;
488+ this . endpointOverflowButton . setAttribute ( "aria-label" , "More endpoint options" ) ;
489+ this . endpointOverflowButton . setAttribute ( "aria-haspopup" , "true" ) ;
490+ this . endpointOverflowButton . setAttribute ( "aria-expanded" , "false" ) ;
491+
492+ this . endpointOverflowButton . addEventListener ( "click" , ( e ) => {
493+ e . stopPropagation ( ) ;
494+ this . toggleOverflowDropdown ( ) ;
495+ } ) ;
496+
497+ this . endpointButtonsContainer . appendChild ( this . endpointOverflowButton ) ;
498+ }
499+
500+ // Update the overflow button's data with which buttons are hidden
501+ this . endpointOverflowButton . dataset . overflowStart = String ( overflowStartIndex ) ;
502+ this . endpointOverflowButton . style . display = "flex" ;
503+ }
504+
505+ private hideOverflowButton ( ) {
506+ if ( this . endpointOverflowButton ) {
507+ this . endpointOverflowButton . style . display = "none" ;
508+ }
509+ this . closeOverflowDropdown ( ) ;
510+ }
511+
512+ private toggleOverflowDropdown ( ) {
513+ if ( this . endpointOverflowDropdown && this . endpointOverflowDropdown . style . display !== "none" ) {
514+ this . closeOverflowDropdown ( ) ;
515+ } else {
516+ this . openOverflowDropdown ( ) ;
517+ }
518+ }
519+
520+ private openOverflowDropdown ( ) {
521+ if ( ! this . endpointOverflowButton || ! this . endpointButtonsContainer ) return ;
522+
523+ const overflowStartIndex = parseInt ( this . endpointOverflowButton . dataset . overflowStart || "0" , 10 ) ;
524+ const overflowButtons = this . endpointButtonConfigs . slice ( overflowStartIndex ) ;
525+
526+ if ( overflowButtons . length === 0 ) return ;
527+
528+ // Create dropdown if it doesn't exist
529+ if ( ! this . endpointOverflowDropdown ) {
530+ this . endpointOverflowDropdown = document . createElement ( "div" ) ;
531+ addClass ( this . endpointOverflowDropdown , "endpointOverflowDropdown" ) ;
532+ this . endpointButtonsContainer . appendChild ( this . endpointOverflowDropdown ) ;
533+ }
534+
535+ // Clear and populate dropdown
536+ this . endpointOverflowDropdown . innerHTML = "" ;
537+
538+ overflowButtons . forEach ( ( buttonConfig ) => {
539+ const item = document . createElement ( "button" ) ;
540+ addClass ( item , "endpointOverflowItem" ) ;
541+ item . textContent = buttonConfig . label ;
542+ item . title = `Set endpoint to ${ buttonConfig . endpoint } ` ;
543+ item . setAttribute ( "aria-label" , `Set endpoint to ${ buttonConfig . endpoint } ` ) ;
544+
545+ item . addEventListener ( "click" , ( ) => {
546+ this . setEndpoint ( buttonConfig . endpoint ) ;
547+ this . closeOverflowDropdown ( ) ;
548+ } ) ;
549+
550+ this . endpointOverflowDropdown ! . appendChild ( item ) ;
551+ } ) ;
552+
553+ // Position and show dropdown
554+ this . endpointOverflowDropdown . style . display = "block" ;
555+ this . endpointOverflowButton . setAttribute ( "aria-expanded" , "true" ) ;
556+
557+ // Add click-outside listener to close dropdown
558+ const closeHandler = ( e : MouseEvent ) => {
559+ if (
560+ this . endpointOverflowDropdown &&
561+ ! this . endpointOverflowDropdown . contains ( e . target as Node ) &&
562+ e . target !== this . endpointOverflowButton
563+ ) {
564+ this . closeOverflowDropdown ( ) ;
565+ document . removeEventListener ( "click" , closeHandler ) ;
566+ }
567+ } ;
568+
569+ // Delay adding listener to avoid immediate close
570+ setTimeout ( ( ) => {
571+ document . addEventListener ( "click" , closeHandler ) ;
572+ } , 0 ) ;
573+ }
574+
575+ private closeOverflowDropdown ( ) {
576+ if ( this . endpointOverflowDropdown ) {
577+ this . endpointOverflowDropdown . style . display = "none" ;
578+ }
579+ if ( this . endpointOverflowButton ) {
580+ this . endpointOverflowButton . setAttribute ( "aria-expanded" , "false" ) ;
581+ }
390582 }
391583
392584 public refreshEndpointButtons ( ) {
393585 if ( ! this . endpointButtonsContainer ) return ;
394586
395- // Clear existing buttons
587+ // Clear existing buttons (but keep overflow button reference)
396588 this . endpointButtonsContainer . innerHTML = "" ;
589+ this . endpointOverflowButton = undefined ;
590+ this . endpointOverflowDropdown = undefined ;
397591
398592 // Get config buttons (for backwards compatibility) and filter out disabled ones
399593 const disabledButtons = this . yasgui . persistentConfig . getDisabledDevButtons ( ) ;
@@ -412,6 +606,9 @@ export class Tab extends EventEmitter {
412606
413607 const allButtons = [ ...configButtons , ...endpointButtons , ...customButtons ] ;
414608
609+ // Store button configs for overflow dropdown
610+ this . endpointButtonConfigs = allButtons ;
611+
415612 if ( allButtons . length === 0 ) {
416613 // Hide container if no buttons
417614 this . endpointButtonsContainer . style . display = "none" ;
@@ -434,6 +631,11 @@ export class Tab extends EventEmitter {
434631
435632 this . endpointButtonsContainer ! . appendChild ( button ) ;
436633 } ) ;
634+
635+ // Trigger overflow check after rendering
636+ requestAnimationFrame ( ( ) => {
637+ this . updateEndpointButtonsOverflow ( ) ;
638+ } ) ;
437639 }
438640
439641 public setEndpoint ( endpoint : string , endpointHistory ?: string [ ] ) {
@@ -1096,6 +1298,12 @@ WHERE {
10961298 document . documentElement . removeEventListener ( "mousemove" , this . doVerticalDrag , false ) ;
10971299 document . documentElement . removeEventListener ( "mouseup" , this . stopVerticalDrag , false ) ;
10981300
1301+ // Clean up resize observer for endpoint buttons overflow
1302+ if ( this . resizeObserver ) {
1303+ this . resizeObserver . disconnect ( ) ;
1304+ this . resizeObserver = undefined ;
1305+ }
1306+
10991307 this . removeAllListeners ( ) ;
11001308 this . settingsModal ?. destroy ( ) ;
11011309 this . endpointSelect ?. destroy ( ) ;
0 commit comments