From f8ddb8e541e2fc7b3955fc36af2f67d240e1aad8 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 9 Nov 2019 16:17:01 -0400 Subject: [PATCH 1/7] fix(v-b-modal): ensure trigger element is keyboard accessible if not a link or button, for A11Y --- src/directives/modal/modal.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/directives/modal/modal.js b/src/directives/modal/modal.js index 530d93421b6..372c446e6a3 100644 --- a/src/directives/modal/modal.js +++ b/src/directives/modal/modal.js @@ -32,9 +32,16 @@ const getTriggerElement = el => { } const setRole = trigger => { - // Only set a role if the trigger element doesn't have one - if (trigger && trigger.tagName !== 'BUTTON' && !hasAttr(trigger, 'role')) { - setAttr(trigger, 'role', 'button') + // Ensure accessibility on non button elements + if (trigger && trigger.tagName !== 'BUTTON') { + // Only set a role if the trigger element doesn't have one + if (!hasAttr(trigger, 'role')) { + setAttr(trigger, 'role', 'button') + } + // Add a tabindex is not a button or link, and tabindex is not provided + if (trigger.tagName !== 'A' && !hasAttr(trigger, 'tabindex')) { + setAttr(trigger, 'tabindex', '0') + } } } From d0c90b40ed9d4128b087d282d484b69e6a816f73 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 9 Nov 2019 16:24:34 -0400 Subject: [PATCH 2/7] Update modal.spec.js --- src/directives/modal/modal.spec.js | 50 ++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/src/directives/modal/modal.spec.js b/src/directives/modal/modal.spec.js index cab39912aca..119d73103c0 100644 --- a/src/directives/modal/modal.spec.js +++ b/src/directives/modal/modal.spec.js @@ -26,6 +26,8 @@ describe('v-b-modal directive', () => { expect(wrapper.isVueInstance()).toBe(true) expect(wrapper.is('button')).toBe(true) + expect(wrapper.find('buttun').attributes('tabindex')).not.toBeDefined() + expect(wrapper.find('buttun').attributes('role')).not.toBeDefined() expect(spy).not.toHaveBeenCalled() const $button = wrapper.find('button') @@ -36,6 +38,48 @@ describe('v-b-modal directive', () => { wrapper.destroy() }) + it('works on links', async () => { + const localVue = new CreateLocalVue() + const spy = jest.fn() + + const App = localVue.extend({ + directives: { + bModal: VBModal + }, + data() { + return { + text: 'link' + } + }, + mounted() { + this.$root.$on(EVENT_SHOW, spy) + }, + beforeDestroy() { + this.$root.$off(EVENT_SHOW, spy) + }, + template: '{{ text }}' + }) + const wrapper = mount(App, { + localVue: localVue + }) + + expect(wrapper.isVueInstance()).toBe(true) + expect(wrapper.is('a')).toBe(true) + expect(spy).not.toHaveBeenCalled() + expect(wrapper.find('a').attributes('role')).toBe('button') + expect(wrapper.find('a').attributes('tabindex')).not.toBeDefined() + expect(wrapper.find('a').text()).toBe('link') + + const $link = wrapper.find('link') + $link.trigger('click') + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toBeCalledWith('test', $link.element) + expect(wrapper.find('a').attributes('role')).toBe('button') + expect(wrapper.find('a').attributes('tabindex')).not.toBeDefined() + + wrapper.destroy() + }) + it('works on non-buttons', async () => { const localVue = new CreateLocalVue() const spy = jest.fn() @@ -55,7 +99,7 @@ describe('v-b-modal directive', () => { beforeDestroy() { this.$root.$off(EVENT_SHOW, spy) }, - template: '{{ text }}' + template: '{{ text }}' }) const wrapper = mount(App, { localVue: localVue @@ -65,6 +109,7 @@ describe('v-b-modal directive', () => { expect(wrapper.is('span')).toBe(true) expect(spy).not.toHaveBeenCalled() expect(wrapper.find('span').attributes('role')).toBe('button') + expect(wrapper.find('span').attributes('tabindex')).toBe('0') expect(wrapper.find('span').text()).toBe('span') const $span = wrapper.find('span') @@ -102,7 +147,7 @@ describe('v-b-modal directive', () => { beforeDestroy() { this.$root.$off(EVENT_SHOW, spy) }, - template: '{{ text }}' + template: '{{ text }}' }) const wrapper = mount(App, { localVue: localVue @@ -112,6 +157,7 @@ describe('v-b-modal directive', () => { expect(wrapper.is('span')).toBe(true) expect(spy).not.toHaveBeenCalled() expect(wrapper.find('span').attributes('role')).toBe('button') + expect(wrapper.find('span').attributes('tabindex')).toBe('0') expect(wrapper.find('span').text()).toBe('span') const $span = wrapper.find('span') From f0a9c6ac7035bb5d1f7daf5ca5c38f8d4a9a0e99 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 9 Nov 2019 16:26:47 -0400 Subject: [PATCH 3/7] Update modal.spec.js --- src/directives/modal/modal.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/directives/modal/modal.spec.js b/src/directives/modal/modal.spec.js index 119d73103c0..efdbc742422 100644 --- a/src/directives/modal/modal.spec.js +++ b/src/directives/modal/modal.spec.js @@ -70,7 +70,7 @@ describe('v-b-modal directive', () => { expect(wrapper.find('a').attributes('tabindex')).not.toBeDefined() expect(wrapper.find('a').text()).toBe('link') - const $link = wrapper.find('link') + const $link = wrapper.find('a') $link.trigger('click') expect(spy).toHaveBeenCalledTimes(1) expect(spy).toBeCalledWith('test', $link.element) From 5b1ca92b75e397f9a9c1c158c6c34f7929aeabc1 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 9 Nov 2019 16:29:54 -0400 Subject: [PATCH 4/7] Update modal.spec.js --- src/directives/modal/modal.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/directives/modal/modal.spec.js b/src/directives/modal/modal.spec.js index efdbc742422..3cf7f622916 100644 --- a/src/directives/modal/modal.spec.js +++ b/src/directives/modal/modal.spec.js @@ -26,8 +26,8 @@ describe('v-b-modal directive', () => { expect(wrapper.isVueInstance()).toBe(true) expect(wrapper.is('button')).toBe(true) - expect(wrapper.find('buttun').attributes('tabindex')).not.toBeDefined() - expect(wrapper.find('buttun').attributes('role')).not.toBeDefined() + expect(wrapper.find('button').attributes('tabindex')).not.toBeDefined() + expect(wrapper.find('button').attributes('role')).not.toBeDefined() expect(spy).not.toHaveBeenCalled() const $button = wrapper.find('button') From 3fa4cb9841c1084eaa37e4754ed97d3703d90e8e Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 9 Nov 2019 16:43:14 -0400 Subject: [PATCH 5/7] Update README.md --- src/components/modal/README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/modal/README.md b/src/components/modal/README.md index fa14bda6564..02b3e85ded1 100644 --- a/src/components/modal/README.md +++ b/src/components/modal/README.md @@ -1020,7 +1020,7 @@ emitted. `` provides several accessibility features, including auto focus, return focus, keyboard (tab) _focus containment_, and automated `aria-*` attributes. -### ARIA attributes +### Modal ARIA attributes The `aria-labelledby` and `aria-describedby` attributes will appear on the modal automatically in most cases. @@ -1158,4 +1158,15 @@ content and can make some of your elements unreachable via keyboard navigation. In some circumstances, you may need to disable the enforce focus feature. You can do this by setting the prop `no-enforce-focus`, although this is highly discouraged. +### `v-b-modal` directive accessibility + +Notes on `v-b-modal` drective accessibility: + +- If the element is anything other than a `