mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-12-02 03:08:21 +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
|
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,
|
loading: Boolean,
|
||||||
disabled: Boolean,
|
disabled: Boolean,
|
||||||
|
contentId: String,
|
||||||
|
ariaLabel: String,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const mentionDropdownEmits = {
|
export const mentionDropdownEmits = {
|
||||||
|
@ -5,16 +5,24 @@
|
|||||||
</div>
|
</div>
|
||||||
<el-scrollbar
|
<el-scrollbar
|
||||||
v-show="options.length > 0 && !loading"
|
v-show="options.length > 0 && !loading"
|
||||||
|
:id="contentId"
|
||||||
ref="scrollbarRef"
|
ref="scrollbarRef"
|
||||||
tag="ul"
|
tag="ul"
|
||||||
:wrap-class="ns.be('dropdown', 'wrap')"
|
:wrap-class="ns.be('dropdown', 'wrap')"
|
||||||
:view-class="ns.be('dropdown', 'list')"
|
:view-class="ns.be('dropdown', 'list')"
|
||||||
|
role="listbox"
|
||||||
|
:aria-label="ariaLabel"
|
||||||
|
aria-orientation="vertical"
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
v-for="(item, index) in options"
|
v-for="(item, index) in options"
|
||||||
|
:id="`${contentId}-${index}`"
|
||||||
ref="optionRefs"
|
ref="optionRefs"
|
||||||
:key="item.value"
|
:key="item.value"
|
||||||
:class="optionkls(item, index)"
|
:class="optionkls(item, index)"
|
||||||
|
role="option"
|
||||||
|
:aria-disabled="item.disabled || disabled || undefined"
|
||||||
|
:aria-selected="hoveringIndex === index"
|
||||||
@mouseenter="handleMouseEnter(index)"
|
@mouseenter="handleMouseEnter(index)"
|
||||||
@click.stop="handleSelect(item)"
|
@click.stop="handleSelect(item)"
|
||||||
>
|
>
|
||||||
@ -134,6 +142,7 @@ watch(() => props.options, resetHoveringIndex, {
|
|||||||
})
|
})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
|
hoveringIndex,
|
||||||
navigateOptions,
|
navigateOptions,
|
||||||
selectHoverOption,
|
selectHoverOption,
|
||||||
hoverOption,
|
hoverOption,
|
||||||
|
@ -4,6 +4,13 @@
|
|||||||
v-bind="mergeProps(passInputProps, $attrs)"
|
v-bind="mergeProps(passInputProps, $attrs)"
|
||||||
ref="elInputRef"
|
ref="elInputRef"
|
||||||
:model-value="modelValue"
|
: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"
|
@input="handleInputChange"
|
||||||
@keydown="handleInputKeyDown"
|
@keydown="handleInputKeyDown"
|
||||||
@mousedown="handleInputMouseDown"
|
@mousedown="handleInputMouseDown"
|
||||||
@ -14,7 +21,7 @@
|
|||||||
</el-input>
|
</el-input>
|
||||||
<el-tooltip
|
<el-tooltip
|
||||||
ref="tooltipRef"
|
ref="tooltipRef"
|
||||||
:visible="visible && (!!filteredOptions.length || loading)"
|
:visible="dropdownVisible"
|
||||||
:popper-class="[ns.e('popper'), popperClass]"
|
:popper-class="[ns.e('popper'), popperClass]"
|
||||||
:popper-options="popperOptions"
|
:popper-options="popperOptions"
|
||||||
:placement="computedPlacement"
|
:placement="computedPlacement"
|
||||||
@ -33,6 +40,8 @@
|
|||||||
:options="filteredOptions"
|
:options="filteredOptions"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
|
:content-id="contentId"
|
||||||
|
:aria-label="ariaLabel"
|
||||||
@select="handleSelect"
|
@select="handleSelect"
|
||||||
@click.stop="elInputRef?.focus"
|
@click.stop="elInputRef?.focus"
|
||||||
>
|
>
|
||||||
@ -48,7 +57,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, mergeProps, nextTick, ref } from 'vue'
|
import { computed, mergeProps, nextTick, ref } from 'vue'
|
||||||
import { pick } from 'lodash-unified'
|
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 ElInput, { inputProps } from '@element-plus/components/input'
|
||||||
import ElTooltip from '@element-plus/components/tooltip'
|
import ElTooltip from '@element-plus/components/tooltip'
|
||||||
import { UPDATE_MODEL_EVENT } from '@element-plus/constants'
|
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 passInputProps = computed(() => pick(props, Object.keys(inputProps)))
|
||||||
|
|
||||||
const ns = useNamespace('mention')
|
const ns = useNamespace('mention')
|
||||||
|
const contentId = useId()
|
||||||
|
|
||||||
const elInputRef = ref<InputInstance>()
|
const elInputRef = ref<InputInstance>()
|
||||||
const tooltipRef = ref<TooltipInstance>()
|
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) => {
|
const handleInputChange = (value: string) => {
|
||||||
emit('update:modelValue', value)
|
emit('update:modelValue', value)
|
||||||
syncAfterCursorMove()
|
syncAfterCursorMove()
|
||||||
@ -121,6 +139,10 @@ const handleInputKeyDown = (e: KeyboardEvent | Event) => {
|
|||||||
} else {
|
} else {
|
||||||
visible.value = false
|
visible.value = false
|
||||||
}
|
}
|
||||||
|
} else if (['Escape'].includes(e.key)) {
|
||||||
|
if (!visible.value) return
|
||||||
|
e.preventDefault()
|
||||||
|
visible.value = false
|
||||||
} else if (['Backspace'].includes(e.key)) {
|
} else if (['Backspace'].includes(e.key)) {
|
||||||
if (props.whole && mentionCtx.value) {
|
if (props.whole && mentionCtx.value) {
|
||||||
const { splitIndex, selectionEnd, pattern, prefixIndex, prefix } =
|
const { splitIndex, selectionEnd, pattern, prefixIndex, prefix } =
|
||||||
|
Loading…
Reference in New Issue
Block a user