Merge pull request #16319 from element-plus/dev

D2M
This commit is contained in:
kooriookami
2024-03-29 19:30:04 +08:00
committed by GitHub
34 changed files with 434 additions and 45 deletions

View File

@@ -1,4 +1,23 @@
## Changelog
### 2.6.3
_2024-03-29_
#### Features
- Components [tree] lazy load provide reject (#16099 by @btea)
- Locale update ru locale (#16192 by @VisualYuki)
- Components [message] add plain prop (#16214 by @kooriookami)
- Components [input-number] support slot custom icons (#16275 by @selicens)
- Components [badge] add color prop (#16069 by @lxw15337674)
#### Bug fixes
- Components [icon] remove repeat style (#16242 by @Fuphoenixes)
- Components [select] fix error in low versions of vue (#16234 by @kooriookami)
- Components [date-picker] click the clear to reset the date panel (#15835 by @Ganlvin)
- Components [tree] dragging a node will deselect the node (#14830 by @Alixhan)
### 2.6.2

View File

@@ -25,8 +25,10 @@
<el-card
v-for="(item, index) in group.children"
:key="index"
tabindex="0"
shadow="hover"
@click="toPage(item.link)"
@keydown.enter="toPage(item.link)"
>
<template #header>
<el-text truncated>{{ item.text }}</el-text>
@@ -140,6 +142,12 @@ const getIcon = (link: string) => {
:deep(.el-card) {
cursor: pointer;
transition: none;
&:focus-visible {
outline: 2px solid var(--el-color-primary);
outline-offset: 1px;
}
.el-card__header {
display: flex;

View File

@@ -51,14 +51,15 @@ badge/dot
### Attributes
| Name | Description | Type | Default |
| ------------------ | ----------------------------------------------------------------------------- | ------------------------------------------------------------------ | ------- |
| value | display value. | ^[string] / ^[number] | '' |
| max | maximum value, shows `{max}+` when exceeded. Only works if value is a number. | ^[number] | 99 |
| is-dot | if a little dot is displayed. | ^[boolean] | false |
| hidden | hidden badge. | ^[boolean] | false |
| type | badge type. | ^[enum]`'primary' \| 'success' \| 'warning' \| 'danger' \| 'info'` | danger |
| show-zero ^(2.6.0) | Whether to show badge when value is zero. | ^[boolean] | true |
| Name | Description | Type | Default |
| ------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | ------- |
| value | display value. | ^[string] / ^[number] | '' |
| max | maximum value, shows `{max}+` when exceeded. Only works if value is a number. | ^[number] | 99 |
| is-dot | if a little dot is displayed. | ^[boolean] | false |
| hidden | hidden badge. | ^[boolean] | false |
| type | badge type. | ^[enum]`'primary' \| 'success' \| 'warning' \| 'danger' \| 'info'` | danger |
| show-zero ^(2.6.0) | Whether to show badge when value is zero. | ^[boolean] | true |
| color ^(2.6.3) | background color of the dot | ^[string] | |
### Slots

View File

@@ -95,7 +95,7 @@ dropdown/sizes
| placement | placement of pop menu | string | top/top-start/top-end/bottom/bottom-start/bottom-end | bottom |
| trigger | how to trigger | string | hover/click/contextmenu | hover |
| hide-on-click | whether to hide menu after clicking menu-item | boolean | — | true |
| show-timeout | Delay time before show a dropdown (only works when trigger is `hover`) | number | — | 250 |
| show-timeout | Delay time before show a dropdown (only works when trigger is `hover`) | number | — | 150 |
| hide-timeout | Delay time before hide a dropdown (only works when trigger is `hover`) | number | — | 150 |
| role | The ARIA role attribute for the dropdown menu. Depending on the use case, you may want to change this to 'navigation' | string | — | 'menu' |
| tabindex | [tabindex](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) of Dropdown | number | — | 0 |

View File

@@ -79,6 +79,14 @@ input-number/controlled
:::
## Custom Icon ^(2.6.3)
:::demo Use `decrease-icon` and `increase-icon` to set custom icons.
input-number/custom
:::
## API
### Attributes
@@ -103,6 +111,13 @@ input-number/controlled
| value-on-clear ^(2.2.0) | value should be set when input box is cleared | ^[number] / ^[null] / ^[enum]`'min' \| 'max'` | — |
| validate-event | whether to trigger form validation | ^[boolean] | true |
### Slots
| Name | Description |
| ---------------------- | ------------------------------------- |
| decrease-icon ^(2.6.3) | custom input box button decrease icon |
| increase-icon ^(2.6.3) | custom input box button increase icon |
### Events
| Name | Description | Type |

View File

@@ -27,6 +27,16 @@ message/different-types
:::
## Plain ^(2.6.3)
Set `plain` to have a plain background.
:::demo
message/plain
:::
## Closable
A close button can be added.
@@ -85,7 +95,7 @@ import { ElMessage } from 'element-plus'
In this case you should call `ElMessage(options)`. We have also registered methods for different types, e.g. `ElMessage.success(options)`. You can call `ElMessage.closeAll()` to manually close all the instances.
## App context inheritance <el-tag> >= 2.0.3</el-tag>
## App context inheritance ^(2.0.3)
Now message accepts a `context` as second parameter of the message constructor which allows you to inject current app's context to message which allows you to inherit all the properties of the app.
@@ -111,9 +121,10 @@ ElMessage({}, appContext)
### Options
| Name | Description | Type | Default |
| ------------------------ | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | ------- |
|--------------------------|------------------------------------------------------------------------------------------------------|------------------------------------------------------|---------|
| message | message text | ^[string] / ^[VNode] / ^[Function]`() => VNode` | '' |
| type | message type | ^[enum]`'success' \| 'warning' \| 'info' \| 'error'` | info |
| plain ^(2.6.3) | whether message is plain | ^[boolean] | false |
| icon | custom icon component, overrides `type` | ^[string] / ^[Component] | — |
| dangerouslyUseHTMLString | whether `message` is treated as HTML string | ^[boolean] | false |
| customClass | custom class name for Message | ^[string] | '' |

View File

@@ -9,6 +9,12 @@ When there are plenty of options, use a drop-down menu to display and select des
:::tip
After version `2.5.0`, the default width of `el-select` changed to `100%`. When used in a inline form, the width will collapse. In order to display the width properly, you need to give `el-select` a specific width.
:::
:::tip
This component requires the `<client-only></client-only>` wrap when used in SSR (eg: [Nuxt](https://nuxt.com/v3)) and SSG (eg: [VitePress](https://vitepress.vuejs.org/)).
:::

View File

@@ -35,6 +35,14 @@ tree/custom-leaf
:::
## Lazy loading multiple times ^(2.6.3)
:::demo When lazily loading node data remotely, lazy loading may sometimes fail. In this case, you can call reject to keep the node status as is and allow remote loading to continue.
tree/multiple-times-load
:::
## Disabled checkbox
The checkbox of a node can be set as disabled.
@@ -122,7 +130,7 @@ tree/draggable
| node-key | unique identity key name for nodes, its value should be unique across the whole tree | string | — | — |
| props | configuration options, see the following table | object | — | — |
| render-after-expand | whether to render child nodes only after a parent node is expanded for the first time | boolean | — | true |
| load | method for loading subtree data, only works when `lazy` is true | function(node, resolve) | — | — |
| load | method for loading subtree data, only works when `lazy` is true | function(node, resolve, reject) | — | — |
| render-content | render function for tree node | Function(h, `{ node, data, store }`) | — | — |
| highlight-current | whether current node is highlighted | boolean | — | false |
| default-expand-all | whether to expand all nodes by default | boolean | — | false |

View File

@@ -11,7 +11,9 @@
<el-badge :value="2" class="item" type="warning">
<el-button>replies</el-button>
</el-badge>
<el-badge :value="1" class="item" color="green">
<el-button>custom background</el-button>
</el-badge>
<el-dropdown trigger="click">
<span class="el-dropdown-link">
Click Me
@@ -39,7 +41,7 @@ import { CaretBottom } from '@element-plus/icons-vue'
<style scoped>
.item {
margin-top: 10px;
margin-right: 40px;
margin-right: 30px;
}
.el-dropdown {

View File

@@ -37,7 +37,7 @@ const shortcuts = [
value: () => {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7)
start.setDate(start.getDate() - 7)
return [start, end]
},
},
@@ -46,7 +46,7 @@ const shortcuts = [
value: () => {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30)
start.setMonth(start.getMonth() - 1)
return [start, end]
},
},
@@ -55,7 +55,7 @@ const shortcuts = [
value: () => {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90)
start.setMonth(start.getMonth() - 3)
return [start, end]
},
},

View File

@@ -46,7 +46,7 @@ const shortcuts = [
text: 'Yesterday',
value: () => {
const date = new Date()
date.setTime(date.getTime() - 3600 * 1000 * 24)
date.setDate(date.getDate() - 1)
return date
},
},
@@ -54,7 +54,7 @@ const shortcuts = [
text: 'A week ago',
value: () => {
const date = new Date()
date.setTime(date.getTime() - 3600 * 1000 * 24 * 7)
date.setDate(date.getDate() - 7)
return date
},
},

View File

@@ -0,0 +1,41 @@
<template>
<el-space direction="vertical">
<el-space>
<el-input-number v-model="num" />
<el-input-number v-model="num">
<template #decrement-icon>
<el-icon>
<ArrowDown />
</el-icon>
</template>
<template #increase-icon>
<el-icon>
<ArrowUp />
</el-icon>
</template>
</el-input-number>
</el-space>
<el-space>
<el-input-number v-model="num" controls-position="right" />
<el-input-number v-model="num" controls-position="right">
<template #decrement-icon>
<el-icon>
<Minus />
</el-icon>
</template>
<template #increase-icon>
<el-icon>
<Plus />
</el-icon>
</template>
</el-input-number>
</el-space>
</el-space>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { ArrowDown, ArrowUp, Minus, Plus } from '@element-plus/icons-vue'
const num = ref(1)
</script>

View File

@@ -0,0 +1,39 @@
<template>
<el-button :plain="true" @click="open1">Success</el-button>
<el-button :plain="true" @click="open2">Warning</el-button>
<el-button :plain="true" @click="open3">Message</el-button>
<el-button :plain="true" @click="open4">Error</el-button>
</template>
<script lang="ts" setup>
import { ElMessage } from 'element-plus'
const open1 = () => {
ElMessage({
message: 'Congrats, this is a success message.',
type: 'success',
plain: true,
})
}
const open2 = () => {
ElMessage({
message: 'Warning, this is a warning message.',
type: 'warning',
plain: true,
})
}
const open3 = () => {
ElMessage({
message: 'This is a message.',
type: 'info',
plain: true,
})
}
const open4 = () => {
ElMessage({
message: 'Oops, this is a error message.',
type: 'error',
plain: true,
})
}
</script>

View File

@@ -0,0 +1,43 @@
<template>
<el-tree style="max-width: 600px" :props="props" :load="loadNode" lazy />
</template>
<script lang="ts" setup>
import type Node from 'element-plus/es/components/tree/src/model/node'
interface Tree {
name: string
leaf?: boolean
}
const props = {
label: 'name',
children: 'zones',
isLeaf: 'leaf',
}
let time = 0
const loadNode = (
node: Node,
resolve: (data: Tree[]) => void,
reject: () => void
) => {
if (node.level === 0) {
return resolve([{ name: 'region' }])
}
time++
if (node.level >= 1) {
setTimeout(() => {
if (time > 3) {
return resolve([
{ name: 'zone1', leaf: true },
{ name: 'zone2', leaf: true },
{ name: 'zone3', leaf: true },
])
} else {
return reject()
}
}, 3000)
}
}
</script>

View File

@@ -39,5 +39,9 @@ export const badgeProps = buildProps({
type: Boolean,
default: true,
},
/**
* @description background color of the Badge
*/
color: String,
} as const)
export type BadgeProps = ExtractPropTypes<typeof badgeProps>

View File

@@ -10,6 +10,7 @@
ns.is('fixed', !!$slots.default),
ns.is('dot', isDot),
]"
:style="{ backgroundColor: color }"
v-text="content"
/>
</transition>

View File

@@ -566,6 +566,37 @@ describe('Datetimerange', () => {
expect(value.value).not.toBe('')
})
it('clear button should empty the input value', async () => {
const value = ref('')
const wrapper = _mount(() => (
<DatePicker v-model={value.value} type="datetimerange" />
))
const input = wrapper.find('input')
input.trigger('focus')
await nextTick()
const dateRow = document.querySelectorAll('.el-date-table__row')
const dateCell = dateRow[1].querySelectorAll<HTMLElement>('.available')
dateCell[0].click()
dateCell[3].click()
await nextTick()
const headerValue = document.querySelectorAll<HTMLInputElement>(
'.el-date-range-picker__time-header input'
)
expect(headerValue[0].value).not.toBe('')
expect(headerValue[1].value).not.toBe('')
const clearBtn = document.querySelectorAll<HTMLButtonElement>(
'.el-picker-panel__footer button'
)[0]
clearBtn.click()
await nextTick()
input.trigger('blur')
await nextTick()
input.trigger('focus')
await nextTick()
expect(headerValue[0].value).toBe('')
expect(headerValue[1].value).toBe('')
})
it('confirm honors disabledDate', async () => {
const value = ref('')
const disabledDate = (date: Date) => {

View File

@@ -679,6 +679,8 @@ const handleClear = () => {
unlinkPanels: props.unlinkPanels,
})[0]
rightDate.value = leftDate.value.add(1, 'month')
maxDate.value = undefined
minDate.value = undefined
emit('pick', null)
}

View File

@@ -1,2 +1 @@
import '@element-plus/components/base/style/css'
import '@element-plus/theme-chalk/el-icon.css'

View File

@@ -1,2 +1 @@
import '@element-plus/components/base/style'
import '@element-plus/theme-chalk/src/icon.scss'

View File

@@ -3,6 +3,7 @@ import { mount } from '@vue/test-utils'
import { describe, expect, it, test, vi } from 'vitest'
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue'
import { ElFormItem } from '@element-plus/components/form'
import { ElIcon } from '@element-plus/components/icon'
import InputNumber from '../src/input-number.vue'
const mouseup = new Event('mouseup')
@@ -568,4 +569,29 @@ describe('InputNumber.vue', () => {
1, 2,
])
})
test('use slot custom icon', async () => {
const wrapper = mount(() => (
<InputNumber
v-slots={{
decreaseIcon: () => (
<ElIcon>
<ArrowDown />
</ElIcon>
),
increaseIcon: () => (
<ElIcon>
<ArrowUp />
</ElIcon>
),
}}
/>
))
const increase = wrapper.find('.el-input-number__increase i')
const decrease = wrapper.find('.el-input-number__decrease i')
expect(increase.exists()).toBe(true)
expect(decrease.exists()).toBe(true)
expect(increase.classes()).toContain('el-icon')
expect(decrease.classes()).toContain('el-icon')
})
})

View File

@@ -17,10 +17,12 @@
:class="[ns.e('decrease'), ns.is('disabled', minDisabled)]"
@keydown.enter="decrease"
>
<el-icon>
<arrow-down v-if="controlsAtRight" />
<minus v-else />
</el-icon>
<slot name="decrease-icon">
<el-icon>
<arrow-down v-if="controlsAtRight" />
<minus v-else />
</el-icon>
</slot>
</span>
<span
v-if="controls"
@@ -30,10 +32,12 @@
:class="[ns.e('increase'), ns.is('disabled', maxDisabled)]"
@keydown.enter="increase"
>
<el-icon>
<arrow-up v-if="controlsAtRight" />
<plus v-else />
</el-icon>
<slot name="increase-icon">
<el-icon>
<arrow-up v-if="controlsAtRight" />
<plus v-else />
</el-icon>
</slot>
</span>
<el-input
:id="id"

View File

@@ -28,6 +28,7 @@ export const messageDefaults = mutable({
onClose: undefined,
showClose: false,
type: 'info',
plain: false,
offset: 16,
zIndex: 0,
grouping: false,
@@ -94,7 +95,7 @@ export const messageProps = buildProps({
*/
onClose: {
type: definePropType<() => void>(Function),
required: false,
default: messageDefaults.onClose,
},
/**
* @description whether to show a close button
@@ -111,6 +112,13 @@ export const messageProps = buildProps({
values: messageTypes,
default: messageDefaults.type,
},
/**
* @description whether message is plain
*/
plain: {
type: Boolean,
default: messageDefaults.plain,
},
/**
* @description set the distance to the top of viewport
*/

View File

@@ -13,6 +13,7 @@
{ [ns.m(type)]: type },
ns.is('center', center),
ns.is('closable', showClose),
ns.is('plain', plain),
customClass,
]"
:style="customStyle"

View File

@@ -220,7 +220,7 @@ export const useSelect = (props: ISelectProps, emit) => {
if (props.filterable && props.remote && isFunction(props.remoteMethod))
return
optionsArray.value.forEach((option) => {
option.updateOption(states.inputValue)
option.updateOption?.(states.inputValue)
})
}

View File

@@ -547,14 +547,31 @@ class Node {
callback.call(this, children)
}
}
const reject = () => {
this.loading = false
}
this.store.load(this, resolve)
this.store.load(this, resolve, reject)
} else {
if (callback) {
callback.call(this)
}
}
}
eachNode(callback: (node: Node) => void) {
const arr: Node[] = [this]
while (arr.length) {
const node = arr.shift()!
arr.unshift(...node.childNodes)
callback(node)
}
}
reInitChecked() {
if (this.store.checkStrictly) return
reInitChecked(this)
}
}
export default Node

View File

@@ -180,6 +180,15 @@ export function useDragNodeHandler({ props, ctx, el$, dropIndicator$, store }) {
}
if (dropType !== 'none') {
store.value.registerNode(draggingNodeCopy)
if (store.value.key) {
//restore checkbox state after dragging
draggingNode.node.eachNode((node) => {
store.value.nodesMap[node.data[store.value.key]]?.setChecked(
node.checked,
!store.value.checkStrictly
)
})
}
}
removeClass(dropNode.$el, ns.is('drop-inner'))

View File

@@ -183,6 +183,11 @@ export default defineComponent({
}
)
watch(
() => props.node.childNodes.length,
() => props.node.reInitChecked()
)
watch(
() => props.node.expanded,
(val) => {

View File

@@ -1,5 +1,5 @@
import { config, mount } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { ID_INJECTION_KEY, useId, useIdInjection } from '../use-id'
describe('no injection value', () => {
@@ -73,3 +73,32 @@ describe('with injection value', () => {
expect(wrapper.vm.id).toBe('el-id-1024-0')
})
})
describe('useId warns in non-client environment with default idInjection', async () => {
const mockGetCurrentInstance = vi.fn(() => false)
const mockWarn = vi.fn()
const mockIsClient = false
beforeEach(() => {
vi.resetModules()
vi.doMock('vue', () => ({
getCurrentInstance: mockGetCurrentInstance,
ref: vi.fn(),
computed: vi.fn(),
}))
vi.doMock('@element-plus/utils', () => ({
debugWarn: mockWarn,
isClient: mockIsClient,
}))
})
afterEach(() => {
vi.doUnmock('@element-plus/utils')
vi.doUnmock('vue')
})
it('should warn', async () => {
const { useId: mockUseId } = await import('../use-id')
mockUseId()
expect(mockWarn).toHaveBeenCalled()
})
})

View File

@@ -2,7 +2,7 @@ export default {
name: 'ru',
el: {
breadcrumb: {
label: 'Breadcrumb', // to be translated
label: 'Хлебные крошки',
},
colorpicker: {
confirm: 'OK',
@@ -79,12 +79,12 @@ export default {
pagesize: ' на странице',
total: 'Всего {total}',
pageClassifier: '',
page: 'Page', // to be translated
prev: 'Go to previous page', // to be translated
next: 'Go to next page', // to be translated
currentPage: 'page {pager}', // to be translated
prevPages: 'Previous {pager} pages', // to be translated
nextPages: 'Next {pager} pages', // to be translated
page: 'Страница',
prev: 'Перейти на предыдущую страницу',
next: 'Перейти на следующую страницу',
currentPage: 'страница {pager}',
prevPages: 'Предыдущие {pager} страниц',
nextPages: 'Следующие {pager} страниц',
},
messagebox: {
title: 'Сообщение',
@@ -122,19 +122,19 @@ export default {
hasCheckedFormat: '{checked}/{total} выбрано',
},
image: {
error: 'FAILED', // to be translated
error: 'ОШИБКА',
},
pageHeader: {
title: 'Back', // to be translated
title: 'Назад',
},
popconfirm: {
confirmButtonText: 'OK',
cancelButtonText: 'Отмена',
},
carousel: {
leftArrow: 'Carousel arrow left', // to be translated
rightArrow: 'Carousel arrow right', // to be translated
indicator: 'Carousel switch to index {index}', // to be translated
leftArrow: 'Слайдер стрелка влево',
rightArrow: 'Слайдер стрелка вправо',
indicator: 'Слайдер перейти на страницу под номером {index}',
},
},
}

View File

@@ -29,6 +29,12 @@
justify-content: center;
}
@include when(plain) {
background-color: getCssVar('bg-color', 'overlay');
border-color: getCssVar('bg-color', 'overlay');
box-shadow: getCssVar('box-shadow-light');
}
p {
margin: 0;
}

View File

@@ -0,0 +1,11 @@
import { describe, expect, it, vi } from 'vitest'
import { composeRefs } from '../..'
describe('composeRefs', () => {
it('ref setter should be called', () => {
const refSetter = vi.fn()
const fn = composeRefs(refSetter)
fn(null)
expect(refSetter).toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,19 @@
import { describe, expect, it } from 'vitest'
import { getComponentSize } from '../..'
describe('vue size', () => {
it('default value', () => {
const value1 = getComponentSize()
const value2 = getComponentSize('default')
expect(value1).toBe(32)
expect(value2).toBe(32)
})
it('small value', () => {
const value = getComponentSize('small')
expect(value).toBe(24)
})
it('large value', () => {
const value = getComponentSize('large')
expect(value).toBe(40)
})
})

View File

@@ -0,0 +1,25 @@
import { describe, expect, it } from 'vitest'
import { isValidComponentSize, isValidDatePickType } from '../..'
describe('validator', () => {
it('isValidComponentSize', () => {
expect(isValidComponentSize('')).toBe(true)
expect(isValidComponentSize('default')).toBe(true)
expect(isValidComponentSize('small')).toBe(true)
expect(isValidComponentSize('large')).toBe(true)
expect(isValidComponentSize('unknown')).toBe(false)
})
it('isValidDatePickType', () => {
expect(isValidDatePickType('year')).toBe(true)
expect(isValidDatePickType('years')).toBe(true)
expect(isValidDatePickType('month')).toBe(true)
expect(isValidDatePickType('date')).toBe(true)
expect(isValidDatePickType('dates')).toBe(true)
expect(isValidDatePickType('week')).toBe(true)
expect(isValidDatePickType('datetime')).toBe(true)
expect(isValidDatePickType('datetimerange')).toBe(true)
expect(isValidDatePickType('daterange')).toBe(true)
expect(isValidDatePickType('monthrange')).toBe(true)
expect(isValidDatePickType('moment')).toBe(false)
})
})