fix(autocomplete): popper usage (#429)

* fix(autocomplete): dropdownWidth

* fix: el-popper usage

* test: change test file

* fix: remove storybook doc dir

* test: sleep time
This commit is contained in:
Caaalabash
2020-10-22 14:02:28 +08:00
committed by GitHub
parent 8e95db293c
commit b4c84cc00a
30 changed files with 132 additions and 1343 deletions

View File

@@ -50,19 +50,23 @@ describe('Autocomplete.vue', () => {
})
test('triggerOnFocus', async () => {
const wrapper = _mount()
const fetchSuggestions = jest.fn()
const wrapper = _mount({
debounce: 10,
fetchSuggestions,
})
await wrapper.setProps({ triggerOnFocus: false })
await wrapper.find('input').trigger('focus')
await sleep(500)
expect(wrapper.findAll('li').length).toBe(0)
await sleep(30)
expect(fetchSuggestions).toHaveBeenCalledTimes(0)
await wrapper.find('input').trigger('blur')
await wrapper.setProps({ triggerOnFocus: true })
await wrapper.find('input').trigger('focus')
await sleep(500)
expect(wrapper.findAll('li').length).toBe(4)
await sleep(30)
expect(fetchSuggestions).toHaveBeenCalledTimes(1)
})
test('popperClass', async () => {
@@ -87,19 +91,14 @@ describe('Autocomplete.vue', () => {
})
test('popperAppendToBody', async () => {
const wrapper = _mount()
await wrapper.setProps({ popperAppendToBody: true })
expect(wrapper.find('.el-popper').exists()).toBe(false)
await wrapper.setProps({ popperAppendToBody: false })
expect(wrapper.find('.el-popper').exists()).toBe(true)
_mount({ popperAppendToBody: false })
expect(document.body.querySelector('.el-popper__mask')).toBeNull()
})
test('debounce / fetchSuggestions', async () => {
const fetchSuggestions = jest.fn()
const wrapper = _mount({
debounce: 500,
debounce: 10,
fetchSuggestions,
})
@@ -110,20 +109,23 @@ describe('Autocomplete.vue', () => {
await wrapper.find('input').trigger('focus')
await wrapper.find('input').trigger('blur')
expect(fetchSuggestions).toHaveBeenCalledTimes(0)
await sleep(600)
await sleep(30)
expect(fetchSuggestions).toHaveBeenCalledTimes(1)
await wrapper.find('input').trigger('focus')
await sleep(600)
await sleep(30)
expect(fetchSuggestions).toHaveBeenCalledTimes(2)
})
test('valueKey / modelValue', async () => {
const wrapper = _mount()
const target = wrapper.findComponent({ ref: 'autocomplete' }).vm as any
await target.select({ value: 'Go', tag: 'go' })
expect(wrapper.vm.state).toBe('Go')
await wrapper.setProps({ valueKey: 'tag' })
await wrapper.find('input').trigger('focus')
await sleep(500)
await wrapper.findAll('li')[1].trigger('click')
await target.select({ value: 'Go', tag: 'go' })
expect(wrapper.vm.state).toBe('go')
})
@@ -131,36 +133,42 @@ describe('Autocomplete.vue', () => {
const wrapper = _mount({
hideLoading: false,
fetchSuggestions: NOOP,
debounce: 10,
})
await wrapper.find('input').trigger('focus')
await sleep(500)
expect(wrapper.find('.el-icon-loading').exists()).toBe(true)
await sleep(30)
expect(document.body.querySelector('.el-icon-loading')).toBeDefined()
await wrapper.setProps({ hideLoading: true })
expect(wrapper.find('.el-icon-loading').exists()).toBe(false)
expect(document.body.querySelector('.el-icon-loading')).toBeNull()
})
test('selectWhenUnmatched', async () => {
const wrapper = mount(Autocomplete, {
props: {
selectWhenUnmatched: true,
debounce: 10,
},
})
wrapper.vm.highlightedIndex = 0
wrapper.vm.handleKeyEnter()
await sleep(500)
await sleep(30)
expect(wrapper.vm.highlightedIndex).toBe(-1)
})
test('highlightFirstItem', async () => {
const wrapper = _mount({ highlightFirstItem: false })
const wrapper = _mount({
highlightFirstItem: false,
debounce: 10,
})
await wrapper.find('input').trigger('focus')
await sleep(500)
expect(wrapper.find('.highlighted').exists()).toBe(false)
await sleep(30)
expect(document.body.querySelector('.highlighted')).toBeNull()
await wrapper.setProps({ highlightFirstItem: true })
await wrapper.find('input').trigger('focus')
await sleep(500)
expect(wrapper.find('.highlighted').text()).toBe('Java')
await sleep(30)
expect(document.body.querySelector('.highlighted')).toBeDefined()
})
})

View File

@@ -1,90 +1,93 @@
<template>
<div
v-clickoutside="close"
class="el-autocomplete"
role="combobox"
aria-haspopup="listbox"
:aria-expanded="suggestionVisible"
:aria-owns="id"
<el-popper
ref="popper"
v-model:visible="suggestionVisible"
:placement="placement"
:popper-class="popperClass"
:append-to-body="popperAppendToBody"
:offset="6"
pure
manual-mode
effect="light"
trigger="click"
>
<el-input
ref="inputRef"
v-bind="$attrs"
:model-value="modelValue"
@input="handleInput"
@change="handleChange"
@focus="handleFocus"
@blur="handleBlur"
@clear="handleClear"
@keydown.up.prevent="highlight(highlightedIndex - 1)"
@keydown.down.prevent="highlight(highlightedIndex + 1)"
@keydown.enter.prevent="handleKeyEnter"
@keydown.tab.prevent="close"
>
<template v-if="$slots.prepend" #prepend>
<slot name="prepend"></slot>
</template>
<template v-if="$slots.append" #append>
<slot name="append"></slot>
</template>
<template v-if="$slots.prefix" #prefix>
<slot name="prefix"></slot>
</template>
<template v-if="$slots.suffix" #suffix>
<slot name="suffix"></slot>
</template>
</el-input>
<el-popper
class="el-autocomplete-suggestion"
:placement="placement"
:popper-class="popperClass"
:append-to-body="popperAppendToBody"
:visible="suggestionVisible"
:show-arrow="false"
pure
effect="light"
trigger="click"
>
<template #trigger>
<transition name="el-zoom-in-top">
<div
v-show="suggestionVisible"
ref="regionRef"
:class="{ 'is-loading': suggestionLoading }"
style="outline: none"
role="region"
<template #trigger>
<div
v-clickoutside="close"
class="el-autocomplete"
role="combobox"
aria-haspopup="listbox"
:aria-expanded="suggestionVisible"
:aria-owns="id"
>
<el-input
ref="inputRef"
v-bind="$attrs"
:model-value="modelValue"
@input="handleInput"
@change="handleChange"
@focus="handleFocus"
@blur="handleBlur"
@clear="handleClear"
@keydown.up.prevent="highlight(highlightedIndex - 1)"
@keydown.down.prevent="highlight(highlightedIndex + 1)"
@keydown.enter.prevent="handleKeyEnter"
@keydown.tab.prevent="close"
>
<template v-if="$slots.prepend" #prepend>
<slot name="prepend"></slot>
</template>
<template v-if="$slots.append" #append>
<slot name="append"></slot>
</template>
<template v-if="$slots.prefix" #prefix>
<slot name="prefix"></slot>
</template>
<template v-if="$slots.suffix" #suffix>
<slot name="suffix"></slot>
</template>
</el-input>
</div>
</template>
<template #default>
<transition name="el-zoom-in-top" @after-leave="doDestroy">
<div
v-show="suggestionVisible"
ref="regionRef"
:class="['el-autocomplete-suggestion', suggestionLoading && 'is-loading']"
:style="{ width: dropdownWidth, outline: 'none' }"
role="region"
>
<el-scrollbar
tag="ul"
wrap-class="el-autocomplete-suggestion__wrap"
view-class="el-autocomplete-suggestion__list"
>
<el-scrollbar
tag="ul"
wrap-class="el-autocomplete-suggestion__wrap"
view-class="el-autocomplete-suggestion__list"
>
<li v-if="suggestionLoading">
<i class="el-icon-loading"></i>
<li v-if="suggestionLoading">
<i class="el-icon-loading"></i>
</li>
<template v-else>
<li
v-for="(item, index) in suggestions"
:id="`${id}-item-${index}`"
:key="index"
:class="{'highlighted': highlightedIndex === index}"
role="option"
:aria-selected="highlightedIndex === index"
@click="select(item)"
>
<slot :item="item">{{ item[valueKey] }}</slot>
</li>
<template v-else>
<li
v-for="(item, index) in suggestions"
:id="`${id}-item-${index}`"
:key="index"
:class="{'highlighted': highlightedIndex === index}"
role="option"
:aria-selected="highlightedIndex === index"
@click="select(item)"
>
<slot :item="item">{{ item[valueKey] }}</slot>
</li>
</template>
</el-scrollbar>
</div>
</transition>
</template>
</el-popper>
</div>
</template>
</el-scrollbar>
</div>
</transition>
</template>
</el-popper>
</template>
<script lang="ts">
import { defineComponent, ref, computed, onMounted, nextTick, PropType } from 'vue'
import { defineComponent, ref, computed, onMounted, nextTick, PropType, watch } from 'vue'
import { NOOP } from '@vue/shared'
import debounce from 'lodash/debounce'
import { ClickOutside } from '@element-plus/directives'
@@ -159,11 +162,13 @@ export default defineComponent({
setup(props, ctx) {
const suggestions = ref([])
const highlightedIndex = ref(-1)
const dropdownWidth = ref('')
const activated = ref(false)
const suggestionDisabled = ref(false)
const loading = ref(false)
const inputRef = ref(null)
const regionRef = ref(null)
const popper = ref(null)
const id = computed(() => {
return `el-autocomplete-${generateId()}`
@@ -176,6 +181,10 @@ export default defineComponent({
return !props.hideLoading && loading.value
})
watch(suggestionVisible, () => {
dropdownWidth.value = `${inputRef.value.$el.offsetWidth}px`
})
onMounted(() => {
inputRef.value.inputOrTextarea.setAttribute('role', 'textbox')
inputRef.value.inputOrTextarea.setAttribute('aria-autocomplete', 'list')
@@ -288,15 +297,20 @@ export default defineComponent({
highlightedIndex.value = index
inputRef.value.inputOrTextarea.setAttribute('aria-activedescendant', `${id.value}-item-${highlightedIndex.value}`)
}
const doDestroy = () => {
popper.value.doDestroy()
}
return {
suggestions,
highlightedIndex,
dropdownWidth,
activated,
suggestionDisabled,
loading,
inputRef,
regionRef,
popper,
id,
suggestionVisible,
@@ -313,6 +327,7 @@ export default defineComponent({
focus,
select,
highlight,
doDestroy,
}
},
})