feat(components): [select] accessibility enhancement (#14503)

* feat: accessibility enhancement

* fix: adjusting the attributes of Scrollbar

* Update docs/en-US/component/scrollbar.md

Co-authored-by: btea <2356281422@qq.com>

* docs: updata

* fix(components): [select] aria-selected error

---------

Co-authored-by: btea <2356281422@qq.com>
This commit is contained in:
qiang
2023-10-12 09:29:00 -05:00
committed by GitHub
parent 9e08fef52c
commit ca8846c532
7 changed files with 112 additions and 19 deletions

View File

@@ -43,19 +43,23 @@ scrollbar/manual-scroll
### Attributes
| Name | Description | Type | Default |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | ------- |
| height | height of scrollbar | ^[string] / ^[number] | — |
| max-height | max height of scrollbar | ^[string] / ^[number] | — |
| native | whether to use the native scrollbar style | ^[boolean] | false |
| wrap-style | style of wrap container | ^[string] / ^[object]`CSSProperties \| CSSProperties[] \| string[]` | — |
| wrap-class | class of wrap container | ^[string] | — |
| view-style | style of view | ^[string] / ^[object]`CSSProperties \| CSSProperties[] \| string[]` | — |
| view-class | class of view | ^[string] | — |
| noresize | do not respond to container size changes, if the container size does not change, it is better to set it to optimize performance | ^[boolean] | false |
| tag | element tag of the view | ^[string] | div |
| always | always show scrollbar | ^[boolean] | false |
| min-size | minimum size of scrollbar | ^[number] | 20 |
| Name | Description | Type | Default |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | ------- |
| height | height of scrollbar | ^[string] / ^[number] | — |
| max-height | max height of scrollbar | ^[string] / ^[number] | — |
| native | whether to use the native scrollbar style | ^[boolean] | false |
| wrap-style | style of wrap container | ^[string] / ^[object]`CSSProperties \| CSSProperties[] \| string[]` | — |
| wrap-class | class of wrap container | ^[string] | — |
| view-style | style of view | ^[string] / ^[object]`CSSProperties \| CSSProperties[] \| string[]` | — |
| view-class | class of view | ^[string] | — |
| noresize | do not respond to container size changes, if the container size does not change, it is better to set it to optimize performance | ^[boolean] | false |
| tag | element tag of the view | ^[string] | div |
| always | always show scrollbar | ^[boolean] | false |
| min-size | minimum size of scrollbar | ^[number] | 20 |
| id ^(2.4.0) | id of view | ^[string] | — |
| role ^(2.4.0) ^(a11y) | role of view | ^[string] | — |
| aria-label ^(2.4.0) ^(a11y) | aria-label of view | ^[string] | — |
| aria-orientation ^(2.4.0) ^(a11y) | aria-orientation of view | ^[enum]`'horizontal' \| 'vertical'` | — |
### Events

View File

@@ -74,6 +74,25 @@ export const scrollbarProps = buildProps({
type: Number,
default: 20,
},
/**
* @description id of view
*/
id: String,
/**
* @description role of view
*/
role: String,
/**
* @description aria-label of view
*/
ariaLabel: String,
/**
* @description aria-orientation of view
*/
ariaOrientation: {
type: String,
values: ['horizontal', 'vertical'],
},
} as const)
export type ScrollbarProps = ExtractPropTypes<typeof scrollbarProps>

View File

@@ -1,11 +1,20 @@
<template>
<div ref="scrollbarRef" :class="ns.b()">
<div ref="wrapRef" :class="wrapKls" :style="style" @scroll="handleScroll">
<div
ref="wrapRef"
:class="wrapKls"
:style="wrapStyle"
@scroll="handleScroll"
>
<component
:is="tag"
:id="id"
ref="resizeRef"
:class="resizeKls"
:style="viewStyle"
:role="role"
:aria-label="ariaLabel"
:aria-orientation="ariaOrientation"
>
<slot />
</component>
@@ -67,7 +76,7 @@ const barRef = ref<BarInstance>()
const ratioY = ref(1)
const ratioX = ref(1)
const style = computed<StyleValue>(() => {
const wrapStyle = computed<StyleValue>(() => {
const style: CSSProperties = {}
if (props.height) style.height = addUnit(props.height)
if (props.maxHeight) style.maxHeight = addUnit(props.maxHeight)

View File

@@ -2435,4 +2435,39 @@ describe('Select', () => {
expect(wrapper.findAll('.el-tag').length).toBe(1)
})
})
it('It should generate accessible attributes', async () => {
wrapper = _mount(
`<el-select v-model="value">
<el-option label="label" value="1" />
<el-option label="disabled" value="2" disabled />
</el-select>`,
() => ({ value: '1' })
)
const dropdown = wrapper.findComponent({ name: 'ElSelectDropdown' })
const input = wrapper.find('input')
const list = dropdown.find('.el-select-dropdown__list')
const option = dropdown.find('.el-select-dropdown__item')
const disabledOption = dropdown.find(
'.el-select-dropdown__item:nth-child(2)'
)
expect(input.attributes('role')).toBe('combobox')
expect(input.attributes('aria-autocomplete')).toBe('none')
expect(input.attributes('aria-controls')).toBe(list.attributes('id'))
expect(input.attributes('aria-expanded')).toBe('false')
expect(input.attributes('aria-haspopup')).toBe('listbox')
expect(input.attributes('aria-activedescendant')).toBe('')
expect(list.attributes('id')).toBeTruthy()
expect(list.attributes('role')).toBe('listbox')
expect(list.attributes('aria-orientation')).toBe('vertical')
expect(option.attributes('id')).toBeTruthy()
expect(option.attributes('role')).toBe('option')
expect(option.attributes('aria-disabled')).toBe(undefined)
expect(option.attributes('aria-selected')).toBe('true')
expect(disabledOption.attributes('aria-disabled')).toBe('true')
})
})

