feat(components): [select & select-v2] Add loading slot (#15540)

* feat(components): [select & select-v2] Add loading slot

* feat(components): update
This commit is contained in:
kooriookami 2024-01-18 12:23:41 +08:00 committed by GitHub
parent c9863647af
commit feb169fe93
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 533 additions and 35 deletions

View File

@ -187,6 +187,16 @@ select-v2/custom-tag
:::
## Custom Loading ^(2.5.2)
Override loading content.
:::demo
select-v2/custom-loading
:::
## API
### Attributes
@ -256,14 +266,15 @@ select-v2/custom-tag
### Slots
| Name | Description |
|-----------------|---------------------------------------|
| default | Option renderer |
| header ^(2.5.2) | content at the top of the dropdown |
| footer ^(2.5.2) | content at the bottom of the dropdown |
| empty | content when options is empty |
| prefix | prefix content of input |
| tag ^(2.5.0) | content as Select tag |
| Name | Description |
|------------------|---------------------------------------|
| default | Option renderer |
| header ^(2.5.2) | content at the top of the dropdown |
| footer ^(2.5.2) | content at the bottom of the dropdown |
| empty | content when options is empty |
| prefix | prefix content of input |
| tag ^(2.5.0) | content as Select tag |
| loading ^(2.5.2) | content as Select loading |
### Exposes

View File

@ -149,6 +149,16 @@ select/custom-tag
:::
## Custom Loading ^(2.5.2)
Override loading content.
:::demo
select/custom-loading
:::
## Select API
### Select Attributes
@ -214,14 +224,15 @@ select/custom-tag
### Select Slots
| Name | Description | Subtags |
|-----------------|---------------------------------------| --------------------- |
| default | option component list | Option Group / Option |
| header ^(2.4.3) | content at the top of the dropdown | — |
| footer ^(2.4.3) | content at the bottom of the dropdown | — |
| prefix | content as Select prefix | — |
| empty | content when there is no options | — |
| tag ^(2.5.0) | content as Select tag | — |
| Name | Description | Subtags |
|------------------|---------------------------------------|-----------------------|
| default | option component list | Option Group / Option |
| header ^(2.4.3) | content at the top of the dropdown | — |
| footer ^(2.4.3) | content at the bottom of the dropdown | — |
| prefix | content as Select prefix | — |
| empty | content when there is no options | — |
| tag ^(2.5.0) | content as Select tag | — |
| loading ^(2.5.2) | content as Select loading | — |
### Select Exposes

View File

@ -0,0 +1,220 @@
<template>
<div class="flex flex-wrap">
<div class="m-4">
<p>loading icon1</p>
<el-select-v2
v-model="value"
multiple
filterable
remote
reserve-keyword
placeholder="Please enter a keyword"
:remote-method="remoteMethod"
:loading="loading"
:options="options"
style="width: 240px"
>
<template #loading>
<svg class="circular" viewBox="0 0 50 50">
<circle class="path" cx="25" cy="25" r="20" fill="none" />
</svg>
</template>
</el-select-v2>
</div>
<div class="m-4">
<p>loading icon2</p>
<el-select-v2
v-model="value"
multiple
filterable
remote
reserve-keyword
placeholder="Please enter a keyword"
:remote-method="remoteMethod"
:loading="loading"
:options="options"
style="width: 240px"
>
<template #loading>
<el-icon class="is-loading">
<svg class="circular" viewBox="0 0 20 20">
<g
class="path2 loading-path"
stroke-width="0"
style="animation: none; stroke: none"
>
<circle r="3.375" class="dot1" rx="0" ry="0" />
<circle r="3.375" class="dot2" rx="0" ry="0" />
<circle r="3.375" class="dot4" rx="0" ry="0" />
<circle r="3.375" class="dot3" rx="0" ry="0" />
</g>
</svg>
</el-icon>
</template>
</el-select-v2>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
interface ListItem {
value: string
label: string
}
const list = ref<ListItem[]>([])
const options = ref<ListItem[]>([])
const value = ref<string[]>([])
const loading = ref(false)
onMounted(() => {
list.value = states.map((item) => {
return { value: `value:${item}`, label: `label:${item}` }
})
})
const remoteMethod = (query: string) => {
if (query) {
loading.value = true
setTimeout(() => {
loading.value = false
options.value = list.value.filter((item) => {
return item.label.toLowerCase().includes(query.toLowerCase())
})
}, 3000)
} else {
options.value = []
}
}
const states = [
'Alabama',
'Alaska',
'Arizona',
'Arkansas',
'California',
'Colorado',
'Connecticut',
'Delaware',
'Florida',
'Georgia',
'Hawaii',
'Idaho',
'Illinois',
'Indiana',
'Iowa',
'Kansas',
'Kentucky',
'Louisiana',
'Maine',
'Maryland',
'Massachusetts',
'Michigan',
'Minnesota',
'Mississippi',
'Missouri',
'Montana',
'Nebraska',
'Nevada',
'New Hampshire',
'New Jersey',
'New Mexico',
'New York',
'North Carolina',
'North Dakota',
'Ohio',
'Oklahoma',
'Oregon',
'Pennsylvania',
'Rhode Island',
'South Carolina',
'South Dakota',
'Tennessee',
'Texas',
'Utah',
'Vermont',
'Virginia',
'Washington',
'West Virginia',
'Wisconsin',
'Wyoming',
]
</script>
<style>
.el-select-dropdown__loading {
display: flex;
justify-content: center;
align-items: center;
height: 100px;
font-size: 20px;
}
.circular {
display: inline;
height: 30px;
width: 30px;
animation: loading-rotate 2s linear infinite;
}
.path {
animation: loading-dash 1.5s ease-in-out infinite;
stroke-dasharray: 90, 150;
stroke-dashoffset: 0;
stroke-width: 2;
stroke: var(--el-color-primary);
stroke-linecap: round;
}
.loading-path .dot1 {
transform: translate(3.75px, 3.75px);
fill: var(--el-color-primary);
animation: custom-spin-move 1s infinite linear alternate;
opacity: 0.3;
}
.loading-path .dot2 {
transform: translate(calc(100% - 3.75px), 3.75px);
fill: var(--el-color-primary);
animation: custom-spin-move 1s infinite linear alternate;
opacity: 0.3;
animation-delay: 0.4s;
}
.loading-path .dot3 {
transform: translate(3.75px, calc(100% - 3.75px));
fill: var(--el-color-primary);
animation: custom-spin-move 1s infinite linear alternate;
opacity: 0.3;
animation-delay: 1.2s;
}
.loading-path .dot4 {
transform: translate(calc(100% - 3.75px), calc(100% - 3.75px));
fill: var(--el-color-primary);
animation: custom-spin-move 1s infinite linear alternate;
opacity: 0.3;
animation-delay: 0.8s;
}
@keyframes loading-rotate {
to {
transform: rotate(360deg);
}
}
@keyframes loading-dash {
0% {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -40px;
}
100% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -120px;
}
}
@keyframes custom-spin-move {
to {
opacity: 1;
}
}
</style>

