Files
2026-07-12 10:17:17 -04:00

2.8 KiB

Dropdown Portal / Teleport Pattern (ShopProQuote)

Problem

The #service-search-results dropdown lives inside #services-content (a collapsible accordion section). When the accordion uses overflow: hidden or any ancestor creates a stacking context, the absolute-positioned dropdown gets clipped — it appears behind the Quote Summary section or is truncated.

CSS z-index: 99999 !important does NOT fix this because z-index is scoped within the parent stacking context.

Fix: Teleport to #dropdown-root

All ShopProQuote pages have <div id="dropdown-root"></div> at the end of <body> (in index.html, dashboard.html, repair-orders.html, customers.html, appointments.html). Use this as a portal target.

Implementation (Vanilla JS)

In renderServiceResults() in both main.js and quote-tab-manager.js:

// 1. Move dropdown to portal root (only on first render)
const dropdownRoot = document.getElementById('dropdown-root');
if (dropdownRoot && serviceSearchResults.parentElement !== dropdownRoot) {
    dropdownRoot.appendChild(serviceSearchResults);
}

// 2. Position relative to the search input using getBoundingClientRect
const rect = serviceSearchInput.getBoundingClientRect();
serviceSearchResults.style.position = 'fixed';
serviceSearchResults.style.top = (rect.bottom + 4) + 'px';
serviceSearchResults.style.left = rect.left + 'px';
serviceSearchResults.style.width = rect.width + 'px';

// 3. Add scroll/resize repositioning
const repositionDropdown = () => {
    if (serviceSearchResults.classList.contains('hidden')) return;
    const rect = serviceSearchInput.getBoundingClientRect();
    serviceSearchResults.style.top = (rect.bottom + 4) + 'px';
    serviceSearchResults.style.left = rect.left + 'px';
    serviceSearchResults.style.width = rect.width + 'px';
};
window.addEventListener('scroll', repositionDropdown, { passive: true });
window.addEventListener('resize', repositionDropdown);

Why This Works

  • appendChild to dropdown-root (direct child of <body>) escapes ALL ancestor stacking contexts
  • position: fixed with getBoundingClientRect() positions the dropdown relative to the viewport, anchored to the input
  • Scroll/resize listener keeps it positioned correctly as the user scrolls

Files to Modify

  • main.js: renderServiceResults() function
  • quote-tab-manager.js: renderServiceResults() function AND initializeServiceSearch() (for the scroll listener)
  • BOTH files have their own service search implementation — both need the fix

React Equivalent (spq-v2)

In the React rewrite, use a portal:

import { createPortal } from 'react-dom';

{open && createPortal(
  <div className="fixed z-50 ..." style={{ top: rect.bottom + 4, left: rect.left, width: rect.width }}>
    {/* dropdown items */}
  </div>,
  document.getElementById('dropdown-root')!
)}