View File

@@ -1,7 +1,11 @@
<template>
<li
v-show="visible"
:id="id"
:class="containerKls"
role="option"
:aria-disabled="isDisabled || undefined"
:aria-selected="itemSelected"
@mouseenter="hoverItem"
@click.stop="selectOptionClick"
>
@@ -23,7 +27,7 @@ import {
toRefs,
unref,
} from 'vue'
import { useNamespace } from '@element-plus/hooks'
import { useId, useNamespace } from '@element-plus/hooks'
import { useOption } from './useOption'
import type { SelectOptionProxy } from './token'
@@ -52,6 +56,7 @@ export default defineComponent({
setup(props) {
const ns = useNamespace('select')
const id = useId()
const containerKls = computed(() => [
ns.be('dropdown', 'item'),
@@ -103,6 +108,7 @@ export default defineComponent({
return {
ns,
id,
containerKls,
currentLabel,
itemSelected,

View File

@@ -149,7 +149,13 @@
:disabled="selectDisabled"
:autocomplete="autocomplete"
:style="inputStyle"
role="combobox"
:aria-activedescendant="hoverOption?.id || ''"
:aria-controls="contentId"
:aria-expanded="dropMenuVisible"
:aria-label="ariaLabel"
aria-autocomplete="none"
aria-haspopup="listbox"
@focus="handleFocus"
@blur="handleBlur"
@keyup="managePlaceholder"
@@ -166,7 +172,6 @@
@input="debouncedQueryChange"
/>
</div>
<!-- fix: https://github.com/element-plus/element-plus/issues/11415 -->
<input
v-if="isIOS && !multiple && filterable && readonly"
ref="iOSInput"
@@ -192,7 +197,13 @@
:validate-event="false"
:class="[nsSelect.is('focus', visible)]"
:tabindex="multiple && filterable ? -1 : undefined"
role="combobox"
:aria-activedescendant="hoverOption?.id || ''"
:aria-controls="contentId"
:aria-expanded="dropMenuVisible"
:label="ariaLabel"
aria-autocomplete="none"
aria-haspopup="listbox"
@focus="handleFocus"
@blur="handleBlur"
@input="debouncedOnInputChange"
@@ -240,11 +251,15 @@
<el-select-menu>
<el-scrollbar
v-show="options.size > 0 && !loading"
:id="contentId"
ref="scrollbar"
tag="ul"
:wrap-class="nsSelect.be('dropdown', 'wrap')"
:view-class="nsSelect.be('dropdown', 'list')"
:class="scrollbarKls"
role="listbox"
:aria-label="ariaLabel"
aria-orientation="vertical"
>
<el-option v-if="showNewOption" :value="query" :created="true" />
<el-options @update-options="onOptionsRendered">
@@ -283,7 +298,7 @@ import {
import { useResizeObserver } from '@vueuse/core'
import { placements } from '@popperjs/core'
import { ClickOutside } from '@element-plus/directives'
import { useLocale, useNamespace } from '@element-plus/hooks'
import { useId, useLocale, useNamespace } from '@element-plus/hooks'
import ElInput from '@element-plus/components/input'
import ElTooltip, {
useTooltipContentProps,
@@ -550,10 +565,12 @@ export default defineComponent({
const nsSelect = useNamespace('select')
const nsInput = useNamespace('input')
const { t } = useLocale()
const contentId = useId()
const states = useSelectStates(props)
const {
optionList,
optionsArray,
hoverOption,
selectSize,
readonly,
handleResize,
@@ -833,6 +850,8 @@ export default defineComponent({
showTagList,
collapseTagList,
tagTooltipRef,
contentId,
hoverOption,
}
},
})

View File

@@ -102,7 +102,7 @@ export const useSelect = (props, states: States, ctx) => {
const scrollbar = ref<{
handleScroll: () => void
} | null>(null)
const hoverOption = ref(-1)
const hoverOption = ref()
const queryChange = shallowRef<QueryChangeCtx>({ query: '' })
const groupQueryChange = shallowRef('')
const optionList = ref<string[]>([])
@@ -980,6 +980,7 @@ export const useSelect = (props, states: States, ctx) => {
return {
optionList,
optionsArray,
hoverOption,
selectSize,
handleResize,
debouncedOnInputChange,