View File

@ -0,0 +1,230 @@
<template>
<div class="flex flex-wrap">
<div class="m-4">
<p>loading icon1</p>
<el-select
v-model="value"
multiple
filterable
remote
reserve-keyword
placeholder="Please enter a keyword"
:remote-method="remoteMethod"
:loading="loading"
style="width: 240px"
>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
<template #loading>
<svg class="circular" viewBox="0 0 50 50">
<circle class="path" cx="25" cy="25" r="20" fill="none" />
</svg>
</template>
</el-select>
</div>
<div class="m-4">
<p>loading icon2</p>
<el-select
v-model="value"
multiple
filterable
remote
reserve-keyword
placeholder="Please enter a keyword"
:remote-method="remoteMethod"
:loading="loading"
style="width: 240px"
>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
<template #loading>
<el-icon class="is-loading">
<svg class="circular" viewBox="0 0 20 20">
<g
class="path2 loading-path"
stroke-width="0"
style="animation: none; stroke: none"
>
<circle r="3.375" class="dot1" rx="0" ry="0" />
<circle r="3.375" class="dot2" rx="0" ry="0" />
<circle r="3.375" class="dot4" rx="0" ry="0" />
<circle r="3.375" class="dot3" rx="0" ry="0" />
</g>
</svg>
</el-icon>
</template>
</el-select>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
interface ListItem {
value: string
label: string
}
const list = ref<ListItem[]>([])
const options = ref<ListItem[]>([])
const value = ref<string[]>([])
const loading = ref(false)
onMounted(() => {
list.value = states.map((item) => {
return { value: `value:${item}`, label: `label:${item}` }
})
})
const remoteMethod = (query: string) => {
if (query) {
loading.value = true
setTimeout(() => {
loading.value = false
options.value = list.value.filter((item) => {
return item.label.toLowerCase().includes(query.toLowerCase())
})
}, 3000)
} else {
options.value = []
}
}
const states = [
'Alabama',
'Alaska',
'Arizona',
'Arkansas',
'California',
'Colorado',
'Connecticut',
'Delaware',
'Florida',
'Georgia',
'Hawaii',
'Idaho',
'Illinois',
'Indiana',
'Iowa',
'Kansas',
'Kentucky',
'Louisiana',
'Maine',
'Maryland',
'Massachusetts',
'Michigan',
'Minnesota',
'Mississippi',
'Missouri',
'Montana',
'Nebraska',
'Nevada',
'New Hampshire',
'New Jersey',
'New Mexico',
'New York',
'North Carolina',
'North Dakota',
'Ohio',
'Oklahoma',
'Oregon',
'Pennsylvania',
'Rhode Island',
'South Carolina',
'South Dakota',
'Tennessee',
'Texas',
'Utah',
'Vermont',
'Virginia',
'Washington',
'West Virginia',
'Wisconsin',
'Wyoming',
]
</script>
<style>
.el-select-dropdown__loading {
display: flex;
justify-content: center;
align-items: center;
height: 100px;
font-size: 20px;
}
.circular {
display: inline;
height: 30px;
width: 30px;
animation: loading-rotate 2s linear infinite;
}
.path {
animation: loading-dash 1.5s ease-in-out infinite;
stroke-dasharray: 90, 150;
stroke-dashoffset: 0;
stroke-width: 2;
stroke: var(--el-color-primary);
stroke-linecap: round;
}
.loading-path .dot1 {
transform: translate(3.75px, 3.75px);
fill: var(--el-color-primary);
animation: custom-spin-move 1s infinite linear alternate;
opacity: 0.3;
}
.loading-path .dot2 {
transform: translate(calc(100% - 3.75px), 3.75px);
fill: var(--el-color-primary);
animation: custom-spin-move 1s infinite linear alternate;
opacity: 0.3;
animation-delay: 0.4s;
}
.loading-path .dot3 {
transform: translate(3.75px, calc(100% - 3.75px));
fill: var(--el-color-primary);
animation: custom-spin-move 1s infinite linear alternate;
opacity: 0.3;
animation-delay: 1.2s;
}
.loading-path .dot4 {
transform: translate(calc(100% - 3.75px), calc(100% - 3.75px));
fill: var(--el-color-primary);
animation: custom-spin-move 1s infinite linear alternate;
opacity: 0.3;
animation-delay: 0.8s;
}
@keyframes loading-rotate {
to {
transform: rotate(360deg);
}
}
@keyframes loading-dash {
0% {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -40px;
}
100% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -120px;
}
}
@keyframes custom-spin-move {
to {
opacity: 1;
}
}
</style>

