Skip to content

Commit

Permalink
feat(menu): improve keyboard navigation
Browse files Browse the repository at this point in the history
  • Loading branch information
m0ksem committed Nov 27, 2023
1 parent 54b99fd commit 7ec193d
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 26 deletions.
1 change: 1 addition & 0 deletions packages/ui/src/components/va-menu-list/VaMenuList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export default defineComponent({
min-width: 200px;
table-layout: fixed;
width: max-content;
outline: none;
.va-menu-item {
// Override VaDropdown style
Expand Down
18 changes: 13 additions & 5 deletions packages/ui/src/components/va-menu-list/components/VaMenuItem.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<template>
<tr class="va-menu-item"
v-bind="makeMenuItemAttributes({ disabled })"
v-on="keyboardFocusListeners"
:class="{
'va-menu-item--disabled': disabled
'va-menu-item--disabled': disabled,
'va-menu-item--keyboard-focus': hasKeyboardFocus,
}"
>
<td class="va-menu-item__cell va-menu-item__cell--left">
Expand Down Expand Up @@ -30,7 +32,7 @@
<script lang="ts">
import { defineComponent, ref, computed } from 'vue'
import { VaIcon } from '../../va-icon/'
import { useColors } from '../../../composables'
import { useColors, useKeyboardOnlyFocusGlobal } from '../../../composables'
import { makeMenuItemAttributes } from '../composables/useMenuKeyboardNavigation'
export default defineComponent({
Expand All @@ -48,9 +50,13 @@ export default defineComponent({
const hoverColor = computed(() => getHoverColor(getColor(props.color)))
const { hasKeyboardFocus, keyboardFocusListeners } = useKeyboardOnlyFocusGlobal()
return {
makeMenuItemAttributes,
hoverColor,
hasKeyboardFocus,
keyboardFocusListeners,
makeMenuItemAttributes,
}
},
})
Expand All @@ -63,8 +69,6 @@ export default defineComponent({
display: table-row;
cursor: pointer;
@include keyboard-focus-outline;
&__cell {
display: table-cell;
vertical-align: middle;
Expand Down Expand Up @@ -105,5 +109,9 @@ export default defineComponent({
opacity: 0.5;
cursor: not-allowed;
}
&--keyboard-focus {
@include focus-outline();
}
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,19 @@ export const makeMenuContainerAttributes = () => ({
})

export const useMenuKeyboardNavigation = (container: Ref<HTMLElement | undefined>) => {
useEvent('focus', ({ target }) => {
if (!container.value) { return }
if (target !== container.value) { return }

const firstItem = container.value.querySelector(NON_DISABLED_MENU_ITEM_SELECTOR)
if (firstItem) { focusElement(firstItem) }
}, container)

useEvent('keydown', ({ key }) => {
if (!container.value) { return }

const items = container.value.querySelectorAll(NON_DISABLED_MENU_ITEM_SELECTOR)
const focusedItem = container.value.querySelector(FOCUSED_MENU_ITEM_SELECTOR)

if (!items.length || !focusedItem) { return }
if (!items.length) { return }

if (!focusedItem) {
const firstItem = container.value.querySelector(NON_DISABLED_MENU_ITEM_SELECTOR)
if (firstItem) { focusElement(firstItem) }
return
}

if (key === 'ArrowDown' || key === 'ArrowRight') {
const focusedElementIndex = Array.from(items).indexOf(focusedItem)
Expand Down
15 changes: 13 additions & 2 deletions packages/ui/src/components/va-menu/VaMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<slot name="anchor" />
</template>

<VaDropdownContent @keydown.esc="close">
<VaDropdownContent @keydown="onKeydown">
<VaMenuList @keydown.enter.space.prevent.stop v-bind="menuListProps" ref="menuList" @selected="$emit('selected', $event); close()">
<template v-if="$slots.default" #default>
<slot />
Expand Down Expand Up @@ -54,12 +54,23 @@ export default defineComponent({
const close = () => {
dropdown.value?.hide()
nextTick(() => {
const el = unwrapEl(dropdown.value?.anchor)
const el = unwrapEl(dropdown.value?.anchorRef)
if (el) { focusFirstFocusableChild(el) }
})
}
const onKeydown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
close()
}
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
event.preventDefault()
}
}
return {
onKeydown,
dropdown,
menuList,
menuListProps: filterComponentProps(VaMenuListProps),
Expand Down
10 changes: 0 additions & 10 deletions packages/ui/src/components/va-menu/hooks/useMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,6 @@ import { VaMenu } from '../va-menu'
import { ExtractComponentPropTypes } from '../../../utils/component-options'
import { onBeforeUnmount, computed } from 'vue'

const getCoordinates = (el: any) => {
if ('getBoundingClientRect' in el) {
return el.getBoundingClientRect()
}

return { x: 0, y: 0 }
}

type OmitMenuProps = 'modelValue' | 'anchor' | 'cursor' | 'stateful' | 'preset'

/** This hook can be used without plugin used */
Expand All @@ -30,8 +22,6 @@ export const useMenu = () => {
anchor: props.event.target,
cursor: {
getBoundingClientRect () {
// anchor position possibly changed, we need to update the position of the floating element
const { x, y } = getCoordinates(props.event.target)
const resX = props.event.clientX
const resY = props.event.clientY
return {
Expand Down
31 changes: 31 additions & 0 deletions packages/ui/src/composables/useKeyboardOnlyFocus.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ref } from 'vue'
import { getWindow } from '../utils/ssr'

export function useKeyboardOnlyFocus () {
const hasKeyboardFocus = ref(false)
Expand Down Expand Up @@ -28,3 +29,33 @@ export function useKeyboardOnlyFocus () {
keyboardFocusListeners,
}
}

let previouslyClicked = false
let timeout: ReturnType<typeof setTimeout>

getWindow()?.addEventListener('mousedown', () => {
previouslyClicked = true
timeout = setTimeout(() => {
previouslyClicked = false
}, 300)
})

export function useKeyboardOnlyFocusGlobal () {
const hasKeyboardFocus = ref(false)
const keyboardFocusListeners = {
focus: () => {
if (!previouslyClicked) {
hasKeyboardFocus.value = true
}
},

blur: () => {
hasKeyboardFocus.value = false
},
}

return {
hasKeyboardFocus,
keyboardFocusListeners,
}
}

0 comments on commit 7ec193d

Please sign in to comment.