Skip to content

Commit

Permalink
Merge pull request #69 from tournantdev/feature/dropdown-test-improve…
Browse files Browse the repository at this point in the history
…ment

Test Dropdown Focus Management && Bugfixes
  • Loading branch information
ovlb authored Apr 15, 2020
2 parents 6d520fc + edb5f12 commit f31c90c
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 23 deletions.
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)
})
})

0 comments on commit f31c90c

Please sign in to comment.