From 50bb1ffd8aa00d85d3a17bc1a4e52d26187afa7e Mon Sep 17 00:00:00 2001 From: Aaron Pham <29749331+aarnphm@users.noreply.github.com> Date: Wed, 31 Jan 2024 12:38:42 -0500 Subject: [PATCH] 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> --- quartz/components/scripts/search.inline.ts | 109 +++++++++++++++------ quartz/components/styles/search.scss | 3 +- 2 files changed, 80 insertions(+), 32 deletions(-) diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts index cbcc9ab5a..0124e1f1d 100644 --- a/quartz/components/scripts/search.inline.ts +++ b/quartz/components/scripts/search.inline.ts @@ -86,7 +86,6 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { const searchIcon = document.getElementById("search-icon") const searchBar = document.getElementById("search-bar") as HTMLInputElement | null const searchLayout = document.getElementById("search-layout") - const resultCards = document.getElementsByClassName("result-card") const idDataMap = Object.keys(data) as FullSlug[] const appendLayout = (el: HTMLElement) => { @@ -151,41 +150,50 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { if (searchBar) searchBar.value = "#" } + const resultCards = document.getElementsByClassName("result-card") + + // If search is active, then we will render the first result and display accordingly if (!container?.classList.contains("active")) return - else if (e.key === "Enter") { - // If result has focus, navigate to that one, otherwise pick first result - if (results?.contains(document.activeElement)) { - const active = document.activeElement as HTMLInputElement + else if (results?.contains(document.activeElement)) { + const active = document.activeElement as HTMLInputElement + await displayPreview(active) + if (e.key === "Enter") { active.click() - } else { - const anchor = document.getElementsByClassName("result-card")[0] as HTMLInputElement | null + } + } else { + const anchor = resultCards[0] as HTMLInputElement | null + await displayPreview(anchor) + if (e.key === "Enter") { anchor?.click() } - } else if (e.key === "ArrowUp" || (e.shiftKey && e.key === "Tab")) { + } + + if (e.key === "ArrowUp" || (e.shiftKey && e.key === "Tab")) { e.preventDefault() if (results?.contains(document.activeElement)) { // If an element in results-container already has focus, focus previous one - const prevResult = document.activeElement?.previousElementSibling as HTMLInputElement | null - if (enablePreview && prevResult?.id) { - await displayPreview(prevResult?.id as FullSlug) - } + const currentResult = document.activeElement as HTMLInputElement | null + const prevResult = currentResult?.previousElementSibling as HTMLInputElement | null + currentResult?.classList.remove("focus") + await displayPreview(prevResult) prevResult?.focus() } } else if (e.key === "ArrowDown" || e.key === "Tab") { e.preventDefault() - // When first pressing ArrowDown, results wont contain the active element, so focus first element + // The results should already been focused, so we need to find the next one. + // The activeElement is the search bar, so we need to find the first result and focus it. if (!results?.contains(document.activeElement)) { const firstResult = resultCards[0] as HTMLInputElement | null - if (enablePreview && firstResult?.id) { - await displayPreview(firstResult?.id as FullSlug) - } - firstResult?.focus() + const secondResult = firstResult?.nextElementSibling as HTMLInputElement | null + firstResult?.classList.remove("focus") + await displayPreview(secondResult) + secondResult?.focus() } else { // If an element in results-container already has focus, focus next one - const nextResult = document.activeElement?.nextElementSibling as HTMLInputElement | null - if (enablePreview && nextResult?.id) { - await displayPreview(nextResult?.id as FullSlug) - } + const active = document.activeElement as HTMLInputElement | null + active?.classList.remove("focus") + const nextResult = active?.nextElementSibling as HTMLInputElement | null + await displayPreview(nextResult) nextResult?.focus() } } @@ -262,19 +270,50 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { const resultToHTML = ({ slug, title, content, tags }: Item) => { const htmlTags = tags.length > 0 ? `` : `` + const resultContent = enablePreview && window.innerWidth > 600 ? "" : `

${content}

` + const itemTile = document.createElement("a") itemTile.classList.add("result-card") - itemTile.id = slug - itemTile.href = resolveUrl(slug).toString() - itemTile.innerHTML = `

${title}

${htmlTags}${enablePreview && window.innerWidth > 600 ? "" : `

${content}

`}` - itemTile.addEventListener("click", (event) => { - if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return - hideSearch() + Object.assign(itemTile, { + id: slug, + href: resolveUrl(slug).toString(), + innerHTML: `

${title}

${htmlTags}${resultContent}`, }) + + async function onMouseEnter(ev: MouseEvent) { + // When search is active, the first element is in focus, so we need to remove focus if given target is not the first element + const firstEl = document.getElementsByClassName("result-card")[0] as HTMLAnchorElement | null + const target = ev.target as HTMLAnchorElement + if (firstEl !== target) { + firstEl?.classList.remove("focus") + } + target.classList.add("focus") + await displayPreview(target) + } + + async function onMouseLeave(ev: MouseEvent) { + const target = ev.target as HTMLAnchorElement + target.classList.remove("focus") + } + + const events = [ + ["mouseenter", onMouseEnter], + ["mouseleave", onMouseLeave], + [ + "click", + (event: MouseEvent) => { + if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return + hideSearch() + }, + ], + ] as [keyof HTMLElementEventMap, (this: HTMLElement) => void][] + + events.forEach(([event, handler]) => itemTile.addEventListener(event, handler)) + return itemTile } - function displayResults(finalResults: Item[]) { + async function displayResults(finalResults: Item[]) { if (!results) return removeAllChildren(results) @@ -286,6 +325,11 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { } else { results.append(...finalResults.map(resultToHTML)) } + // focus on first result, then also dispatch preview immediately + if (results?.firstElementChild) { + results?.firstElementChild?.classList.add("focus") + await displayPreview(results?.firstElementChild as HTMLElement) + } } async function fetchContent(slug: FullSlug): Promise { @@ -309,8 +353,11 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { return contents } - async function displayPreview(slug: FullSlug) { - if (!searchLayout || !enablePreview) return + async function displayPreview(el: HTMLElement | null) { + if (!searchLayout || !enablePreview || !el) return + + const slug = el.id as FullSlug + el.classList.add("focus") removeAllChildren(preview as HTMLElement) const contentDetails = await fetchContent(slug) @@ -366,7 +413,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { ...getByField("tags"), ]) const finalResults = [...allIds].map((id) => formatForDisplay(term, id)) - displayResults(finalResults) + await displayResults(finalResults) } if (prevShortcutHandler) { diff --git a/quartz/components/styles/search.scss b/quartz/components/styles/search.scss index f88803b4b..53046189b 100644 --- a/quartz/components/styles/search.scss +++ b/quartz/components/styles/search.scss @@ -162,7 +162,8 @@ } &:hover, - &:focus { + &:focus, + &.focus { background: var(--lightgray); }