diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f27efcdd..f86485c1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,12 @@ changes. are added. JSON uses the same starter values without activating example paths or sidebar rules. Long time coming honestly. +### Fixed + +- Option filtering keeps cards in place and hides nonmatches, reducing delays + from browser extensions that rescan removed and reinserted content. Results + retain their original order, and keyboard navigation skips hidden cards. + ## [2.10.2] ### Fixed diff --git a/crates/ndg-templates/templates/default.css b/crates/ndg-templates/templates/default.css index afc8c4d26..98c1b9374 100644 --- a/crates/ndg-templates/templates/default.css +++ b/crates/ndg-templates/templates/default.css @@ -1398,35 +1398,51 @@ mark { /* Dark mode SVG icon adjustments */ @media (prefers-color-scheme: dark) { - :where(:root:not([data-theme="light"])) .admonition.note .admonition-title::before { + :where(:root:not([data-theme="light"])) + .admonition.note + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%2360a5fa' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='16' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='8' x2='12.01' y2='8'%3E%3C/line%3E%3C/svg%3E"); } - :where(:root:not([data-theme="light"])) .admonition.tip .admonition-title::before { + :where(:root:not([data-theme="light"])) + .admonition.tip + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%2334d399' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); } - :where(:root:not([data-theme="light"])) .admonition.warning .admonition-title::before { + :where(:root:not([data-theme="light"])) + .admonition.warning + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23fbbf24' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z'%3E%3C/path%3E%3Cline x1='12' y1='9' x2='12' y2='13'%3E%3C/line%3E%3Cline x1='12' y1='17' x2='12.01' y2='17'%3E%3C/line%3E%3C/svg%3E"); } - :where(:root:not([data-theme="light"])) .admonition.danger .admonition-title::before { + :where(:root:not([data-theme="light"])) + .admonition.danger + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23f87171' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolygon points='7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2'%3E%3C/polygon%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E"); } - :where(:root:not([data-theme="light"])) .admonition.info .admonition-title::before { + :where(:root:not([data-theme="light"])) + .admonition.info + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23818cf8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 16v-4'%3E%3C/path%3E%3Cpath d='M12 8h.01'%3E%3C/path%3E%3C/svg%3E"); } - :where(:root:not([data-theme="light"])) .admonition.important .admonition-title::before { + :where(:root:not([data-theme="light"])) + .admonition.important + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23a78bfa' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 8v4'%3E%3C/path%3E%3Cpath d='M12 16h.01'%3E%3C/path%3E%3C/svg%3E"); } - :where(:root:not([data-theme="light"])) .admonition.example .admonition-title::before { + :where(:root:not([data-theme="light"])) + .admonition.example + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%2322d3ee' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='4 17 10 11 4 5'%3E%3C/polyline%3E%3Cpolyline points='12 19 20 19'%3E%3C/polyline%3E%3C/svg%3E"); } - :where(:root:not([data-theme="light"])) .admonition.caution .admonition-title::before { + :where(:root:not([data-theme="light"])) + .admonition.caution + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23fb923c' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z'%3E%3C/path%3E%3C/svg%3E"); } } @@ -1451,7 +1467,9 @@ mark { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23818cf8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 16v-4'%3E%3C/path%3E%3Cpath d='M12 8h.01'%3E%3C/path%3E%3C/svg%3E"); } -:where(:root[data-theme="dark"]) .admonition.important .admonition-title::before { +:where(:root[data-theme="dark"]) + .admonition.important + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23a78bfa' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 8v4'%3E%3C/path%3E%3Cpath d='M12 16h.01'%3E%3C/path%3E%3C/svg%3E"); } @@ -1465,35 +1483,51 @@ mark { /* Light mode SVG icon adjustments */ @media (prefers-color-scheme: light) { - :where(:root:not([data-theme="dark"])) .admonition.note .admonition-title::before { + :where(:root:not([data-theme="dark"])) + .admonition.note + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%233b82f6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='16' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='8' x2='12.01' y2='8'%3E%3C/line%3E%3C/svg%3E"); } - :where(:root:not([data-theme="dark"])) .admonition.tip .admonition-title::before { + :where(:root:not([data-theme="dark"])) + .admonition.tip + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%2310b981' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); } - :where(:root:not([data-theme="dark"])) .admonition.warning .admonition-title::before { + :where(:root:not([data-theme="dark"])) + .admonition.warning + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23f59e0b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z'%3E%3C/path%3E%3Cline x1='12' y1='9' x2='12' y2='13'%3E%3C/line%3E%3Cline x1='12' y1='17' x2='12.01' y2='17'%3E%3C/line%3E%3C/svg%3E"); } - :where(:root:not([data-theme="dark"])) .admonition.danger .admonition-title::before { + :where(:root:not([data-theme="dark"])) + .admonition.danger + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23ef4444' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolygon points='7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2'%3E%3C/polygon%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E"); } - :where(:root:not([data-theme="dark"])) .admonition.info .admonition-title::before { + :where(:root:not([data-theme="dark"])) + .admonition.info + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%236366f1' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 16v-4'%3E%3C/path%3E%3Cpath d='M12 8h.01'%3E%3C/path%3E%3C/svg%3E"); } - :where(:root:not([data-theme="dark"])) .admonition.important .admonition-title::before { + :where(:root:not([data-theme="dark"])) + .admonition.important + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%238b5cf6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 8v4'%3E%3C/path%3E%3Cpath d='M12 16h.01'%3E%3C/path%3E%3C/svg%3E"); } - :where(:root:not([data-theme="dark"])) .admonition.example .admonition-title::before { + :where(:root:not([data-theme="dark"])) + .admonition.example + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%2306b6d4' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='4 17 10 11 4 5'%3E%3C/polyline%3E%3Cpolyline points='12 19 20 19'%3E%3C/polyline%3E%3C/svg%3E"); } - :where(:root:not([data-theme="dark"])) .admonition.caution .admonition-title::before { + :where(:root:not([data-theme="dark"])) + .admonition.caution + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23f97316' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z'%3E%3C/path%3E%3C/svg%3E"); } } @@ -1506,7 +1540,9 @@ mark { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%2310b981' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); } -:where(:root[data-theme="light"]) .admonition.warning .admonition-title::before { +:where(:root[data-theme="light"]) + .admonition.warning + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23f59e0b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z'%3E%3C/path%3E%3Cline x1='12' y1='9' x2='12' y2='13'%3E%3C/line%3E%3Cline x1='12' y1='17' x2='12.01' y2='17'%3E%3C/line%3E%3C/svg%3E"); } @@ -1518,15 +1554,21 @@ mark { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%236366f1' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 16v-4'%3E%3C/path%3E%3Cpath d='M12 8h.01'%3E%3C/path%3E%3C/svg%3E"); } -:where(:root[data-theme="light"]) .admonition.important .admonition-title::before { +:where(:root[data-theme="light"]) + .admonition.important + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%238b5cf6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpath d='M12 8v4'%3E%3C/path%3E%3Cpath d='M12 16h.01'%3E%3C/path%3E%3C/svg%3E"); } -:where(:root[data-theme="light"]) .admonition.example .admonition-title::before { +:where(:root[data-theme="light"]) + .admonition.example + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%2306b6d4' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='4 17 10 11 4 5'%3E%3C/polyline%3E%3Cpolyline points='12 19 20 19'%3E%3C/polyline%3E%3C/svg%3E"); } -:where(:root[data-theme="light"]) .admonition.caution .admonition-title::before { +:where(:root[data-theme="light"]) + .admonition.caution + .admonition-title::before { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='%23f97316' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z'%3E%3C/path%3E%3C/svg%3E"); } @@ -2005,7 +2047,10 @@ h6:hover .copy-link { color: white; } -/* Option styling */ +.option[hidden] { + display: none; +} + .options-page .content { max-width: 960px; } diff --git a/crates/ndg-templates/templates/main.js b/crates/ndg-templates/templates/main.js index d018bdaae..43c05ae85 100644 --- a/crates/ndg-templates/templates/main.js +++ b/crates/ndg-templates/templates/main.js @@ -425,7 +425,7 @@ function setupOptionKeyboardNavigation(signal) { const optionLinks = () => Array.from( container.querySelectorAll(".option .option-anchor, .option-page-row"), - ); + ).filter((link) => !link.closest("[hidden]")); const focusOption = (link) => { link.focus({ preventScroll: true }); link @@ -638,7 +638,20 @@ function setupOptionChunkLoading(signal) { return { hashReady, loadAll }; } -function getFilterMatches(searchTerm, originalOrder, data) { +function getFilterRank({ name, searchText }, term) { + const position = name.indexOf(term); + return { + rank: position === -1 ? 1 : 0, + position: position === -1 ? searchText.indexOf(term) : position, + }; +} + +function getFilterMatches( + searchTerm, + originalOrder, + data, + preserveOrder = false, +) { if (searchTerm === "") { return originalOrder.map((element, index) => ({ element, index })); } @@ -646,20 +659,28 @@ function getFilterMatches(searchTerm, originalOrder, data) { const terms = searchTerm.split(/\s+/).filter(Boolean); const firstTerm = terms[0] || ""; - return data - .filter((item) => terms.every((term) => item.searchText.includes(term))) - .sort((a, b) => { - const aRank = a.name.includes(firstTerm) ? 0 : 1; - const bRank = b.name.includes(firstTerm) ? 0 : 1; - if (aRank !== bRank) return aRank - bRank; - const aPos = a.name.includes(firstTerm) - ? a.name.indexOf(firstTerm) - : a.searchText.indexOf(firstTerm); - const bPos = b.name.includes(firstTerm) - ? b.name.indexOf(firstTerm) - : b.searchText.indexOf(firstTerm); - return aPos - bPos || a.index - b.index; - }); + const matches = data.filter((item) => + terms.every((term) => item.searchText.includes(term)), + ); + if (preserveOrder) return matches; + + return matches.sort((a, b) => { + const aRank = getFilterRank(a, firstTerm); + const bRank = getFilterRank(b, firstTerm); + return ( + aRank.rank - bRank.rank || + aRank.position - bRank.position || + a.index - b.index + ); + }); +} + +function setFilteredItemVisibility(data, matches) { + const visibleElements = new Set(matches.map((item) => item.element)); + for (const { element } of data) { + const hidden = !visibleElements.has(element); + if (element.hidden !== hidden) element.hidden = hidden; + } } function reconcileFilteredItems({ @@ -670,7 +691,16 @@ function reconcileFilteredItems({ reduceMotion, animateChanges, isCurrentRun, + preserveOrder = false, }) { + if (preserveOrder) { + setFilteredItemVisibility(data, matches); + return; + } + + for (const { element } of data) { + element.classList.remove("filter-entering", "filter-leaving"); + } const visibleElements = new Set(matches.map((item) => item.element)); const leaving = []; @@ -730,15 +760,25 @@ function reconcileFilteredItems({ } function setupListFilter( - { inputId, containerSelector, itemSelector, nameSelector, noun, prepare }, + { + inputId, + containerSelector, + itemSelector, + nameSelector, + noun, + prepare, + preserveOrder = false, + }, signal, ) { const input = document.getElementById(inputId); const container = document.querySelector(containerSelector); if (!input || !container) return; - const hiddenContainer = document.createElement("template"); - document.body.appendChild(hiddenContainer); + const hiddenContainer = preserveOrder + ? null + : document.createElement("template"); + if (hiddenContainer) document.body.appendChild(hiddenContainer); const filterResults = document.createElement("div"); filterResults.className = "filter-results"; @@ -790,11 +830,12 @@ function setupListFilter( lastTerm = searchTerm; filterRun += 1; const currentRun = filterRun; - for (const item of data) { - item.element.classList.remove("filter-entering", "filter-leaving"); - } - - const matches = getFilterMatches(searchTerm, originalOrder, data); + const matches = getFilterMatches( + searchTerm, + originalOrder, + data, + preserveOrder, + ); reconcileFilteredItems({ container, hiddenContainer, @@ -803,6 +844,7 @@ function setupListFilter( reduceMotion, animateChanges, isCurrentRun: () => currentRun === filterRun, + preserveOrder, }); if (searchTerm !== "" && matches.length < totalCount) { @@ -840,7 +882,7 @@ function setupListFilter( "abort", () => { clearTimeout(timeout); - hiddenContainer.remove(); + hiddenContainer?.remove(); }, { once: true }, ); @@ -1042,6 +1084,7 @@ function setupOptionsFilter(signal, optionChunks) { nameSelector: ".option-name", noun: "options", prepare: optionChunks?.loadAll, + preserveOrder: true, }; if (optionsIndexList) { config = { diff --git a/crates/ndg-templates/templates/main.test.js b/crates/ndg-templates/templates/main.test.js index 3b3366662..f7cb6621c 100644 --- a/crates/ndg-templates/templates/main.test.js +++ b/crates/ndg-templates/templates/main.test.js @@ -17,7 +17,7 @@ function loadMain(requestAnimationFrame, browser = {}) { "fetch", "DOMParser", "IntersectionObserver", - `${source}; return { loadClientPage, scrollToOption, transitionClientPage, setupOptionChunkLoading, syncThemedImages, nextTheme };`, + `${source}; return { loadClientPage, scrollToOption, transitionClientPage, setupOptionChunkLoading, syncThemedImages, nextTheme, getFilterMatches, setFilteredItemVisibility };`, )( window, document, @@ -28,6 +28,90 @@ function loadMain(requestAnimationFrame, browser = {}) { ); } +Deno.test( + "option filtering preserves order and restores cards when cleared", + () => { + const { getFilterMatches, setFilteredItemVisibility } = loadMain(() => {}); + const original = [{ hidden: false }, { hidden: false }, { hidden: false }]; + const data = [ + { + element: original[0], + index: 0, + name: "alpha", + searchText: "alpha files source", + }, + { + element: original[1], + index: 1, + name: "files", + searchText: "files source", + }, + { + element: original[2], + index: 2, + name: "other", + searchText: "other enable", + }, + ]; + + const matches = getFilterMatches("files source", original, data, true); + assert(matches.length === 2, "every query term must match"); + assert(matches[0].element === original[0], "option order stays stable"); + setFilteredItemVisibility(data, matches); + assert( + !original[0].hidden && !original[1].hidden, + "matches remain visible", + ); + assert(original[2].hidden, "nonmatches are hidden"); + + setFilteredItemVisibility( + data, + getFilterMatches("missing", original, data, true), + ); + assert( + original.every((element) => element.hidden), + "no matches hides all cards", + ); + setFilteredItemVisibility(data, getFilterMatches("", original, data, true)); + assert( + original.every((element) => !element.hidden), + "clearing restores all cards", + ); + assert( + getFilterMatches("files", original, data)[0].element === original[1], + "other list filters retain relevance sorting", + ); + }, +); + +Deno.test( + "unchanged option visibility does not trigger attribute mutations", + () => { + const { setFilteredItemVisibility } = loadMain(() => {}); + let hidden = false; + let writes = 0; + const element = { + get hidden() { + return hidden; + }, + set hidden(value) { + hidden = value; + writes += 1; + }, + }; + const data = [{ element }]; + + setFilteredItemVisibility(data, data); + assert(writes === 0, "visible matches need no attribute write"); + setFilteredItemVisibility(data, []); + assert(writes === 1 && hidden, "a newly hidden card changes once"); + setFilteredItemVisibility(data, []); + assert(writes === 1, "already hidden cards need no attribute write"); + setFilteredItemVisibility(data, data); + assert(writes === 2 && !hidden, "a restored card changes once"); + }, +); + Deno.test("scrolling loads one option chunk at a time", async () => { const requested = []; let observe; diff --git a/crates/ndg-templates/templates/options.html b/crates/ndg-templates/templates/options.html index 8bad20ae1..434ce79ae 100644 --- a/crates/ndg-templates/templates/options.html +++ b/crates/ndg-templates/templates/options.html @@ -138,6 +138,7 @@

