mirror of
https://github.com/lucide-icons/lucide.git
synced 2025-12-16 12:37:43 +01:00
feat(docs): add keyboard shortcut for search (#3718)
This commit is contained in:
committed by
GitHub
parent
d3826ce952
commit
190e8372af
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
35
docs/.vitepress/theme/utils/useSearchShortcut.ts
Normal file
35
docs/.vitepress/theme/utils/useSearchShortcut.ts
Normal 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user