View File

@ -28,6 +28,7 @@ export default defineComponent({
name: 'ElSelectDropdown',
props: {
loading: Boolean,
data: {
type: Array,
required: true,
@ -225,7 +226,7 @@ export default defineComponent({
const { data, width } = props
const { height, multiple, scrollbarAlwaysOn } = select.props
if (data.length === 0) {
if (slots.loading || slots.empty) {
return (
<div
class={ns.b('dropdown')}
@ -233,7 +234,7 @@ export default defineComponent({
width: `${width}px`,
}}
>
{slots.empty?.()}
{slots.loading?.() || slots.empty?.()}
</div>
)
}

View File

@ -225,12 +225,17 @@
<template #default="scope">
<slot v-bind="scope" />
</template>
<template #empty>
<slot name="empty">
<p :class="nsSelect.be('dropdown', 'empty')">
{{ emptyText ? emptyText : '' }}
</p>
</slot>
<template v-if="$slots.loading && loading" #loading>
<div :class="nsSelect.be('dropdown', 'loading')">
<slot name="loading" />
</div>
</template>
<template v-else-if="loading || filteredOptions.length === 0" #empty>
<div :class="nsSelect.be('dropdown', 'empty')">
<slot name="empty">
<span>{{ emptyText }}</span>
</slot>
</div>
</template>
<template v-if="$slots.footer" #footer>
<div :class="nsSelect.be('dropdown', 'footer')">
@ -255,6 +260,7 @@ import ElSelectMenu from './select-dropdown'
import useSelect from './useSelect'
import { SelectProps } from './defaults'
import { selectV2InjectionKey } from './token'
export default defineComponent({
name: 'ElSelectV2',
components: {

View File

@ -358,6 +358,8 @@ const useSelect = (props: ISelectV2Props, emit) => {
// methods
const toggleMenu = () => {
if (selectDisabled.value) return
if (props.filterable && props.remote && isFunction(props.remoteMethod))
return
if (states.menuVisibleOnFocus) {
// controlled by automaticDropdown
states.menuVisibleOnFocus = false
@ -367,6 +369,9 @@ const useSelect = (props: ISelectV2Props, emit) => {
}
const onInputChange = () => {
if (states.inputValue.length > 0 && !expanded.value) {
expanded.value = true
}
createNewOption(states.inputValue)
handleQueryChange(states.inputValue)
}
@ -681,9 +686,6 @@ const useSelect = (props: ISelectV2Props, emit) => {
const onInput = (event) => {
states.inputValue = event.target.value
if (states.inputValue.length > 0 && !expanded.value) {
expanded.value = true
}
if (props.remote) {
debouncedOnInputChange()
} else {

View File

@ -234,13 +234,20 @@
<slot />
</el-options>
</el-scrollbar>
<template v-if="loading || filteredOptionsCount === 0">
<div
v-if="$slots.loading && loading"
:class="nsSelect.be('dropdown', 'loading')"
>
<slot name="loading" />
</div>
<div
v-else-if="loading || filteredOptionsCount === 0"
:class="nsSelect.be('dropdown', 'empty')"
>
<slot name="empty">
<p :class="nsSelect.be('dropdown', 'empty')">
{{ emptyText }}
</p>
<span>{{ emptyText }}</span>
</slot>
</template>
</div>
<div v-if="$slots.footer" :class="nsSelect.be('dropdown', 'footer')">
<slot name="footer" />
</div>

View File

@ -489,14 +489,14 @@ export const useSelect = (props: ISelectProps, emit) => {
}
const onInputChange = () => {
if (states.inputValue.length > 0 && !expanded.value) {
expanded.value = true
}
handleQueryChange(states.inputValue)
}
const onInput = (event) => {
states.inputValue = event.target.value
if (states.inputValue.length > 0 && !expanded.value) {
expanded.value = true
}
if (props.remote) {
debouncedOnInputChange()
} else {
@ -687,6 +687,8 @@ export const useSelect = (props: ISelectProps, emit) => {
const toggleMenu = () => {
if (selectDisabled.value) return
if (props.filterable && props.remote && isFunction(props.remoteMethod))
return
if (states.menuVisibleOnFocus) {
// controlled by automaticDropdown
states.menuVisibleOnFocus = false

View File

@ -13,6 +13,14 @@
}
}
@include b(select-dropdown__loading) {
padding: map.get($select-dropdown, 'empty-padding');
margin: 0;
text-align: center;
color: map.get($select-dropdown, 'empty-color');
font-size: getCssVar('select-font-size');
}
@include b(select-dropdown__empty) {
padding: map.get($select-dropdown, 'empty-padding');
margin: 0;