From af732b124bdb5c57e45688c07fcb8cf72e44a15a Mon Sep 17 00:00:00 2001
From: opengraphica <57385187+opengraphica@users.noreply.github.com>
Date: Sun, 29 May 2022 23:51:25 -0400
Subject: [PATCH] feat(components): [dropdown] a11y (#7946)
* feat(components): [dropdown] always use button trigger
* feat(components): [dropdown] a11y unit tests
* feat(components): [dropdown] pr comments
---
docs/en-US/component/dropdown.md | 31 ++--
.../dropdown/__tests__/dropdown.test.ts | 155 +++++++++++++++++-
.../dropdown/src/dropdown-item-impl.vue | 18 +-
.../components/dropdown/src/dropdown-item.vue | 2 +-
.../components/dropdown/src/dropdown-menu.vue | 10 +-
packages/components/dropdown/src/dropdown.ts | 6 +
packages/components/dropdown/src/dropdown.vue | 46 +++++-
packages/components/dropdown/src/tokens.ts | 4 +-
packages/components/popper/src/content.ts | 4 +
packages/components/popper/src/content.vue | 56 ++++++-
packages/components/popper/src/trigger.vue | 41 ++++-
packages/components/tooltip/src/content.vue | 1 +
packages/components/tooltip/src/tooltip.ts | 7 +-
packages/components/tooltip/src/tooltip.vue | 26 +--
packages/components/tooltip/src/trigger.vue | 4 +-
packages/hooks/use-model-toggle/index.ts | 29 ++--
packages/locale/lang/en.ts | 3 +
17 files changed, 381 insertions(+), 62 deletions(-)
diff --git a/docs/en-US/component/dropdown.md b/docs/en-US/component/dropdown.md
index 464f568cc6..77a4feaca9 100644
--- a/docs/en-US/component/dropdown.md
+++ b/docs/en-US/component/dropdown.md
@@ -79,21 +79,22 @@ dropdown/sizes
## Dropdown Attributes
-| Attribute | Description | Type | Accepted Values | Default |
-| -------------- | ---------------------------------------------------------------------------------------------------- | --------------- | ------------------------------------------------------------------ | ------------------------------------------------------- |
-| type | menu button type, refer to `Button` Component, only works when `split-button` is true | string | — | — |
-| size | menu size, also works on the split button | string | large / default / small | default |
-| max-height | the max height of menu | string / number | — | — |
-| split-button | whether a button group is displayed | boolean | — | false |
-| disabled | Whether to disable | boolean | — | false |
-| placement | placement of pop menu | string | top/top-start/top-end/bottom/bottom-start/bottom-end | bottom |
-| trigger | how to trigger | string | hover/click/contextmenu | hover |
-| hide-on-click | whether to hide menu after clicking menu-item | boolean | — | true |
-| show-timeout | Delay time before show a dropdown (only works when trigger is `hover`) | number | — | 250 |
-| hide-timeout | Delay time before hide a dropdown (only works when trigger is `hover`) | number | — | 150 |
-| tabindex | [tabindex](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) of Dropdown | number | — | 0 |
-| popper-class | custom class name for Dropdown's dropdown | string | — | — |
-| popper-options | [popper.js](https://popper.js.org/documentation.html) parameters | Object | refer to [popper.js](https://popper.js.org/documentation.html) doc | `{ boundariesElement: 'body', gpuAcceleration: false }` |
+| Attribute | Description | Type | Accepted Values | Default |
+| -------------- | --------------------------------------------------------------------------------------------------------------------- | --------------- | ------------------------------------------------------------------ | ------------------------------------------------------- |
+| type | menu button type, refer to `Button` Component, only works when `split-button` is true | string | — | — |
+| size | menu size, also works on the split button | string | large / default / small | default |
+| max-height | the max height of menu | string / number | — | — |
+| split-button | whether a button group is displayed | boolean | — | false |
+| disabled | Whether to disable | boolean | — | false |
+| placement | placement of pop menu | string | top/top-start/top-end/bottom/bottom-start/bottom-end | bottom |
+| trigger | how to trigger | string | hover/click/contextmenu | hover |
+| hide-on-click | whether to hide menu after clicking menu-item | boolean | — | true |
+| show-timeout | Delay time before show a dropdown (only works when trigger is `hover`) | number | — | 250 |
+| hide-timeout | Delay time before hide a dropdown (only works when trigger is `hover`) | number | — | 150 |
+| role | The ARIA role attribute for the dropdown menu. Depending on the use case, you may want to change this to 'navigation' | string | — | 'menu' |
+| tabindex | [tabindex](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) of Dropdown | number | — | 0 |
+| popper-class | custom class name for Dropdown's dropdown | string | — | — |
+| popper-options | [popper.js](https://popper.js.org/documentation.html) parameters | Object | refer to [popper.js](https://popper.js.org/documentation.html) doc | `{ boundariesElement: 'body', gpuAcceleration: false }` |
## Dropdown Slots
diff --git a/packages/components/dropdown/__tests__/dropdown.test.ts b/packages/components/dropdown/__tests__/dropdown.test.ts
index 32e5e7948d..c0097b9cb2 100644
--- a/packages/components/dropdown/__tests__/dropdown.test.ts
+++ b/packages/components/dropdown/__tests__/dropdown.test.ts
@@ -4,6 +4,7 @@ import { describe, expect, test, vi } from 'vitest'
import { rAF } from '@element-plus/test-utils/tick'
import { EVENT_CODE } from '@element-plus/constants'
import { ElTooltip } from '@element-plus/components/tooltip'
+import Button from '@element-plus/components/button'
import Dropdown from '../src/dropdown.vue'
import DropdownItem from '../src/dropdown-item.vue'
import DropdownMenu from '../src/dropdown-menu.vue'
@@ -15,6 +16,7 @@ const CONTEXTMENU = 'contextmenu'
const _mount = (template: string, data, otherObj?) =>
mount({
components: {
+ [Button.name]: Button,
[Dropdown.name]: Dropdown,
[DropdownItem.name]: DropdownItem,
[DropdownMenu.name]: DropdownMenu,
@@ -352,7 +354,7 @@ describe('Dropdown', () => {
dropdown
-
+
Apple
Orange
Cherry
@@ -365,7 +367,7 @@ describe('Dropdown', () => {
() => ({})
)
await nextTick()
- const content = wrapper.findComponent({ ref: 'a' })
+ const content = wrapper.findComponent({ ref: 'dropdown-menu' })
const triggerElm = wrapper.find('.el-tooltip__trigger')
await triggerElm.trigger(MOUSE_ENTER_EVENT)
await rAF()
@@ -482,6 +484,9 @@ describe('Dropdown', () => {
const wrapper = _mount(
`
+
+ Custom Attributes
+
Item
@@ -604,4 +609,150 @@ describe('Dropdown', () => {
expect(tooltipElement.vm.showAfter).toBe(0)
expect(tooltipElement.vm.hideAfter).toBe(0)
})
+
+ describe('accessibility', () => {
+ test('Custom span trigger has proper attributes', async () => {
+ const wrapper = _mount(
+ `
+
+
+ Dropdown List
+
+
+
+ Item
+
+
+
+ `,
+ () => ({})
+ )
+ await nextTick()
+ const trigger = wrapper.find('[data-test-ref="trigger"]')
+ const menu = wrapper.findComponent({ ref: 'menu' })
+ expect(trigger.attributes()['role']).toBe('button')
+ expect(trigger.attributes()['tabindex']).toBe('0')
+ expect(trigger.attributes()['aria-haspopup']).toBe('menu')
+ expect(trigger.attributes()['id']).toBe(
+ menu.attributes()['aria-labelledby']
+ )
+ expect(trigger.attributes()['aria-controls']).toBe(
+ menu.attributes()['id']
+ )
+ })
+
+ test('ElButton trigger has proper attributes', async () => {
+ const wrapper = _mount(
+ `
+
+
+ Dropdown List
+
+
+
+ Item
+
+
+
+ `,
+ () => ({})
+ )
+ await nextTick()
+ const trigger = wrapper.findComponent({ ref: 'trigger' })
+ const menu = wrapper.findComponent({ ref: 'menu' })
+ expect(trigger.attributes()['role']).toBe('button')
+ expect(trigger.attributes()['tabindex']).toBe('0')
+ expect(trigger.attributes()['aria-haspopup']).toBe('menu')
+ expect(trigger.attributes()['id']).toBe(
+ menu.attributes()['aria-labelledby']
+ )
+ expect(trigger.attributes()['aria-controls']).toBe(
+ menu.attributes()['id']
+ )
+ })
+
+ test('Split button trigger has proper attributes', async () => {
+ const wrapper = _mount(
+ `
+
+
+
+ Item
+
+
+
+ `,
+ () => ({})
+ )
+ await nextTick()
+ const trigger = wrapper.find('.el-dropdown__caret-button')
+ const menu = wrapper.findComponent({ ref: 'menu' })
+ expect(trigger.attributes()['role']).toBe('button')
+ expect(trigger.attributes()['tabindex']).toBe('0')
+ expect(trigger.attributes()['aria-haspopup']).toBe('menu')
+ expect(trigger.attributes()['id']).toBe(
+ menu.attributes()['aria-labelledby']
+ )
+ expect(trigger.attributes()['aria-controls']).toBe(
+ menu.attributes()['id']
+ )
+ })
+
+ test('Menu items with "menu" role', async () => {
+ const wrapper = _mount(
+ `
+
+
+
+ Item
+
+
+
+ `,
+ () => ({})
+ )
+ const menu = wrapper.findComponent({ ref: 'menu' })
+ const menuItem = menu.find('.el-dropdown-menu__item')
+ expect(menu.attributes()['role']).toBe('menu')
+ expect(menuItem.attributes()['role']).toBe('menuitem')
+ })
+
+ test('Menu items with "navigation" role', async () => {
+ const wrapper = _mount(
+ `
+
+
+
+ Item
+
+
+
+ `,
+ () => ({})
+ )
+ const menu = wrapper.findComponent({ ref: 'menu' })
+ const menuItem = menu.find('.el-dropdown-menu__item')
+ expect(menu.attributes()['role']).toBe('navigation')
+ expect(menuItem.attributes()['role']).toBe('link')
+ })
+
+ test('Menu items with "group" role', async () => {
+ const wrapper = _mount(
+ `
+
+
+
+ Item
+
+
+
+ `,
+ () => ({})
+ )
+ const menu = wrapper.findComponent({ ref: 'menu' })
+ const menuItem = menu.find('.el-dropdown-menu__item')
+ expect(menu.attributes()['role']).toBe('group')
+ expect(menuItem.attributes()['role']).toBe('button')
+ })
+ })
})
diff --git a/packages/components/dropdown/src/dropdown-item-impl.vue b/packages/components/dropdown/src/dropdown-item-impl.vue
index f63bd78ad5..9b6bff3313 100644
--- a/packages/components/dropdown/src/dropdown-item-impl.vue
+++ b/packages/components/dropdown/src/dropdown-item-impl.vue
@@ -1,6 +1,7 @@
@@ -10,7 +11,7 @@
:aria-disabled="disabled"
:class="[ns.be('menu', 'item'), ns.is('disabled', disabled)]"
:tabindex="tabIndex"
- role="menuitem"
+ :role="role"
@click="(e) => $emit('clickimpl', e)"
@focus="handleFocus"
@keydown="handleKeydown"
@@ -24,7 +25,7 @@