diff --git a/helper/isOutsidePath.js b/helper/isOutsidePath.js
index 88bbf48..98757f3 100644
--- a/helper/isOutsidePath.js
+++ b/helper/isOutsidePath.js
@@ -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) => {
diff --git a/packages/dropdown/src/index.vue b/packages/dropdown/src/index.vue
index 4d528af..5bf461b 100644
--- a/packages/dropdown/src/index.vue
+++ b/packages/dropdown/src/index.vue
@@ -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
@@ -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)) {
@@ -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 => {
@@ -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
diff --git a/packages/dropdown/tests/dropdown.stories.js b/packages/dropdown/tests/dropdown.stories.js
index 61b446a..b6167b3 100644
--- a/packages/dropdown/tests/dropdown.stories.js
+++ b/packages/dropdown/tests/dropdown.stories.js
@@ -18,9 +18,26 @@ export const Basic = () => ({
`
})
+export const TabAway = () => ({
+ components: { TournantDropdown },
+ template: `
+
+
Above dropdown with a placeholder link.
+
+
+ Toggle
+
+ ${items}
+
+
Some more content underneath the item.
+
And another paragraph with a link.
+
+ `
+})
+
export const Positioning = () => ({
components: { TournantDropdown },
- template: `
+ template: `
Toggle
diff --git a/packages/dropdown/tests/unit/Dropdown.spec.js b/packages/dropdown/tests/unit/Dropdown.spec.js
index 5b204ca..a8cb8d3 100644
--- a/packages/dropdown/tests/unit/Dropdown.spec.js
+++ b/packages/dropdown/tests/unit/Dropdown.spec.js
@@ -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 = `
+
+
+
+
+
test
+
+`
-localVue.directive('clickaway', {})
+const localVue = createLocalVue()
describe('Dropdown', () => {
let wrapper
let button
+ let menu
beforeEach(() => {
wrapper = shallowMount(Dropdown, {
slots: {
'button-text': 'Options',
- items:
- ' '
+ items: `
+
+ `
},
- 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()
})
})
@@ -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')
})
})
@@ -140,3 +209,59 @@ describe('Dropdown', () => {
})
})
})
+
+describe('Dropdown – menuitemcheckbox', () => {
+ let wrapper
+ let button
+
+ beforeEach(() => {
+ wrapper = shallowMount(Dropdown, {
+ slots: {
+ 'button-text': 'Options',
+ items: `
+
+ `
+ },
+ 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: `
+
+ `
+ },
+ 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)
+ })
+})