diff --git a/src/css/custom.css b/src/css/custom.css index dec597b75e..64c9272c6a 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -62,6 +62,10 @@ --ifm-color-success: #34C263; --ifm-color-success-dark: #27914A; --ifm-button-background-color: #5851DB; + /* Infima defaults this to `transparent` (relying on the browser's white + canvas). An explicit opaque value is needed so that sticky elements + like search-page controls can hide content scrolling behind them. */ + --ifm-background-color: #fff; } .table-of-contents { @@ -407,37 +411,77 @@ html { .DocSearch-Clear { font-size: 0 !important; } -.DocSearch-Clear span { - display: none; -} -/* Strip the duplicate chrome from the form itself. */ +/* Strip the duplicate chrome from the form itself. + width: auto !important overrides DocSearch's dynamically-loaded width: 100%, + which would otherwise force the form to consume the entire first flex row and + push the × button onto its own line on mobile. */ .DocSearch-Form { flex: 1 1 auto; - width: auto; + width: auto !important; background: transparent; border-block-end: none !important; border-radius: 0; } -/* On mobile, wrap the custom controls onto their own row below the search input. */ +/* Hide the DocSearch-owned close button — we render our own in the portal so + we never mutate React-owned DOM nodes (which causes reconciliation crashes). */ +.DocSearch-Close { + display: none !important; +} + +/* Custom controls (Typo Tolerance toggle + product filter) portaled into the search bar. */ +.search-custom-controls { + display: flex; + align-items: center; + gap: 8px; + margin-right: 8px; +} + +/* Our replacement close button rendered inside the portal. */ +.search-modal-close { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + background: none; + border: none; + cursor: pointer; + color: var(--docsearch-muted-color); + padding: 0; + margin: 0 16px 0 8px; +} +.search-modal-close:hover { + color: var(--docsearch-text-color); +} +[data-theme='dark'] .search-modal-close { + color: white; +} + +/* On mobile, wrap the custom controls onto their own row below the search input, + but keep the × button on the first row (upper-right corner). */ @media (max-width: 768px) { .DocSearch-SearchBar { flex-wrap: wrap; } + .DocSearch-Form { + order: 1; + } + + .search-modal-close { + order: 2; + margin: 0 16px 0 8px; + } + .search-custom-controls { + order: 3; flex: 0 0 100%; - margin-right: 0 !important; + margin-right: 0; padding: 6px 16px 8px; border-bottom: 1px solid var(--docsearch-subtle-color); } - -} - -/* Responsive improvements */ -@media (max-width: 768px) { .DocSearch-Button-Keys { display: none; } diff --git a/src/theme/SearchBar/index.js b/src/theme/SearchBar/index.js index 4c912a2cd4..83cb7e1920 100644 --- a/src/theme/SearchBar/index.js +++ b/src/theme/SearchBar/index.js @@ -441,20 +441,20 @@ function DocSearch({externalUrlRegex, onModalOpen, selectedProductsRef, typoTole } }, []); + const closeModal = useCallback(() => { + setIsOpen(false); + searchButtonRef.current?.focus(); + setInitialQuery(undefined); + }, []); + const openModal = useCallback(() => { prepareSearchContainer(); importDocSearchModalIfNeeded().then(() => { setIsOpen(true); // Let React render the modal, then caller can locate DOM nodes - setTimeout(() => onModalOpen?.(), 0); + setTimeout(() => onModalOpen?.(closeModal), 0); }); - }, [prepareSearchContainer, onModalOpen]); - - const closeModal = useCallback(() => { - setIsOpen(false); - searchButtonRef.current?.focus(); - setInitialQuery(undefined); - }, []); + }, [prepareSearchContainer, onModalOpen, closeModal]); const handleInput = useCallback( (event) => { @@ -684,22 +684,20 @@ export default function SearchBar() { // This is where we will portal the filters into the modal DOM. const [modalHeaderEl, setModalHeaderEl] = useState(null); + // Holds the closeModal function passed up from the inner DocSearch component. + const closeModalRef = useRef(null); - const onModalOpen = useCallback(() => { + const onModalOpen = useCallback((closeModal) => { // Target .DocSearch-SearchBar so our controls are a sibling of .DocSearch-Form. // This lets us move them below the search row on mobile via flex-wrap. const el = document.querySelector('.DocSearch-SearchBar') || document.querySelector('.DocSearch-Modal'); + closeModalRef.current = closeModal ?? null; setModalHeaderEl(el || null); }, []); - const portalTarget = useMemo(() => { - if (!modalHeaderEl) return null; - return modalHeaderEl; - }, [modalHeaderEl]); - // Keep contextualSearch stable to prevent query reset // Product filters are handled via transformSearchClient instead const contextualSearch = false; @@ -714,45 +712,52 @@ export default function SearchBar() { onModalOpen={onModalOpen} /> - {portalTarget && + {modalHeaderEl && createPortal( - // Wrapper to align with DocSearch input -