feat(docs): add keyboard shortcut for search (#3718)

This commit is contained in:
Rokas Brazdžionis
2025-11-07 11:04:40 +02:00
committed by GitHub
parent d3826ce952
commit 190e8372af
7 changed files with 139 additions and 7 deletions

View File

@@ -3,12 +3,20 @@ import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'
import { search } from '../../../data/iconNodes'
const SearchIcon = createLucideIcon('search', search)
defineProps({
shortcut: {
type: String,
required: false
}
})
</script>
<template>
<button class="fake-input">
<component :is="SearchIcon" class="search-icon"/>
<slot/>
<kbd v-if="shortcut" class="shortcut">{{ shortcut }}</kbd>
</button>
</template>
@@ -33,4 +41,23 @@ const SearchIcon = createLucideIcon('search', search)
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-alt);
}
.shortcut {
margin-left: auto;
padding: 2px 6px;
font-size: 12px;
font-family: inherit;
font-weight: 500;
line-height: 1.5;
color: var(--vp-c-text-3);
background: var(--vp-c-default-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
}
@media (hover: none) {
.shortcut {
display: none;
}
}
</style>

View File

@@ -6,20 +6,35 @@ export default {
export interface InputProps {
type: string
modelValue: string
shortcut?: string
}
</script>
<script setup lang="ts">
import { ref } from 'vue'
import { ref, onMounted, nextTick, watch } from 'vue'
const props = withDefaults(defineProps<InputProps>(), {
type: 'text'
})
const input = ref()
const wrapperEl = ref()
const shortcutEl = ref()
defineEmits(['change', 'input', 'update:modelValue'])
const updateShortcutSpacing = () => {
nextTick(() => {
if (shortcutEl.value && wrapperEl.value) {
const shortcutWidth = shortcutEl.value.offsetWidth
wrapperEl.value.style.setProperty('--shortcut-width', `${shortcutWidth}px`)
}
})
}
onMounted(updateShortcutSpacing)
watch(() => props.shortcut, updateShortcutSpacing)
defineExpose({
focus: () => {
input.value.focus()
@@ -28,17 +43,18 @@ defineExpose({
</script>
<template>
<div class="input-wrapper">
<div class="input-wrapper" ref="wrapperEl">
<slot name="icon" class="icon" />
<input
:type="type"
class="input"
:class="{'has-icon': $slots.icon}"
:class="{'has-icon': $slots.icon, 'has-shortcut': shortcut}"
ref="input"
:value="modelValue"
v-bind="$attrs"
@input="$emit('update:modelValue', $event.target.value)"
/>
<kbd v-if="shortcut" class="shortcut" ref="shortcutEl">{{ shortcut }}</kbd>
</div>
</template>
@@ -57,6 +73,10 @@ defineExpose({
font-size: 14px;
}
.input.has-shortcut {
padding-right: calc(var(--shortcut-width, 40px) + 22px);
}
.input:hover, .input:focus {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-alt);
@@ -66,7 +86,28 @@ defineExpose({
padding-left: 52px;
}
.shortcut {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
padding: 2px 6px;
font-size: 12px;
font-family: inherit;
font-weight: 500;
line-height: 1.5;
color: var(--vp-c-text-3);
background: var(--vp-c-default-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
pointer-events: none;
}
@media (hover: none) {
.shortcut {
display: none;
}
}
</style>
<style>

View File

@@ -14,6 +14,7 @@ const SearchIcon = createLucideIcon('search', search)
interface Props {
modelValue: string
shortcut?: string
}
const props = defineProps<Props>()
@@ -39,6 +40,7 @@ const value = computed({
ref="input"
type="search"
autofocus
:shortcut="shortcut"
v-bind="$attrs"
v-model="value"
class="input-wrapper"
@@ -71,5 +73,4 @@ const value = computed({
font-size: 14px;
height: 48px;
}
</style>

View File

@@ -5,10 +5,15 @@ import LucideIcon from '../base/LucideIcon.vue'
import { useRouter } from 'vitepress';
import { random } from 'lodash-es'
import FakeInput from '../base/FakeInput.vue'
import useSearchShortcut from '../../utils/useSearchShortcut'
const { go } = useRouter()
const intervalTime = shallowRef()
const { shortcutText: kbdSearchShortcut } = useSearchShortcut(() => {
go('/icons/?focus')
})
const getInitialItems = () => data.icons.slice(0, 48)
const items = ref(getInitialItems())
let id = items.value.length + 1
@@ -64,7 +69,11 @@ onBeforeUnmount(() => {
</div>
</TransitionGroup>
</div>
<FakeInput @click="go('/icons/?focus')" class="search-box">
<FakeInput
@click="go('/icons/?focus')"
:shortcut="kbdSearchShortcut"
class="search-box"
>
Search {{ data.iconsCount }} icons...
</FakeInput>
</div>

View File

@@ -4,6 +4,7 @@ import type { IconEntity, Category } from '../../types';
import useSearch from '../../composables/useSearch';
import InputSearch from '../base/InputSearch.vue';
import useSearchInput from '../../composables/useSearchInput';
import useSearchShortcut from '../../utils/useSearchShortcut';
import StickyBar from './StickyBar.vue';
import IconsCategory from './IconsCategory.vue';
import useFetchTags from '../../composables/useFetchTags';
@@ -27,6 +28,10 @@ const activeIconName = ref(null);
const { searchInput, searchQuery, searchQueryDebounced } = useSearchInput();
const isSearching = computed(() => !!searchQuery.value);
const { shortcutText: kbdSearchShortcut } = useSearchShortcut(() => {
searchInput.value?.focus();
});
function setActiveIconName(name: string) {
activeIconName.value = name;
}
@@ -154,6 +159,7 @@ watchEffect(() => {
<InputSearch
:placeholder="`Search ${icons.length} icons ...`"
v-model="searchQuery"
:shortcut="kbdSearchShortcut"
class="input-wrapper"
ref="searchInput"
@focus="onFocusSearchInput"

View File

@@ -1,11 +1,13 @@
<script setup lang="ts">
import { ref, computed, defineAsyncComponent, onMounted, watch } from 'vue';
import { ref, computed, defineAsyncComponent, onMounted, onBeforeUnmount, watch } from 'vue';
import type { IconEntity } from '../../types';
import { useElementSize, useEventListener, useVirtualList } from '@vueuse/core';
import { useRoute } from 'vitepress';
import IconGrid from './IconGrid.vue';
import InputSearch from '../base/InputSearch.vue';
import useSearch from '../../composables/useSearch';
import useSearchInput from '../../composables/useSearchInput';
import useSearchShortcut from '../../utils/useSearchShortcut';
import StickyBar from './StickyBar.vue';
import useFetchTags from '../../composables/useFetchTags';
import useFetchCategories from '../../composables/useFetchCategories';
@@ -58,6 +60,11 @@ const mappedIcons = computed(() => {
});
const { searchInput, searchQuery, searchQueryDebounced } = useSearchInput();
const { shortcutText: kbdSearchShortcut } = useSearchShortcut(() => {
searchInput.value?.focus();
});
const searchResults = useSearch(searchQueryDebounced, mappedIcons, [
{ name: 'name', weight: 3 },
{ name: 'aliases', weight: 3 },
@@ -80,8 +87,13 @@ const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(
onMounted(() => {
containerProps.ref.value = document.documentElement;
useEventListener(window, 'scroll', containerProps.onScroll)
})
// Check if we should focus the search input from URL parameter
const route = useRoute()
if (route.data?.relativePath && window.location.search.includes('focus')) {
searchInput.value?.focus()
}
})
function setActiveIconName(name: string) {
activeIconName.value = name;
@@ -118,6 +130,7 @@ function handleCloseDrawer() {
:placeholder="`Search ${icons.length} icons ...`"
v-model="searchQuery"
ref="searchInput"
:shortcut="kbdSearchShortcut"
class="input-wrapper"
@focus="onFocusSearchInput"
/>

View File

@@ -0,0 +1,35 @@
import { ref, onMounted, onBeforeUnmount } from 'vue';
/**
* Composable for handling search keyboard shortcuts.
* Listens for Cmd/Ctrl+K and "/" keys to trigger a search action.
*
* @param callback - Function to execute when shortcut is triggered
* @returns Object containing the platform-specific shortcut display text
*/
export default function useSearchShortcut(callback: () => void) {
const shortcutText = ref('');
function handleKeydown(event: KeyboardEvent) {
// Check for Cmd+K (Mac), Ctrl+K (Windows/Linux), or forward slash
if (((event.metaKey || event.ctrlKey) && event.key === 'k') || event.key === '/') {
event.preventDefault();
callback();
}
}
onMounted(() => {
// Detect platform and set appropriate keyboard shortcut for search focus
const isMac = /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform);
shortcutText.value = isMac ? '⌘K' : 'Ctrl+K';
// Add keyboard shortcut listener
window.addEventListener('keydown', handleKeydown);
});
onBeforeUnmount(() => {
window.removeEventListener('keydown', handleKeydown);
});
return { shortcutText };
}