Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 20 additions & 102 deletions app/components/HomeSearch.vue
Original file line number Diff line number Diff line change
@@ -1,109 +1,28 @@
<script setup lang="ts">
import type { SearchRow } from '../composables/useSkillSearch'
import { onClickOutside, onKeyStroke } from '@vueuse/core'

const route = useRoute()
const container = useTemplateRef<HTMLElement>('container')
const input = useTemplateRef<{ inputRef?: HTMLInputElement }>('input')

// Behaviour lives in the shared box composable, so the hero and the header
// search answer, select, and navigate the same way.
const {
query,
trimmedQuery,
state,
open,
rows,
activeIndex,
activeRow,
close,
reset,
rememberQuery,
loadTypeaheadIndex,
submitRepository,
} = useSkillSearch()

const container = useTemplateRef<HTMLElement>('container')
const input = useTemplateRef<{ inputRef?: HTMLInputElement }>('input')
const keyboardSelection = ref(false)

function clearSelection(): void {
keyboardSelection.value = false
activeIndex.value = -1
}

watch([trimmedQuery, rows], clearSelection, { flush: 'sync', immediate: true })
watch(open, (visible) => {
if (!visible)
clearSelection()
}, { flush: 'sync' })
onBeforeUnmount(() => {
activeIndex.value = 0
})

function openPanel(): void {
if (!open.value)
clearSelection()
open.value = true
void loadTypeaheadIndex()
}

function dismiss(): void {
close()
input.value?.inputRef?.blur()
}

async function goToResults(): Promise<void> {
const term = trimmedQuery.value
rememberQuery(term)
dismiss()
await navigateTo(term ? { path: '/skills', query: { q: term } } : '/skills')
}

async function select(row: SearchRow): Promise<void> {
if (row._tag === 'repository') {
await submitRepository(row.repository)
return
}
if (row._tag === 'all') {
await goToResults()
return
}
if (state.value._tag !== 'repository')
rememberQuery(trimmedQuery.value)
dismiss()
reset()
await navigateTo(row.skill.registryPath)
}

function submitQuery(): void {
if (state.value._tag === 'repository') {
if (state.value.status._tag === 'idle')
void submitRepository(state.value.repository)
return
}
void goToResults()
}

function onEnter(event: KeyboardEvent): void {
if (event.isComposing || event.keyCode === 229)
return
event.preventDefault()
if (open.value && keyboardSelection.value && activeRow.value) {
void select(activeRow.value)
return
}
submitQuery()
}

function onArrow(event: KeyboardEvent, delta: number): void {
if (event.isComposing || event.keyCode === 229)
return
event.preventDefault()
openPanel()
const count = rows.value.length
if (!count)
return
activeIndex.value = keyboardSelection.value
? (activeIndex.value + delta + count) % count
: delta > 0 ? 0 : count - 1
keyboardSelection.value = true
}
openPanel,
dismiss,
select,
submitQuery,
onEnter,
onArrow,
onColumn,
gridId,
activeDescendant,
} = useSkillSearchBox({ inputs: () => [input.value?.inputRef] })

function onFocusOut(event: FocusEvent): void {
if (!container.value?.contains(event.relatedTarget as Node | null))
Expand All @@ -124,10 +43,6 @@ watch(() => route.fullPath, () => {
close()
})

const listboxId = computed(() => (open.value && rows.value.length ? 'skill-search-listbox' : undefined))
const activeDescendant = computed(() =>
open.value && keyboardSelection.value && activeRow.value ? `skill-search-row-${activeIndex.value}` : undefined,
)
const submitDisabled = computed(() => state.value._tag === 'repository' && state.value.status._tag !== 'idle')
const submitLabel = computed(() => (state.value._tag === 'repository' ? 'Index repository' : 'Search'))
</script>
Expand All @@ -150,8 +65,9 @@ const submitLabel = computed(() => (state.value._tag === 'repository' ? 'Index r
autocomplete="off"
role="combobox"
aria-label="Search skills or index a GitHub repository"
aria-haspopup="grid"
:aria-expanded="open"
:aria-controls="listboxId"
:aria-controls="gridId"
:aria-activedescendant="activeDescendant"
class="w-full"
:ui="{
Expand All @@ -164,6 +80,8 @@ const submitLabel = computed(() => (state.value._tag === 'repository' ? 'Index r
@input="openPanel"
@keydown.down="onArrow($event, 1)"
@keydown.up="onArrow($event, -1)"
@keydown.left="onColumn($event, 0)"
@keydown.right="onColumn($event, 1)"
@keydown.enter="onEnter"
>
<template #trailing>
Expand All @@ -185,7 +103,7 @@ const submitLabel = computed(() => (state.value._tag === 'repository' ? 'Index r
<Transition name="home-search-panel">
<div v-if="open" class="absolute inset-x-0 top-full z-30 mt-2">
<!-- Lazy, as in SkillSearchTrigger: nothing renders it until search opens. -->
<LazySkillSearchPanel :show-preview="false" @select="(row) => { void select(row) }" />
<LazySkillSearchPanel @select="(row) => { void select(row) }" />
</div>
</Transition>

Expand Down
Loading