2.8 KiB
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
appendChildtodropdown-root(direct child of<body>) escapes ALL ancestor stacking contextsposition: fixedwithgetBoundingClientRect()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()functionquote-tab-manager.js:renderServiceResults()function ANDinitializeServiceSearch()(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')!
)}