Merge pull request #19258 from element-plus/dev

D2M
This commit is contained in:
iamkun 2024-12-13 18:18:59 +08:00 committed by GitHub
commit 4ea2be7966
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
52 changed files with 343 additions and 168 deletions

View File

@ -13,7 +13,6 @@ on:
jobs:
build:
runs-on: ubuntu-latest
if: ${{ github.repository == 'element-plus/element-plus' }}
steps:
- name: Checkout code
uses: actions/checkout@v4

View File

@ -1,5 +1,30 @@
## Changelog
### 2.9.1
_2024-12-13_
#### Features
- Components [dropdown] add triggerKeys attribute (#19124 by @hanchao-c0ldwave)
- Components [tree-v2] `filter-method` support third parameter (#19177 by @btea)
- Components [badge] add content slot (#18922 by @xing403)
#### Bug fixes
- Components [input-tag] trigger the add of tag when Chinese is Composing (#19079 by @tolking)
- Components [notification] display html as string (#19068 by @Dsaquel)
- Components [select] use option.isDisabled as the source of truth (#19137 by @makedopamine)
- Components [input] disabled state wrapper cursor style (#19176 by @btea)
- Components [select-v2] empty value check error in inputRef (#19140 by @Liao-js)
- Components [select] modify the logic of update the watch option (#18931 by @YiMo1)
- Components [table-v2] dynamic height causes overall calculation errors (#19082 by @hanchao-c0ldwave)
- Revert pkg pr new to main version (#19254 by @Aslemammad)
#### Refactors
- Components [alert] introduce hasDesc helper (#19085 by @zhangenming)
### 2.9.0
_2024-11-29_

View File

@ -100,11 +100,6 @@ You can also try Element Plus out with the components built-in playground.
<img width="130px" src="https://user-images.githubusercontent.com/17680888/173179536-30e35fd1-cd5a-482a-bc41-9d5f0aa66fd4.png">
</a>
</td>
<td align="center" valign="middle">
<a href="http://www.i-renderer.love/home/index" target="_blank">
<img width="130px" src="https://github.com/element-plus/element-plus/assets/82012629/0004917d-71ad-48f9-b3ce-9299f0ff78c6">
</a>
</td>
<td align="center" valign="middle">
<a href="https://bit.dev/?from=element-ui" target="_blank">
<img width="130px" src="https://user-images.githubusercontent.com/10095631/41342907-e44e7196-6f2f-11e8-92f2-47702dc8f059.png">

View File

@ -21,14 +21,6 @@ export const rightLogoSmallSponsors = [
slogan: 'Vue3 open source admin system',
slogan_cn: 'Vue3企业级开源后台管理系统',
},
{
name: '百搭云',
img: '/images/baidayun-logo.png',
imgL: '/images/baidayun.png',
url: 'http://www.i-renderer.love/home/index',
slogan: 'Fast and elegant low-code dev platform',
slogan_cn: '快速且优雅的低代码平台',
},
{
name: 'bit',
img: '/images/bit.svg',

View File

@ -72,6 +72,7 @@ import OvTooltip from './ov-tooltip.vue'
import OvDivider from './ov-divider.vue'
import OvWatermark from './ov-watermark.vue'
import OvMention from './ov-mention.vue'
import OvInputTag from './ov-input-tag.vue'
export default {
button: OvButton,
@ -95,6 +96,7 @@ export default {
form: OvForm,
input: OvInput,
'input-number': OvInputNumber,
'input-tag': OvInputTag,
radio: OvRadio,
rate: OvRate,
select: OvSelect,

View File

@ -0,0 +1,44 @@
<template>
<svg
width="270"
height="170"
viewBox="0 0 270 170"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="270" height="170" rx="6" fill="var(--el-fill-color-light)" />
<rect
x="85.5"
y="75.5"
width="99"
height="19"
rx="1.5"
fill="var(--el-fill-color-blank)"
stroke="var(--el-color-primary)"
/>
<rect
x="89"
y="79"
width="25"
height="11.111"
rx="1.852"
fill="var(--el-color-primary-light-8)"
/>
<path
d="M92.787 82.422h3.25v.556h-1.3v3.411h-.65v-3.411h-1.3v-.555zm4.973 1.017c.423 0 .74.106.94.317.177.183.266.45.266.8v1.833h-.589v-.378c-.111.145-.25.25-.411.328a1.56 1.56 0 0 1-.65.128c-.3 0-.533-.078-.706-.228a.746.746 0 0 1-.266-.589c0-.322.122-.572.378-.744.233-.167.56-.256.983-.267l.628-.016v-.112c0-.377-.206-.56-.617-.56-.178 0-.317.027-.422.094a.499.499 0 0 0-.24.333l-.627-.05c.061-.317.217-.55.467-.694.21-.134.5-.195.866-.195zm.573 1.639l-.584.017c-.505.01-.755.189-.755.533 0 .1.039.183.128.25.088.067.2.106.338.106.245 0 .45-.073.617-.212a.701.701 0 0 0 .256-.55v-.144zm2.496-1.639c.355 0 .639.139.85.428v-.35h.633v2.689c0 .905-.45 1.36-1.35 1.36-.406 0-.711-.077-.922-.221-.211-.156-.345-.39-.4-.711h.633c.033.16.1.272.206.338.1.062.261.095.483.095.478 0 .717-.256.717-.767v-.422a.997.997 0 0 1-.85.445c-.389 0-.706-.134-.945-.39-.244-.26-.36-.61-.36-1.044 0-.433.116-.783.36-1.05.239-.266.556-.4.945-.4zm.1.511c-.239 0-.423.084-.556.25-.133.161-.2.39-.2.69 0 .271.05.483.156.638.122.183.316.278.594.278.245 0 .433-.084.567-.24.128-.166.194-.388.194-.677 0-.294-.066-.522-.194-.689-.134-.166-.322-.25-.561-.25zM109.356 83.482l-.912.911-.911-.911a.12.12 0 0 0-.082-.033.108.108 0 0 0-.079.035.11.11 0 0 0-.035.08c0 .03.011.057.033.08l.911.912-.911.912c-.031.03-.042.068-.031.112.011.043.037.07.08.081.042.01.08 0 .114-.03l.911-.912.912.911a.118.118 0 0 0 .081.033.11.11 0 0 0 .08-.035.109.109 0 0 0 .034-.08.117.117 0 0 0-.033-.08l-.911-.912.911-.912a.11.11 0 0 0 .031-.112.105.105 0 0 0-.081-.081.11.11 0 0 0-.112.03z"
fill="var(--el-color-primary)"
/>
<rect
x="117"
y="79"
width="25"
height="11.111"
rx="1.852"
fill="var(--el-color-primary-light-8)"
/>
<path
d="M120.787 82.422h3.25v.556h-1.3v3.411h-.65v-3.411h-1.3v-.555zm4.973 1.017c.423 0 .739.106.939.317.178.183.267.45.267.8v1.833h-.589v-.378c-.111.145-.25.25-.411.328-.183.084-.4.128-.65.128-.3 0-.533-.078-.706-.228a.748.748 0 0 1-.266-.589c0-.322.122-.572.378-.744.233-.167.561-.256.983-.267l.628-.016v-.112c0-.377-.206-.56-.617-.56-.178 0-.317.027-.422.094a.498.498 0 0 0-.239.333l-.628-.05c.061-.317.217-.55.467-.694.211-.134.5-.195.866-.195zm.573 1.639l-.584.017c-.505.01-.755.189-.755.533 0 .1.039.183.128.25.088.067.2.106.338.106.245 0 .45-.073.617-.212a.7.7 0 0 0 .256-.55v-.144zm2.496-1.639c.355 0 .639.139.85.428v-.35h.633v2.689c0 .905-.45 1.36-1.35 1.36-.406 0-.711-.077-.922-.221-.211-.156-.345-.39-.4-.711h.633c.033.16.1.272.206.338.1.062.261.095.483.095.478 0 .717-.256.717-.767v-.422a.997.997 0 0 1-.85.445c-.389 0-.706-.134-.945-.39-.244-.26-.361-.61-.361-1.044 0-.433.117-.783.361-1.05.239-.266.556-.4.945-.4zm.1.511c-.239 0-.423.084-.556.25-.133.161-.2.39-.2.69 0 .271.05.483.156.638.122.183.316.278.594.278.245 0 .433-.084.567-.24.128-.166.194-.388.194-.677 0-.294-.066-.522-.194-.689-.134-.166-.322-.25-.561-.25zM137.356 83.482l-.912.911-.911-.911a.12.12 0 0 0-.082-.033.108.108 0 0 0-.079.035.11.11 0 0 0-.035.08c0 .03.011.057.033.08l.911.912-.911.912c-.031.03-.042.068-.031.112.011.043.037.07.08.081.042.01.08 0 .114-.03l.911-.912.912.911a.118.118 0 0 0 .081.033.11.11 0 0 0 .08-.035.109.109 0 0 0 .034-.08.117.117 0 0 0-.033-.08l-.911-.912.911-.912a.11.11 0 0 0 .031-.112.105.105 0 0 0-.081-.081.11.11 0 0 0-.112.03z"
fill="var(--el-color-primary)"
/>
</svg>
</template>

View File

@ -10,7 +10,7 @@ const sponsor = computed(() => sponsorLocale[lang.value])
</script>
<template>
<div class="page-sidebar-group">
<div class="page-sidebar-table">
<p class="title">{{ sponsor.sponsoredBy }}</p>
<VPSponsorLarge />
<VPSponsorSmall />
@ -18,7 +18,7 @@ const sponsor = computed(() => sponsorLocale[lang.value])
</template>
<style lang="scss" scoped>
.page-sidebar-group {
.page-sidebar-table {
padding-bottom: 10px;
padding-top: 0;
.title {

View File

@ -79,6 +79,7 @@ declare module 'vue' {
OvInfiniteScroll: typeof import('./.vitepress/vitepress/components/overview-icons/ov-infinite-scroll.vue')['default']
OvInput: typeof import('./.vitepress/vitepress/components/overview-icons/ov-input.vue')['default']
OvInputNumber: typeof import('./.vitepress/vitepress/components/overview-icons/ov-input-number.vue')['default']
OvInputTag: typeof import('./.vitepress/vitepress/components/overview-icons/ov-input-tag.vue')['default']
OvLayout: typeof import('./.vitepress/vitepress/components/overview-icons/ov-layout.vue')['default']
OvLink: typeof import('./.vitepress/vitepress/components/overview-icons/ov-link.vue')['default']
OvLoading: typeof import('./.vitepress/vitepress/components/overview-icons/ov-loading.vue')['default']

View File

@ -29,9 +29,9 @@ badge/max
## Customizations
Displays text content other than numbers.
Displays text content other than numbers. Or you can use the `content` slot to customize content.
:::demo When value is a String, it can display customized text.
:::demo When value is a String, it can display customized text. Or use the `content` slot.
badge/customize
@ -74,6 +74,7 @@ badge/offset
### Slots
| Name | Description |
| ------- | ------------------------- |
| default | customize default content |
| Name | Description | Type |
| ---------------- | ------------------------- | ---------------------------- |
| default | customize default content | - |
| content ^(2.9.1) | customize barge content | ^[object]`{ value: string }` |

View File

@ -31,7 +31,7 @@ card/simple
Display richer content by adding some configs.
:::demo The `body-style` attribute defines CSS style of custom `body`. This example also uses `el-col` for layout.
:::demo The `body-style` attribute defines CSS style of custom `body`.
card/with-images

View File

@ -92,7 +92,7 @@ dropdown/sizes
### Dropdown Attributes
| Name | Description | Type | Default |
| -------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- |
| -------------------- |-----------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- |
| type | menu button type, refer to `Button` Component, only works when `split-button` is true | ^[enum]`'' \| 'default' \| 'primary' \| 'success' \| 'warning' \| 'info' \| 'danger' \| 'text' (deprecated)` | '' |
| size | menu size, also works on the split button | ^[enum]`'' \| 'large' \| 'default' \| 'small'` | '' |
| max-height | the max height of menu | ^[string] / ^[number] | '' |
@ -100,6 +100,7 @@ dropdown/sizes
| disabled | whether to disable | ^[boolean] | false |
| placement | placement of pop menu | ^[enum]`'top' \| 'top-start' \| 'top-end' \| 'bottom' \| 'bottom-start' \| 'bottom-end'` | bottom |
| trigger | how to trigger | ^[enum]`'hover' \| 'click' \| 'contextmenu'` | hover |
| triggerKeys ^(2.9.1) | specify whick keys on the keyboard can trigger when pressed | ^[array]`string[]` | `['Enter', 'Space', 'ArrowDown', 'NumpadEnter']`
| 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] | 150 |
| hide-timeout | delay time before hide a dropdown (only works when trigger is `hover`) | ^[number] | 150 |

View File

@ -249,7 +249,7 @@ table/table-layout
| Name | Description | Type | Default |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| data | table data | ^[object]`any[]` | [] |
| data | table data | ^[array]`any[]` | [] |
| height | table's height. By default it has an `auto` height. If its value is a number, the height is measured in pixels; if its value is a string, the value will be assigned to element's style.height, the height is affected by external styles | ^[string] / ^[number] | — |
| max-height | table's max-height. The legal value is a number or the height in px | ^[string] / ^[number] | — |
| stripe | whether Table is striped | ^[boolean] | false |
@ -270,7 +270,7 @@ table/table-layout
| row-key | key of row data, used for optimizing rendering. Required if `reserve-selection` is on or display tree data. When its type is String, multi-level access is supported, e.g. `user.info.id`, but `user.info[0].id` is not supported, in which case `Function` should be used | ^[function]`(row: any) => string` / ^[string] | — |
| empty-text | displayed text when data is empty. You can customize this area with `#empty` | ^[string] | No Data |
| default-expand-all | whether expand all rows by default, works when the table has a column type="expand" or contains tree structure data | ^[boolean] | false |
| expand-row-keys | set expanded rows by this prop, prop's value is the keys of expand rows, you should set row-key before using this prop | ^[Array]`string[]` | — |
| expand-row-keys | set expanded rows by this prop, prop's value is the keys of expand rows, you should set row-key before using this prop | ^[array]`string[]` | — |
| default-sort | set the default sort column and order. property `prop` is used to set default sort column, property `order` is used to set default sort order | ^[object]`Sort` | if `prop` is set, and `order` is not set, then `order` is default to ascending |
| tooltip-effect | the `effect` of the overflow tooltip | ^[enum]`'dark' \| 'light'` | dark |
| tooltip-options ^(2.2.28) | the options for the overflow tooltip, [see the following tooltip component](tooltip.html#attributes) | ^[object]`Pick<ElTooltipProps, 'effect' \| 'enterable' \| 'hideAfter' \| 'offset' \| 'placement' \| 'popperClass' \| 'popperOptions' \| 'showAfter' \| 'showArrow'>` | ^[object]`{ enterable: true, placement: 'top', showArrow: true, hideAfter: 200, popperOptions: { strategy: 'fixed' } }` |
@ -370,7 +370,7 @@ table/table-layout
| label-class-name | class name of the label of this column | ^[string] | — |
| selectable | function that determines if a certain row can be selected, works when `type` is 'selection' | ^[Function]`(row: any, index: number) => boolean` | — |
| reserve-selection | whether to reserve selection after data refreshing, works when `type` is 'selection'. Note that `row-key` is required for this to work | ^[boolean] | false |
| filters | an array of data filtering options. For each element in this array, `text` and `value` are required | ^[object]`Array<{text: string, value: string}>` | — |
| filters | an array of data filtering options. For each element in this array, `text` and `value` are required | ^[array]`Array<{text: string, value: string}>` | — |
| filter-placement | placement for the filter dropdown | ^[enum]`'top' \| 'top-start' \| 'top-end' \| 'bottom' \| 'bottom-start' \| 'bottom-end' \| 'left' \| 'left-start' \| 'left-end' \| 'right' \| 'right-start' \| 'right-end'` | — |
| filter-class-name ^(2.5.0) | className for the filter dropdown | ^[string] | — |
| filter-multiple | whether data filtering supports multiple options | ^[boolean] | true |

View File

@ -132,7 +132,13 @@ tooltip/controlled
## Animations
Tooltip can be customized animated, you can set the desired animation function as you desired.
Tooltip can be customized animated, you can set the desired animation use `transition`.
:::tip
Transition Classes, more information can be found at [Vue Transition](https://vuejs.org/guide/built-ins/transition.html#css-based-transitions).
:::
:::demo

View File

@ -69,6 +69,9 @@ tree-v2/custom-node-class
## Tree node filtering
::: ^(2.9.1)
The `filter-method` method can only accept the third parameter after version 2.9.1.
:::
Tree nodes can be filtered
:::demo Invoke the `filter` method of the Tree instance to filter tree nodes. Its parameter is the filtering keyword. Note that for it to work, `filter-method` is required, and its value is the filtering method.
@ -79,33 +82,33 @@ tree-v2/filter
## TreeV2 Attributes
| Name | Description | Type | Default |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | ------- |
| data | tree data | array | — |
| empty-text | text displayed when data is void | string | — |
| props | configuration options, see the following table | object | — |
| highlight-current | whether current node is highlighted | boolean | false |
| expand-on-click-node | whether to expand or collapse node when clicking on the node, if false, then expand or collapse node only when clicking on the arrow icon. | boolean | true |
| check-on-click-node | whether to check or uncheck node when clicking on the node, if false, the node can only be checked or unchecked by clicking on the checkbox. | boolean | false |
| default-expanded-keys | array of keys of initially expanded nodes | array | — |
| show-checkbox | whether node is selectable | boolean | false |
| check-strictly | whether checked state of a node not affects its father and child nodes when `show-checkbox` is `true` | boolean | false |
| default-checked-keys | array of keys of initially checked nodes | array | — |
| current-node-key | key of initially selected node | string / number | — |
| filter-method | this function will be executed on each node when use filter method. if return `false`, tree node will be hidden. | Function(value, data) | — |
| indent | horizontal indentation of nodes in adjacent levels in pixels | number | 16 |
| icon | custom tree node icon | `string \| Component` | — |
| item-size ^(2.2.33) | custom tree node height | number | 26 |
| Name | Description | Type | Default |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | ------- |
| data | tree data | array | — |
| empty-text | text displayed when data is void | string | — |
| props | configuration options, see the following table | object | — |
| highlight-current | whether current node is highlighted | boolean | false |
| expand-on-click-node | whether to expand or collapse node when clicking on the node, if false, then expand or collapse node only when clicking on the arrow icon. | boolean | true |
| check-on-click-node | whether to check or uncheck node when clicking on the node, if false, the node can only be checked or unchecked by clicking on the checkbox. | boolean | false |
| default-expanded-keys | array of keys of initially expanded nodes | array | — |
| show-checkbox | whether node is selectable | boolean | false |
| check-strictly | whether checked state of a node not affects its father and child nodes when `show-checkbox` is `true` | boolean | false |
| default-checked-keys | array of keys of initially checked nodes | array | — |
| current-node-key | key of initially selected node | string / number | — |
| filter-method | this function will be executed on each node when use filter method. if return `false`, tree node will be hidden. | Function(value, data, node) | — |
| indent | horizontal indentation of nodes in adjacent levels in pixels | number | 16 |
| icon | custom tree node icon | `string \| Component` | — |
| item-size ^(2.2.33) | custom tree node height | number | 26 |
## props
| Attribute | Description | Type | Default |
| -------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------ | -------- |
| value | unique identity key name for nodes, its value should be unique across the whole tree | string | id |
| label | specify which key of node object is used as the node's label | string | label |
| children | specify which node object is used as the node's subtree | string | children |
| disabled | specify which key of node object represents if node's checkbox is disabled | string | disabled |
| class ^(2.9.0) | custom node class name | ^[string] \| ^[Function]`(data, node) => string` | — |
| Attribute | Description | Type | Default |
| -------------- | ------------------------------------------------------------------------------------ | ----------------------------------------------- | -------- |
| value | unique identity key name for nodes, its value should be unique across the whole tree | string | id |
| label | specify which key of node object is used as the node's label | string | label |
| children | specify which node object is used as the node's subtree | string | children |
| disabled | specify which key of node object represents if node's checkbox is disabled | string | disabled |
| class ^(2.9.0) | custom node class name | ^[string] / ^[Function]`(data, node) => string` | — |
## TreeV2 Method

View File

@ -5,11 +5,31 @@
<el-badge value="hot" class="item">
<el-button>replies</el-button>
</el-badge>
<el-badge value="99" class="item">
<el-button>share</el-button>
<template #content="{ value }">
<div class="custom-content">
<el-icon>
<Message />
</el-icon>
<span>{{ value }}</span>
</div>
</template>
</el-badge>
</template>
<script setup lang="ts">
import { Message } from '@element-plus/icons-vue'
</script>
<style scoped>
.item {
margin-top: 10px;
margin-right: 40px;
}
.custom-content {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
</style>

View File

@ -3,6 +3,7 @@
<el-container>
<el-aside width="200px">Aside</el-aside>
<el-main>Main</el-main>
<el-aside width="200px">Aside</el-aside>
</el-container>
</div>
</template>

View File

@ -16,17 +16,3 @@ import { ref } from 'vue'
const disabled = ref(false)
</script>
<style>
.slide-fade-enter-active {
transition: all 0.3s ease;
}
.slide-fade-leave-active {
transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-enter,
.expand-fade-leave-active {
margin-left: 20px;
opacity: 0;
}
</style>

View File

@ -1,7 +1,22 @@
<template>
<el-tooltip content="I am an el-tooltip">
<el-tooltip content="I am an el-tooltip" transition="slide-fade">
<el-button>trigger me</el-button>
</el-tooltip>
</template>
<script lang="ts" setup></script>
<style>
.slide-fade-enter-active {
transition: all 0.3s ease-out;
}
.slide-fade-leave-active {
transition: all 0.8s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateX(120px);
opacity: 0;
}
</style>

View File

@ -5,18 +5,21 @@
:class="[ns.b(), ns.m(type), ns.is('center', center), ns.is(effect)]"
role="alert"
>
<el-icon v-if="showIcon && iconComponent" :class="iconClass">
<el-icon
v-if="showIcon && iconComponent"
:class="[ns.e('icon'), { [ns.is('big')]: hasDesc }]"
>
<component :is="iconComponent" />
</el-icon>
<div :class="ns.e('content')">
<span
v-if="title || $slots.title"
:class="[ns.e('title'), withDescription]"
:class="[ns.e('title'), { 'with-description': hasDesc }]"
>
<slot name="title">{{ title }}</slot>
</span>
<p v-if="$slots.default || description" :class="ns.e('description')">
<p v-if="hasDesc" :class="ns.e('description')">
<slot>
{{ description }}
</slot>
@ -60,14 +63,7 @@ const visible = ref(true)
const iconComponent = computed(() => TypeComponentsMap[props.type])
const iconClass = computed(() => [
ns.e('icon'),
{ [ns.is('big')]: !!props.description || !!slots.default },
])
const withDescription = computed(() => {
return { 'with-description': props.description || slots.default }
})
const hasDesc = computed(() => !!(props.description || slots.default))
const close = (evt: MouseEvent) => {
visible.value = false

View File

@ -1,6 +1,8 @@
import { nextTick, ref } from 'vue'
import { mount } from '@vue/test-utils'
import { describe, expect, test } from 'vitest'
import { InfoFilled } from '@element-plus/icons-vue'
import { ElIcon } from '@element-plus/components/icon'
import Badge from '../src/badge.vue'
const AXIOM = 'Rem is the best girl'
@ -146,4 +148,23 @@ describe('Badge', () => {
'margin-top: 10px'
)
})
test('content slot', () => {
const wrapper = mount(() => (
<Badge
value={99}
v-slots={{
content: ({ value }: { value: string }) => (
<div class="custom">
<ElIcon>
<InfoFilled />
</ElIcon>
<span>{value}</span>
</div>
),
}}
/>
))
expect(wrapper.find('.el-badge__content .custom').exists()).toBe(true)
})
})

View File

@ -3,7 +3,7 @@
<slot />
<transition :name="`${ns.namespace.value}-zoom-in-center`">
<sup
v-show="!hidden && (content || isDot)"
v-show="!hidden && (content || isDot || $slots.content)"
:class="[
ns.e('content'),
ns.em('content', type),
@ -13,8 +13,11 @@
badgeClass,
]"
:style="style"
v-text="content"
/>
>
<slot name="content" :value="content">
{{ content }}
</slot>
</sup>
</transition>
</div>
</template>

View File

@ -30,6 +30,15 @@ export const dropdownProps = buildProps({
* @description how to trigger
*/
trigger: useTooltipTriggerProps.trigger,
triggerKeys: {
type: definePropType<string[]>(Array),
default: () => [
EVENT_CODE.enter,
EVENT_CODE.numpadEnter,
EVENT_CODE.space,
EVENT_CODE.down,
],
},
effect: {
...useTooltipContentProps.effect,
default: 'light',

View File

@ -112,7 +112,6 @@ import { ElOnlyChild } from '@element-plus/components/slot'
import { useFormSize } from '@element-plus/components/form'
import { addUnit, ensureArray } from '@element-plus/utils'
import { ArrowDown } from '@element-plus/icons-vue'
import { EVENT_CODE } from '@element-plus/constants'
import { useId, useLocale, useNamespace } from '@element-plus/hooks'
import { ElCollection as ElDropdownCollection, dropdownProps } from './dropdown'
import { DROPDOWN_INJECTION_KEY } from './tokens'
@ -148,12 +147,6 @@ export default defineComponent({
const scrollbar = ref(null)
const currentTabId = ref<string | null>(null)
const isUsingKeyboard = ref(false)
const triggerKeys = [
EVENT_CODE.enter,
EVENT_CODE.numpadEnter,
EVENT_CODE.space,
EVENT_CODE.down,
]
const wrapStyle = computed<CSSProperties>(() => ({
maxHeight: addUnit(props.maxHeight),
@ -300,7 +293,6 @@ export default defineComponent({
dropdownTriggerKls,
dropdownSize,
triggerId,
triggerKeys,
currentTabId,
handleCurrentTabIdChange,
handlerMainButtonClick,

View File

@ -14,7 +14,7 @@ import { rAF } from '@element-plus/test-utils/tick'
import Input from '@element-plus/components/input'
import Form from '../src/form.vue'
import FormItem from '../src/form-item.vue'
import DynamicFormItem from '../mocks/mock-data'
import DynamicFormItem from './mock-data'
import type { VueWrapper } from '@vue/test-utils'
import type { MockInstance } from 'vitest'

View File

@ -19,7 +19,7 @@ import {
import Input from '@element-plus/components/input'
import Form from '../src/form.vue'
import FormItem from '../src/form-item.vue'
import DynamicDomainForm, { formatDomainError } from '../mocks/mock-data'
import DynamicDomainForm, { formatDomainError } from './mock-data'
import type { VueWrapper } from '@vue/test-utils'
import type { FormRules } from '@element-plus/components/form'

View File

@ -51,6 +51,7 @@ export function useInputTag({ props, emit, formItem }: UseInputTagOptions) {
}
const handleKeydown = (event: KeyboardEvent) => {
if (isComposing.value) return
switch (event.code) {
case props.trigger:
event.preventDefault()

View File

@ -140,7 +140,7 @@ export const menuProps = buildProps({
* @description Tooltip theme, built-in theme: `dark` / `light` when menu is collapsed
*/
popperEffect: {
type: definePropType<PopperEffect | string>(String),
type: definePropType<PopperEffect>(String),
default: 'dark',
},
/**

View File

@ -1,4 +1,4 @@
import { createVNode, render } from 'vue'
import { createVNode, isVNode, render } from 'vue'
import {
debugWarn,
hasOwn,
@ -8,7 +8,6 @@ import {
isObject,
isString,
isUndefined,
isVNode,
} from '@element-plus/utils'
import MessageBoxConstructor from './index.vue'

View File

@ -1,4 +1,4 @@
import { createVNode, render } from 'vue'
import { createVNode, isVNode, render } from 'vue'
import {
debugWarn,
isBoolean,
@ -7,7 +7,6 @@ import {
isFunction,
isNumber,
isString,
isVNode,
} from '@element-plus/utils'
import { messageConfig } from '@element-plus/components/config-provider'
import MessageConstructor from './message.vue'

View File

@ -40,6 +40,18 @@ describe('Notification on command', () => {
close()
})
it('it should be able to render function that return vnode', async () => {
const testClassName = 'test-classname'
const { close } = Notification({
duration: 0,
message: () => <div class={testClassName}>test-content</div>,
})
await rAF()
expect(document.querySelector(`.${testClassName}`)).toBeTruthy()
close()
})
it('it should be able to close notification by manually close', async () => {
const { close } = Notification({
duration: 0,
@ -137,4 +149,31 @@ describe('Notification on command', () => {
expect(onClose).toHaveBeenCalledTimes(1)
expect(onClose).toHaveLastReturnedWith(localContext)
})
it('set dangerouslyUseHTMLString should render html string', async () => {
const htmlString = '<div class="test-html-string">test-html-string</div>'
const { close } = Notification({
duration: 0,
message: htmlString,
dangerouslyUseHTMLString: true,
})
await rAF()
expect(document.querySelector('.test-html-string')).toBeDefined()
close()
})
it('not set dangerouslyUseHTMLString should render text', async () => {
const text = '<div class="test-html-string">test-html-string</div>'
const { close } = Notification({
duration: 0,
message: text,
})
await rAF()
expect(
document.querySelector('.el-notification__content')!.textContent
).toBe(text)
close()
})
})

View File

@ -1,4 +1,4 @@
import { createVNode, render } from 'vue'
import { createVNode, isVNode, render } from 'vue'
import {
debugWarn,
isClient,
@ -6,7 +6,6 @@ import {
isFunction,
isString,
isUndefined,
isVNode,
} from '@element-plus/utils'
import NotificationConstructor from './notification.vue'
import { notificationTypes } from './notification'
@ -82,7 +81,11 @@ const notify: NotifyFn & Partial<Notify> = function (options = {}, context) {
const vm = createVNode(
NotificationConstructor,
props,
isFunction(props.message) ? props.message : () => props.message
isFunction(props.message)
? props.message
: isVNode(props.message)
? () => props.message
: null
)
vm.appContext = isUndefined(context) ? notify._context : context

View File

@ -74,7 +74,7 @@ export const popperContentProps = buildProps({
type: definePropType<ClassType>([String, Array, Object]),
},
effect: {
type: definePropType<PopperEffect | string>(String),
type: definePropType<PopperEffect>(String),
default: 'dark',
},
visible: Boolean,

View File

@ -24,7 +24,9 @@ export const roleTypes = [
'tree',
] as const
export type PopperEffect = typeof effects[number]
export type PopperEffect =
| typeof effects[number]
| (string & NonNullable<unknown>)
export type PopperTrigger = typeof triggers[number]
export const popperProps = buildProps({

View File

@ -1371,6 +1371,31 @@ describe('Select', () => {
expect(placeholder.text()).toBe('option_a')
})
it('the scroll position of the dropdown should be correct when value is 0', async () => {
const options = Array.from({ length: 1000 }).map((_, idx) => ({
value: 999 - idx,
label: `options ${999 - idx}`,
}))
const wrapper = createSelect({
data() {
return {
value: 0,
options,
}
},
})
await nextTick()
await wrapper.find(`.${WRAPPER_CLASS_NAME}`).trigger('click')
const optionsDoms = Array.from(
document.querySelectorAll(`.${OPTION_ITEM_CLASS_NAME}`)
)
const result = optionsDoms.some((option) => {
const text = option.textContent
return text === 'options 0'
})
expect(result).toBeTruthy()
})
it('emptyText error show', async () => {
const wrapper = createSelect({
data() {

View File

@ -57,7 +57,7 @@ export const SelectProps = buildProps({
* @description tooltip theme, built-in theme: `dark` / `light`
*/
effect: {
type: definePropType<PopperEffect | string>(String),
type: definePropType<PopperEffect>(String),
default: 'light',
},
/**

View File

@ -330,7 +330,7 @@ const useSelect = (props: ISelectV2Props, emit: SelectEmitFn) => {
}
} else {
if (
props.modelValue &&
!isEmptyValue(props.modelValue) &&
filteredOptionsValueMap.value.has(props.modelValue)
) {
const { index } = filteredOptionsValueMap.value.get(props.modelValue)

View File

@ -905,6 +905,23 @@ describe('Select', () => {
expect(selectVm.states.hoveringIndex).toBe(4)
})
// #19136
test('keyboard operations when options are disabled due to multiple-limit', async () => {
wrapper = getSelectVm({ multiple: true, multipleLimit: 2 })
const select = wrapper.findComponent({ name: 'ElSelect' })
await wrapper.setProps({
modelValue: ['选项1', '选项2'],
})
const selectVm = select.vm as any
const input = select.find('input')
await input.trigger('click')
expect(selectVm.states.hoveringIndex).toBe(0)
selectVm.navigateOptions('next')
expect(selectVm.states.hoveringIndex).toBe(1)
selectVm.navigateOptions('next')
expect(selectVm.states.hoveringIndex).toBe(0)
})
test('clearable', async () => {
wrapper = getSelectVm({ clearable: true })
const select = wrapper.findComponent({ name: 'ElSelect' })

View File

@ -49,7 +49,7 @@ export const SelectProps = buildProps({
* @description tooltip theme, built-in theme: `dark` / `light`
*/
effect: {
type: definePropType<PopperEffect | string>(String),
type: definePropType<PopperEffect>(String),
default: 'light',
},
/**

View File

@ -312,15 +312,7 @@ export const useSelect = (props: ISelectProps, emit) => {
() => {
if (!isClient) return
// tooltipRef.value?.updatePopper?.()
const inputs = selectRef.value?.querySelectorAll('input') || []
if (
(!props.filterable &&
!props.defaultFirstOption &&
!isUndefined(props.modelValue)) ||
!Array.from(inputs).includes(document.activeElement as HTMLInputElement)
) {
setSelected()
}
setSelected()
if (
props.defaultFirstOption &&
(props.filterable || props.remote) &&
@ -697,7 +689,7 @@ export const useSelect = (props: ISelectProps, emit) => {
toggleMenu()
} else {
const option = optionsArray.value[states.hoveringIndex]
if (option && !option.disabled && !option.states.groupDisabled) {
if (option && !option.isDisabled) {
handleOptionSelect(option)
}
}
@ -710,7 +702,7 @@ export const useSelect = (props: ISelectProps, emit) => {
const optionsAllDisabled = computed(() =>
optionsArray.value
.filter((option) => option.visible)
.every((option) => option.disabled)
.every((option) => option.isDisabled)
)
const showTagList = computed(() => {
@ -756,11 +748,7 @@ export const useSelect = (props: ISelectProps, emit) => {
}
}
const option = optionsArray.value[states.hoveringIndex]
if (
option.disabled === true ||
option.states.groupDisabled === true ||
!option.visible
) {
if (option.isDisabled || !option.visible) {
navigateOptions(direction)
}
nextTick(() => scrollToOption(hoverOption.value))

View File

@ -1,24 +1,23 @@
import { computed, unref } from 'vue'
import { ComputedRef, computed, unref } from 'vue'
import { addUnit, isNumber } from '@element-plus/utils'
import { enforceUnit, sum } from '../utils'
import type { CSSProperties } from 'vue'
import type { TableV2Props } from '../table'
import type { UseColumnsReturn } from './use-columns'
import type { UseDataReturn } from './use-data'
type UseStyleProps = {
columnsTotalWidth: UseColumnsReturn['columnsTotalWidth']
data: UseDataReturn['data']
fixedColumnsOnLeft: UseColumnsReturn['fixedColumnsOnLeft']
fixedColumnsOnRight: UseColumnsReturn['fixedColumnsOnRight']
rowsHeight: ComputedRef<number>
}
export const useStyles = (
props: TableV2Props,
{
columnsTotalWidth,
data,
rowsHeight,
fixedColumnsOnLeft,
fixedColumnsOnRight,
}: UseStyleProps
@ -47,16 +46,6 @@ export const useStyles = (
return height - footerHeight
})
const rowsHeight = computed(() => {
const { rowHeight, estimatedRowHeight } = props
const _data = unref(data)
if (isNumber(estimatedRowHeight)) {
return _data.length * estimatedRowHeight
}
return _data.length * rowHeight
})
const fixedTableHeight = computed(() => {
const { maxHeight } = props
const tableHeight = unref(mainTableHeight)
@ -114,7 +103,6 @@ export const useStyles = (
leftTableWidth,
rightTableWidth,
headerWidth,
rowsHeight,
windowHeight,
footerHeight,
emptyStyle,

View File

@ -7,7 +7,7 @@ import {
unref,
watch,
} from 'vue'
import { isArray } from '@element-plus/utils'
import { isArray, isNumber } from '@element-plus/utils'
import { useNamespace } from '@element-plus/hooks'
import {
useColumns,
@ -84,6 +84,20 @@ function useTable(props: TableV2Props) {
resetAfterIndex,
})
const rowsHeight = computed(() => {
const { estimatedRowHeight, rowHeight } = props
const _data = unref(data)
if (isNumber(estimatedRowHeight)) {
// calculate the actual height
return Object.values(unref(rowHeights)).reduce(
(acc, curr) => acc + curr,
0
)
}
return _data.length * rowHeight
})
const {
bodyWidth,
fixedTableHeight,
@ -91,7 +105,6 @@ function useTable(props: TableV2Props) {
leftTableWidth,
rightTableWidth,
headerWidth,
rowsHeight,
windowHeight,
footerHeight,
emptyStyle,
@ -99,9 +112,9 @@ function useTable(props: TableV2Props) {
headerHeight,
} = useStyles(props, {
columnsTotalWidth,
data,
fixedColumnsOnLeft,
fixedColumnsOnRight,
rowsHeight,
})
// DOM/Component refs

View File

@ -32,7 +32,7 @@ export const timeSelectProps = buildProps({
* @description Tooltip theme, built-in theme: `dark` / `light`
*/
effect: {
type: definePropType<PopperEffect | string>(String),
type: definePropType<PopperEffect>(String),
default: 'light',
},
/**

View File

@ -28,7 +28,7 @@ export const tooltipV2ContentProps = buildProps({
default: 5,
},
effect: {
type: definePropType<PopperEffect | string>(String),
type: definePropType<PopperEffect>(String),
default: 'light',
},
contentClass: String,

View File

@ -80,7 +80,8 @@ export function useCheck(props: TreeProps, tree: Ref<Tree | undefined>) {
const toggleCheckbox = (
node: TreeNode,
isChecked: CheckboxValueType,
nodeClick = true
nodeClick = true,
immediateUpdate = true
) => {
const checkedKeySet = checkedKeys.value
const toggle = (node: TreeNode, checked: CheckboxValueType) => {
@ -97,7 +98,9 @@ export function useCheck(props: TreeProps, tree: Ref<Tree | undefined>) {
}
}
toggle(node, isChecked)
updateCheckedKeys()
if (immediateUpdate) {
updateCheckedKeys()
}
if (nodeClick) {
afterNodeCheck(node, isChecked)
}
@ -196,13 +199,14 @@ export function useCheck(props: TreeProps, tree: Ref<Tree | undefined>) {
function _setCheckedKeys(keys: TreeKey[]) {
if (tree?.value) {
const { treeNodeMap } = tree.value
if (props.showCheckbox && treeNodeMap && keys) {
if (props.showCheckbox && treeNodeMap && keys?.length > 0) {
for (const key of keys) {
const node = treeNodeMap.get(key)
if (node && !isChecked(node)) {
toggleCheckbox(node, true, false)
toggleCheckbox(node, true, false, false)
}
}
updateCheckedKeys()
}
}
}

View File

@ -28,7 +28,7 @@ export function useFilter(props: TreeProps, tree: Ref<Tree | undefined>) {
function traverse(nodes: TreeNode[]) {
nodes.forEach((node) => {
family.push(node)
if (filter?.(query, node.data)) {
if (filter?.(query, node.data, node)) {
family.forEach((member) => {
expandKeySet.add(member.key)
})

View File

@ -48,7 +48,11 @@ export interface Tree {
maxLevel: number
}
export type FilterMethod = (query: string, node: TreeNodeData) => boolean
export type FilterMethod = (
query: string,
data: TreeNodeData,
node: TreeNode
) => boolean
export interface CheckedInfo {
checkedKeys: TreeKey[]

View File

@ -1,5 +1,5 @@
import { shallowRef } from 'vue'
import { flattedChildren, isVNode } from '@element-plus/utils'
import { isVNode, shallowRef } from 'vue'
import { flattedChildren } from '@element-plus/utils'
import type { ComponentInternalInstance, VNode } from 'vue'

View File

@ -319,6 +319,7 @@
.#{$namespace}-input__wrapper {
background-color: map.get($input-disabled, 'fill');
cursor: not-allowed;
@include mixed-input-border(map.get($input-disabled, 'border'));
}

View File

@ -1,4 +1,3 @@
import * as vue from 'vue'
import * as vueShared from '@vue/shared'
import { describe, expect, it } from 'vitest'
import {
@ -15,7 +14,6 @@ import {
isString,
isSymbol,
isUndefined,
isVNode,
} from '..'
describe('types', () => {
@ -29,10 +27,6 @@ describe('types', () => {
expect(isSymbol).toBe(vueShared.isSymbol)
})
it('re-export from vue', () => {
expect(isVNode).toBe(vue.isVNode)
})
it('isBoolean and isNumber should work', () => {
expect(isBoolean(true)).toBe(true)
expect(isBoolean(false)).toBe(true)

View File

@ -11,7 +11,6 @@ export {
isSymbol,
isPlainObject,
} from '@vue/shared'
export { isVNode } from 'vue'
export const isUndefined = (val: any): val is undefined => val === undefined
export const isBoolean = (val: any): val is boolean => typeof val === 'boolean'

View File

@ -8,7 +8,6 @@ import Inspect from 'vite-plugin-inspect'
import mkcert from 'vite-plugin-mkcert'
import glob from 'fast-glob'
import VueMacros from 'unplugin-vue-macros/vite'
import esbuild from 'rollup-plugin-esbuild'
import {
epPackage,
epRoot,
@ -16,17 +15,6 @@ import {
pkgRoot,
projRoot,
} from '@element-plus/build-utils'
import type { Plugin } from 'vite'
const esbuildPlugin = (): Plugin => ({
...esbuild({
target: 'chrome64',
loaders: {
'.vue': 'js',
},
}),
enforce: 'post',
})
export default defineConfig(async ({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
@ -74,7 +62,6 @@ export default defineConfig(async ({ mode }) => {
vueJsx: vueJsx(),
},
}),
esbuildPlugin(),
Components({
include: `${__dirname}/**`,
resolvers: ElementPlusResolver({