Skip to content

Commit

Permalink
feat(VMenu): add submenu prop (#20092)
Browse files Browse the repository at this point in the history
closes #19093
closes #20130
  • Loading branch information
KaelWD authored Jul 30, 2024
1 parent c923066 commit 306a262
Show file tree
Hide file tree
Showing 8 changed files with 79 additions and 13 deletions.
3 changes: 2 additions & 1 deletion packages/api-generator/src/locale/en/VMenu.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"openDelay": "Milliseconds to wait before opening component. Only works with the **open-on-hover** prop.",
"openOnClick": "Designates whether menu should open on activator click.",
"openOnHover": "Designates whether menu should open on activator hover.",
"returnValue": "The value that is updated when the menu is closed - must be primitive. Dot notation is supported."
"returnValue": "The value that is updated when the menu is closed - must be primitive. Dot notation is supported.",
"submenu": "Opens with right arrow and closes on left instead of up/down. Implies `location=\"end\"`. Directions are reversed for RTL."
}
}
4 changes: 2 additions & 2 deletions packages/docs/src/examples/v-menu/misc-use-in-components.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
sm="6"
>
<v-card height="200px">
<v-card-title class="bg-blue">
<v-card-title class="bg-blue d-flex align-center">
<span class="text-h5">Menu</span>

<v-spacer></v-spacer>

<v-menu>
<template v-slot:activator="{ props }">
<v-btn icon="mdi-dots-vertical" v-bind="props"></v-btn>
<v-btn icon="mdi-dots-vertical" variant="text" v-bind="props"></v-btn>
</template>

<v-list>
Expand Down
37 changes: 37 additions & 0 deletions packages/docs/src/examples/v-menu/prop-submenu.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<template>
<div class="text-center">
<v-btn color="primary">
Open menu

<v-menu activator="parent">
<v-list>
<v-list-item v-for="i in 5" :key="i" link>
<v-list-item-title>Item {{ i }}</v-list-item-title>
<template v-slot:append>
<v-icon icon="mdi-menu-right" size="x-small"></v-icon>
</template>

<v-menu :open-on-focus="false" activator="parent" open-on-hover submenu>
<v-list>
<v-list-item v-for="j in 5" :key="j" link>
<v-list-item-title>Item {{ i }} - {{ j }}</v-list-item-title>
<template v-slot:append>
<v-icon icon="mdi-menu-right" size="x-small"></v-icon>
</template>

