Skip to content

Commit d9cf2d2

Browse files
committed
feat(search-result-modal): add formatted result count string,
semantic elements
1 parent ac74aa1 commit d9cf2d2

3 files changed

Lines changed: 96 additions & 32 deletions

File tree

sass/parts/_search.scss

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,33 @@ body.modal-open {
230230
color: rgba(0, 0, 0, 0.4);
231231
}
232232

233+
/* Results count display */
234+
.search-results-count {
235+
padding: 0.75rem 1.5rem 0.5rem;
236+
font-size: 0.875rem;
237+
color: rgba(255, 255, 255, 0.5);
238+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
239+
display: none;
240+
241+
&.visible {
242+
display: block;
243+
}
244+
245+
strong {
246+
color: rgba(255, 255, 255, 0.8);
247+
font-weight: 600;
248+
}
249+
}
250+
251+
.light .search-results-count {
252+
color: rgba(0, 0, 0, 0.5);
253+
border-bottom-color: rgba(0, 0, 0, 0.1);
254+
255+
strong {
256+
color: rgba(0, 0, 0, 0.8);
257+
}
258+
}
259+
233260
/* Results container */
234261
.search-results__items {
235262
list-style: none;

static/js/search.js

Lines changed: 61 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ function makeTeaser(body, terms) {
152152
* @return {string} - The escaped string safe for HTML rendering.
153153
*/
154154
function escapeHtml(unsafe) {
155-
if (typeof unsafe !== 'string') return '';
155+
if (typeof unsafe !== "string") return "";
156156
return unsafe
157157
.replace(/&/g, "&")
158158
.replace(/</g, "&lt;")
@@ -170,16 +170,16 @@ function escapeHtml(unsafe) {
170170
*/
171171
function formatSearchResultItem(item, terms) {
172172
// Create article element
173-
const article = document.createElement('article');
174-
article.className = 'search-results__item';
173+
const article = document.createElement("article");
174+
article.className = "search-results__item";
175175

176176
// Create link with title
177-
const link = document.createElement('a');
177+
const link = document.createElement("a");
178178
link.href = item.ref;
179179
link.textContent = item.doc.title;
180180

181181
// Create section with teaser (makeTeaser returns HTML string with <b> tags)
182-
const section = document.createElement('section');
182+
const section = document.createElement("section");
183183
section.innerHTML = makeTeaser(item.doc.body, terms);
184184

185185
article.appendChild(link);
@@ -197,6 +197,7 @@ function initSearch() {
197197
var $searchModalInput = document.getElementById("search-modal");
198198
var $searchResults = document.querySelector(".search-results");
199199
var $searchResultsItems = document.querySelector(".search-results__items");
200+
var $searchResultsCount = document.getElementById("search-results-count");
200201
var $searchBackdrop = document.querySelector(".search-backdrop");
201202
var $slashIcon = document.getElementById("slash-icon");
202203
var MAX_ITEMS = 10;
@@ -223,7 +224,8 @@ function initSearch() {
223224
scrollPosition = window.pageYOffset || document.documentElement.scrollTop;
224225

225226
// Calculate scrollbar width to prevent layout shift
226-
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
227+
const scrollbarWidth =
228+
window.innerWidth - document.documentElement.clientWidth;
227229

228230
document.body.classList.add("modal-open");
229231
document.body.style.top = `-${scrollPosition}px`;
@@ -243,8 +245,8 @@ function initSearch() {
243245
$searchResults.classList.remove("active");
244246
$searchBackdrop.classList.remove("active");
245247
document.body.classList.remove("modal-open");
246-
document.body.style.top = '';
247-
document.body.style.paddingRight = '';
248+
document.body.style.top = "";
249+
document.body.style.paddingRight = "";
248250

249251
// Restore scroll position
250252
window.scrollTo(0, scrollPosition);
@@ -253,16 +255,32 @@ function initSearch() {
253255
$searchInput.value = "";
254256
$searchModalInput.value = "";
255257
$searchResultsItems.innerHTML = "";
258+
$searchResultsCount.classList.remove("visible");
259+
$searchResultsCount.innerHTML = "";
256260
selectedIndex = -1;
257261
}
258262

263+
// Function to update results count display
264+
function updateResultsCount(count, query) {
265+
if (!$searchResultsCount) return;
266+
267+
if (count > 0 && query) {
268+
const resultText = count === 1 ? "result" : "results";
269+
$searchResultsCount.innerHTML = `${count} ${resultText} for "<strong>${escapeHtml(query)}</strong>"`;
270+
$searchResultsCount.classList.add("visible");
271+
} else {
272+
$searchResultsCount.classList.remove("visible");
273+
$searchResultsCount.innerHTML = "";
274+
}
275+
}
276+
259277
// Function to update selected item
260278
function updateSelection(newIndex) {
261-
const items = $searchResultsItems.querySelectorAll('.search-results__item');
279+
const items = $searchResultsItems.querySelectorAll(".search-results__item");
262280
if (items.length === 0) return;
263281

264282
// Remove previous selection
265-
items.forEach(item => item.classList.remove('selected'));
283+
items.forEach((item) => item.classList.remove("selected"));
266284

267285
// Clamp index
268286
if (newIndex < 0) newIndex = 0;
@@ -272,8 +290,11 @@ function initSearch() {
272290

273291
// Add selection to new item
274292
if (items[selectedIndex]) {
275-
items[selectedIndex].classList.add('selected');
276-
items[selectedIndex].scrollIntoView({ block: 'nearest', behavior: 'smooth' });
293+
items[selectedIndex].classList.add("selected");
294+
items[selectedIndex].scrollIntoView({
295+
block: "nearest",
296+
behavior: "smooth",
297+
});
277298
}
278299
}
279300

@@ -323,7 +344,10 @@ function initSearch() {
323344
function updateSlashIconVisibility() {
324345
if ($slashIcon) {
325346
// Hide if field is focused OR has text, show only when unfocused AND empty
326-
if (document.activeElement === $searchInput || $searchInput.value.trim() !== "") {
347+
if (
348+
document.activeElement === $searchInput ||
349+
$searchInput.value.trim() !== ""
350+
) {
327351
$slashIcon.classList.add("hidden");
328352
} else {
329353
$slashIcon.classList.remove("hidden");
@@ -335,7 +359,7 @@ function initSearch() {
335359
$searchInput.addEventListener("input", updateSlashIconVisibility);
336360

337361
// Hide slash icon when search field is focused
338-
$searchInput.addEventListener("focus", function() {
362+
$searchInput.addEventListener("focus", function () {
339363
updateSlashIconVisibility();
340364
// Open modal when navbar search is focused
341365
showSearchModal();
@@ -345,12 +369,12 @@ function initSearch() {
345369
$searchInput.addEventListener("blur", updateSlashIconVisibility);
346370

347371
// When typing in navbar search, open modal and sync to modal input
348-
$searchInput.addEventListener("input", function() {
372+
$searchInput.addEventListener("input", function () {
349373
if ($searchInput.value.length > 0) {
350374
showSearchModal();
351375
$searchModalInput.value = $searchInput.value;
352376
// Trigger search on modal input
353-
$searchModalInput.dispatchEvent(new Event('input'));
377+
$searchModalInput.dispatchEvent(new Event("input"));
354378
}
355379
});
356380

@@ -369,37 +393,45 @@ function initSearch() {
369393

370394
if (term === "") {
371395
// Show empty state but keep modal open
396+
updateResultsCount(0, "");
372397
return;
373398
}
374399

375400
var results = (await initIndex()).search(term, options);
376401
if (results.length === 0) {
377402
// show "No results found"
378403
const noResultsItem = document.createElement("li");
379-
noResultsItem.className = "search-results__item search-results__no-results";
404+
noResultsItem.className =
405+
"search-results__item search-results__no-results";
380406
noResultsItem.textContent = "No results found...";
381407
$searchResultsItems.appendChild(noResultsItem);
408+
updateResultsCount(0, "");
382409
return;
383410
}
384411

412+
// Update results count with total results (not just displayed items)
413+
updateResultsCount(results.length, term);
414+
385415
// Sanitize user input (search terms) before using
386-
const sanitizedTerms = term.split(" ").map(t => escapeHtml(t));
416+
const sanitizedTerms = term.split(" ").map((t) => escapeHtml(t));
387417

388418
for (var i = 0; i < Math.min(results.length, MAX_ITEMS); i++) {
389419
var item = document.createElement("li");
390420
item.appendChild(formatSearchResultItem(results[i], sanitizedTerms));
391421

392422
// Add click handler for each item
393-
item.addEventListener('click', function() {
394-
const link = this.querySelector('a');
423+
item.addEventListener("click", function () {
424+
const link = this.querySelector("a");
395425
if (link) {
396426
window.location.href = link.href;
397427
}
398428
});
399429

400430
// Add hover handler to update selection
401-
item.addEventListener('mouseenter', function() {
402-
const items = Array.from($searchResultsItems.querySelectorAll('.search-results__item'));
431+
item.addEventListener("mouseenter", function () {
432+
const items = Array.from(
433+
$searchResultsItems.querySelectorAll(".search-results__item"),
434+
);
403435
const index = items.indexOf(this);
404436
if (index >= 0) {
405437
updateSelection(index);
@@ -416,7 +448,7 @@ function initSearch() {
416448

417449
// Handle arrow key navigation and Enter key
418450
$searchModalInput.addEventListener("keydown", function (e) {
419-
const items = $searchResultsItems.querySelectorAll('.search-results__item');
451+
const items = $searchResultsItems.querySelectorAll(".search-results__item");
420452
if (items.length === 0) return;
421453

422454
if (e.key === "ArrowDown") {
@@ -427,7 +459,7 @@ function initSearch() {
427459
updateSelection(selectedIndex - 1);
428460
} else if (e.key === "Enter" && selectedIndex >= 0) {
429461
e.preventDefault();
430-
const link = items[selectedIndex].querySelector('a');
462+
const link = items[selectedIndex].querySelector("a");
431463
if (link) {
432464
window.location.href = link.href;
433465
}
@@ -447,7 +479,11 @@ function initSearch() {
447479
// event listener for `/` to open search modal
448480
document.addEventListener("keydown", function (e) {
449481
// Only open modal if we're not already typing in an input
450-
if (e.key === "/" && document.activeElement.tagName !== "INPUT" && document.activeElement.tagName !== "TEXTAREA") {
482+
if (
483+
e.key === "/" &&
484+
document.activeElement.tagName !== "INPUT" &&
485+
document.activeElement.tagName !== "TEXTAREA"
486+
) {
451487
// don't have input be `/`
452488
e.preventDefault();
453489
showSearchModal();

templates/macros/index_macros.html

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
</nav>
2424
<aside class="user-actions-container">
2525
{% if config.extra.enable_search %}
26-
<div class="search-container">
26+
<section class="search-container">
2727
<svg
2828
xmlns="http://www.w3.org/2000/svg"
2929
fill="none"
@@ -44,7 +44,7 @@
4444
class="slash-icon"
4545
alt="Press / to search"
4646
/>
47-
</div>
47+
</section>
4848
{% endif %} {% if config.extra.theme | default(value="toggle") ==
4949
"toggle" %}
5050
<a id="dark-mode-toggle" href="#">
@@ -104,9 +104,9 @@
104104
</section>
105105
{% if config.extra.enable_search %}
106106
<!-- Search modal overlay and results (positioned globally) -->
107-
<div class="search-backdrop"></div>
108-
<section class="search-results" aria-live="polite">
109-
<div class="search-modal-header">
107+
<section class="search-backdrop"></section>
108+
<dialog class="search-results" aria-live="polite">
109+
<article class="search-modal-header">
110110
<svg
111111
xmlns="http://www.w3.org/2000/svg"
112112
fill="none"
@@ -121,14 +121,15 @@
121121
/>
122122
</svg>
123123
<input type="text" id="search-modal" placeholder="Search..." />
124-
</div>
124+
</article>
125+
<article class="search-results-count" id="search-results-count"></article>
125126
<article class="search-results__items" role="list"></article>
126127
<footer class="search-modal-footer">
127128
<span><kbd></kbd><kbd></kbd> to navigate</span>
128129
<span><kbd></kbd> to select</span>
129130
<span><kbd>esc</kbd> to close</span>
130131
</footer>
131-
</section>
132+
</dialog>
132133
{% endif %}
133134
{% endif %}
134135
{% endmacro header %}

0 commit comments

Comments
 (0)