diff --git a/blocks/atomic-search/atomic-search.css b/blocks/atomic-search/atomic-search.css index 0e487377e..f49103ef8 100644 --- a/blocks/atomic-search/atomic-search.css +++ b/blocks/atomic-search/atomic-search.css @@ -25,6 +25,18 @@ --atomic-search-skeleton-margin-top: 0px; } +/* Flatten Atomic 3.60+ Lit theme tokens to match ExL prod (Atomic 3.13) chrome. */ +.atomic-search atomic-search-interface { + --atomic-border-radius: 0; + --atomic-border-radius-md: 0; + --atomic-border-radius-lg: 0; + --atomic-background: transparent; + + /* Kill Atomic 3.60 default 1.5rem/1rem layout gaps that inflate section margins. */ + --atomic-layout-spacing-x: 0; + --atomic-layout-spacing-y: 0; +} + .atomic-search-premium-search-wrapper { position: relative; min-height: var(--atomic-search-premium-search-height, 0); @@ -403,9 +415,11 @@ } @media only screen and (min-width: 1024px) { - .atomic-search atomic-search-interface atomic-search-layout[id^='atomic-search-layout'] { + .atomic-search atomic-search-interface atomic-search-layout[id^='atomic-search-layout'], + .atomic-search atomic-search-interface atomic-search-layout { grid-template-columns: 0 20% 80% !important; - grid-column-gap: 16px; + grid-column-gap: 0; + column-gap: 0; } .atomic-load-skeleton-main { @@ -476,7 +490,18 @@ } .section.atomic-search-container .atomic-search-wrapper { + box-sizing: border-box; + width: 100%; max-width: 1440px; + margin-left: auto; + margin-right: auto; + } + + .section.atomic-search-container .atomic-search-wrapper .atomic-search, + .section.atomic-search-container .atomic-search-wrapper atomic-search-interface, + .section.atomic-search-container .atomic-search-wrapper atomic-search-layout { + width: 100%; + max-width: 100%; } .atomic-search button.facet-close-btn { diff --git a/blocks/atomic-search/atomic-search.js b/blocks/atomic-search/atomic-search.js index a57312023..bb5b1e6d6 100644 --- a/blocks/atomic-search/atomic-search.js +++ b/blocks/atomic-search/atomic-search.js @@ -156,8 +156,10 @@ export default function decorate(block) { atomicResultPageHandler(block.querySelector('atomic-results-per-page')); }; + // Wait for components we actually mount. Atomic 3.60+ lazy-registers CE definitions; + // we use atomic-folded-result-list (not atomic-result-list), so waiting on the latter hangs forever. Promise.all([ - customElements.whenDefined('atomic-result-list'), + customElements.whenDefined('atomic-folded-result-list'), customElements.whenDefined('atomic-result'), customElements.whenDefined('atomic-result-multi-value-text'), customElements.whenDefined('atomic-search-box'), @@ -166,11 +168,18 @@ export default function decorate(block) { customElements.whenDefined('atomic-breadbox'), customElements.whenDefined('atomic-pager'), customElements.whenDefined('atomic-no-results'), - customElements.whenDefined('atomic-search-box'), customElements.whenDefined('atomic-results-per-page'), customElements.whenDefined('atomic-notifications'), ]).then(() => { atomicNoResultHandler(block, placeholders); + // Register before commonActionHandler — facet init can fire FACET_LOADED synchronously. + document.addEventListener( + CUSTOM_EVENTS.FACET_LOADED, + () => { + resolveBlockLevelSkeleton(block); + }, + { once: true }, + ); commonActionHandler(); decorateIcons(block); let eventFiredOnPageLoad = false; @@ -224,14 +233,6 @@ export default function decorate(block) { filters: placeholders.searchFiltersLabel || 'Filters', }); - document.addEventListener( - CUSTOM_EVENTS.FACET_LOADED, - () => { - resolveBlockLevelSkeleton(block); - }, - { once: true }, - ); - const trackingHandler = (event) => { const isResizeAction = event?.detail?.callFrom === 'resize'; if (isResizeAction) { diff --git a/blocks/atomic-search/components/atomic-facet-engine-helpers.js b/blocks/atomic-search/components/atomic-facet-engine-helpers.js index 58e0223af..019ac1343 100644 --- a/blocks/atomic-search/components/atomic-facet-engine-helpers.js +++ b/blocks/atomic-search/components/atomic-facet-engine-helpers.js @@ -167,7 +167,7 @@ export function applyFacetRawValuesToDom(parentWrapper, engineValues, placeholde delete li.dataset.updated; delete li.dataset.childfacet; delete li.dataset.parent; - li.querySelector('[part="only-facet-btn"]')?.remove(); + li.querySelector('[part~="only-facet-btn"]')?.remove(); delete li.dataset.onlyfacet; li.part?.remove('facet-child-element'); const labelEl = li.querySelector('label'); diff --git a/blocks/atomic-search/components/atomic-search-breadbox.js b/blocks/atomic-search/components/atomic-search-breadbox.js index eba7ac82e..934ba35ad 100644 --- a/blocks/atomic-search/components/atomic-search-breadbox.js +++ b/blocks/atomic-search/components/atomic-search-breadbox.js @@ -1,8 +1,12 @@ -import { CUSTOM_EVENTS, isUserClick } from './atomic-search-utils.js'; +import { CUSTOM_EVENTS, isUserClick, waitFor } from './atomic-search-utils.js'; export default function atomicBreadBoxHandler(baseElement) { + let listObserver = null; + let listObserveRetryPending = false; + function updateFilterClearBtnStyles(enabled) { const clearBtn = document.querySelector('.clear-label'); + if (!clearBtn) return; if (enabled) { clearBtn.classList.add('clear-btn-enabled'); } else { @@ -15,6 +19,22 @@ export default function atomicBreadBoxHandler(baseElement) { document.dispatchEvent(event); } + /** + * Atomic 3.13 toggled visibility via `.atomic-hidden` on the host. + * Atomic 3.60+ (Lit) often leaves className empty and only swaps display / crumbs. + * Drive Clear All from active breadcrumbs so both versions stay in sync with prod. + */ + function hasActiveBreadcrumbs() { + const list = baseElement.shadowRoot?.querySelector('[part="breadcrumb-list"]'); + if (!list) return false; + return list.querySelectorAll('li.breadcrumb').length > 0; + } + + function isBreadboxEnabled() { + if (baseElement.className?.includes('atomic-hidden')) return false; + return hasActiveBreadcrumbs(); + } + function attachListeners() { const parentWrapper = baseElement.shadowRoot.querySelector(`[part="breadcrumb-list"]`); if (!parentWrapper) { @@ -74,32 +94,56 @@ export default function atomicBreadBoxHandler(baseElement) { }); } - function observeBreadboxUI(enabled) { - const targetElement = baseElement.shadowRoot.querySelector(`[part="breadcrumb-list"]`); + function syncFromBreadcrumbs({ emitFilterUpdate = true } = {}) { + const enabled = isBreadboxEnabled(); + updateFilterClearBtnStyles(enabled); if (enabled) { - const observer = new MutationObserver(() => { - attachListeners(); - onFilterUpdate(); - }); - observer.observe(targetElement, { childList: true }); - } else { + attachListeners(); + } + if (emitFilterUpdate) { onFilterUpdate(); } } + function observeBreadcrumbList() { + const targetElement = baseElement.shadowRoot?.querySelector(`[part="breadcrumb-list"]`); + if (!targetElement) { + // Retry until Lit mounts the breadcrumb list; coalesce concurrent retries. + if (!listObserveRetryPending) { + listObserveRetryPending = true; + waitFor(() => { + listObserveRetryPending = false; + observeBreadcrumbList(); + }); + } + return; + } + listObserveRetryPending = false; + if (listObserver) { + listObserver.disconnect(); + } + listObserver = new MutationObserver(() => { + syncFromBreadcrumbs(); + }); + listObserver.observe(targetElement, { childList: true, subtree: true }); + syncFromBreadcrumbs(); + } + const observer = new MutationObserver((mutationsList) => { mutationsList.forEach((mutation) => { if (mutation.type === 'attributes' && mutation.attributeName === 'class') { - const enabled = !baseElement.className.includes('atomic-hidden'); - updateFilterClearBtnStyles(enabled); - observeBreadboxUI(enabled); - attachListeners(); + // Keep legacy Atomic 3.13 class-toggle path. + syncFromBreadcrumbs(); + observeBreadcrumbList(); + } + if (mutation.type === 'childList') { + observeBreadcrumbList(); } }); }); function onResultsUpdate() { - onFilterUpdate(); + syncFromBreadcrumbs(); } function hideSection() { @@ -110,9 +154,10 @@ export default function atomicBreadBoxHandler(baseElement) { baseElement.style.display = ''; } - document.addEventListener(CUSTOM_EVENTS.RESULT_UPDATED, onResultsUpdate, { once: true }); + document.addEventListener(CUSTOM_EVENTS.RESULT_UPDATED, onResultsUpdate); document.addEventListener(CUSTOM_EVENTS.NO_RESULT_FOUND, hideSection); document.addEventListener(CUSTOM_EVENTS.RESULT_FOUND, showSection); observer.observe(baseElement, { attributes: true, attributeFilter: ['class'], childList: true }); + observeBreadcrumbList(); } diff --git a/blocks/atomic-search/components/atomic-search-facet.js b/blocks/atomic-search/components/atomic-search-facet.js index 938cc3447..8eecaf1e4 100644 --- a/blocks/atomic-search/components/atomic-search-facet.js +++ b/blocks/atomic-search/components/atomic-search-facet.js @@ -69,41 +69,55 @@ export default function atomicFacetHandler(block, placeholders, searchInterface) } } } - if (!facet.dataset.evented) { - facet.dataset.evented = 'true'; - const clickHandler = (e, onlyOptionClicked = false) => { - const userClickAction = isUserClick(e); - if (!userClickAction || facet.dataset.filterclick === 'true') { - return; - } + const runFacetClick = (e, onlyOptionClicked = false) => { + const userClickAction = isUserClick(e); + if (!userClickAction || facet.dataset.filterclick === 'true') { + return; + } - const shimmer = atomicElement.shadowRoot.querySelector('.facet-shimmer'); - shimmer?.part.add('show-shimmer'); - const filtersChanged = syncFacetParentChildFilters({ facet, atomicElement, onlyOptionClicked }); - if (!filtersChanged && shimmer) { - shimmer.part.remove('show-shimmer'); - } - }; + const shimmer = atomicElement.shadowRoot.querySelector('.facet-shimmer'); + shimmer?.part.add('show-shimmer'); + const filtersChanged = syncFacetParentChildFilters({ facet, atomicElement, onlyOptionClicked }); + if (!filtersChanged && shimmer) { + shimmer.part.remove('show-shimmer'); + } + }; - const debouncedHandler = debounce(100, clickHandler); - facet.addEventListener('click', debouncedHandler); - const onlyFilterEl = facet.querySelector(`[part="only-facet-btn"]`); - if (onlyFilterEl) { - const filterHandler = (e) => { + // "Only" hover/click must bind even if row was evented before the Only span existed + // (Lit / hash-driven re-renders). Re-query the span on each event. + if (!facet.dataset.onlyHoverBound) { + facet.dataset.onlyHoverBound = 'true'; + facet.addEventListener('mouseenter', () => { + facet.querySelector('[part~="only-facet-btn"]')?.part.add('only-facet-visible'); + }); + facet.addEventListener('mouseleave', () => { + facet.querySelector('[part~="only-facet-btn"]')?.part.remove('only-facet-visible'); + }); + } + + if (!facet.dataset.onlyClickBound) { + facet.dataset.onlyClickBound = 'true'; + const debouncedOnlyClick = debounce(100, (e) => { + runFacetClick(e, true); + facet.dataset.filterclick = 'true'; + }); + // stopImmediatePropagation must be sync; debouncing it lets the row click win. + facet.addEventListener( + 'click', + (e) => { + const target = e.target instanceof Element ? e.target : e.target?.parentElement; + const onlyFilterEl = target?.closest?.('[part~="only-facet-btn"]'); + if (!onlyFilterEl || !facet.contains(onlyFilterEl)) return; e.stopImmediatePropagation(); - clickHandler(e, true); - facet.dataset.filterclick = 'true'; - }; - const debouncedFilterClickHandler = debounce(100, filterHandler); - onlyFilterEl.addEventListener('click', debouncedFilterClickHandler); - facet.addEventListener('mouseenter', () => { - onlyFilterEl.part.add('only-facet-visible'); - }); + debouncedOnlyClick(e); + }, + true, + ); + } - facet.addEventListener('mouseleave', () => { - onlyFilterEl.part.remove('only-facet-visible'); - }); - } + if (!facet.dataset.evented) { + facet.dataset.evented = 'true'; + facet.addEventListener('click', debounce(100, runFacetClick)); } }; @@ -117,7 +131,8 @@ export default function atomicFacetHandler(block, placeholders, searchInterface) const sortFacetsInOrder = (parentWrapper) => { const children = Array.from(parentWrapper.children); const sortedChildren = sortElementsByLabel(children); - parentWrapper.innerHTML = ''; + // Move nodes in place — do NOT clear innerHTML. Atomic 3.60 (Lit) owns these + // value rows; destroying/recreating them breaks checkbox click bindings until reload. sortedChildren.forEach((item) => parentWrapper.appendChild(item)); }; @@ -146,6 +161,12 @@ export default function atomicFacetHandler(block, placeholders, searchInterface) const updateChildElementUI = (parentWrapper, facetParent) => { const children = Array.from(parentWrapper.children); + // Product/Role/etc. have no parent/child rows — skip. Clearing values DOM here + // breaks Lit Atomic checkbox bindings after the first selection until reload. + if (!children.some((el) => el.dataset.childfacet === 'true')) { + return; + } + const finalList = []; let tempGroup = []; @@ -203,7 +224,7 @@ export default function atomicFacetHandler(block, placeholders, searchInterface) facetParent.dataset.observed = ''; } } - parentWrapper.innerHTML = ''; + // Reorder by moving existing nodes — never innerHTML='' (Lit Atomic 3.60). finalList.forEach((item) => parentWrapper.appendChild(item)); }; @@ -347,6 +368,121 @@ export default function atomicFacetHandler(block, placeholders, searchInterface) } }; + /** + * Atomic 3.60 (Lit) can leave checkbox visuals stale after Clear All / hash-driven + * deselect while the headless engine is already idle. Keep DOM parts/classes aligned + * with engine selection, and strip Atomic primary Tailwind utilities that fight ExL grey chrome. + */ + const PRIMARY_UTILITY_CLASSES = [ + 'bg-primary', + 'hover:bg-primary-light', + 'focus-visible:bg-primary-light', + 'hover:border-primary-light', + 'focus-visible:border-primary-light', + ]; + + // While Clear All is in flight, ignore engine selection until a post-clear RESULT_UPDATED + // reports idle (or a different selection than the clear-time baseline). + let facetClearInProgress = false; + let facetClearSafetyTimerId = 0; + let clearBaselineFingerprint = ''; + + const applyCheckboxVisualState = (btn, isSelected) => { + btn.classList.remove(...PRIMARY_UTILITY_CLASSES); + const icon = btn.querySelector('[part~="value-checkbox-icon"]'); + if (isSelected) { + btn.classList.add('selected'); + btn.part.add('value-checkbox-checked'); + btn.setAttribute('aria-checked', 'true'); + // Atomic 3.60 may leave the tick `display:none` when we only sync parts/classes. + if (icon) { + icon.style.display = 'block'; + icon.style.stroke = '#fff'; + icon.style.color = '#fff'; + } + } else { + btn.classList.remove('selected'); + btn.part.remove('value-checkbox-checked'); + btn.setAttribute('aria-checked', 'false'); + if (icon) { + icon.style.display = 'none'; + icon.style.removeProperty('stroke'); + icon.style.removeProperty('color'); + } + } + }; + + const facetSelectionFingerprint = () => { + const facetSet = searchInterface?.engine?.state?.facetSet || {}; + return Object.entries(facetSet) + .map(([facetId, facetState]) => { + const selected = (facetState.request?.currentValues || []) + .filter((value) => value.state === 'selected') + .map((value) => String(value.value)) + .sort() + .join(','); + return selected ? `${facetId}:${selected}` : ''; + }) + .filter(Boolean) + .sort() + .join('|'); + }; + + const clearFacetCheckboxVisuals = (atomicFacet) => { + const rows = atomicFacet.shadowRoot?.querySelector('[part="values"]')?.querySelectorAll(':scope > li') || []; + rows.forEach((li) => { + const btn = li.querySelector('[part~="value-checkbox"]'); + if (btn) applyCheckboxVisualState(btn, false); + }); + }; + + const endFacetClearInProgress = () => { + facetClearInProgress = false; + clearBaselineFingerprint = ''; + if (facetClearSafetyTimerId) { + clearTimeout(facetClearSafetyTimerId); + facetClearSafetyTimerId = 0; + } + }; + + const maybeEndFacetClearInProgress = () => { + if (!facetClearInProgress) return; + const fingerprint = facetSelectionFingerprint(); + // Idle engine, or user re-selected something different than the clear-time baseline. + if (!fingerprint || fingerprint !== clearBaselineFingerprint) { + endFacetClearInProgress(); + } + }; + + const syncFacetCheckboxVisuals = (atomicFacet) => { + if (facetClearInProgress) { + clearFacetCheckboxVisuals(atomicFacet); + return; + } + + const facetId = atomicFacet.facetId || atomicFacet.getAttribute('field'); + const currentValues = searchInterface?.engine?.state?.facetSet?.[facetId]?.request?.currentValues || []; + const selectedValues = new Set( + currentValues.filter((value) => value.state === 'selected').map((value) => String(value.value).toLowerCase()), + ); + + const rows = atomicFacet.shadowRoot?.querySelector('[part="values"]')?.querySelectorAll(':scope > li') || []; + rows.forEach((li) => { + const btn = li.querySelector('[part~="value-checkbox"]'); + if (!btn) return; + + const candidates = [ + li.dataset.facetRawValue, + li.dataset.contenttype, + li.querySelector('.value-label')?.getAttribute('title'), + ] + .filter(Boolean) + .map((value) => String(value).toLowerCase()); + const isSelected = candidates.some((candidate) => selectedValues.has(candidate)); + applyCheckboxVisualState(btn, isSelected); + }); + }; + const handleAtomicFacetUI = (atomicFacet) => { if (atomicFacet.getAttribute('id') === 'facetStatus') { // Hide the facetStatus if no filters are selected @@ -380,7 +516,12 @@ export default function atomicFacetHandler(block, placeholders, searchInterface) facets.forEach((facet) => { updateFacetUI(facet, atomicFacet, false); }); - sortFacetsInOrder(parentWrapper); + // Only reorder Content Type (parent/child hierarchy). Other facets are already + // alphanumeric from Atomic; reordering all product rows after each search was + // churning Lit DOM and blocking subsequent product checkbox clicks. + if (isContentTypeFacet(atomicFacet)) { + sortFacetsInOrder(parentWrapper); + } const sortedFacets = Array.from(parentWrapper.children); sortedFacets.forEach((facet) => { adjustChildElementsPosition(facet, atomicFacet); @@ -388,6 +529,7 @@ export default function atomicFacetHandler(block, placeholders, searchInterface) // Update parent facet counts with sum of child counts updateParentFacetCounts(parentWrapper); + syncFacetCheckboxVisuals(atomicFacet); const facetParent = atomicFacet.shadowRoot.querySelector('[part="facet"]'); updateChildElementUI(parentWrapper, facetParent); @@ -433,6 +575,8 @@ export default function atomicFacetHandler(block, placeholders, searchInterface) resultTimerId = 0; } resultTimerId = setTimeout(() => { + // Only lift the Clear All gate once engine left the clear-time selection (or went idle). + maybeEndFacetClearInProgress(); const atomicFacets = document.querySelectorAll('atomic-facet'); atomicFacets.forEach((atomicFacet) => { handleAtomicFacetUI(atomicFacet); @@ -460,6 +604,8 @@ export default function atomicFacetHandler(block, placeholders, searchInterface) }, 100); }; + let facetEventListenersBound = false; + const initAtomicFacetUI = (removeSkeleton = false) => { const atomicFacets = document.querySelectorAll('atomic-facet'); atomicFacets.forEach((atomicFacet) => { @@ -476,8 +622,52 @@ export default function atomicFacetHandler(block, placeholders, searchInterface) observeFacetValuesList(atomicFacet); handleAtomicFacetUI(atomicFacet); }); + if (facetEventListenersBound) { + return; + } + facetEventListenersBound = true; + + const onSearchCleared = () => { + // Cancel any pending RESULT_UPDATED sync that would re-apply stale checked chrome. + if (resultTimerId) { + clearTimeout(resultTimerId); + resultTimerId = 0; + } + clearBaselineFingerprint = facetSelectionFingerprint(); + facetClearInProgress = true; + if (facetClearSafetyTimerId) { + clearTimeout(facetClearSafetyTimerId); + } + // Keep forcing clear visuals until engine leaves the clear-time selection. + // Cap retries so a hung clear cannot poll forever. + let clearSafetyAttempts = 0; + const scheduleClearSafetyCheck = () => { + facetClearSafetyTimerId = setTimeout(() => { + maybeEndFacetClearInProgress(); + clearSafetyAttempts += 1; + if (facetClearInProgress && clearSafetyAttempts < 5) { + document.querySelectorAll('atomic-facet').forEach((atomicFacet) => { + clearFacetCheckboxVisuals(atomicFacet); + }); + scheduleClearSafetyCheck(); + return; + } + if (facetClearInProgress) { + // Last resort after ~10s — avoid leaving UI permanently gated. + endFacetClearInProgress(); + } + facetClearSafetyTimerId = 0; + }, 2000); + }; + scheduleClearSafetyCheck(); + document.querySelectorAll('atomic-facet').forEach((atomicFacet) => { + clearFacetCheckboxVisuals(atomicFacet); + }); + }; + document.addEventListener(CUSTOM_EVENTS.RESULT_UPDATED, onResultsUpdate); document.addEventListener(CUSTOM_EVENTS.NO_RESULT_FOUND, onNoResultFoundUpdate); + document.addEventListener(CUSTOM_EVENTS.SEARCH_CLEARED, onSearchCleared); }; const onAtomicFacetUIReady = () => { diff --git a/blocks/atomic-search/components/atomic-search-result.js b/blocks/atomic-search/components/atomic-search-result.js index 884957953..7afa5e33b 100644 --- a/blocks/atomic-search/components/atomic-search-result.js +++ b/blocks/atomic-search/components/atomic-search-result.js @@ -62,18 +62,22 @@ export const atomicResultStyles = ` .atomic-search-result-item.mobile-only .result-field.result-thumbnail { margin-top: 10px; } - .result-root.recommendation-badge { + /* Atomic 3.60 renders results under .result-component (was .result-root). */ + .result-root.recommendation-badge, + .result-component.recommendation-badge { margin: 40px 0px 0px; } .atomic-search-result-item .result-field.text-thumbnail { display: flex; gap: 18px; } - .atomic-search-result-item .result-field.text-thumbnail:not(:has(.result-thumbnail)) { + /* Atomic 3.60 keeps inactive field-condition nodes in the DOM with [hidden]; + :has(.result-thumbnail) alone falsely enables the video flex layout. */ + .atomic-search-result-item .result-field.text-thumbnail:not(:has(atomic-field-condition:not([hidden]) .result-thumbnail)) { gap: 0; display: block; } - .atomic-search-result-item .result-field.text-thumbnail:has(.result-thumbnail) .result-text { + .atomic-search-result-item .result-field.text-thumbnail:has(atomic-field-condition:not([hidden]) .result-thumbnail) .result-text { flex: 0 0 56%; } .atomic-search-result-item.result-item .thumbnail-wrapper { @@ -384,6 +388,11 @@ export const atomicResultListStyles = ` atomic-folded-result-list::part(result-list) { grid-row-gap: 0; } + atomic-folded-result-list::part(outline) { + border: none; + border-radius: 0; + background-color: transparent; + } atomic-folded-result-list::part(outline)::before { background-color:var(--footer-border-color); display: block; @@ -393,6 +402,9 @@ export const atomicResultListStyles = ` } atomic-folded-result-list::part(first-result) { padding-top: 1rem; + border: none; + border-radius: 0; + background-color: transparent; } atomic-folded-result-list::part(first-result)::before { display: none; @@ -556,21 +568,41 @@ export default function atomicResultHandler(block, placeholders) { return; } container.parentElement.part.add('list-wrap'); - // Make result section hidden and start adding skeleton. - container.style.cssText = 'display: none;'; - container.dataset.view = isMobile() ? 'mobile' : 'desktop'; - const skeletonWrapper = htmlToElement(`
`); - skeletonWrapper.innerHTML = renderAtomicSekeletonUI(); - container.parentElement.appendChild(skeletonWrapper); + + // Atomic 3.60+ (Lit) re-renders its shadow DOM. Hiding/replacing nodes fights Lit and + // can wipe results permanently. Only show an in-shadow skeleton when results are not yet present. + const hasResultsAlready = shadow.querySelectorAll('atomic-result').length > 0; + if (!hasResultsAlready) { + container.style.cssText = 'display: none;'; + container.dataset.view = isMobile() ? 'mobile' : 'desktop'; + const skeletonWrapper = htmlToElement(``); + skeletonWrapper.innerHTML = renderAtomicSekeletonUI(); + container.parentElement.appendChild(skeletonWrapper); + } else { + container.dataset.view = isMobile() ? 'mobile' : 'desktop'; + baseElement.classList.remove('list-wrap-skeleton'); + } function onClearBtnClick() { const atomicBreadBox = document.querySelector('atomic-breadbox'); - const coveoClearBtn = atomicBreadBox?.shadowRoot?.querySelector('[part="clear"]'); + // Atomic 3.60 may expose clear as part token list; keep exact + contains match. + const coveoClearBtn = + atomicBreadBox?.shadowRoot?.querySelector('[part="clear"]') || + atomicBreadBox?.shadowRoot?.querySelector('button[part~="clear"]:not([part~="breadcrumb-clear"])'); if (coveoClearBtn) { - const event = new CustomEvent(CUSTOM_EVENTS.SEARCH_CLEARED); - document.dispatchEvent(event); coveoClearBtn.click(); + } else { + // Fallback when breadbox clear control is not in the DOM yet: drop facet hash segments. + const hash = window.location.hash.slice(1); + if (hash) { + const kept = hash + .split('&') + .filter((part) => part && !part.startsWith('f-') && !part.startsWith('ff-') && !part.startsWith('rf-')); + window.location.hash = kept.join('&'); + } } + // Emit after clear starts so facet handlers do not re-sync still-selected engine state. + document.dispatchEvent(new CustomEvent(CUSTOM_EVENTS.SEARCH_CLEARED)); } function decorateExternalLink(link) { @@ -813,7 +845,10 @@ export default function atomicResultHandler(block, placeholders) { }; const updateAtomicResultUI = (callFrom) => { - const results = container.querySelectorAll('atomic-result'); + // Prefer light query on the list part; fall back to full shadow (Lit may move nodes). + const results = container.querySelectorAll('atomic-result').length + ? container.querySelectorAll('atomic-result') + : shadow.querySelectorAll('atomic-result'); const isMobileView = isMobile(); container.dataset.view = isMobileView ? 'mobile' : 'desktop'; results.forEach((resultElement, index) => { @@ -898,8 +933,8 @@ export default function atomicResultHandler(block, placeholders) { const recommendationBadgeExists = !!resultItem.querySelector('.atomic-recommendation-badge'); if (recommendationBadgeExists) { - const resultRoot = resultShadow.querySelector('.result-root'); - resultRoot.classList.add('recommendation-badge'); + const resultRoot = resultShadow.querySelector('.result-root, .result-component'); + resultRoot?.classList.add('recommendation-badge'); } // Handle el_kudo_status field - support both legacy numeric and new user ID format @@ -1173,8 +1208,13 @@ export default function atomicResultHandler(block, placeholders) { const resizeObserver = new ResizeObserver(debouncedResize); resizeObserver.observe(container); - // Add observer to check the loading of result items. - const observer = new MutationObserver(updateAtomicResultUI); - observer.observe(container, { childList: true, subtree: false }); + // Watch the whole shadow tree — Lit Atomic renders results reactively. + const observer = new MutationObserver(() => updateAtomicResultUI()); + observer.observe(shadow, { childList: true, subtree: true }); updateAtomicResultUI(); + + // Fail-safe: never leave the Lit result list permanently hidden behind our skeleton. + setTimeout(() => { + removeBlockSkeleton(); + }, 4000); } diff --git a/blocks/atomic-search/components/atomic-search-template.js b/blocks/atomic-search/components/atomic-search-template.js index 091e69005..b4f584750 100644 --- a/blocks/atomic-search/components/atomic-search-template.js +++ b/blocks/atomic-search/components/atomic-search-template.js @@ -30,6 +30,15 @@ const getCoveoAtomicMarkup = (placeholders) => {