<v-menu :open-on-focus="false" activator="parent" open-on-hover submenu>
<v-list>
<v-list-item v-for="k in 5" :key="k" link>
<v-list-item-title>Item {{ i }} - {{ j }} - {{ k }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-list-item>
</v-list>
</v-menu>
</v-list-item>
</v-list>
</v-menu>
</v-btn>
</div>
</template>
6 changes: 6 additions & 0 deletions packages/docs/src/pages/en/components/menus.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ Menus can be accessed using hover instead of clicking with the **open-on-hover**

<ExamplesExample file="v-menu/prop-open-on-hover" />

#### Nested menus

Menus with other menus inside them will not close until their children are closed. The **submenu** prop changes keyboard behaviour to open and close with left/right arrow keys instead of up/down.

<ExamplesExample file="v-menu/prop-submenu" />

### Slots

#### Activator and tooltip
Expand Down
2 changes: 1 addition & 1 deletion packages/vuetify/src/components/VList/VListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ export const VListItem = genericComponent<VListItemSlots>()({
function onKeyDown (e: KeyboardEvent) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
onClick(e as any as MouseEvent)
e.target!.dispatchEvent(new MouseEvent('click', e))
}
}

Expand Down
24 changes: 21 additions & 3 deletions packages/vuetify/src/components/VMenu/VMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { makeVOverlayProps } from '@/components/VOverlay/VOverlay'

// Composables
import { forwardRefs } from '@/composables/forwardRefs'
import { useRtl } from '@/composables/locale'
import { useProxiedModel } from '@/composables/proxiedModel'
import { useScopeId } from '@/composables/scopeId'

Expand Down Expand Up @@ -46,11 +47,13 @@ export const makeVMenuProps = propsFactory({
// TODO
// disableKeys: Boolean,
id: String,
submenu: Boolean,

...omit(makeVOverlayProps({
closeDelay: 250,
closeOnContentClick: true,
locationStrategy: 'connected' as const,
location: undefined,
openDelay: 300,
scrim: false,
scrollStrategy: 'reposition' as const,
Expand All @@ -70,6 +73,7 @@ export const VMenu = genericComponent<OverlaySlots>()({
setup (props, { slots }) {
const isActive = useProxiedModel(props, 'modelValue')
const { scopeId } = useScopeId()
const { isRtl } = useRtl()

const uid = getUid()
const id = computed(() => props.id || `v-menu-${uid}`)
Expand Down Expand Up @@ -157,9 +161,9 @@ export const VMenu = genericComponent<OverlaySlots>()({
isActive.value = false
overlay.value?.activatorEl?.focus()
}
} else if (['Enter', ' '].includes(e.key) && props.closeOnContentClick) {
} else if (props.submenu && e.key === (isRtl.value ? 'ArrowRight' : 'ArrowLeft')) {
isActive.value = false
parent?.closeParents()
overlay.value?.activatorEl?.focus()
}
}

Expand All @@ -170,12 +174,25 @@ export const VMenu = genericComponent<OverlaySlots>()({
if (el && isActive.value) {
if (e.key === 'ArrowDown') {
e.preventDefault()
e.stopImmediatePropagation()
focusChild(el, 'next')
} else if (e.key === 'ArrowUp') {
e.preventDefault()
e.stopImmediatePropagation()
focusChild(el, 'prev')
} else if (props.submenu) {
if (e.key === (isRtl.value ? 'ArrowRight' : 'ArrowLeft')) {
isActive.value = false
} else if (e.key === (isRtl.value ? 'ArrowLeft' : 'ArrowRight')) {
e.preventDefault()
focusChild(el, 'first')
}
}
} else if (['ArrowDown', 'ArrowUp'].includes(e.key)) {
} else if (
props.submenu
? e.key === (isRtl.value ? 'ArrowLeft' : 'ArrowRight')
: ['ArrowDown', 'ArrowUp'].includes(e.key)
) {
isActive.value = true
e.preventDefault()
setTimeout(() => setTimeout(() => onActivatorKeydown(e)))
Expand Down Expand Up @@ -207,6 +224,7 @@ export const VMenu = genericComponent<OverlaySlots>()({
v-model={ isActive.value }
absolute
activatorProps={ activatorProps.value }
location={ props.location ?? (props.submenu ? 'end' : 'bottom') }
onClick:outside={ onClickOutside }
onKeydown={ onKeydown }
{ ...scopeId }
Expand Down
8 changes: 4 additions & 4 deletions packages/vuetify/src/components/VOverlay/VOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ export const VOverlay = genericComponent<OverlaySlots>()({
},

setup (props, { slots, attrs, emit }) {
const root = ref<HTMLElement>()
const scrimEl = ref<HTMLElement>()
const contentEl = ref<HTMLElement>()
const model = useProxiedModel(props, 'modelValue')
const isActive = computed({
get: () => model.value,
Expand All @@ -153,7 +156,7 @@ export const VOverlay = genericComponent<OverlaySlots>()({
activatorEvents,
contentEvents,
scrimEvents,
} = useActivator(props, { isActive, isTop: localTop })
} = useActivator(props, { isActive, isTop: localTop, contentEl })
const { teleportTarget } = useTeleport(() => {
const target = props.attach || props.contained
if (target) return target
Expand All @@ -169,9 +172,6 @@ export const VOverlay = genericComponent<OverlaySlots>()({
if (v) isActive.value = false
})

const root = ref<HTMLElement>()
const scrimEl = ref<HTMLElement>()
const contentEl = ref<HTMLElement>()
const { contentStyles, updateLocation } = useLocationStrategies(props, {
isRtl,
contentEl,
Expand Down
8 changes: 6 additions & 2 deletions packages/vuetify/src/components/VOverlay/useActivator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,11 @@ export const makeActivatorProps = propsFactory({

export function useActivator (
props: ActivatorProps,
{ isActive, isTop }: { isActive: Ref<boolean>, isTop: Ref<boolean> }
{ isActive, isTop, contentEl }: {
isActive: Ref<boolean>
isTop: Ref<boolean>
contentEl: Ref<HTMLElement | undefined>
}
) {
const vm = getCurrentInstance('useActivator')
const activatorEl = ref<HTMLElement>()
Expand Down Expand Up @@ -215,7 +219,7 @@ export function useActivator (
if (val && (
(props.openOnHover && !isHovered && (!openOnFocus.value || !isFocused)) ||
(openOnFocus.value && !isFocused && (!props.openOnHover || !isHovered))
)) {
) && !contentEl.value?.contains(document.activeElement)) {
isActive.value = false
}
})
Expand Down

0 comments on commit 306a262

Please sign in to comment.