mirror of
https://github.com/element-plus/element-plus.git
synced 2026-03-13 07:51:17 +08:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user