From bf82b4bc03adbd14e01d35c02abb8ca31616fcd3 Mon Sep 17 00:00:00 2001 From: Diane Batres Date: Tue, 2 Jun 2026 14:50:12 -0400 Subject: [PATCH 1/5] fix: empty search and product checkboxes jumpiness --- hlx_statics/blocks/header/header.js | 54 ++++++++++++++--------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/hlx_statics/blocks/header/header.js b/hlx_statics/blocks/header/header.js index 0fb4842e..629123a2 100644 --- a/hlx_statics/blocks/header/header.js +++ b/hlx_statics/blocks/header/header.js @@ -70,11 +70,25 @@ async function initSearch() { const { connectAutocomplete } = instantsearch.connectors; - const searchClient = window.algoliasearch.algoliasearch(ALGOLIA_CONFIG.APP_KEY, ALGOLIA_CONFIG.API_KEY); + const algoliaClient = window.algoliasearch.algoliasearch(ALGOLIA_CONFIG.APP_KEY, ALGOLIA_CONFIG.API_KEY); const SUGGESTION_MAX_RESULTS = 50; const SEARCH_MAX_RESULTS = 100; - const SEARCH_MIN_QUERY_LENGTH = 3; - const isSearchableQuery = (q) => q.trim().length >= SEARCH_MIN_QUERY_LENGTH; + const isSearchableQuery = (q) => q.trim() !== ''; + const searchClient = { // "To prevent the initial empty query, you must wrap a custom search client around..." source: https://www.algolia.com/doc/guides/building-search-ui/going-further/conditional-requests/js + ...algoliaClient, + search(requests) { + if (requests.every(({ params }) => !isSearchableQuery(params.query ?? ''))) { + return Promise.resolve({ + results: requests.map(() => ({ + hits: [], nbHits: 0, nbPages: 0, page: 0, + processingTimeMS: 0, hitsPerPage: 0, + exhaustiveNbHits: false, query: '', params: '', + })), + }); + } + return algoliaClient.search(requests); + }, + }; const indices = window.adp_search.indices const indexToProduct = window.adp_search.index_to_product; @@ -230,8 +244,12 @@ async function initSearch() { if (event.key === 'Enter') { searchCleared = false; // Reset cleared flag when user presses Enter const trimmed = searchInput.value.trim(); - if (trimmed !== '' && !isSearchableQuery(trimmed)) { - event.preventDefault(); + if (trimmed === '') { // If users presses enter with an empty query while in a full search, clear results and show suggestions again + searchResults.classList.remove('has-results'); + searchResults.style.visibility = 'hidden'; + outerSearchSuggestions.style.display = 'flex'; + suggestionsFlag = true; + searchExecuted = false; return; } helper.setQuery(searchInput.value).search(); @@ -405,24 +423,7 @@ async function initSearch() { }); } - // Function that is called after each search render to hide/show product checkboxes - function updateCheckboxVisibility(productsWithResults) { - const allSelected = selectedProducts.length === allProducts.length; - - // loop over each product‐wrapper - document.querySelectorAll('.search-checkbox-div[data-product]').forEach((div) => { - const product = div.dataset.product; - if (allSelected) { - // only show those with results - div.style.display = productsWithResults.has(product) ? '' : 'none'; - } else { - // specific‐product mode: show all product checkboxes - div.style.display = ''; - } - }); - } - - // Function that attaches event listeners to each checkbox +// Function that attaches event listeners to each checkbox function attachCheckboxEventListeners() { const allProductsCheckbox = document.getElementById('checkbox-all-products'); const productCheckboxes = document.querySelectorAll('.filters input[type="checkbox"]:not(#checkbox-all-products)'); @@ -447,6 +448,9 @@ async function initSearch() { selectedProducts = Array.from(productCheckboxes) .filter((cb) => cb.checked) // Get checked product checkboxes .map((cb) => cb.value); + if (checkbox.checked) { // Add new selected product to the beginning of the list to prioritize it in results + selectedProducts = [checkbox.value, ...selectedProducts.filter((p) => p !== checkbox.value)]; + } if (selectedProducts.length === 0) { // If no products selected, revert to "All Products" @@ -476,9 +480,6 @@ async function initSearch() { // figure out which products have at least one hit const productsWithResults = new Set(productGroupedResults.keys()); - // hide/show checkboxes based on current results + mode - updateCheckboxVisibility(productsWithResults); - // determine display order const allProductsCheckbox = document.getElementById('checkbox-all-products'); const productsToShow = allProductsCheckbox.checked ? allProducts : selectedProducts; @@ -536,7 +537,6 @@ async function initSearch() { // compute who has any suggestions const productsWithResults = new Set(productGroupedResults.keys()); - updateCheckboxVisibility(productsWithResults); const allProductsCheckbox = document.getElementById('checkbox-all-products'); const productsToShow = allProductsCheckbox.checked From 991f02f520c1654aec8839a55c685c996267f795 Mon Sep 17 00:00:00 2001 From: Diane Batres Date: Tue, 2 Jun 2026 16:20:09 -0400 Subject: [PATCH 2/5] fix: lag in between clicking products and All Products --- hlx_statics/blocks/header/header.js | 52 ++++++++++++++++++----------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/hlx_statics/blocks/header/header.js b/hlx_statics/blocks/header/header.js index 629123a2..037775c2 100644 --- a/hlx_statics/blocks/header/header.js +++ b/hlx_statics/blocks/header/header.js @@ -139,9 +139,17 @@ async function initSearch() { let results = new Map(); search.start(); + + let currentDynamicWidgets = []; // Widgets that change on each call + let staticWidgetsAdded = false; // One-time widgets // Function to initialize or update the search function updateSearch() { + // Remove widgets from the previous call before adding new ones + if (currentDynamicWidgets.length) { + search.removeWidgets(currentDynamicWidgets); + currentDynamicWidgets = []; + } // Get indices corresponding to selected products const selectedIndices = indices.filter((indexName) => { const product = indexToProduct[indexName]; @@ -157,14 +165,14 @@ async function initSearch() { // Calculate hits dynamically based number of selected indices const hits = Math.min(15, Math.max(4, Math.floor(SUGGESTION_MAX_RESULTS / selectedIndices.length))); - // Add common widgets like hits per index and how long results are (content) - search.addWidgets([ - instantsearch.widgets.configure({ - hitsPerPage: hits, - attributesToHighlight: ['title', 'content'], - attributesToSnippet: ['content:50'], - }), - ]); + // Add common widgets like hits per index and how long results are (content) - and save reference so it can be removed on the next call + const configureWidget = instantsearch.widgets.configure({ + hitsPerPage: hits, + attributesToHighlight: ['title', 'content'], + attributesToSnippet: ['content:50'], + }); + currentDynamicWidgets.push(configureWidget); + search.addWidgets([configureWidget]); // Custom InstantSearch search box to deal with suggestions and full results which depends on user input function customSearchBox() { return { init({ helper }) { @@ -316,21 +324,25 @@ async function initSearch() { } const customMergedHits = connectAutocomplete(mergedHits); - search.addWidgets([ - customSearchBox(), - customMergedHits({ - container: document.querySelector(searchBoxContainer) - }), - ]); - - // Loop through rest of indices - selectedIndices.slice(1).forEach((indexName) => { + // Only add the search box and hits renderer once — fixes event listeners duplicates + if (!staticWidgetsAdded) { search.addWidgets([ - instantsearch.widgets.index({ - indexName: indexName, + customSearchBox(), + customMergedHits({ + container: document.querySelector(searchBoxContainer) }), ]); - }); + staticWidgetsAdded = true; + } + + // Instead of looping through other indices - add a child index widget for rest of indices (the main index is always searched, so it doesn't need a widget) + const indexWidgets = selectedIndices + .filter((indexName) => indexName !== initialIndex) + .map((indexName) => instantsearch.widgets.index({ indexName })); + if (indexWidgets.length) { + currentDynamicWidgets.push(...indexWidgets); + search.addWidgets(indexWidgets); + } search.refresh(); } From a8dd487b96a12656f2c4b1f465d7834c718bddc8 Mon Sep 17 00:00:00 2001 From: yuxuanj Date: Tue, 2 Jun 2026 13:44:38 -0700 Subject: [PATCH 3/5] collapse code block with toggle icon --- hlx_statics/blocks/codeblock/codeblock.css | 41 ++++++++++++++++++++++ hlx_statics/blocks/codeblock/codeblock.js | 19 +++++++++- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/hlx_statics/blocks/codeblock/codeblock.css b/hlx_statics/blocks/codeblock/codeblock.css index 352b8ed9..e8ae6037 100644 --- a/hlx_statics/blocks/codeblock/codeblock.css +++ b/hlx_statics/blocks/codeblock/codeblock.css @@ -118,6 +118,47 @@ main div.codeblock-wrapper div.codeblock .hidden { display: none; } +main div.codeblock-wrapper div.codeblock .right-controls { + display: flex; + align-items: center; + gap: 4px; + margin-left: auto; + margin-right: 4px; + flex-shrink: 0; +} + +main div.codeblock-wrapper div.codeblock .collapse-toggle { + flex: 0 0 auto; + padding: 4px; + background: none; + border: none; + cursor: pointer; + color: rgb(209, 209, 209); + display: flex; + align-items: center; + border-radius: 4px; + transition: color 0.15s, background-color 0.15s; +} + +main div.codeblock-wrapper div.codeblock .collapse-toggle:hover { + color: rgb(255, 255, 255); + background-color: rgba(255, 255, 255, 0.1); +} + +main div.codeblock-wrapper div.codeblock .collapse-toggle svg { + fill: currentColor; + display: block; + transition: transform 0.2s ease; +} + +main div.codeblock-wrapper div.codeblock.collapsed .collapse-toggle svg { + transform: rotate(-90deg); +} + +main div.codeblock-wrapper div.codeblock.collapsed .tabs-panel { + display: none; +} + main div.codeblock-wrapper div.codeblock pre[class*=language-].no-line-numbers .line-highlight { transform: translateY(-1.6em); } diff --git a/hlx_statics/blocks/codeblock/codeblock.js b/hlx_statics/blocks/codeblock/codeblock.js index 1dd1d5fa..3a03f238 100644 --- a/hlx_statics/blocks/codeblock/codeblock.js +++ b/hlx_statics/blocks/codeblock/codeblock.js @@ -84,11 +84,15 @@ export default function decorate(block) { } }); + const rightControls = document.createElement('div'); + rightControls.className = 'right-controls'; + controlBar.append(rightControls); + const select = document.createElement('select'); select.id = selectId; select.classList.toggle('hidden', !areTabsGrouped); select.addEventListener('change', handleSelectChange); - controlBar.append(select); + rightControls.append(select); // set up customizable select (as opposed to classic which can't be styled) as described in https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Forms/Customizable_select const selectButton = document.createElement('button'); @@ -113,6 +117,19 @@ export default function decorate(block) { decoratePreformattedCode(panel); }); + const collapseToggle = document.createElement('button'); + collapseToggle.className = 'collapse-toggle'; + collapseToggle.setAttribute('type', 'button'); + collapseToggle.setAttribute('aria-label', 'Collapse code'); + collapseToggle.setAttribute('aria-expanded', 'true'); + collapseToggle.innerHTML = ''; + collapseToggle.addEventListener('click', () => { + const isCollapsed = block.classList.toggle('collapsed'); + collapseToggle.setAttribute('aria-expanded', String(!isCollapsed)); + collapseToggle.setAttribute('aria-label', isCollapsed ? 'Expand code' : 'Collapse code'); + }); + rightControls.append(collapseToggle); + // initialize by simulating a click on the first tab const firstTab = block.querySelector('[role=tab]'); if (firstTab) { From f4dadf0c99947dfb538135c8d73af81759bd22d3 Mon Sep 17 00:00:00 2001 From: yuxuanj Date: Tue, 2 Jun 2026 13:51:48 -0700 Subject: [PATCH 4/5] collapse code block with text --- hlx_statics/blocks/codeblock/codeblock.css | 10 ---------- hlx_statics/blocks/codeblock/codeblock.js | 3 ++- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/hlx_statics/blocks/codeblock/codeblock.css b/hlx_statics/blocks/codeblock/codeblock.css index e8ae6037..a7e67e41 100644 --- a/hlx_statics/blocks/codeblock/codeblock.css +++ b/hlx_statics/blocks/codeblock/codeblock.css @@ -145,16 +145,6 @@ main div.codeblock-wrapper div.codeblock .collapse-toggle:hover { background-color: rgba(255, 255, 255, 0.1); } -main div.codeblock-wrapper div.codeblock .collapse-toggle svg { - fill: currentColor; - display: block; - transition: transform 0.2s ease; -} - -main div.codeblock-wrapper div.codeblock.collapsed .collapse-toggle svg { - transform: rotate(-90deg); -} - main div.codeblock-wrapper div.codeblock.collapsed .tabs-panel { display: none; } diff --git a/hlx_statics/blocks/codeblock/codeblock.js b/hlx_statics/blocks/codeblock/codeblock.js index 3a03f238..02c2c48b 100644 --- a/hlx_statics/blocks/codeblock/codeblock.js +++ b/hlx_statics/blocks/codeblock/codeblock.js @@ -122,11 +122,12 @@ export default function decorate(block) { collapseToggle.setAttribute('type', 'button'); collapseToggle.setAttribute('aria-label', 'Collapse code'); collapseToggle.setAttribute('aria-expanded', 'true'); - collapseToggle.innerHTML = ''; + collapseToggle.textContent = 'Hide'; collapseToggle.addEventListener('click', () => { const isCollapsed = block.classList.toggle('collapsed'); collapseToggle.setAttribute('aria-expanded', String(!isCollapsed)); collapseToggle.setAttribute('aria-label', isCollapsed ? 'Expand code' : 'Collapse code'); + collapseToggle.textContent = isCollapsed ? 'Show' : 'Hide'; }); rightControls.append(collapseToggle); From 3e104f2f7d2c10b4b0bde5a3e792da092a722d1b Mon Sep 17 00:00:00 2001 From: yuxuanj Date: Mon, 8 Jun 2026 13:14:20 -0700 Subject: [PATCH 5/5] updated Hide/Show text styling --- hlx_statics/blocks/codeblock/codeblock.css | 29 ++++++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/hlx_statics/blocks/codeblock/codeblock.css b/hlx_statics/blocks/codeblock/codeblock.css index b9c0b68e..55612eaa 100644 --- a/hlx_statics/blocks/codeblock/codeblock.css +++ b/hlx_statics/blocks/codeblock/codeblock.css @@ -29,6 +29,7 @@ main div.codeblock-wrapper div.codeblock .inline-code { main div.codeblock-wrapper div.codeblock .control-bar { display: flex; + align-items: center; justify-content: space-between; } @@ -37,6 +38,7 @@ main div.codeblock-wrapper div.codeblock .control-bar { main div.codeblock-wrapper div.codeblock .control-bar, main div.codeblock-wrapper div.codeblock .control-bar .tabs-list { display: flex; + align-items: center; gap: 16px; max-width: 100%; overflow-x: auto; @@ -59,6 +61,9 @@ main div.codeblock-wrapper div.codeblock .control-bar select { white-space: unset; cursor: pointer; background-color: initial; +} + +main div.codeblock-wrapper div.codeblock .control-bar .tabs-list button { border-bottom: 3px solid transparent; } @@ -84,18 +89,30 @@ main div.codeblock-wrapper div.codeblock .control-bar select::picker(select) { main div.codeblock-wrapper div.codeblock .control-bar select { border: none; + display: flex; + align-items: center; +} + +main div.codeblock-wrapper div.codeblock .control-bar select > button { + all: unset; + display: flex; + align-items: center; + gap: 4px; + cursor: pointer; + color: rgb(209, 209, 209); } main div.codeblock-wrapper div.codeblock .control-bar select selectedcontent { - margin: auto; + display: flex; + align-items: center; } main div.codeblock-wrapper div.codeblock .control-bar select::picker-icon { content: "⌄"; - margin: auto; - padding-bottom: 6px; + display: flex; + align-items: center; font-weight: bold; - transform: scaleX(1.5); + transform: scaleX(1.5) translateY(-1px); } main div.codeblock-wrapper div.codeblock .control-bar select::picker(select) { @@ -134,7 +151,9 @@ main div.codeblock-wrapper div.codeblock .right-controls { main div.codeblock-wrapper div.codeblock .collapse-toggle { flex: 0 0 auto; - padding: 4px; + margin-left: 8px; + padding-top: 2px; + font-size: 14px; background: none; border: none; cursor: pointer;