Skip to content

Commit fe41577

Browse files
committed
Improve dropdown expansion and viewport fit logic
Refactors the expansion logic in SearchableDropdown to more accurately calculate and set container heights and positions before applying changes, ensuring the dropdown fits within the viewport without overflow. Removes the bottom margin when expanded and eliminates reliance on requestAnimationFrame for position adjustment.
1 parent 5521077 commit fe41577

1 file changed

Lines changed: 73 additions & 40 deletions

File tree

js/SearchableDropdown.js

Lines changed: 73 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export class SearchableDropdown {
2020
// Constants for layout calculations
2121
this.ITEM_HEIGHT = 28; // Height per item in pixels (padding + line-height)
2222
this.DEFAULT_MAX_HEIGHT = 300; // Default max-height of itemsContainer
23-
this.EXPANDED_BOTTOM_MARGIN = 100; // Bottom margin when expanded
23+
this.EXPANDED_BOTTOM_MARGIN = 0; // Bottom margin when expanded
2424

2525
// Load custom preset icon
2626
this.customPresetIcon = null;
@@ -445,26 +445,45 @@ export class SearchableDropdown {
445445
applyExpandedState() {
446446
if (!this.isExpanded) return;
447447

448-
// Expand to show all items
449-
const neededHeight = this.filteredItems.length * this.ITEM_HEIGHT + 20;
450-
const maxExpandedHeight = Math.min(neededHeight, window.innerHeight - this.EXPANDED_BOTTOM_MARGIN);
448+
// Get current dimensions BEFORE making any changes
449+
const currentRect = this.container.getBoundingClientRect();
450+
const currentItemsHeight = this.itemsContainer.getBoundingClientRect().height;
451451

452-
this.itemsContainer.style.maxHeight = `${maxExpandedHeight}px`;
453-
this.container.style.maxHeight = `${maxExpandedHeight + 200}px`;
454-
this.expandButton.textContent = 'Show Less';
452+
// Calculate overhead (everything except the items container)
453+
const overhead = currentRect.height - currentItemsHeight;
454+
455+
// Calculate how much height we need for all items (no extra padding)
456+
const neededHeight = this.filteredItems.length * this.ITEM_HEIGHT;
457+
458+
// Determine the actual expanded height we'll use for items
459+
const maxAllowedItemsHeight = window.innerHeight - this.EXPANDED_BOTTOM_MARGIN - overhead;
460+
const expandedItemsHeight = Math.min(neededHeight, maxAllowedItemsHeight);
461+
462+
// Calculate the total container height (items + overhead)
463+
const totalContainerHeight = expandedItemsHeight + overhead;
455464

456-
// Adjust position to prevent going off-screen
457-
requestAnimationFrame(() => {
458-
const containerRect = this.container.getBoundingClientRect();
459-
const bottomOverflow = containerRect.bottom - window.innerHeight;
465+
// Use viewport-relative position (getBoundingClientRect gives us position relative to viewport)
466+
const currentViewportTop = currentRect.top;
467+
468+
// Calculate what the bottom would be if we expand from current viewport position
469+
const potentialBottom = currentViewportTop + totalContainerHeight;
470+
471+
// If it would overflow viewport, calculate new position BEFORE setting heights
472+
if (potentialBottom > window.innerHeight - this.EXPANDED_BOTTOM_MARGIN) {
473+
// Calculate new viewport-relative top position
474+
let newViewportTop = window.innerHeight - totalContainerHeight - this.EXPANDED_BOTTOM_MARGIN;
475+
// Ensure it doesn't go above screen top
476+
newViewportTop = Math.max(10, newViewportTop);
460477

461-
if (bottomOverflow > 0) {
462-
const currentTop = parseInt(this.container.style.top);
463-
let newTop = currentTop - bottomOverflow - 10;
464-
newTop = Math.max(10, newTop);
465-
this.container.style.top = `${newTop}px`;
466-
}
467-
});
478+
// Convert viewport-relative position to document-relative position
479+
const newDocumentTop = newViewportTop + window.pageYOffset;
480+
this.container.style.top = `${newDocumentTop}px`;
481+
}
482+
483+
// Now apply the expanded heights
484+
this.itemsContainer.style.maxHeight = `${expandedItemsHeight}px`;
485+
this.container.style.maxHeight = `${totalContainerHeight}px`;
486+
this.expandButton.textContent = 'Show Less';
468487
}
469488

470489
/**
@@ -479,31 +498,45 @@ export class SearchableDropdown {
479498
}
480499

481500
if (this.isExpanded) {
482-
// Expand to show all items
483-
const neededHeight = this.filteredItems.length * this.ITEM_HEIGHT + 20;
484-
const maxExpandedHeight = Math.min(neededHeight, window.innerHeight - this.EXPANDED_BOTTOM_MARGIN);
501+
// Get current dimensions BEFORE making any changes
502+
const currentRect = this.container.getBoundingClientRect();
503+
const currentItemsHeight = this.itemsContainer.getBoundingClientRect().height;
485504

486-
this.itemsContainer.style.maxHeight = `${maxExpandedHeight}px`;
487-
this.container.style.maxHeight = `${maxExpandedHeight + 200}px`;
488-
this.expandButton.textContent = 'Show Less';
505+
// Calculate overhead (everything except the items container)
506+
const overhead = currentRect.height - currentItemsHeight;
489507

490-
// Adjust position to prevent going off-screen
491-
// Need to wait for next frame to get accurate dimensions after height change
492-
requestAnimationFrame(() => {
493-
const containerRect = this.container.getBoundingClientRect();
494-
const bottomOverflow = containerRect.bottom - window.innerHeight;
508+
// Calculate how much height we need for all items (no extra padding)
509+
const neededHeight = this.filteredItems.length * this.ITEM_HEIGHT;
510+
511+
// Determine the actual expanded height we'll use for items
512+
const maxAllowedItemsHeight = window.innerHeight - this.EXPANDED_BOTTOM_MARGIN - overhead;
513+
const expandedItemsHeight = Math.min(neededHeight, maxAllowedItemsHeight);
514+
515+
// Calculate the total container height (items + overhead)
516+
const totalContainerHeight = expandedItemsHeight + overhead;
517+
518+
// Use viewport-relative position (getBoundingClientRect gives us position relative to viewport)
519+
const currentViewportTop = currentRect.top;
520+
521+
// Calculate what the bottom would be if we expand from current viewport position
522+
const potentialBottom = currentViewportTop + totalContainerHeight;
523+
524+
// If it would overflow viewport, calculate new position BEFORE setting heights
525+
if (potentialBottom > window.innerHeight - this.EXPANDED_BOTTOM_MARGIN) {
526+
// Calculate new viewport-relative top position
527+
let newViewportTop = window.innerHeight - totalContainerHeight - this.EXPANDED_BOTTOM_MARGIN;
528+
// Ensure it doesn't go above screen top
529+
newViewportTop = Math.max(10, newViewportTop);
495530

496-
if (bottomOverflow > 0) {
497-
// Move container up to fit on screen
498-
const currentTop = parseInt(this.container.style.top);
499-
let newTop = currentTop - bottomOverflow - 10; // 10px margin from bottom
500-
501-
// Ensure it doesn't go above screen top
502-
newTop = Math.max(10, newTop);
503-
504-
this.container.style.top = `${newTop}px`;
505-
}
506-
});
531+
// Convert viewport-relative position to document-relative position
532+
const newDocumentTop = newViewportTop + window.pageYOffset;
533+
this.container.style.top = `${newDocumentTop}px`;
534+
}
535+
536+
// Now apply the expanded heights
537+
this.itemsContainer.style.maxHeight = `${expandedItemsHeight}px`;
538+
this.container.style.maxHeight = `${totalContainerHeight}px`;
539+
this.expandButton.textContent = 'Show Less';
507540
} else {
508541
// Collapse back to default
509542
this.itemsContainer.style.maxHeight = `${this.DEFAULT_MAX_HEIGHT}px`;

0 commit comments

Comments
 (0)