mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-11-29 17:58:08 +08:00
feat(components): [mention] accessibility enhancement (#17848)
This commit is contained in:
parent
aca1b0ac58
commit
d59cdc9855
@ -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')
|
||||
})
|
||||
})
|
||||
|
@ -9,6 +9,8 @@ export const mentionDropdownProps = buildProps({
|
||||
},
|
||||
loading: Boolean,
|
||||
disabled: Boolean,
|
||||
contentId: String,
|
||||
ariaLabel: String,
|
||||
})
|
||||
|
||||
export const mentionDropdownEmits = {
|
||||
|
@ -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,
|
||||
|
@ -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 } =
|
||||
|
Loading…
Reference in New Issue
Block a user