{{ heading }}

/tmp/manpage-urls.json <<'JSON'\n{}\nJSON\n") - machine.succeed("cp ${sampleOptions} /tmp/options.json") + testScript = + /* + py + */ + '' + machine.start() + machine.succeed("cat > /tmp/manpage-urls.json <<'JSON'\n{}\nJSON\n") + machine.succeed("cp ${sampleOptions} /tmp/options.json") - machine.succeed("${nrd}/bin/nrd options commonmark --manpage-urls /tmp/manpage-urls.json --revision local /tmp/options.json /tmp/ndg-default.md") - machine.succeed("${pkgs.nixos-render-docs}/bin/nixos-render-docs options commonmark --manpage-urls /tmp/manpage-urls.json --revision local /tmp/options.json /tmp/nrd-default.md") - machine.succeed("cmp /tmp/ndg-default.md /tmp/nrd-default.md") + machine.succeed("${nrd}/bin/nrd options commonmark --manpage-urls /tmp/manpage-urls.json --revision local /tmp/options.json /tmp/ndg-default.md") + machine.succeed("${pkgs.nixos-render-docs}/bin/nixos-render-docs options commonmark --manpage-urls /tmp/manpage-urls.json --revision local /tmp/options.json /tmp/nrd-default.md") + machine.succeed("cmp /tmp/ndg-default.md /tmp/nrd-default.md") - machine.succeed("${nrd}/bin/nixos-render-docs options commonmark --manpage-urls /tmp/manpage-urls.json --revision local --anchor-style legacy --anchor-prefix opt- /tmp/options.json /tmp/ndg-legacy.md") - machine.succeed("${pkgs.nixos-render-docs}/bin/nixos-render-docs options commonmark --manpage-urls /tmp/manpage-urls.json --revision local --anchor-style legacy --anchor-prefix opt- /tmp/options.json /tmp/nrd-legacy.md") - machine.succeed("cmp /tmp/ndg-legacy.md /tmp/nrd-legacy.md") - ''; + machine.succeed("${nrd}/bin/nixos-render-docs options commonmark --manpage-urls /tmp/manpage-urls.json --revision local --anchor-style legacy --anchor-prefix opt- /tmp/options.json /tmp/ndg-legacy.md") + machine.succeed("${pkgs.nixos-render-docs}/bin/nixos-render-docs options commonmark --manpage-urls /tmp/manpage-urls.json --revision local --anchor-style legacy --anchor-prefix opt- /tmp/options.json /tmp/nrd-legacy.md") + machine.succeed("cmp /tmp/ndg-legacy.md /tmp/nrd-legacy.md") + ''; }; }