Skip to content

Commit 04799b6

Browse files
authored
fix(NavigationMenu): behavior should match radix ui navigation menu w… (#2667)
* fix(NavigationMenu): behavior should match radix ui navigation menu when entering another trigger while menu is open * chore: lint * chore: add original comment back * fix(navigation-menu): clear skipNextClose before content close * test(NavigationMenu): add timing regression tests for trigger switching and content leave
1 parent 396ba29 commit 04799b6

2 files changed

Lines changed: 77 additions & 3 deletions

File tree

packages/core/src/NavigationMenu/NavigationMenu.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,11 @@ describe('given default NavigationMenu', () => {
137137

138138
const findLinkContent = () => wrapper.find('[data-dismissable-layer]')
139139

140+
async function useRealDebounceFn() {
141+
const { useDebounceFn: realUseDebounceFn } = await vi.importActual<typeof import('@vueuse/core')>('@vueuse/core')
142+
vi.mocked(useDebounceFn).mockImplementation(realUseDebounceFn)
143+
}
144+
140145
it('should open menu on click by default', async () => {
141146
const button = findTriggerButton()
142147

@@ -210,5 +215,58 @@ describe('given default NavigationMenu', () => {
210215
// Menu should be closed
211216
expect(findLinkContent().exists()).toBeFalsy()
212217
})
218+
219+
it('switching triggers keeps menu open', async () => {
220+
vi.useFakeTimers()
221+
await useRealDebounceFn()
222+
223+
const localWrapper = mount(NavigationMenu, { attachTo: document.body })
224+
const triggers = localWrapper.findAll('[data-navigation-menu-trigger]')
225+
const findContent = () => localWrapper.find('[data-dismissable-layer]')
226+
227+
await triggers[0].trigger('pointermove', { pointerType: 'mouse' })
228+
await vi.advanceTimersByTimeAsync(200)
229+
await nextTick()
230+
231+
expect(findContent().exists()).toBe(true)
232+
233+
await triggers[0].trigger('pointerleave', { pointerType: 'mouse' })
234+
await triggers[1].trigger('pointermove', { pointerType: 'mouse' })
235+
await nextTick()
236+
237+
expect(triggers[1].attributes('data-state')).toBe('open')
238+
expect(findContent().exists()).toBe(true)
239+
240+
await vi.advanceTimersByTimeAsync(150)
241+
await nextTick()
242+
243+
expect(findContent().exists()).toBe(true)
244+
245+
localWrapper.unmount()
246+
vi.useRealTimers()
247+
})
248+
249+
it('leaving content closes menu', async () => {
250+
vi.useFakeTimers()
251+
await useRealDebounceFn()
252+
253+
const localWrapper = mount(NavigationMenu, { attachTo: document.body })
254+
const findContent = () => localWrapper.find('[data-dismissable-layer]')
255+
256+
await localWrapper.find('[data-navigation-menu-trigger]').trigger('pointermove', { pointerType: 'mouse' })
257+
await vi.advanceTimersByTimeAsync(200)
258+
await nextTick()
259+
260+
expect(findContent().exists()).toBe(true)
261+
262+
await findContent().trigger('pointerleave', { pointerType: 'mouse' })
263+
await vi.advanceTimersByTimeAsync(150)
264+
await nextTick()
265+
266+
expect(findContent().exists()).toBe(false)
267+
268+
localWrapper.unmount()
269+
vi.useRealTimers()
270+
})
213271
})
214272
})

packages/core/src/NavigationMenu/NavigationMenuRoot.vue

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ const { delayDuration, skipDelayDuration, dir: propDir, disableClickTrigger, dis
140140
const dir = useDirection(propDir)
141141
142142
const isDelaySkipped = refAutoReset(false, skipDelayDuration)
143+
const skipNextClose = ref(false)
143144
const computedDelay = computed(() => {
144145
const isOpen = modelValue.value !== ''
145146
if (isOpen || isDelaySkipped.value)
@@ -150,8 +151,14 @@ const computedDelay = computed(() => {
150151
const debouncedFn = useDebounceFn((val?: string) => {
151152
// passing `undefined` meant to reset the debounce timer
152153
if (typeof val === 'string') {
154+
if (val === '' && skipNextClose.value) {
155+
skipNextClose.value = false
156+
return
157+
}
153158
previousValue.value = modelValue.value
154159
modelValue.value = val
160+
if (val === '')
161+
isDelaySkipped.value = true
155162
}
156163
}, computedDelay)
157164
@@ -188,18 +195,27 @@ provideNavigationMenuContext({
188195
viewport.value = val
189196
},
190197
onTriggerEnter: (val) => {
191-
debouncedFn(val)
198+
if (modelValue.value !== '') {
199+
skipNextClose.value = true
200+
previousValue.value = modelValue.value
201+
modelValue.value = val
202+
}
203+
else {
204+
debouncedFn(val)
205+
}
192206
},
193207
onTriggerLeave: () => {
194-
isDelaySkipped.value = true
208+
skipNextClose.value = false
195209
debouncedFn('')
196210
},
197211
onContentEnter: () => {
198212
debouncedFn()
199213
},
200214
onContentLeave: () => {
201-
if (!props.disablePointerLeaveClose)
215+
if (!props.disablePointerLeaveClose) {
216+
skipNextClose.value = false
202217
debouncedFn('')
218+
}
203219
},
204220
onItemSelect: (val) => {
205221
// When selecting item we trigger update immediately

0 commit comments

Comments
 (0)