feat(components): [mention] accessibility enhancement (#17848)

This commit is contained in:
qiang 2024-08-13 17:59:57 +08:00 committed by GitHub
parent aca1b0ac58
commit d59cdc9855
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 66 additions and 2 deletions

View File

@ -86,4 +86,35 @@ describe('Mention.vue', () => {
4
)
})
test('It should generate accessible attributes', async () => {
const wrapper = mount(Mention, {
attachTo: document.body,
props: { options },
})
const input = wrapper.find('input')
expect(input.attributes('role')).toBe(undefined)
expect(input.attributes('aria-autocomplete')).toBe(undefined)
expect(input.attributes('aria-controls')).toBe(undefined)
expect(input.attributes('aria-expanded')).toBe(undefined)
expect(input.attributes('aria-haspopup')).toBe(undefined)
expect(input.attributes('aria-activedescendant')).toBe(undefined)
wrapper.find('input').trigger('focus')
input.element.value = '@'
wrapper.find('input').trigger('input')
await sleep(150)
const dropdown = wrapper.findComponent({ name: 'ElMentionDropdown' })
const list = dropdown.find('.el-mention-dropdown__list')
const option = dropdown.find('.el-mention-dropdown__item')
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')
})
})

View File

@ -9,6 +9,8 @@ export const mentionDropdownProps = buildProps({
},
loading: Boolean,
disabled: Boolean,
contentId: String,
ariaLabel: String,
})
export const mentionDropdownEmits = {

View File

@ -5,16 +5,24 @@
</div>
<el-scrollbar
v-show="options.length > 0 && !loading"
:id="contentId"
ref="scrollbarRef"
tag="ul"
:wrap-class="ns.be('dropdown', 'wrap')"
:view-class="ns.be('dropdown', 'list')"
role="listbox"
:aria-label="ariaLabel"
aria-orientation="vertical"
>
<li
v-for="(item, index) in options"
:id="`${contentId}-${index}`"
ref="optionRefs"
:key="item.value"
:class="optionkls(item, index)"
role="option"
:aria-disabled="item.disabled || disabled || undefined"
:aria-selected="hoveringIndex === index"
@mouseenter="handleMouseEnter(index)"
@click.stop="handleSelect(item)"
>
@ -134,6 +142,7 @@ watch(() => props.options, resetHoveringIndex, {
})
defineExpose({
hoveringIndex,
navigateOptions,
selectHoverOption,
hoverOption,

View File

@ -4,6 +4,13 @@
v-bind="mergeProps(passInputProps, $attrs)"
ref="elInputRef"
:model-value="modelValue"
:role="dropdownVisible ? 'combobox' : undefined"
:aria-activedescendant="dropdownVisible ? hoveringId || '' : undefined"
:aria-controls="dropdownVisible ? contentId : undefined"
:aria-expanded="dropdownVisible || undefined"
:aria-label="ariaLabel"
:aria-autocomplete="dropdownVisible ? 'none' : undefined"
:aria-haspopup="dropdownVisible ? 'listbox' : undefined"
@input="handleInputChange"
@keydown="handleInputKeyDown"
@mousedown="handleInputMouseDown"
@ -14,7 +21,7 @@
</el-input>
<el-tooltip
ref="tooltipRef"
:visible="visible && (!!filteredOptions.length || loading)"
:visible="dropdownVisible"
:popper-class="[ns.e('popper'), popperClass]"
:popper-options="popperOptions"
:placement="computedPlacement"
@ -33,6 +40,8 @@
:options="filteredOptions"
:disabled="disabled"
:loading="loading"
:content-id="contentId"
:aria-label="ariaLabel"
@select="handleSelect"
@click.stop="elInputRef?.focus"
>
@ -48,7 +57,7 @@
<script lang="ts" setup>
import { computed, mergeProps, nextTick, ref } from 'vue'
import { pick } from 'lodash-unified'
import { useFocusController, useNamespace } from '@element-plus/hooks'
import { useFocusController, useId, useNamespace } from '@element-plus/hooks'
import ElInput, { inputProps } from '@element-plus/components/input'
import ElTooltip from '@element-plus/components/tooltip'
import { UPDATE_MODEL_EVENT } from '@element-plus/constants'
@ -73,6 +82,7 @@ const emit = defineEmits(mentionEmits)
const passInputProps = computed(() => pick(props, Object.keys(inputProps)))
const ns = useNamespace('mention')
const contentId = useId()
const elInputRef = ref<InputInstance>()
const tooltipRef = ref<TooltipInstance>()
@ -98,6 +108,14 @@ const filteredOptions = computed(() => {
)
})
const dropdownVisible = computed(() => {
return visible.value && (!!filteredOptions.value.length || props.loading)
})
const hoveringId = computed(() => {
return `${contentId.value}-${dropdownRef.value?.hoveringIndex}`
})
const handleInputChange = (value: string) => {
emit('update:modelValue', value)
syncAfterCursorMove()
@ -121,6 +139,10 @@ const handleInputKeyDown = (e: KeyboardEvent | Event) => {
} else {
visible.value = false
}
} else if (['Escape'].includes(e.key)) {
if (!visible.value) return
e.preventDefault()
visible.value = false
} else if (['Backspace'].includes(e.key)) {
if (props.whole && mentionCtx.value) {
const { splitIndex, selectionEnd, pattern, prefixIndex, prefix } =