Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion helper/isOutsidePath.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Check if the target of an event is outside of an element
*
* @param {Event} evt
* @param {Node} element
* @param {HTMLElement} element
* @returns
*/
const isOutsidePath = (evt, element) => {
Expand Down
16 changes: 8 additions & 8 deletions packages/dropdown/src/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,11 @@ export default {

this.checkForAccessibleName()

document.addEventListener('keydown', this.handleGlobalKeyDown)
document.addEventListener('focusin', this.handleGlobalFocus)
document.documentElement.addEventListener('click', this.handleGlobalClick)
},
beforeDestroy() {
document.removeEventListener('keydown', this.handleGlobalKeyDown)
document.removeEventListener('focusin', this.handleGlobalFocus)
document.documentElement.removeEventListener(
'click',
this.handleGlobalClick
Expand Down Expand Up @@ -99,10 +99,10 @@ export default {
this.open()
}
},
handleGlobalKeyDown(evt) {
if (evt.keyCode === 9 && isOutsidePath(evt, this.$el)) {
this.close(false)
}
handleGlobalFocus() {
if (this.$el.contains(document.activeElement)) return

this.close(false)
},
handleGlobalClick(evt) {
if (isOutsidePath(evt, this.$el)) {
Expand All @@ -117,7 +117,7 @@ export default {

this.$nextTick().then(() => {
this.items = Array.from(
this.$refs.menu.querySelectorAll('[role=menuitem]:not([disabled])')
this.$refs.menu.querySelectorAll('[role^="menuitem"]:not([disabled])')
)

this.items.forEach(button => {
Expand All @@ -128,7 +128,7 @@ export default {
})
},
close(setFocus = true) {
// Method will be called from the `clickaway` directive on every component instance
// Multiple instances might have added event listeners
// Limit work and ensure correct handling of focus by having an additional check for visibility
if (this.isVisible) {
this.isVisible = false
Expand Down
19 changes: 18 additions & 1 deletion packages/dropdown/tests/dropdown.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,26 @@ export const Basic = () => ({
</tournant-dropdown>`
})

export const TabAway = () => ({
components: { TournantDropdown },
template: `
<div>
<p>Above dropdown with a <a href="#">placeholder link</a>.</p>
<tournant-dropdown >
<template v-slot:button-text>
Toggle
</template>
${items}
</tournant-dropdown>
<p>Some more content underneath the item.</p>
<p>And another paragraph with <a href="#">a link</a>.
</div>
`
})

export const Positioning = () => ({
components: { TournantDropdown },
template: `<tournant-dropdown x-position="right" >
template: `<tournant-dropdown x-position="right">
<template v-slot:button-text>
Toggle
</template>
Expand Down
151 changes: 138 additions & 13 deletions packages/dropdown/tests/unit/Dropdown.spec.js
Original file line number Diff line number Diff line change
@@ -1,51 +1,120 @@
// global.console = { warn: jest.fn() }

import { shallowMount, createLocalVue } from '@vue/test-utils'

import Dropdown from '@p/dropdown/src/index.vue'

const localVue = createLocalVue()
document.body.innerHTML = `
<div>
<button id="btn1">btn 1 </button>
<button id="btn2">btn 2 </button>
<button id="btn3">btn 3 </button>
<a href="#" id="test-link">test</a>
</div>
`

localVue.directive('clickaway', {})
const localVue = createLocalVue()

describe('Dropdown', () => {
let wrapper
let button
let menu

beforeEach(() => {
wrapper = shallowMount(Dropdown, {
slots: {
'button-text': 'Options',
items:
'<button role="menuitem" tabindex="-1">Rename</button> <button role="menuitem" tabindex="-1">Delete</button>'
items: `<button role="menuitem" tabindex="-1">Rename</button>
<button role="menuitem" tabindex="-1">Delete</button>
`
},
localVue
localVue,
attachToDocument: true
})

button = wrapper.find('button')
})

describe('Events', () => {
it('@click - open and close menu', () => {
let firstMenuItem

it('@click - open and close menu', async () => {
button.trigger('click')
await wrapper.vm.$nextTick()
expect(wrapper.vm.$refs.menu).toBeDefined()

button.trigger('click')

await wrapper.vm.$nextTick()
expect(wrapper.vm.isVisible).toBeFalsy()
})

it('@keydown.down - open menu', () => {
it('@keydown.down - open menu', async () => {
button.trigger('keydown.down')

await wrapper.vm.$nextTick()

expect(wrapper.vm.$refs.menu).toBeDefined()
})

it('@keydown.down > @keydown.up - opens and closes the menu', async () => {
await button.trigger('keydown.down')
await wrapper.vm.$nextTick()
expect(wrapper.vm.$refs.menu).toBeDefined()

button.trigger('keydown.up')
await wrapper.vm.$nextTick()
expect(wrapper.vm.$refs.menu).toBeUndefined()
})

it('@keydown.down - focuses first menu item', async () => {
button.trigger('keydown.down')

await wrapper.vm.$nextTick()

firstMenuItem = wrapper.findAll('[role="menuitem"]').at(0)

expect(firstMenuItem.element).toBe(document.activeElement)
})

it('@keydown.down - loops through items', async () => {
// open
button.trigger('keydown.down')

const { length } = wrapper.findAll('[role="menuitem"]')

for (let i = 0; i < length; i++) {
button.trigger('keydown.down')
}

firstMenuItem = wrapper.findAll('[role="menuitem"]').at(0)

await wrapper.vm.$nextTick()

expect(firstMenuItem.element).toBe(document.activeElement)
})

it('@keydown.up - focusses last item if at the beginning', async () => {
// open
button.trigger('keydown.down')

await wrapper.vm.$nextTick()

menu = wrapper.find('[role="menu"]')
menu.trigger('keydown.up')

const { length } = wrapper.findAll('[role="menuitem"]')
const lastItem = wrapper.findAll('[role="menuitem"]').at(length - 1)

expect(lastItem.element).toBe(document.activeElement)
})

it('closes the menu if click happens outside of it', async () => {
button.trigger('keydown.down')

await wrapper.vm.$nextTick()

document.getElementById('test-link').click()

await wrapper.vm.$nextTick()

expect(wrapper.vm.$refs.menu).toBeUndefined()
})
})
Expand All @@ -59,13 +128,13 @@ describe('Dropdown', () => {
expect(button.attributes('aria-haspopup')).toBeTruthy()
})

it('changes its `aria-expanded` attribute', () => {
it('changes its `aria-expanded` attribute', async () => {
button.trigger('click')

await wrapper.vm.$nextTick()
expect(button.attributes('aria-expanded')).toBe('true')

button.trigger('click')

await wrapper.vm.$nextTick()
expect(button.attributes('aria-expanded')).toBe('false')
})
})
Expand Down Expand Up @@ -140,3 +209,59 @@ describe('Dropdown', () => {
})
})
})

describe('Dropdown – menuitemcheckbox', () => {
let wrapper
let button

beforeEach(() => {
wrapper = shallowMount(Dropdown, {
slots: {
'button-text': 'Options',
items: `<button role="menuitemcheckbox" tabindex="-1">Rename</button>
<button role="menuitemcheckbox" tabindex="-1">Delete</button>
`
},
localVue,
attachToDocument: true
})

button = wrapper.find('button')
})

it('detects `menuitemradio` button', async () => {
button.trigger('click')

await wrapper.vm.$nextTick()

expect(wrapper.vm.items).toHaveLength(2)
})
})

describe('Dropdown – menuitemradio', () => {
let wrapper
let button

beforeEach(() => {
wrapper = shallowMount(Dropdown, {
slots: {
'button-text': 'Options',
items: `<button role="menuitemradio" tabindex="-1">Rename</button>
<button role="menuitemradio" tabindex="-1">Delete</button>
`
},
localVue,
attachToDocument: true
})

button = wrapper.find('button')
})

it('detects `menuitemradio` button', async () => {
button.trigger('click')

await wrapper.vm.$nextTick()

expect(wrapper.vm.items).toHaveLength(2)
})
})