feat: show local search loading state (#5252)

triage
T 2 months ago committed by GitHub
parent 0100d4db7d
commit 7e2273a3e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -3,6 +3,51 @@ describe('local search', () => {
await goto('/')
})
test.runIf(!process.env.VITE_TEST_BUILD)(
'shows progress while loading search index',
async () => {
const indexRoute = /@localSearchIndexroot/
let delayedIndex = false
await page.route(indexRoute, async (route) => {
delayedIndex = true
await new Promise((resolve) => setTimeout(resolve, 800))
await route.continue()
})
try {
await page.locator('.VPNavBarSearchButton').click()
const loading = page.locator('.search-loading')
const results = page.locator('.results')
await page.waitForFunction(() =>
document
.querySelector('.search-loading')
?.classList.contains('active')
)
expect(delayedIndex).toBe(true)
expect(await loading.getAttribute('role')).toBe('status')
expect(await loading.getAttribute('aria-label')).toBe(
'Loading search results'
)
expect(await results.getAttribute('aria-busy')).toBe('true')
await page.waitForFunction(
() =>
!document
.querySelector('.search-loading')
?.classList.contains('active')
)
expect(await results.getAttribute('aria-busy')).toBe('false')
} finally {
await page.unroute(indexRoute)
}
}
)
test('exclude content from search results', async () => {
await page.locator('.VPNavBarSearchButton').click()

@ -67,25 +67,34 @@ const { activate } = useFocusTrap(el, {
escapeDeactivates: true
})
const { localeIndex, theme } = vitePressData
const searchIndex = computedAsync(async () =>
markRaw(
MiniSearch.loadJSON<Result>(
(await searchIndexData.value[localeIndex.value]?.())?.default,
{
fields: ['title', 'titles', 'text'],
storeFields: ['title', 'titles'],
searchOptions: {
fuzzy: 0.2,
prefix: true,
boost: { title: 4, text: 2, titles: 1 },
const isSearchIndexLoading = ref(false)
const isSearching = ref(false)
const showSearchSpinner = computed(() => {
return isSearchIndexLoading.value || isSearching.value
})
const searchIndex = computedAsync(
async () =>
markRaw(
MiniSearch.loadJSON<Result>(
(await searchIndexData.value[localeIndex.value]?.())?.default,
{
fields: ['title', 'titles', 'text'],
storeFields: ['title', 'titles'],
searchOptions: {
fuzzy: 0.2,
prefix: true,
boost: { title: 4, text: 2, titles: 1 },
...(theme.value.search?.provider === 'local' &&
theme.value.search.options?.miniSearch?.searchOptions)
},
...(theme.value.search?.provider === 'local' &&
theme.value.search.options?.miniSearch?.searchOptions)
},
...(theme.value.search?.provider === 'local' &&
theme.value.search.options?.miniSearch?.options)
}
)
)
theme.value.search.options?.miniSearch?.options)
}
)
),
undefined,
isSearchIndexLoading
)
const disableQueryPersistence = computed(() => {
@ -145,9 +154,15 @@ watchDebounced(
let canceled = false
onCleanup(() => {
canceled = true
isSearching.value = false
})
if (!index) return
if (!index) {
results.value = []
return
}
isSearching.value = true
// Search
results.value = index
@ -232,6 +247,7 @@ watchDebounced(
}
// FIXME: without this whole page scrolls to the bottom
resultsEl.value?.firstElementChild?.scrollIntoView({ block: 'start' })
isSearching.value = false
},
{ debounce: 200, immediate: true }
)
@ -487,6 +503,14 @@ function onMouseMove(e: MouseEvent) {
type="search"
/>
<div class="search-actions">
<span
class="search-loading"
:class="{ active: showSearchSpinner }"
:role="showSearchSpinner ? 'status' : undefined"
aria-live="polite"
:aria-label="showSearchSpinner ? 'Loading search results' : undefined"
/>
<button
v-if="!disableDetailedView"
class="toggle-layout-button"
@ -516,6 +540,7 @@ function onMouseMove(e: MouseEvent) {
ref="resultsEl"
:id="results?.length ? 'localsearch-list' : undefined"
:role="results?.length ? 'listbox' : undefined"
:aria-busy="showSearchSpinner ? 'true' : 'false'"
:aria-labelledby="results?.length ? 'localsearch-label' : undefined"
class="results"
@mousemove="onMouseMove"
@ -724,6 +749,34 @@ function onMouseMove(e: MouseEvent) {
opacity: 0.37;
}
.search-loading {
visibility: hidden;
margin: 8px;
width: 18px;
height: 18px;
flex: none;
border: 2px solid var(--vp-c-divider);
border-top-color: var(--vp-c-brand-1);
border-radius: 50%;
}
.search-loading.active {
visibility: visible;
animation: local-search-loading 0.8s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.search-loading.active {
animation: none;
}
}
@keyframes local-search-loading {
to {
transform: rotate(360deg);
}
}
.search-keyboard-shortcuts {
font-size: 0.8rem;
opacity: 75%;

Loading…
Cancel
Save