Skip to content

Commit dcc6a94

Browse files
committed
fix: fixed arrow navigation
1 parent 37a82ad commit dcc6a94

File tree

4 files changed

+117
-11
lines changed

4 files changed

+117
-11
lines changed

playground/App.vue

+9-7
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@ import SimpleDropdown from '@/VueSimpleDropdown.vue'
33
import { ref } from 'vue'
44
55
const example = ref('vue-simple-dropdown')
6+
const toggle = ref(true)
67
</script>
78

89
<template>
910
<div class="container mx-auto px-4">
1011
<div h1 class="text-3xl font-bold pt-4">Playground for {{ example }}</div>
1112
<div class="mt-4">
13+
<button type="button" class="border p-2" @click="toggle = !toggle">Toggle</button>
14+
</div>
15+
<div v-if="toggle" class="mt-4">
1216
<SimpleDropdown class="inline" popper-class="border rounded">
1317
<!-- This will be the popover reference (for the events and position) -->
1418
<button
@@ -19,21 +23,19 @@ const example = ref('vue-simple-dropdown')
1923

2024
<!-- This will be the content of the popover -->
2125
<template #popper="{ hide }">
22-
<div class="w-44 bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700">
26+
<div class="w-44 bg-white rounded shadow dark:bg-gray-700">
2327
<ul class="py-1 text-sm text-gray-700 dark:text-gray-200">
2428
<li>
25-
<a href="#" class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white" @click="hide"
26-
>Dashboard</a
27-
>
29+
<a href="#" class="block py-2 px-4 hover:bg-gray-100 focus:bg-gray-100 outline-none" @click="hide">Dashboard</a>
2830
</li>
2931
<li>
30-
<a href="#" class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Settings</a>
32+
<a href="#" class="block py-2 px-4 hover:bg-gray-100 focus:bg-gray-100 outline-none">Settings</a>
3133
</li>
3234
<li>
33-
<a href="#" class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Earnings</a>
35+
<a href="#" class="block py-2 px-4 hover:bg-gray-100 focus:bg-gray-100 outline-none">Earnings</a>
3436
</li>
3537
<li>
36-
<a href="#" class="block py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Sign out</a>
38+
<a href="#" class="block py-2 px-4 hover:bg-gray-100 focus:bg-gray-100 outline-none">Sign out</a>
3739
</li>
3840
</ul>
3941
</div>

src/VueSimpleDropdown.vue

+64-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
<template>
2-
<BaseDropdown :distance="14" placement="bottom-start" :triggers="['click']" auto-hide>
2+
<BaseDropdown
3+
ref="baseDropdown"
4+
:distance="14"
5+
placement="bottom-start"
6+
:triggers="['click']"
7+
auto-hide
8+
@show="show"
9+
@hide="hide"
10+
>
311
<template v-for="(_, slot) in $slots" #[slot]="scope">
412
<slot :name="slot" v-bind="scope || {}" />
513
</template>
@@ -8,10 +16,64 @@
816

917
<script setup lang="ts">
1018
import BaseDropdown from './BaseDropdown.vue'
19+
import { getNextActiveElement, isVisible } from './utils'
20+
import { onBeforeUnmount, ref } from 'vue'
1121
import 'floating-vue/dist/style.css'
22+
23+
interface Props {
24+
dropdownItemSelector?: string
25+
enableArrowNavigation?: boolean
26+
}
27+
28+
const props = withDefaults(defineProps<Props>(), {
29+
dropdownItemSelector: 'li > a:not(.disabled):not(:disabled)'
30+
})
31+
32+
type popperContentRef = { $el: HTMLElement }
33+
type baseDropdownRef = { hide: () => void; $el: HTMLElement; $refs: { popperContent: popperContentRef } }
34+
35+
const ARROW_UP_KEY = 'ArrowUp'
36+
const ARROW_DOWN_KEY = 'ArrowDown'
37+
const ESCAPE_KEY = 'Escape'
38+
const baseDropdown = ref<baseDropdownRef | null>(null)
39+
40+
const popoverKeydown = (e: KeyboardEvent) => {
41+
const popover = baseDropdown.value as baseDropdownRef
42+
const popperContentEl = popover.$refs.popperContent.$el
43+
44+
if ([ARROW_UP_KEY, ARROW_DOWN_KEY].includes(e.key)) {
45+
e.preventDefault()
46+
47+
let items = [...popperContentEl.querySelectorAll(`${props.dropdownItemSelector}`)] as HTMLElement[]
48+
49+
items = items.filter((element) => isVisible(element))
50+
51+
if (!items.length) {
52+
return
53+
}
54+
55+
const target = e.target as HTMLInputElement
56+
57+
getNextActiveElement(items, target, e.key === ARROW_DOWN_KEY, !items.includes(target)).focus()
58+
}
59+
if (e.key === ESCAPE_KEY) {
60+
popover.hide()
61+
}
62+
}
63+
const show = () => {
64+
document.addEventListener('keydown', popoverKeydown)
65+
}
66+
67+
const hide = () => {
68+
document.removeEventListener('keydown', popoverKeydown)
69+
}
70+
71+
onBeforeUnmount(() => {
72+
document.removeEventListener('keydown', popoverKeydown)
73+
})
1274
</script>
1375

14-
<style>
76+
<style scoped>
1577
.v-popper--theme-simple .v-popper__inner {
1678
background: #fff;
1779
}

src/utils.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* Checks whether an element is visible
3+
*/
4+
export const isVisible = (element: HTMLElement) => {
5+
if (!(element instanceof Element)) {
6+
throw Error('You must provide a DOM element.')
7+
}
8+
9+
return (
10+
!!(element.offsetWidth || element.offsetHeight || element.getClientRects().length) &&
11+
window.getComputedStyle(element).visibility !== 'hidden' &&
12+
window.getComputedStyle(element).display !== 'none'
13+
)
14+
}
15+
16+
/**
17+
* Return the previous/next element of a list.
18+
*/
19+
export const getNextActiveElement = (
20+
list: HTMLElement[],
21+
activeElement: HTMLElement,
22+
shouldGetNext: boolean,
23+
isCycleAllowed: boolean
24+
) => {
25+
const listLength = list.length
26+
let index = list.indexOf(activeElement)
27+
28+
// if the element does not exist in the list return an element
29+
// depending on the direction and if cycle is allowed
30+
if (index === -1) {
31+
return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0]
32+
}
33+
34+
index += shouldGetNext ? 1 : -1
35+
36+
if (isCycleAllowed) {
37+
index = (index + listLength) % listLength
38+
}
39+
40+
return list[Math.max(0, Math.min(index, listLength - 1))]
41+
}

tsconfig.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
"resolveJsonModule": true,
1313
"esModuleInterop": true,
1414
"lib": [
15-
"esnext",
16-
"dom"
15+
"ES2016",
16+
"DOM",
17+
"DOM.Iterable"
1718
],
1819
"paths": {
1920
"@/*": [

0 commit comments

Comments
 (0)