The Trans Directory
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(usability): update functions for search (#774)

* feat(usability): update functions for search

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

* perf: slightly cleaner variables

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

---------

Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com>

authored by

Aaron Pham and committed by
GitHub
50bb1ffd fee3ef9b

+80 -32
+78 -31
quartz/components/scripts/search.inline.ts
··· 86 86 const searchIcon = document.getElementById("search-icon") 87 87 const searchBar = document.getElementById("search-bar") as HTMLInputElement | null 88 88 const searchLayout = document.getElementById("search-layout") 89 - const resultCards = document.getElementsByClassName("result-card") 90 89 const idDataMap = Object.keys(data) as FullSlug[] 91 90 92 91 const appendLayout = (el: HTMLElement) => { ··· 151 150 if (searchBar) searchBar.value = "#" 152 151 } 153 152 153 + const resultCards = document.getElementsByClassName("result-card") 154 + 155 + // If search is active, then we will render the first result and display accordingly 154 156 if (!container?.classList.contains("active")) return 155 - else if (e.key === "Enter") { 156 - // If result has focus, navigate to that one, otherwise pick first result 157 - if (results?.contains(document.activeElement)) { 158 - const active = document.activeElement as HTMLInputElement 157 + else if (results?.contains(document.activeElement)) { 158 + const active = document.activeElement as HTMLInputElement 159 + await displayPreview(active) 160 + if (e.key === "Enter") { 159 161 active.click() 160 - } else { 161 - const anchor = document.getElementsByClassName("result-card")[0] as HTMLInputElement | null 162 + } 163 + } else { 164 + const anchor = resultCards[0] as HTMLInputElement | null 165 + await displayPreview(anchor) 166 + if (e.key === "Enter") { 162 167 anchor?.click() 163 168 } 164 - } else if (e.key === "ArrowUp" || (e.shiftKey && e.key === "Tab")) { 169 + } 170 + 171 + if (e.key === "ArrowUp" || (e.shiftKey && e.key === "Tab")) { 165 172 e.preventDefault() 166 173 if (results?.contains(document.activeElement)) { 167 174 // If an element in results-container already has focus, focus previous one 168 - const prevResult = document.activeElement?.previousElementSibling as HTMLInputElement | null 169 - if (enablePreview && prevResult?.id) { 170 - await displayPreview(prevResult?.id as FullSlug) 171 - } 175 + const currentResult = document.activeElement as HTMLInputElement | null 176 + const prevResult = currentResult?.previousElementSibling as HTMLInputElement | null 177 + currentResult?.classList.remove("focus") 178 + await displayPreview(prevResult) 172 179 prevResult?.focus() 173 180 } 174 181 } else if (e.key === "ArrowDown" || e.key === "Tab") { 175 182 e.preventDefault() 176 - // When first pressing ArrowDown, results wont contain the active element, so focus first element 183 + // The results should already been focused, so we need to find the next one. 184 + // The activeElement is the search bar, so we need to find the first result and focus it. 177 185 if (!results?.contains(document.activeElement)) { 178 186 const firstResult = resultCards[0] as HTMLInputElement | null 179 - if (enablePreview && firstResult?.id) { 180 - await displayPreview(firstResult?.id as FullSlug) 181 - } 182 - firstResult?.focus() 187 + const secondResult = firstResult?.nextElementSibling as HTMLInputElement | null 188 + firstResult?.classList.remove("focus") 189 + await displayPreview(secondResult) 190 + secondResult?.focus() 183 191 } else { 184 192 // If an element in results-container already has focus, focus next one 185 - const nextResult = document.activeElement?.nextElementSibling as HTMLInputElement | null 186 - if (enablePreview && nextResult?.id) { 187 - await displayPreview(nextResult?.id as FullSlug) 188 - } 193 + const active = document.activeElement as HTMLInputElement | null 194 + active?.classList.remove("focus") 195 + const nextResult = active?.nextElementSibling as HTMLInputElement | null 196 + await displayPreview(nextResult) 189 197 nextResult?.focus() 190 198 } 191 199 } ··· 262 270 263 271 const resultToHTML = ({ slug, title, content, tags }: Item) => { 264 272 const htmlTags = tags.length > 0 ? `<ul>${tags.join("")}</ul>` : `` 273 + const resultContent = enablePreview && window.innerWidth > 600 ? "" : `<p>${content}</p>` 274 + 265 275 const itemTile = document.createElement("a") 266 276 itemTile.classList.add("result-card") 267 - itemTile.id = slug 268 - itemTile.href = resolveUrl(slug).toString() 269 - itemTile.innerHTML = `<h3>${title}</h3>${htmlTags}${enablePreview && window.innerWidth > 600 ? "" : `<p>${content}</p>`}` 270 - itemTile.addEventListener("click", (event) => { 271 - if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return 272 - hideSearch() 277 + Object.assign(itemTile, { 278 + id: slug, 279 + href: resolveUrl(slug).toString(), 280 + innerHTML: `<h3>${title}</h3>${htmlTags}${resultContent}`, 273 281 }) 282 + 283 + async function onMouseEnter(ev: MouseEvent) { 284 + // When search is active, the first element is in focus, so we need to remove focus if given target is not the first element 285 + const firstEl = document.getElementsByClassName("result-card")[0] as HTMLAnchorElement | null 286 + const target = ev.target as HTMLAnchorElement 287 + if (firstEl !== target) { 288 + firstEl?.classList.remove("focus") 289 + } 290 + target.classList.add("focus") 291 + await displayPreview(target) 292 + } 293 + 294 + async function onMouseLeave(ev: MouseEvent) { 295 + const target = ev.target as HTMLAnchorElement 296 + target.classList.remove("focus") 297 + } 298 + 299 + const events = [ 300 + ["mouseenter", onMouseEnter], 301 + ["mouseleave", onMouseLeave], 302 + [ 303 + "click", 304 + (event: MouseEvent) => { 305 + if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return 306 + hideSearch() 307 + }, 308 + ], 309 + ] as [keyof HTMLElementEventMap, (this: HTMLElement) => void][] 310 + 311 + events.forEach(([event, handler]) => itemTile.addEventListener(event, handler)) 312 + 274 313 return itemTile 275 314 } 276 315 277 - function displayResults(finalResults: Item[]) { 316 + async function displayResults(finalResults: Item[]) { 278 317 if (!results) return 279 318 280 319 removeAllChildren(results) ··· 286 325 } else { 287 326 results.append(...finalResults.map(resultToHTML)) 288 327 } 328 + // focus on first result, then also dispatch preview immediately 329 + if (results?.firstElementChild) { 330 + results?.firstElementChild?.classList.add("focus") 331 + await displayPreview(results?.firstElementChild as HTMLElement) 332 + } 289 333 } 290 334 291 335 async function fetchContent(slug: FullSlug): Promise<Element[]> { ··· 309 353 return contents 310 354 } 311 355 312 - async function displayPreview(slug: FullSlug) { 313 - if (!searchLayout || !enablePreview) return 356 + async function displayPreview(el: HTMLElement | null) { 357 + if (!searchLayout || !enablePreview || !el) return 358 + 359 + const slug = el.id as FullSlug 360 + el.classList.add("focus") 314 361 315 362 removeAllChildren(preview as HTMLElement) 316 363 const contentDetails = await fetchContent(slug) ··· 366 413 ...getByField("tags"), 367 414 ]) 368 415 const finalResults = [...allIds].map((id) => formatForDisplay(term, id)) 369 - displayResults(finalResults) 416 + await displayResults(finalResults) 370 417 } 371 418 372 419 if (prevShortcutHandler) {
+2 -1
quartz/components/styles/search.scss
··· 162 162 } 163 163 164 164 &:hover, 165 - &:focus { 165 + &:focus, 166 + &.focus { 166 167 background: var(--lightgray); 167 168 } 168 169