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
7 changes: 7 additions & 0 deletions app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,17 @@ if (import.meta.server) {
}

const keyboardShortcuts = useKeyboardShortcuts()
const { settings } = useSettings()

onKeyDown(
'/',
e => {
if (e.ctrlKey) {
e.preventDefault()
settings.value.instantSearch = !settings.value.instantSearch
return
}

if (!keyboardShortcuts.value || isEditableElement(e.target)) return
e.preventDefault()

Expand Down
54 changes: 54 additions & 0 deletions app/components/InstantSearch.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<script setup lang="ts">
import { useSettings } from '~/composables/useSettings'
const { settings } = useSettings()
onPrehydrate(el => {
const settingsSaved = JSON.parse(localStorage.getItem('npmx-settings') || '{}')
const enabled = settingsSaved.instantSearch
if (enabled === false) {
el.querySelector('[data-instant-search-on]')!.className = 'hidden'
el.querySelector('[data-instant-search-off]')!.className = ''
}
})
</script>

<template>
<p id="instant-search-advisory" class="text-fg-muted text-sm text-pretty">
<span
class="i-lucide:zap align-middle text-fg relative top-[-0.1em] me-1"
style="font-size: 0.8em"
aria-hidden="true"
/>
<span data-instant-search-on :class="settings.instantSearch ? '' : 'hidden'">
<i18n-t keypath="search.instant_search_advisory">
<template #label>
{{ $t('search.instant_search') }}
</template>
<template #state>
<strong>{{ $t('search.instant_search_on') }}</strong>
</template>
<template #action>
<button type="button" class="underline" @click="settings.instantSearch = false">
{{ $t('search.instant_search_turn_off') }}
</button>
</template>
</i18n-t>
</span>
<span data-instant-search-off :class="settings.instantSearch ? 'hidden' : ''">
<i18n-t keypath="search.instant_search_advisory">
<template #label>
{{ $t('search.instant_search') }}
</template>
<template #state>
<strong>{{ $t('search.instant_search_off') }}</strong>
</template>
<template #action>
<button type="button" class="underline" @click="settings.instantSearch = true">
{{ $t('search.instant_search_turn_on') }}
</button>
</template>
</i18n-t>
</span>
</p>
</template>
14 changes: 11 additions & 3 deletions app/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,20 @@ defineOgImageComponent('Default', {
{{ $t('tagline') }}
</p>
<search
class="w-full max-w-xl motion-safe:animate-slide-up motion-safe:animate-fill-both"
class="w-full max-w-2xl motion-safe:animate-slide-up motion-safe:animate-fill-both"
style="animation-delay: 0.2s"
>
<form method="GET" action="/search" class="relative" @submit.prevent.trim="search">
<form
method="GET"
action="/search"
class="relative grid justify-items-center gap-4"
@submit.prevent.trim="search"
>
<label for="home-search" class="sr-only">
{{ $t('search.label') }}
</label>

<div class="relative group" :class="{ 'is-focused': isSearchFocused }">
<div class="relative group w-full max-w-xl" :class="{ 'is-focused': isSearchFocused }">
<div
class="absolute z-1 -inset-px pointer-events-none rounded-lg bg-gradient-to-r from-fg/0 to-accent/5 opacity-0 transition-opacity duration-500 blur-sm group-[.is-focused]:opacity-100"
/>
Expand All @@ -82,6 +87,7 @@ defineOgImageComponent('Default', {
no-correct
size="large"
class="w-full ps-8 pe-24"
aria-describedby="instant-search-advisory"
@focus="isSearchFocused = true"
@blur="isSearchFocused = false"
/>
Expand All @@ -98,6 +104,8 @@ defineOgImageComponent('Default', {
</ButtonBase>
</div>
</div>

<InstantSearch />
</form>
</search>

Expand Down
8 changes: 7 additions & 1 deletion i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,13 @@
"org": "org",
"view_user_packages": "View packages by this user",
"view_org_packages": "View packages by this organization"
}
},
"instant_search": "Instant search",
"instant_search_on": "on",
"instant_search_off": "off",
"instant_search_turn_on": "turn on",
"instant_search_turn_off": "turn off",
"instant_search_advisory": "{label} {state} — {action}"
},
"nav": {
"main_navigation": "Main",
Expand Down
18 changes: 18 additions & 0 deletions i18n/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,24 @@
}
},
"additionalProperties": false
},
"instant_search": {
"type": "string"
},
"instant_search_on": {
"type": "string"
},
"instant_search_off": {
"type": "string"
},
"instant_search_turn_on": {
"type": "string"
},
"instant_search_turn_off": {
"type": "string"
},
"instant_search_advisory": {
"type": "string"
}
},
"additionalProperties": false
Expand Down
9 changes: 9 additions & 0 deletions test/nuxt/a11y.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ import {
HeaderAccountMenu,
HeaderConnectorModal,
HeaderSearchBox,
InstantSearch,
InputBase,
LicenseDisplay,
LoadingSpinner,
Expand Down Expand Up @@ -2657,6 +2658,14 @@ describe('component accessibility audits', () => {
})
})

describe('InstantSearch', () => {
it('should have no accessibility violations', async () => {
const component = await mountSuspended(InstantSearch)
const results = await runAxe(component)
expect(results.violations).toEqual([])
})
})

describe('SearchProviderToggle', () => {
it('should have no accessibility violations', async () => {
const component = await mountSuspended(SearchProviderToggle)
Expand Down
Loading