refactor(components): [autocomplete] refactor autocomplete (#6067)

Co-authored-by: 三咲智子 <sxzz@sxzz.moe>
This commit is contained in:
bqy 2022-03-01 11:48:57 +08:00 committed by GitHub
parent 5e0bdf758d
commit cde87c5590
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 456 additions and 447 deletions

View File

@ -1,8 +1,8 @@
import { nextTick } from 'vue'
import { nextTick, reactive } from 'vue'
import { mount } from '@vue/test-utils'
import { NOOP } from '@vue/shared'
import { POPPER_CONTAINER_SELECTOR } from '@element-plus/hooks'
import Autocomplete from '../src/index.vue'
import Autocomplete from '../src/autocomplete.vue'
jest.unmock('lodash')
@ -10,12 +10,9 @@ jest.useFakeTimers()
const _mount = (payload = {}) =>
mount({
components: {
'el-autocomplete': Autocomplete,
},
data() {
return {
state: '',
setup() {
const state = reactive({
value: '',
list: [
{ value: 'Java', tag: 'java' },
{ value: 'Go', tag: 'go' },
@ -23,27 +20,30 @@ const _mount = (payload = {}) =>
{ value: 'Python', tag: 'python' },
],
payload,
}
},
methods: {
querySearch(queryString, cb) {
})
const querySearch = (
queryString: string,
cb: (arg: typeof state.list) => void
) => {
cb(
queryString
? this.list.filter(
? state.list.filter(
(i) => i.value.indexOf(queryString.toLowerCase()) === 0
)
: this.list
: state.list
)
},
}
return () => (
<Autocomplete
ref="autocomplete"
v-model={state.value}
fetch-suggestions={querySearch}
{...state.payload}
/>
)
},
template: `
<el-autocomplete
ref="autocomplete"
v-model="state"
:fetch-suggestions="querySearch"
v-bind="payload"
/>
`,
})
describe('Autocomplete.vue', () => {
@ -91,20 +91,20 @@ describe('Autocomplete.vue', () => {
await wrapper.setProps({ popperClass: 'error' })
expect(
document.body.querySelector('.el-popper').classList.contains('error')
document.body.querySelector('.el-popper')?.classList.contains('error')
).toBe(true)
await wrapper.setProps({ popperClass: 'success' })
expect(
document.body.querySelector('.el-popper').classList.contains('error')
document.body.querySelector('.el-popper')?.classList.contains('error')
).toBe(false)
expect(
document.body.querySelector('.el-popper').classList.contains('success')
document.body.querySelector('.el-popper')?.classList.contains('success')
).toBe(true)
})
test('popperAppendToBody', async () => {
_mount({ popperAppendToBody: false })
test('teleported', async () => {
_mount({ teleported: false })
expect(document.body.querySelector('.el-popper__mask')).toBeNull()
})
@ -137,16 +137,19 @@ describe('Autocomplete.vue', () => {
test('valueKey / modelValue', async () => {
const wrapper = _mount()
await nextTick()
const target = wrapper.findComponent({ ref: 'autocomplete' })
.vm as InstanceType<typeof Autocomplete>
await target.select({ value: 'Go', tag: 'go' })
expect(wrapper.vm.state).toBe('Go')
const target = wrapper.getComponent(Autocomplete).vm as InstanceType<
typeof Autocomplete
>
await target.handleSelect({ value: 'Go', tag: 'go' })
expect(target.modelValue).toBe('Go')
await wrapper.setProps({ valueKey: 'tag' })
await target.select({ value: 'Go', tag: 'go' })
expect(wrapper.vm.state).toBe('go')
await target.handleSelect({ value: 'Go', tag: 'go' })
expect(target.modelValue).toBe('go')
})
test('hideLoading', async () => {
@ -166,20 +169,21 @@ describe('Autocomplete.vue', () => {
})
test('selectWhenUnmatched', async () => {
const wrapper = mount(Autocomplete, {
props: {
selectWhenUnmatched: true,
debounce: 10,
},
const wrapper = _mount({
selectWhenUnmatched: true,
debounce: 10,
})
await nextTick()
const target = wrapper.getComponent(Autocomplete).vm as InstanceType<
typeof Autocomplete
>
wrapper.vm.highlightedIndex = 0
wrapper.vm.handleKeyEnter()
target.highlightedIndex = 0
target.handleKeyEnter()
jest.runAllTimers()
await nextTick()
expect(wrapper.vm.highlightedIndex).toBe(-1)
expect(target.highlightedIndex).toBe(-1)
})
test('highlightFirstItem', async () => {
@ -211,7 +215,7 @@ describe('Autocomplete.vue', () => {
await nextTick()
expect(
document.body.querySelector(POPPER_CONTAINER_SELECTOR).innerHTML
document.body.querySelector(POPPER_CONTAINER_SELECTOR)?.innerHTML
).not.toBe('')
})
@ -223,7 +227,7 @@ describe('Autocomplete.vue', () => {
await nextTick()
expect(
document.body.querySelector(POPPER_CONTAINER_SELECTOR).innerHTML
document.body.querySelector(POPPER_CONTAINER_SELECTOR)?.innerHTML
).toBe('')
})
})

View File

@ -1,12 +1,8 @@
import Autocomplete from './src/index.vue'
import type { App } from 'vue'
import type { SFCWithInstall } from '@element-plus/utils'
import { withInstall } from '@element-plus/utils'
import Autocomplete from './src/autocomplete.vue'
Autocomplete.install = (app: App): void => {
app.component(Autocomplete.name, Autocomplete)
}
export const ElAutocomplete = withInstall(Autocomplete)
const _Autocomplete = Autocomplete as SFCWithInstall<typeof Autocomplete>
export default ElAutocomplete
export default _Autocomplete
export const ElAutocomplete = _Autocomplete
export * from './src/autocomplete'

View File

@ -0,0 +1,84 @@
import { NOOP } from '@vue/shared'
import {
isString,
isObject,
buildProps,
definePropType,
} from '@element-plus/utils'
import { useTooltipContentProps } from '@element-plus/components/tooltip'
import { UPDATE_MODEL_EVENT } from '@element-plus/constants'
import type { ExtractPropTypes } from 'vue'
import type Autocomplete from './autocomplete.vue'
import type { Placement } from '@element-plus/components/popper'
export const autocompleteProps = buildProps({
valueKey: {
type: String,
default: 'value',
},
modelValue: {
type: [String, Number],
default: '',
},
debounce: {
type: Number,
default: 300,
},
placement: {
type: definePropType<Placement>(String),
values: [
'top',
'top-start',
'top-end',
'bottom',
'bottom-start',
'bottom-end',
],
default: 'bottom-start',
},
fetchSuggestions: {
type: definePropType<
(queryString: string, cb: (data: any[]) => void) => void
>(Function),
default: NOOP,
},
popperClass: {
type: String,
default: '',
},
triggerOnFocus: {
type: Boolean,
default: true,
},
selectWhenUnmatched: {
type: Boolean,
default: false,
},
hideLoading: {
type: Boolean,
default: false,
},
popperAppendToBody: {
type: Boolean,
default: undefined,
},
teleported: useTooltipContentProps.teleported,
highlightFirstItem: {
type: Boolean,
default: false,
},
} as const)
export type AutocompleteProps = ExtractPropTypes<typeof autocompleteProps>
export const autocompleteEmits = {
[UPDATE_MODEL_EVENT]: (value: string) => isString(value),
input: (value: string) => isString(value),
change: (value: string) => isString(value),
focus: (evt: FocusEvent) => evt instanceof FocusEvent,
blur: (evt: FocusEvent) => evt instanceof FocusEvent,
clear: () => true,
select: (item: { value: any }) => isObject(item),
}
export type AutocompleteEmits = typeof autocompleteEmits
export type AutocompleteInstance = InstanceType<typeof Autocomplete>

View File

@ -0,0 +1,316 @@
<template>
<el-tooltip
ref="popperRef"
v-model:visible="suggestionVisible"
:placement="placement"
:fallback-placements="['bottom-start', 'top-start']"
:popper-class="[ns.e('popper'), popperClass]"
:teleported="compatTeleported"
:gpu-acceleration="false"
pure
manual-mode
effect="light"
trigger="click"
:transition="`${ns.namespace.value}-zoom-in-top`"
persistent
@show="onSuggestionShow"
>
<div
ref="listboxRef"
:class="[ns.b(), $attrs.class]"
:style="styles"
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="handleKeyEnter"
@keydown.tab="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 #content>
<div
ref="regionRef"
:class="[ns.b('suggestion'), ns.is('loading', suggestionLoading)]"
:style="{ minWidth: dropdownWidth, outline: 'none' }"
role="region"
>
<el-scrollbar
:id="id"
tag="ul"
:wrap-class="ns.be('suggestion', 'wrap')"
:view-class="ns.be('suggestion', 'list')"
role="listbox"
>
<li v-if="suggestionLoading">
<el-icon :class="ns.is('loading')"><Loading /></el-icon>
</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="handleSelect(item)"
>
<slot :item="item">{{ item[valueKey] }}</slot>
</li>
</template>
</el-scrollbar>
</div>
</template>
</el-tooltip>
</template>
<script lang="ts" setup>
import {
ref,
computed,
onMounted,
nextTick,
useAttrs as useCompAttrs,
} from 'vue'
import { debounce } from 'lodash-unified'
import { onClickOutside } from '@vueuse/core'
import { useAttrs, useNamespace } from '@element-plus/hooks'
import { generateId, isArray, throwError } from '@element-plus/utils'
import { UPDATE_MODEL_EVENT } from '@element-plus/constants'
import ElInput from '@element-plus/components/input'
import ElScrollbar from '@element-plus/components/scrollbar'
import ElTooltip from '@element-plus/components/tooltip'
import { useDeprecateAppendToBody } from '@element-plus/components/popper'
import ElIcon from '@element-plus/components/icon'
import { Loading } from '@element-plus/icons-vue'
import { autocompleteProps, autocompleteEmits } from './autocomplete'
import type { TooltipInstance } from '@element-plus/components/tooltip'
import type { StyleValue } from 'vue'
defineOptions({
name: 'ElAutocomplete',
inheritAttrs: false,
})
const COMPONENT_NAME = 'ElAutocomplete'
const props = defineProps(autocompleteProps)
const emit = defineEmits(autocompleteEmits)
const ns = useNamespace('autocomplete')
const { compatTeleported } = useDeprecateAppendToBody(
COMPONENT_NAME,
'popperAppendToBody'
)
const attrs = useAttrs()
const compAttrs = useCompAttrs()
const suggestions = ref<any[]>([])
const highlightedIndex = ref(-1)
const dropdownWidth = ref('')
const activated = ref(false)
const suggestionDisabled = ref(false)
const loading = ref(false)
const inputRef = ref<{
inputOrTextarea: HTMLInputElement | HTMLTextAreaElement
focus: () => void
$el: HTMLElement
}>()
const regionRef = ref<HTMLElement>()
const popperRef = ref<TooltipInstance>()
const listboxRef = ref<HTMLElement>()
const id = computed(() => {
return ns.b(String(generateId()))
})
const styles = computed(() => compAttrs.style as StyleValue)
const suggestionVisible = computed(() => {
const isValidData = isArray(suggestions.value) && suggestions.value.length > 0
return (isValidData || loading.value) && activated.value
})
const suggestionLoading = computed(() => {
return !props.hideLoading && loading.value
})
const onSuggestionShow = () => {
nextTick(() => {
if (suggestionVisible.value) {
dropdownWidth.value = `${inputRef.value!.$el.offsetWidth}px`
}
})
}
const getData = (queryString: string) => {
if (suggestionDisabled.value) {
return
}
loading.value = true
props.fetchSuggestions(queryString, (suggestionsArg) => {
loading.value = false
if (suggestionDisabled.value) {
return
}
if (isArray(suggestionsArg)) {
suggestions.value = suggestionsArg
highlightedIndex.value = props.highlightFirstItem ? 0 : -1
} else {
throwError(COMPONENT_NAME, 'autocomplete suggestions must be an array')
}
})
}
const debouncedGetData = debounce(getData, props.debounce)
const handleInput = (value: string) => {
emit('input', value)
emit(UPDATE_MODEL_EVENT, value)
suggestionDisabled.value = false
if (!props.triggerOnFocus && !value) {
suggestionDisabled.value = true
suggestions.value = []
return
}
debouncedGetData(value)
}
const handleChange = (value: string) => {
emit('change', value)
}
const handleFocus = (evt: FocusEvent) => {
activated.value = true
emit('focus', evt)
if (props.triggerOnFocus) {
debouncedGetData(String(props.modelValue))
}
}
const handleBlur = (evt: FocusEvent) => {
emit('blur', evt)
}
const handleClear = () => {
activated.value = false
emit(UPDATE_MODEL_EVENT, '')
emit('clear')
}
const handleKeyEnter = () => {
if (
suggestionVisible.value &&
highlightedIndex.value >= 0 &&
highlightedIndex.value < suggestions.value.length
) {
handleSelect(suggestions.value[highlightedIndex.value])
} else if (props.selectWhenUnmatched) {
emit('select', { value: props.modelValue })
nextTick(() => {
suggestions.value = []
highlightedIndex.value = -1
})
}
}
const close = () => {
activated.value = false
}
const focus = () => {
inputRef.value?.focus()
}
const handleSelect = (item: any) => {
emit('input', item[props.valueKey])
emit(UPDATE_MODEL_EVENT, item[props.valueKey])
emit('select', item)
nextTick(() => {
suggestions.value = []
highlightedIndex.value = -1
})
}
const highlight = (index: number) => {
if (!suggestionVisible.value || loading.value) {
return
}
if (index < 0) {
highlightedIndex.value = -1
return
}
if (index >= suggestions.value.length) {
index = suggestions.value.length - 1
}
const suggestion = regionRef.value!.querySelector(
`.${ns.be('suggestion', 'wrap')}`
)!
const suggestionList = suggestion.querySelectorAll<HTMLElement>(
`.${ns.be('suggestion', 'list')} li`
)!
const highlightItem = suggestionList[index]
const scrollTop = suggestion.scrollTop
const { offsetTop, scrollHeight } = highlightItem
if (offsetTop + scrollHeight > scrollTop + suggestion.clientHeight) {
suggestion.scrollTop += scrollHeight
}
if (offsetTop < scrollTop) {
suggestion.scrollTop -= scrollHeight
}
highlightedIndex.value = index
inputRef.value!.inputOrTextarea.setAttribute(
'aria-activedescendant',
`${id.value}-item-${highlightedIndex.value}`
)
}
onClickOutside(listboxRef, close)
onMounted(() => {
inputRef.value!.inputOrTextarea.setAttribute('role', 'textbox')
inputRef.value!.inputOrTextarea.setAttribute('aria-autocomplete', 'list')
inputRef.value!.inputOrTextarea.setAttribute('aria-controls', 'id')
inputRef.value!.inputOrTextarea.setAttribute(
'aria-activedescendant',
`${id.value}-item-${highlightedIndex.value}`
)
})
defineExpose({
/** @description the index of the currently highlighted item */
highlightedIndex,
/** @description autocomplete whether activated */
activated,
/** @description remote search loading status */
loading,
/** @description el-input component instance */
inputRef,
/** @description el-tooltip component instance */
popperRef,
/** @description fetch suggestions result */
suggestions,
/** @description triggers when a suggestion is clicked */
handleSelect,
/** @description handle keyboard enter event */
handleKeyEnter,
/** @description focus the input element */
focus,
/** @description close suggestion */
close,
/** @description highlight an item in a suggestion */
highlight,
})
</script>

View File

@ -1,394 +0,0 @@
<template>
<el-tooltip
ref="popper"
v-model:visible="suggestionVisible"
:placement="placement"
:fallback-placements="['bottom-start', 'top-start']"
:popper-class="`${ns.e('popper')} ${popperClass}`"
:teleported="compatTeleported"
:gpu-acceleration="false"
pure
manual-mode
effect="light"
trigger="click"
:transition="`${ns.namespace.value}-zoom-in-top`"
persistent
@show="onSuggestionShow"
>
<div
v-clickoutside="close"
:class="[ns.b(), $attrs.class]"
:style="$attrs.style"
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="handleKeyEnter"
@keydown.tab="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 #content>
<div
ref="regionRef"
:class="[ns.b('suggestion'), ns.is('loading', suggestionLoading)]"
:style="{ minWidth: dropdownWidth, outline: 'none' }"
role="region"
>
<el-scrollbar
:id="id"
tag="ul"
:wrap-class="ns.be('suggestion', 'wrap')"
:view-class="ns.be('suggestion', 'list')"
role="listbox"
>
<li v-if="suggestionLoading">
<el-icon class="is-loading"><loading /></el-icon>
</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>
</template>
</el-tooltip>
</template>
<script lang="ts">
import { defineComponent, ref, computed, onMounted, nextTick } from 'vue'
import { NOOP } from '@vue/shared'
import { debounce } from 'lodash-unified'
import { useAttrs, useNamespace } from '@element-plus/hooks'
import { ClickOutside } from '@element-plus/directives'
import { generateId, isArray, throwError } from '@element-plus/utils'
import { UPDATE_MODEL_EVENT } from '@element-plus/constants'
import ElInput from '@element-plus/components/input'
import ElScrollbar from '@element-plus/components/scrollbar'
import ElTooltip, {
useTooltipContentProps,
} from '@element-plus/components/tooltip'
import { useDeprecateAppendToBody } from '@element-plus/components/popper'
import ElIcon from '@element-plus/components/icon'
import { Loading } from '@element-plus/icons-vue'
import type { Placement } from '@element-plus/components/popper'
import type { PropType } from 'vue'
const COMPONENT_NAME = 'ElAutocomplete'
export default defineComponent({
name: COMPONENT_NAME,
components: {
ElTooltip,
ElInput,
ElScrollbar,
ElIcon,
Loading,
},
directives: {
clickoutside: ClickOutside,
},
inheritAttrs: false,
props: {
valueKey: {
type: String,
default: 'value',
},
modelValue: {
type: [String, Number],
default: '',
},
debounce: {
type: Number,
default: 300,
},
placement: {
type: String as PropType<Placement>,
validator: (val: string): boolean => {
return [
'top',
'top-start',
'top-end',
'bottom',
'bottom-start',
'bottom-end',
].includes(val)
},
default: 'bottom-start',
},
fetchSuggestions: {
type: Function as PropType<
(queryString: string, cb: (data: any[]) => void) => void
>,
default: NOOP,
},
popperClass: {
type: String,
default: '',
},
triggerOnFocus: {
type: Boolean,
default: true,
},
selectWhenUnmatched: {
type: Boolean,
default: false,
},
hideLoading: {
type: Boolean,
default: false,
},
popperAppendToBody: {
type: Boolean,
default: undefined,
},
teleported: useTooltipContentProps.teleported,
highlightFirstItem: {
type: Boolean,
default: false,
},
},
emits: [
UPDATE_MODEL_EVENT,
'input',
'change',
'focus',
'blur',
'clear',
'select',
],
setup(props, ctx) {
const ns = useNamespace('autocomplete')
const { compatTeleported } = useDeprecateAppendToBody(
COMPONENT_NAME,
'popperAppendToBody'
)
const attrs = useAttrs()
const suggestions = ref<any[]>([])
const highlightedIndex = ref(-1)
const dropdownWidth = ref('')
const activated = ref(false)
const suggestionDisabled = ref(false)
const loading = ref(false)
const inputRef = ref<{
inputOrTextarea: HTMLInputElement | HTMLTextAreaElement
focus: () => void
$el: HTMLElement
} | null>(null)
const regionRef = ref<HTMLElement | null>(null)
const popper = ref(null)
const id = computed(() => {
return ns.b(String(generateId()))
})
const suggestionVisible = computed(() => {
const isValidData =
isArray(suggestions.value) && suggestions.value.length > 0
return (isValidData || loading.value) && activated.value
})
const suggestionLoading = computed(() => {
return !props.hideLoading && loading.value
})
const onSuggestionShow = () => {
nextTick(() => {
if (suggestionVisible.value) {
dropdownWidth.value = `${inputRef.value!.$el.offsetWidth}px`
}
})
}
onMounted(() => {
inputRef.value!.inputOrTextarea.setAttribute('role', 'textbox')
inputRef.value!.inputOrTextarea.setAttribute('aria-autocomplete', 'list')
inputRef.value!.inputOrTextarea.setAttribute('aria-controls', 'id')
inputRef.value!.inputOrTextarea.setAttribute(
'aria-activedescendant',
`${id.value}-item-${highlightedIndex.value}`
)
})
const getData = (queryString: string) => {
if (suggestionDisabled.value) {
return
}
loading.value = true
props.fetchSuggestions(queryString, (suggestionsArg) => {
loading.value = false
if (suggestionDisabled.value) {
return
}
if (isArray(suggestionsArg)) {
suggestions.value = suggestionsArg
highlightedIndex.value = props.highlightFirstItem ? 0 : -1
} else {
throwError(
'ElAutocomplete',
'autocomplete suggestions must be an array'
)
}
})
}
const debouncedGetData = debounce(getData, props.debounce)
const handleInput = (value: string) => {
ctx.emit('input', value)
ctx.emit(UPDATE_MODEL_EVENT, value)
suggestionDisabled.value = false
if (!props.triggerOnFocus && !value) {
suggestionDisabled.value = true
suggestions.value = []
return
}
debouncedGetData(value)
}
const handleChange = (value) => {
ctx.emit('change', value)
}
const handleFocus = (e) => {
activated.value = true
ctx.emit('focus', e)
if (props.triggerOnFocus) {
debouncedGetData(String(props.modelValue))
}
}
const handleBlur = (e) => {
ctx.emit('blur', e)
}
const handleClear = () => {
activated.value = false
ctx.emit(UPDATE_MODEL_EVENT, '')
ctx.emit('clear')
}
const handleKeyEnter = () => {
if (
suggestionVisible.value &&
highlightedIndex.value >= 0 &&
highlightedIndex.value < suggestions.value.length
) {
select(suggestions.value[highlightedIndex.value])
} else if (props.selectWhenUnmatched) {
ctx.emit('select', { value: props.modelValue })
nextTick(() => {
suggestions.value = []
highlightedIndex.value = -1
})
}
}
const close = () => {
activated.value = false
}
const focus = () => {
inputRef.value?.focus()
}
const select = (item) => {
ctx.emit('input', item[props.valueKey])
ctx.emit(UPDATE_MODEL_EVENT, item[props.valueKey])
ctx.emit('select', item)
nextTick(() => {
suggestions.value = []
highlightedIndex.value = -1
})
}
const highlight = (index: number) => {
if (!suggestionVisible.value || loading.value) {
return
}
if (index < 0) {
highlightedIndex.value = -1
return
}
if (index >= suggestions.value.length) {
index = suggestions.value.length - 1
}
const suggestion = regionRef.value!.querySelector(
`.${ns.be('suggestion', 'wrap')}`
)!
const suggestionList = suggestion.querySelectorAll(
`.${ns.be('suggestion', 'list')} li`
)!
const highlightItem = suggestionList[index]
const scrollTop = suggestion.scrollTop
const { offsetTop, scrollHeight } = highlightItem as HTMLElement
if (offsetTop + scrollHeight > scrollTop + suggestion.clientHeight) {
suggestion.scrollTop += scrollHeight
}
if (offsetTop < scrollTop) {
suggestion.scrollTop -= scrollHeight
}
highlightedIndex.value = index
inputRef.value!.inputOrTextarea.setAttribute(
'aria-activedescendant',
`${id.value}-item-${highlightedIndex.value}`
)
}
return {
attrs,
suggestions,
highlightedIndex,
dropdownWidth,
activated,
suggestionDisabled,
loading,
inputRef,
regionRef,
popper,
id,
suggestionVisible,
suggestionLoading,
// deprecation in 2.1.0
compatTeleported,
getData,
handleInput,
handleChange,
handleFocus,
handleBlur,
handleClear,
handleKeyEnter,
close,
focus,
select,
highlight,
onSuggestionShow,
ns,
}
},
})
</script>

View File

@ -7,6 +7,7 @@ import {
useDelayedToggleProps,
POPPER_CONTAINER_SELECTOR,
} from '@element-plus/hooks'
import type Tooltip from '../src/tooltip.vue'
import type { ExtractPropTypes } from 'vue'
@ -90,3 +91,5 @@ export type ElTooltipTriggerProps = ExtractPropTypes<
export type ElTooltipProps = ExtractPropTypes<typeof useTooltipProps> &
ElTooltipContentProps &
ElTooltipTriggerProps
export type TooltipInstance = InstanceType<typeof Tooltip>