feat: adds arrow keys navigation on powersearch
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
v-for="(shortcut, shortcutIndex) in searchResults"
|
v-for="(shortcut, shortcutIndex) in searchResults"
|
||||||
:key="`shortcut-${shortcutIndex}`"
|
:key="`shortcut-${shortcutIndex}`"
|
||||||
:ref="`item-${shortcutIndex}`"
|
:ref="`item-${shortcutIndex}`"
|
||||||
|
:active="shortcutIndex === selectedEntry"
|
||||||
:shortcut="shortcut.item"
|
:shortcut="shortcut.item"
|
||||||
@action="$emit('action', shortcut.item.action)"
|
@action="$emit('action', shortcut.item.action)"
|
||||||
/>
|
/>
|
||||||
@@ -20,8 +21,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from "@nuxtjs/composition-api"
|
import {
|
||||||
|
computed,
|
||||||
|
onUnmounted,
|
||||||
|
onMounted,
|
||||||
|
getCurrentInstance,
|
||||||
|
} from "@nuxtjs/composition-api"
|
||||||
import Fuse from "fuse.js"
|
import Fuse from "fuse.js"
|
||||||
|
import { useArrowKeysNavigation } from "~/helpers/powerSearchNavigation"
|
||||||
|
import { HoppAction } from "~/helpers/actions"
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
input: Record<string, any>[]
|
input: Record<string, any>[]
|
||||||
@@ -35,4 +43,23 @@ const options = {
|
|||||||
const fuse = new Fuse(props.input, options)
|
const fuse = new Fuse(props.input, options)
|
||||||
|
|
||||||
const searchResults = computed(() => fuse.search(props.search))
|
const searchResults = computed(() => fuse.search(props.search))
|
||||||
|
|
||||||
|
const searchResultsItems = computed(() =>
|
||||||
|
searchResults.value.map((searchResult: any) => searchResult.item)
|
||||||
|
)
|
||||||
|
|
||||||
|
const currentInstance = getCurrentInstance()
|
||||||
|
|
||||||
|
const emitSearchAction = (action: HoppAction) =>
|
||||||
|
currentInstance.emit("action", action)
|
||||||
|
|
||||||
|
const { bindArrowKeysListerners, unbindArrowKeysListerners, selectedEntry } =
|
||||||
|
useArrowKeysNavigation(searchResultsItems, {
|
||||||
|
onEnter: emitSearchAction,
|
||||||
|
stopPropagation: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(bindArrowKeysListerners)
|
||||||
|
|
||||||
|
onUnmounted(unbindArrowKeysListerners)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
v-for="(shortcut, shortcutIndex) in map.shortcuts"
|
v-for="(shortcut, shortcutIndex) in map.shortcuts"
|
||||||
:key="`map-${mapIndex}-shortcut-${shortcutIndex}`"
|
:key="`map-${mapIndex}-shortcut-${shortcutIndex}`"
|
||||||
:shortcut="shortcut"
|
:shortcut="shortcut"
|
||||||
|
:active="shortcutsItems.indexOf(shortcut) === selectedEntry"
|
||||||
@action="runAction"
|
@action="runAction"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -56,9 +57,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "@nuxtjs/composition-api"
|
import { ref, computed, onUnmounted, onMounted } from "@nuxtjs/composition-api"
|
||||||
import { HoppAction, invokeAction } from "~/helpers/actions"
|
import { HoppAction, invokeAction } from "~/helpers/actions"
|
||||||
import { spotlight as mappings, fuse } from "~/helpers/shortcuts"
|
import { spotlight as mappings, fuse } from "~/helpers/shortcuts"
|
||||||
|
import { useArrowKeysNavigation } from "~/helpers/powerSearchNavigation"
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
show: boolean
|
show: boolean
|
||||||
@@ -79,4 +81,20 @@ const runAction = (command: HoppAction) => {
|
|||||||
invokeAction(command)
|
invokeAction(command)
|
||||||
hideModal()
|
hideModal()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const shortcutsItems = computed(() =>
|
||||||
|
mappings.reduce(
|
||||||
|
(shortcuts, section) => [...shortcuts, ...section.shortcuts],
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const { bindArrowKeysListerners, unbindArrowKeysListerners, selectedEntry } =
|
||||||
|
useArrowKeysNavigation(shortcutsItems, {
|
||||||
|
onEnter: runAction,
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(bindArrowKeysListerners)
|
||||||
|
|
||||||
|
onUnmounted(unbindArrowKeysListerners)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
focus-visible:bg-primaryLight
|
focus-visible:bg-primaryLight
|
||||||
search-entry
|
search-entry
|
||||||
"
|
"
|
||||||
|
:class="{ active, 'outline-none': active, 'focus-visible': active }"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@click="$emit('action', shortcut.action)"
|
@click="$emit('action', shortcut.action)"
|
||||||
@keydown.enter="$emit('action', shortcut.action)"
|
@keydown.enter="$emit('action', shortcut.action)"
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
group-hover:text-secondaryDark group-hover:opacity-100
|
group-hover:text-secondaryDark group-hover:opacity-100
|
||||||
group-focus:opacity-100
|
group-focus:opacity-100
|
||||||
"
|
"
|
||||||
|
:class="{ 'opacity-100': active, 'text-secondaryDark': active }"
|
||||||
:name="shortcut.icon"
|
:name="shortcut.icon"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -36,6 +38,7 @@
|
|||||||
group-hover:text-secondaryDark
|
group-hover:text-secondaryDark
|
||||||
group-focus:text-secondaryDark
|
group-focus:text-secondaryDark
|
||||||
"
|
"
|
||||||
|
:class="{ 'text-secondaryDark': active }"
|
||||||
>
|
>
|
||||||
{{ $t(shortcut.label) }}
|
{{ $t(shortcut.label) }}
|
||||||
</span>
|
</span>
|
||||||
@@ -52,6 +55,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineProps<{
|
defineProps<{
|
||||||
shortcut: Object
|
shortcut: Object
|
||||||
|
active: Boolean
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -73,7 +77,8 @@ defineProps<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:hover::after,
|
&:hover::after,
|
||||||
&:focus::after {
|
&:focus::after,
|
||||||
|
&.active::after {
|
||||||
@apply bg-accentLight;
|
@apply bg-accentLight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
54
packages/hoppscotch-app/helpers/powerSearchNavigation.ts
Normal file
54
packages/hoppscotch-app/helpers/powerSearchNavigation.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { ref } from "@nuxtjs/composition-api"
|
||||||
|
|
||||||
|
const NAVIGATION_KEYS = ["ArrowDown", "ArrowUp", "Enter"]
|
||||||
|
|
||||||
|
export function useArrowKeysNavigation(searchItems: any, options: any = {}) {
|
||||||
|
function handleArrowKeysNavigation(
|
||||||
|
event: any,
|
||||||
|
itemIndex: any,
|
||||||
|
preventPropagation: Boolean
|
||||||
|
) {
|
||||||
|
if (!NAVIGATION_KEYS.includes(event.key)) return
|
||||||
|
|
||||||
|
if (preventPropagation) event.stopImmediatePropagation()
|
||||||
|
|
||||||
|
const itemsLength = searchItems.value.length
|
||||||
|
const lastItemIndex = itemsLength - 1
|
||||||
|
const itemIndexValue = itemIndex.value
|
||||||
|
const action = searchItems.value[itemIndexValue].action
|
||||||
|
|
||||||
|
if (action && event.key === "Enter" && options.onEnter) {
|
||||||
|
options.onEnter(action)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === "ArrowDown") {
|
||||||
|
itemIndex.value = itemIndexValue < lastItemIndex ? itemIndexValue + 1 : 0
|
||||||
|
} else if (itemIndexValue === 0) itemIndex.value = lastItemIndex
|
||||||
|
else if (event.key === "ArrowUp") itemIndex.value = itemIndexValue - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const preventPropagation = options && options.stopPropagation
|
||||||
|
|
||||||
|
const selectedEntry = ref(0)
|
||||||
|
|
||||||
|
const onKeyUp = (event: any) => {
|
||||||
|
handleArrowKeysNavigation(event, selectedEntry, preventPropagation)
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindArrowKeysListerners() {
|
||||||
|
window.addEventListener("keydown", onKeyUp, { capture: preventPropagation })
|
||||||
|
}
|
||||||
|
|
||||||
|
function unbindArrowKeysListerners() {
|
||||||
|
window.removeEventListener("keydown", onKeyUp, {
|
||||||
|
capture: preventPropagation,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
bindArrowKeysListerners,
|
||||||
|
unbindArrowKeysListerners,
|
||||||
|
selectedEntry,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user