[
slots.suffix || props.suffixIcon || props.clearable || props.showPassword,
[nsInput.bm('suffix', 'password-clear')]:
showClear.value && showPwdVisible.value,
+ [nsInput.b('hidden')]: props.type === 'hidden',
},
rawAttrs.class,
])
diff --git a/packages/components/menu/src/menu.ts b/packages/components/menu/src/menu.ts
index a0b6e51f42804..bf66e31721d06 100644
--- a/packages/components/menu/src/menu.ts
+++ b/packages/components/menu/src/menu.ts
@@ -301,6 +301,7 @@ export default defineComponent({
let isFirstTimeRender = true
const handleResize = () => {
+ if (sliceIndex.value === calcSliceIndex()) return
const callback = () => {
sliceIndex.value = -1
nextTick(() => {
diff --git a/packages/components/menu/src/sub-menu.ts b/packages/components/menu/src/sub-menu.ts
index f87eef9e95a5a..9888f6f20c795 100644
--- a/packages/components/menu/src/sub-menu.ts
+++ b/packages/components/menu/src/sub-menu.ts
@@ -23,7 +23,7 @@ import {
isString,
throwError,
} from '@element-plus/utils'
-import { useDeprecated, useNamespace } from '@element-plus/hooks'
+import { useNamespace } from '@element-plus/hooks'
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue'
import { ElIcon } from '@element-plus/components/icon'
import useMenu from './use-menu'
@@ -42,10 +42,6 @@ export const subMenuProps = buildProps({
hideTimeout: Number,
popperClass: String,
disabled: Boolean,
- popperAppendToBody: {
- type: Boolean,
- default: undefined,
- },
teleported: {
type: Boolean,
default: undefined,
@@ -72,17 +68,6 @@ export default defineComponent({
props: subMenuProps,
setup(props, { slots, expose }) {
- useDeprecated(
- {
- from: 'popper-append-to-body',
- replacement: 'teleported',
- scope: COMPONENT_NAME,
- version: '2.3.0',
- ref: 'https://element-plus.org/en-US/component/menu.html#submenu-attributes',
- },
- computed(() => props.popperAppendToBody !== undefined)
- )
-
const instance = getCurrentInstance()!
const { indexPath, parentMenu } = useMenu(
instance,
@@ -130,7 +115,7 @@ export default defineComponent({
return subMenu.level === 0
})
const appendToBody = computed(() => {
- const value = props.teleported ?? props.popperAppendToBody
+ const value = props.teleported
return value === undefined ? isFirstLevel.value : value
})
const menuTransitionName = computed(() =>
diff --git a/packages/components/message/__tests__/message-manager.test.tsx b/packages/components/message/__tests__/message-manager.test.tsx
index 3e6e98ab40ff2..227fd1949bd30 100644
--- a/packages/components/message/__tests__/message-manager.test.tsx
+++ b/packages/components/message/__tests__/message-manager.test.tsx
@@ -106,7 +106,7 @@ describe('Message on command', () => {
test('correct space when set offset', async () => {
const offset = 100
- const space = 20
+ const space = 16
const messages = [Message({ offset }), Message({ offset })]
await rAF()
diff --git a/packages/components/message/src/instance.ts b/packages/components/message/src/instance.ts
index 15ff2357e0761..9740a17072ad7 100644
--- a/packages/components/message/src/instance.ts
+++ b/packages/components/message/src/instance.ts
@@ -31,5 +31,5 @@ export const getLastOffset = (id: string): number => {
export const getOffsetOrSpace = (id: string, offset: number) => {
const idx = instances.findIndex((instance) => instance.id === id)
- return idx > 0 ? 20 : offset
+ return idx > 0 ? 16 : offset
}
diff --git a/packages/components/radio/__tests__/radio.test.tsx b/packages/components/radio/__tests__/radio.test.tsx
index 0f9c4cd47a06c..a88c07f737beb 100644
--- a/packages/components/radio/__tests__/radio.test.tsx
+++ b/packages/components/radio/__tests__/radio.test.tsx
@@ -70,13 +70,13 @@ describe('Radio group', () => {
const radio = ref(3)
const wrapper = mount(() => (
-
+
3
-
+
6
- 9
+ 9
))
await nextTick()
@@ -91,26 +91,26 @@ describe('Radio group', () => {
const radioValue1 = ref(3)
const wrapper1 = mount(() => (
-
+
3
-
+
6
- 9
+ 9
))
const radioValue2 = ref(3)
const wrapper2 = mount(() => (
-
+
3
-
+
6
- 9
+ 9
))
@@ -124,13 +124,13 @@ describe('Radio group', () => {
const radio = ref(3)
const wrapper = mount(() => (
-
+
3
-
+
6
- 9
+ 9
))
expect(wrapper.find('label.is-disabled').exists()).toBe(true)
@@ -149,11 +149,11 @@ describe('Radio group', () => {
}
const wrapper = mount(() => (
- 3
-
+ 3
+
6
- 9
+ 9
))
const radio2 = wrapper.findAll('.el-radio').at(1)
@@ -169,11 +169,11 @@ describe('Radio group', () => {
}
mount(() => (
- 3
-
+ 3
+
6
- 9
+ 9
))
@@ -185,13 +185,13 @@ describe('Radio group', () => {
const radio = ref(3)
const wrapper = mount(() => (
-
+
3
-
+
6
- 9
+ 9
))
@@ -209,13 +209,13 @@ describe('Radio Button', () => {
const radio = ref(3)
const wrapper = mount(() => (
-
+
3
-
+
6
- 9
+ 9
))
const [radio1, radio2] = wrapper.findAll('.el-radio-button')
@@ -228,13 +228,13 @@ describe('Radio Button', () => {
const radio = ref(3)
const wrapper = mount(() => (
-
+
3
-
+
6
- 9
+ 9
))
const radio1 = wrapper.find('.el-radio-button')
@@ -250,13 +250,13 @@ describe('Radio Button', () => {
}
const wrapper = mount(() => (
-
+
3
-
+
6
- 9
+ 9
))
const radio2 = wrapper.findAll('.el-radio-button').at(1)
@@ -271,13 +271,13 @@ describe('Radio Button', () => {
}
mount(() => (
-
+
3
-
+
6
- 9
+ 9
))
@@ -290,13 +290,13 @@ describe('Radio Button', () => {
const radio = ref(3)
const wrapper = mount(() => (
-
+
3
-
+
6
- 9
+ 9
))
expect(wrapper.findAll('.el-radio-button--large').length).toBe(3)
@@ -307,8 +307,8 @@ describe('Radio Button', () => {
const wrapper = mount(() => (
-
-
+
+
))
@@ -328,8 +328,8 @@ describe('Radio Button', () => {
const wrapper = mount(() => (
-
-
+
+
))
@@ -347,12 +347,12 @@ describe('Radio Button', () => {
const wrapper = mount(() => (
-
-
+
+
-
-
+
+
))
diff --git a/packages/components/radio/src/radio-button.ts b/packages/components/radio/src/radio-button.ts
index fdf263de0d7c5..40bfcaa45412a 100644
--- a/packages/components/radio/src/radio-button.ts
+++ b/packages/components/radio/src/radio-button.ts
@@ -5,13 +5,6 @@ import type RadioButton from './radio-button.vue'
export const radioButtonProps = buildProps({
...radioPropsBase,
- /**
- * @description native 'name' attribute
- */
- name: {
- type: String,
- default: '',
- },
} as const)
export type RadioButtonProps = ExtractPropTypes
diff --git a/packages/components/radio/src/radio-button.vue b/packages/components/radio/src/radio-button.vue
index ea9909bca9bcd..4ae91c45cfc09 100644
--- a/packages/components/radio/src/radio-button.vue
+++ b/packages/components/radio/src/radio-button.vue
@@ -2,7 +2,7 @@
@@ -46,7 +46,7 @@ defineOptions({
const props = defineProps(radioButtonProps)
const ns = useNamespace('radio')
-const { radioRef, focus, size, disabled, modelValue, radioGroup } =
+const { radioRef, focus, size, disabled, modelValue, radioGroup, actualValue } =
useRadio(props)
const activeStyle = computed(() => {
diff --git a/packages/components/radio/src/radio-group.ts b/packages/components/radio/src/radio-group.ts
index 6a649e5a2d905..47900728980b9 100644
--- a/packages/components/radio/src/radio-group.ts
+++ b/packages/components/radio/src/radio-group.ts
@@ -25,7 +25,7 @@ export const radioGroupProps = buildProps({
*/
modelValue: {
type: [String, Number, Boolean],
- default: '',
+ default: undefined,
},
/**
* @description border and background color when button is active
diff --git a/packages/components/radio/src/radio.ts b/packages/components/radio/src/radio.ts
index 973312474f489..b1958b14ec502 100644
--- a/packages/components/radio/src/radio.ts
+++ b/packages/components/radio/src/radio.ts
@@ -5,6 +5,13 @@ import type { ExtractPropTypes } from 'vue'
import type Radio from './radio.vue'
export const radioPropsBase = buildProps({
+ /**
+ * @description binding value
+ */
+ modelValue: {
+ type: [String, Number, Boolean],
+ default: undefined,
+ },
/**
* @description size of the Radio
*/
@@ -14,30 +21,30 @@ export const radioPropsBase = buildProps({
*/
disabled: Boolean,
/**
- * @description the value of Radio
+ * @description the label of Radio
*/
label: {
type: [String, Number, Boolean],
- default: '',
+ default: undefined,
},
-})
-
-export const radioProps = buildProps({
- ...radioPropsBase,
/**
- * @description binding value
+ * @description the value of Radio
*/
- modelValue: {
+ value: {
type: [String, Number, Boolean],
- default: '',
+ default: undefined,
},
/**
* @description native `name` attribute
*/
name: {
type: String,
- default: '',
+ default: undefined,
},
+})
+
+export const radioProps = buildProps({
+ ...radioPropsBase,
/**
* @description whether to add a border around Radio
*/
@@ -45,9 +52,9 @@ export const radioProps = buildProps({
} as const)
export const radioEmits = {
- [UPDATE_MODEL_EVENT]: (val: string | number | boolean) =>
+ [UPDATE_MODEL_EVENT]: (val: string | number | boolean | undefined) =>
isString(val) || isNumber(val) || isBoolean(val),
- [CHANGE_EVENT]: (val: string | number | boolean) =>
+ [CHANGE_EVENT]: (val: string | number | boolean | undefined) =>
isString(val) || isNumber(val) || isBoolean(val),
}
diff --git a/packages/components/radio/src/radio.vue b/packages/components/radio/src/radio.vue
index 0405318945714..cce65b0a9ce32 100644
--- a/packages/components/radio/src/radio.vue
+++ b/packages/components/radio/src/radio.vue
@@ -5,7 +5,7 @@
ns.is('disabled', disabled),
ns.is('focus', focus),
ns.is('bordered', border),
- ns.is('checked', modelValue === label),
+ ns.is('checked', modelValue === actualValue),
ns.m(size),
]"
>
@@ -13,14 +13,14 @@
:class="[
ns.e('input'),
ns.is('disabled', disabled),
- ns.is('checked', modelValue === label),
+ ns.is('checked', modelValue === actualValue),
]"
>
emit('change', modelValue.value))
diff --git a/packages/components/radio/src/use-radio.ts b/packages/components/radio/src/use-radio.ts
index c9275c07111fe..f048370dafea7 100644
--- a/packages/components/radio/src/use-radio.ts
+++ b/packages/components/radio/src/use-radio.ts
@@ -1,18 +1,28 @@
import { computed, inject, ref } from 'vue'
import { UPDATE_MODEL_EVENT } from '@element-plus/constants'
import { useFormDisabled, useFormSize } from '@element-plus/components/form'
+import { useDeprecated } from '@element-plus/hooks'
+import { isPropAbsent } from '@element-plus/utils'
import { radioGroupKey } from './constants'
+import type { RadioButtonProps } from './radio-button'
import type { SetupContext } from 'vue'
import type { RadioEmits, RadioProps } from './radio'
-
export const useRadio = (
- props: { label: RadioProps['label']; modelValue?: RadioProps['modelValue'] },
+ props: RadioProps | RadioButtonProps,
emit?: SetupContext['emit']
) => {
const radioRef = ref()
const radioGroup = inject(radioGroupKey, undefined)
const isGroup = computed(() => !!radioGroup)
+ const actualValue = computed(() => {
+ // In version 2.x, if there's no props.value, props.label will act as props.value
+ // In version 3.x, remove this computed value, use props.value instead.
+ if (!isPropAbsent(props.value)) {
+ return props.value
+ }
+ return props.label
+ })
const modelValue = computed({
get() {
return isGroup.value ? radioGroup!.modelValue : props.modelValue!
@@ -23,7 +33,7 @@ export const useRadio = (
} else {
emit && emit(UPDATE_MODEL_EVENT, val)
}
- radioRef.value!.checked = props.modelValue === props.label
+ radioRef.value!.checked = props.modelValue === actualValue.value
},
})
@@ -31,11 +41,23 @@ export const useRadio = (
const disabled = useFormDisabled(computed(() => radioGroup?.disabled))
const focus = ref(false)
const tabIndex = computed(() => {
- return disabled.value || (isGroup.value && modelValue.value !== props.label)
+ return disabled.value ||
+ (isGroup.value && modelValue.value !== actualValue.value)
? -1
: 0
})
+ useDeprecated(
+ {
+ from: 'label act as value',
+ replacement: 'value',
+ version: '3.0.0',
+ scope: 'el-radio',
+ ref: 'https://element-plus.org/en-US/component/radio.html',
+ },
+ computed(() => isGroup.value && isPropAbsent(props.value))
+ )
+
return {
radioRef,
isGroup,
@@ -45,5 +67,6 @@ export const useRadio = (
disabled,
tabIndex,
modelValue,
+ actualValue,
}
}
diff --git a/packages/components/select-v2/__tests__/select.test.ts b/packages/components/select-v2/__tests__/select.test.ts
index 2c48dc8f3c281..28bc7b77e6b82 100644
--- a/packages/components/select-v2/__tests__/select.test.ts
+++ b/packages/components/select-v2/__tests__/select.test.ts
@@ -803,6 +803,26 @@ describe('Select', () => {
placeholder = wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`)
expect(placeholder.exists()).toBeTruthy()
})
+
+ it('set modelValue when filtering in multiple select', async () => {
+ const wrapper = createSelect({
+ data: () => {
+ return {
+ multiple: true,
+ filterable: true,
+ value: [],
+ }
+ },
+ })
+ const vm = wrapper.vm as any
+ const input = wrapper.find('input')
+ await input.trigger('click')
+ input.element.value = '1111'
+ await input.trigger('input')
+ vm.value = ['option_1']
+ await nextTick()
+ expect(wrapper.find('.el-select__tags-text').text()).toBe('a0')
+ })
})
describe('event', () => {
diff --git a/packages/components/select-v2/src/select-dropdown.tsx b/packages/components/select-v2/src/select-dropdown.tsx
index 8bd5a5bab81d1..e4d324f052bf4 100644
--- a/packages/components/select-v2/src/select-dropdown.tsx
+++ b/packages/components/select-v2/src/select-dropdown.tsx
@@ -226,40 +226,34 @@ export default defineComponent({
const { data, width } = props
const { height, multiple, scrollbarAlwaysOn } = select.props
- if (slots.loading || slots.empty) {
- return (
-
- {slots.loading?.() || slots.empty?.()}
-
- )
- }
-
const List = unref(isSized) ? FixedSizeList : DynamicSizeList
return (
-
+
{slots.header?.()}
-
- {{
- default: (props: ItemProps) => ,
- }}
-
+ {slots.loading?.() || slots.empty?.() || (
+
+ {{
+ default: (props: ItemProps) => ,
+ }}
+
+ )}
{slots.footer?.()}
)
diff --git a/packages/components/select-v2/src/useSelect.ts b/packages/components/select-v2/src/useSelect.ts
index ea9fbd6d8a1ed..551b89fdf8209 100644
--- a/packages/components/select-v2/src/useSelect.ts
+++ b/packages/components/select-v2/src/useSelect.ts
@@ -13,6 +13,7 @@ import {
findLastIndex,
get,
isEqual,
+ isNil,
debounce as lodashDebounce,
} from 'lodash-unified'
import { useResizeObserver } from '@vueuse/core'
@@ -126,12 +127,15 @@ const useSelect = (props: ISelectV2Props, emit) => {
return totalHeight > props.height ? props.height : totalHeight
})
+ const hasEmptyStringOption = computed(() =>
+ allOptions.value.some((option) => getValue(option) === '')
+ )
+
const hasModelValue = computed(() => {
return props.multiple
? isArray(props.modelValue) && props.modelValue.length > 0
- : props.modelValue !== undefined &&
- props.modelValue !== null &&
- props.modelValue !== ''
+ : !isNil(props.modelValue) &&
+ (props.modelValue !== '' || hasEmptyStringOption.value)
})
const showClearBtn = computed(() => {
@@ -223,6 +227,15 @@ const useSelect = (props: ISelectV2Props, emit) => {
filteredOptions.value = filterOptions(states.inputValue) as OptionType[]
}
+ const allOptionsValueMap = computed(() => {
+ const valueMap = new Map()
+
+ allOptions.value.forEach((option, index) => {
+ valueMap.set(getValueKey(getValue(option)), { option, index })
+ })
+ return valueMap
+ })
+
const filteredOptionsValueMap = computed(() => {
const valueMap = new Map()
@@ -669,17 +682,11 @@ const useSelect = (props: ISelectV2Props, emit) => {
return getValueKey(item) === getValueKey(props.modelValue)
})
} else {
- if (props.modelValue.length > 0) {
- states.hoveringIndex = Math.min(
- ...props.modelValue.map((selected) => {
- return filteredOptions.value.findIndex((item) => {
- return getValue(item) === selected
- })
- })
+ states.hoveringIndex = filteredOptions.value.findIndex((item) =>
+ props.modelValue.some(
+ (modelValue) => getValueKey(modelValue) === getValueKey(item)
)
- } else {
- states.hoveringIndex = -1
- }
+ )
}
}
@@ -717,8 +724,8 @@ const useSelect = (props: ISelectV2Props, emit) => {
// match the option with the given value, if not found, create a new option
const selectValue = getValueKey(value)
- if (filteredOptionsValueMap.value.has(selectValue)) {
- const { option } = filteredOptionsValueMap.value.get(selectValue)
+ if (allOptionsValueMap.value.has(selectValue)) {
+ const { option } = allOptionsValueMap.value.get(selectValue)
return option
}
diff --git a/packages/components/select/__tests__/select.test.ts b/packages/components/select/__tests__/select.test.ts
index 238b7bd3962a5..80ee27c9d6501 100644
--- a/packages/components/select/__tests__/select.test.ts
+++ b/packages/components/select/__tests__/select.test.ts
@@ -871,7 +871,7 @@ describe('Select', () => {
const iconClear = wrapper.findComponent(CircleClose)
expect(iconClear.exists()).toBe(true)
await iconClear.trigger('click')
- expect(vm.value).toBe('')
+ expect(vm.value).toBe(undefined)
})
test('suffix icon', async () => {
@@ -1418,7 +1418,7 @@ describe('Select', () => {
const iconClear = wrapper.findComponent(CircleClose)
expect(iconClear.exists()).toBe(true)
await iconClear.trigger('click')
- expect(vm.value).toBe('')
+ expect(vm.value).toBe(undefined)
expect(handleFocus).toHaveBeenCalledTimes(1)
expect(handleBlur).not.toHaveBeenCalled()
diff --git a/packages/components/select/src/select.ts b/packages/components/select/src/select.ts
index c6576a634026d..ca1b8ce2dba69 100644
--- a/packages/components/select/src/select.ts
+++ b/packages/components/select/src/select.ts
@@ -197,13 +197,6 @@ export const SelectProps = buildProps({
* @description in remote search method show suffix icon
*/
remoteShowSuffix: Boolean,
- /**
- * @deprecated will be removed in version 2.4.0, please use override style scheme
- */
- suffixTransition: {
- type: Boolean,
- default: true,
- },
/**
* @description position of dropdown
*/
diff --git a/packages/components/select/src/useSelect.ts b/packages/components/select/src/useSelect.ts
index b8439c7e70695..23b0cc2fbcf79 100644
--- a/packages/components/select/src/useSelect.ts
+++ b/packages/components/select/src/useSelect.ts
@@ -14,6 +14,7 @@ import {
findLastIndex,
get,
isEqual,
+ isNil,
debounce as lodashDebounce,
} from 'lodash-unified'
import { useResizeObserver } from '@vueuse/core'
@@ -28,12 +29,10 @@ import {
isClient,
isFunction,
isNumber,
- isString,
isUndefined,
scrollIntoView,
} from '@element-plus/utils'
import {
- useDeprecated,
useFocusController,
useId,
useLocale,
@@ -75,17 +74,6 @@ export const useSelect = (props: ISelectProps, emit) => {
isBeforeHide: false,
})
- useDeprecated(
- {
- from: 'suffixTransition',
- replacement: 'override style scheme',
- version: '2.3.0',
- scope: 'props',
- ref: 'https://element-plus.org/en-US/component/select.html#select-attributes',
- },
- computed(() => props.suffixTransition === false)
- )
-
// template refs
const selectRef = ref
(null)
const selectionRef = ref(null)
@@ -135,12 +123,15 @@ export const useSelect = (props: ISelectProps, emit) => {
const selectDisabled = computed(() => props.disabled || form?.disabled)
+ const hasEmptyStringOption = computed(() =>
+ optionsArray.value.some((option) => option.value === '')
+ )
+
const hasModelValue = computed(() => {
return props.multiple
? isArray(props.modelValue) && props.modelValue.length > 0
- : props.modelValue !== undefined &&
- props.modelValue !== null &&
- props.modelValue !== ''
+ : !isNil(props.modelValue) &&
+ (props.modelValue !== '' || hasEmptyStringOption.value)
})
const showClose = computed(() => {
@@ -157,10 +148,7 @@ export const useSelect = (props: ISelectProps, emit) => {
: props.suffixIcon
)
const iconReverse = computed(() =>
- nsSelect.is(
- 'reverse',
- iconComponent.value && expanded.value && props.suffixTransition
- )
+ nsSelect.is('reverse', iconComponent.value && expanded.value)
)
const validateState = computed(() => formItem?.validateState || '')
@@ -453,17 +441,11 @@ export const useSelect = (props: ISelectProps, emit) => {
return getValueKey(item) === getValueKey(states.selected)
})
} else {
- if (states.selected.length > 0) {
- states.hoveringIndex = Math.min(
- ...states.selected.map((selected) => {
- return optionsArray.value.findIndex((item) => {
- return getValueKey(item) === getValueKey(selected)
- })
- })
+ states.hoveringIndex = optionsArray.value.findIndex((item) =>
+ states.selected.some(
+ (selected) => getValueKey(selected) === getValueKey(item)
)
- } else {
- states.hoveringIndex = -1
- }
+ )
}
}
@@ -545,8 +527,8 @@ export const useSelect = (props: ISelectProps, emit) => {
const deleteSelected = (event) => {
event.stopPropagation()
- const value: string | any[] = props.multiple ? [] : ''
- if (!isString(value)) {
+ const value: string | any[] = props.multiple ? [] : undefined
+ if (props.multiple) {
for (const item of states.selected) {
if (item.isDisabled) value.push(item.value)
}
diff --git a/packages/components/switch/__tests__/switch.test.tsx b/packages/components/switch/__tests__/switch.test.tsx
index ca3a6bf419a78..96b592df629c2 100644
--- a/packages/components/switch/__tests__/switch.test.tsx
+++ b/packages/components/switch/__tests__/switch.test.tsx
@@ -21,18 +21,10 @@ describe('Switch.vue', () => {
const props = {
activeText: 'on',
inactiveText: 'off',
- activeColor: '#0f0',
- inactiveColor: '#f00',
width: 100,
}
const wrapper = mount(() => )
const vm = wrapper.vm
- expect(vm.$el.style.getPropertyValue('--el-switch-on-color')).toEqual(
- '#0f0'
- )
- expect(vm.$el.style.getPropertyValue('--el-switch-off-color')).toEqual(
- '#f00'
- )
expect(vm.$el.classList.contains('is-checked')).false
const coreEl = vm.$el.querySelector('.el-switch__core')
expect(coreEl.style.width).toEqual('100px')
@@ -55,18 +47,10 @@ describe('Switch.vue', () => {
inlinePrompt: true,
activeText: 'on',
inactiveText: 'off',
- activeColor: '#0f0',
- inactiveColor: '#f00',
width: 100,
}
const wrapper = mount(() => )
const vm = wrapper.vm
- expect(vm.$el.style.getPropertyValue('--el-switch-on-color')).toEqual(
- '#0f0'
- )
- expect(vm.$el.style.getPropertyValue('--el-switch-off-color')).toEqual(
- '#f00'
- )
expect(vm.$el.classList.contains('is-checked')).false
const coreEl = vm.$el.querySelector('.el-switch__core')
expect(coreEl.style.width).toEqual('100px')
@@ -87,16 +71,8 @@ describe('Switch.vue', () => {
test('value correctly update', async () => {
const value = ref(true)
- const wrapper = mount(() => (
-
- ))
+ const wrapper = mount(() => )
const vm = wrapper.vm
- expect(vm.$el.style.getPropertyValue('--el-switch-on-color')).toEqual(
- '#0f0'
- )
- expect(vm.$el.style.getPropertyValue('--el-switch-off-color')).toEqual(
- '#f00'
- )
expect(vm.$el.classList.contains('is-checked')).true
const coreWrapper = wrapper.find('.el-switch__core')
await coreWrapper.trigger('click')
@@ -178,7 +154,7 @@ describe('Switch.vue', () => {
})
test('value is the single source of truth', async () => {
- const wrapper = mount(() => )
+ const wrapper = mount(() => )
const vm = wrapper.vm
const coreWrapper = wrapper.find('.el-switch__core')
diff --git a/packages/components/switch/src/switch.ts b/packages/components/switch/src/switch.ts
index a68c3a55934d0..da91d6c3fa783 100644
--- a/packages/components/switch/src/switch.ts
+++ b/packages/components/switch/src/switch.ts
@@ -111,27 +111,6 @@ export const switchProps = buildProps({
type: [Boolean, String, Number],
default: false,
},
- /**
- * @deprecated background color when in `on` state ( deprecated, use CSS var `--el-switch-on-color` instead )
- */
- activeColor: {
- type: String,
- default: '',
- },
- /**
- * @deprecated background color when in `off` state ( deprecated, use CSS var `--el-switch-off-color` instead )
- */
- inactiveColor: {
- type: String,
- default: '',
- },
- /**
- * @deprecated border color of the switch ( deprecated, use CSS var `--el-switch-border-color` instead )
- */
- borderColor: {
- type: String,
- default: '',
- },
/**
* @description input name of Switch
*/
@@ -162,13 +141,6 @@ export const switchProps = buildProps({
tabindex: {
type: [String, Number],
},
- /**
- * @deprecated binding value ( deprecated, use `model-value / v-model` instead )
- */
- value: {
- type: [Boolean, String, Number],
- default: false,
- },
/**
* @description native input aria-label
*/
diff --git a/packages/components/switch/src/switch.vue b/packages/components/switch/src/switch.vue
index 4d5a53b4e64bc..f46e8fe7cbaab 100644
--- a/packages/components/switch/src/switch.vue
+++ b/packages/components/switch/src/switch.vue
@@ -1,5 +1,5 @@
-
+
diff --git a/packages/components/tour/src/helper.ts b/packages/components/tour/src/helper.ts
index 53d5f1aefb618..aa80bbeee7516 100644
--- a/packages/components/tour/src/helper.ts
+++ b/packages/components/tour/src/helper.ts
@@ -263,7 +263,11 @@ export const useFloating = (
let cleanup: any
onMounted(() => {
- cleanup = autoUpdate(unref(referenceRef)!, unref(contentRef)!, update)
+ const referenceEl = unref(referenceRef)
+ const contentEl = unref(contentRef)
+ if (referenceEl && contentEl) {
+ cleanup = autoUpdate(referenceEl, contentEl, update)
+ }
watchEffect(() => {
update()
diff --git a/packages/components/tree-select/__tests__/tree-select.test.tsx b/packages/components/tree-select/__tests__/tree-select.test.tsx
index 8962ccc8a9685..b5e195632ef9f 100644
--- a/packages/components/tree-select/__tests__/tree-select.test.tsx
+++ b/packages/components/tree-select/__tests__/tree-select.test.tsx
@@ -494,40 +494,121 @@ describe('TreeSelect.vue', () => {
expect(select.vm.states.selectedLabel).toBe('1-label')
})
- test('filter-method', async () => {
- const modelValue = ref(1)
- const data = ref([
- { value: 1, label: '1' },
- { value: 2, label: '2' },
- { value: 3, label: '3' },
- ])
- const filterMethod = vi.fn()
- const { select, tree } = createComponent({
- props: {
- modelValue,
- data,
- filterable: true,
- filterMethod,
- // trigger cache data
- renderAfterExpand: true,
- },
+ describe('filter method and remote method', () => {
+ const data = ref
([])
+ const setData = (keywords: string) => {
+ data.value = [
+ {
+ value: keywords,
+ label: keywords,
+ children: [
+ { value: `${keywords}-child`, label: `${keywords}-child` },
+ ],
+ },
+ ]
+ }
+ const testMethod = async (props: any) => {
+ if (props.filterMethod) {
+ props.filterMethod = vi.fn(props.filterMethod)
+ } else if (props.remoteMethod) {
+ props.remoteMethod = vi.fn(props.remoteMethod)
+ }
+
+ const { select, tree } = createComponent({
+ props: {
+ data,
+ multiple: true,
+ filterable: true,
+ ...props,
+ },
+ })
+
+ await nextTick()
+
+ const method = props.filterMethod || props.remoteMethod
+ const input = select.find('input')
+
+ // show drop menu
+ await input.trigger('click')
+ expect(method).toBeCalledWith('')
+
+ const testKeywords = async (keywords: string) => {
+ await input.setValue(keywords)
+
+ // await debounce
+ await new Promise((resolve) => setTimeout(resolve, 300))
+ expect(method).toBeCalledWith(keywords)
+
+ // await remote
+ await new Promise((resolve) => setTimeout(resolve, 200))
+ expect(tree.vm.data).toEqual(data.value)
+ expect(
+ tree.findAll('.el-select-dropdown__item').map((item) => item.text())
+ ).toEqual([keywords, `${keywords}-child`])
+
+ const treeNode = tree.find('.el-tree-node')
+ expect(treeNode.classes('is-expanded')).toBe(true)
+ }
+
+ await testKeywords('a')
+
+ // check first child
+ await tree
+ .findAll(
+ props.showCheckbox
+ ? '.el-checkbox__original'
+ : '.el-select-dropdown__item'
+ )[1]
+ .trigger('click')
+ expect(select.vm.modelValue).toEqual([`a-child`])
+
+ await testKeywords('aa')
+
+ // check first child again
+ await tree
+ .findAll(
+ props.showCheckbox
+ ? '.el-checkbox__original'
+ : '.el-select-dropdown__item'
+ )[1]
+ .trigger('click')
+ expect(select.vm.modelValue).toEqual(['a-child', 'aa-child'])
+
+ // hide drop menu
+ await input.trigger('blur')
+ expect(method).toBeCalledWith('')
+ }
+
+ test('filter-method', async () => {
+ await testMethod({
+ filterMethod: setData,
+ })
})
- await nextTick()
- expect(tree.vm.data.length).toBe(3)
+ test('filter-method and checkbox', async () => {
+ await testMethod({
+ filterMethod: setData,
+ showCheckbox: true,
+ })
+ })
- const input = select.find('input')
- await input.trigger('click')
- await input.setValue('')
- expect(select.vm.states.selectedLabel).toBe('1')
- expect(filterMethod).toHaveBeenCalled()
+ test('filter-method and checkbox & check-strictly', async () => {
+ await testMethod({
+ filterMethod: setData,
+ showCheckbox: true,
+ checkStrictly: true,
+ })
+ })
- // await debounce
- await input.setValue('2')
- modelValue.value = 2
- await nextTick()
- expect(select.vm.states.selectedLabel).toBe('2')
- expect(filterMethod).toHaveBeenCalledTimes(3)
+ test('remote-method', async () => {
+ await testMethod({
+ remote: true,
+ remoteMethod: async (keywords: string) => {
+ await new Promise((resolve) => setTimeout(resolve, 200))
+ setData(keywords)
+ },
+ })
+ })
})
test('check/check-change events can accept correct params', async () => {
@@ -600,7 +681,7 @@ describe('TreeSelect.vue', () => {
expect(node1.text()).toBe('1-label')
await node1Checkbox.trigger('click')
- expect(modelValue.value).toEqual([1, 2])
+ expect(modelValue.value).toEqual([2, 1])
})
test('cached checked node can be canceled', async () => {
diff --git a/packages/components/tree-select/src/select.ts b/packages/components/tree-select/src/select.ts
index 1bc9eb312d638..7d2b145990e83 100644
--- a/packages/components/tree-select/src/select.ts
+++ b/packages/components/tree-select/src/select.ts
@@ -1,5 +1,5 @@
// @ts-nocheck
-import { computed, nextTick, toRefs } from 'vue'
+import { computed, nextTick, toRefs, watch } from 'vue'
import { pick } from 'lodash-unified'
import ElSelect from '@element-plus/components/select'
import { useNamespace } from '@element-plus/hooks'
@@ -11,6 +11,7 @@ export const useSelect = (
props,
{ attrs, emit },
{
+ select,
tree,
key,
}: {
@@ -21,6 +22,20 @@ export const useSelect = (
) => {
const ns = useNamespace('tree-select')
+ // update tree data when use filterMethod/remoteMethod
+ watch(
+ () => props.data,
+ () => {
+ if (props.filterable) {
+ nextTick(() => {
+ // let tree node expand only, same with tree filter
+ tree.value?.filter(select.value?.states.inputValue)
+ })
+ }
+ },
+ { flush: 'post' }
+ )
+
const result = {
...pick(toRefs(props), Object.keys(ElSelect.props)),
...attrs,
@@ -35,19 +50,13 @@ export const useSelect = (
return classes.join(' ')
}),
filterMethod: (keyword = '') => {
- if (props.filterMethod) props.filterMethod(keyword)
-
- nextTick(() => {
+ if (props.filterMethod) {
+ props.filterMethod(keyword)
+ } else if (props.remoteMethod) {
+ props.remoteMethod(keyword)
+ } else {
// let tree node expand only, same with tree filter
tree.value?.filter(keyword)
- })
- },
- // clear filter text when visible change
- onVisibleChange: (visible: boolean) => {
- attrs.onVisibleChange?.(visible)
-
- if (props.filterable && visible) {
- result.filterMethod()
}
},
}
diff --git a/packages/components/tree-select/src/tree.ts b/packages/components/tree-select/src/tree.ts
index 6ef64385c9adf..c41bc66150de6 100644
--- a/packages/components/tree-select/src/tree.ts
+++ b/packages/components/tree-select/src/tree.ts
@@ -112,13 +112,6 @@ export const useTree = (
return options
})
- const cacheOptionsMap = computed(() => {
- return cacheOptions.value.reduce(
- (prev, next) => ({ ...prev, [next.value]: next }),
- {}
- )
- })
-
return {
...pick(toRefs(props), Object.keys(ElTree.props)),
...attrs,
@@ -183,18 +176,21 @@ export const useTree = (
if (!props.showCheckbox) return
const dataValue = getNodeValByProp('value', data)
+ const dataMap = {}
+ treeEach(
+ [tree.value.store.root],
+ (node) => (dataMap[node.key] = node),
+ (node) => node.childNodes
+ )
// fix: checkedKeys has not cached keys
const uncachedCheckedKeys = params.checkedKeys
const cachedKeys = props.multiple
? toValidArray(props.modelValue).filter(
- (item) =>
- item in cacheOptionsMap.value &&
- !tree.value.getNode(item) &&
- !uncachedCheckedKeys.includes(item)
+ (item) => !(item in dataMap) && !uncachedCheckedKeys.includes(item)
)
: []
- const checkedKeys = uncachedCheckedKeys.concat(cachedKeys)
+ const checkedKeys = cachedKeys.concat(uncachedCheckedKeys)
if (props.checkStrictly) {
emit(
@@ -212,7 +208,9 @@ export const useTree = (
if (props.multiple) {
emit(
UPDATE_MODEL_EVENT,
- (tree.value as InstanceType).getCheckedKeys(true)
+ cachedKeys.concat(
+ (tree.value as InstanceType).getCheckedKeys(true)
+ )
)
} else {
// select first leaf node when check parent
diff --git a/packages/components/tree-v2/src/composables/useCheck.ts b/packages/components/tree-v2/src/composables/useCheck.ts
index d462d18d6f168..9c402743eb8dc 100644
--- a/packages/components/tree-v2/src/composables/useCheck.ts
+++ b/packages/components/tree-v2/src/composables/useCheck.ts
@@ -179,7 +179,9 @@ export function useCheck(props: TreeProps, tree: Ref) {
function setCheckedKeys(keys: TreeKey[]) {
checkedKeys.value.clear()
indeterminateKeys.value.clear()
- _setCheckedKeys(keys)
+ nextTick(() => {
+ _setCheckedKeys(keys)
+ })
}
function setChecked(key: TreeKey, isChecked: boolean) {
diff --git a/packages/components/upload/src/upload-dragger.vue b/packages/components/upload/src/upload-dragger.vue
index dcc98a493f772..a71e1fe71d5a7 100644
--- a/packages/components/upload/src/upload-dragger.vue
+++ b/packages/components/upload/src/upload-dragger.vue
@@ -44,35 +44,7 @@ const onDrop = (e: DragEvent) => {
e.stopPropagation()
const files = Array.from(e.dataTransfer!.files)
- const accept = uploaderContext.accept.value
- if (!accept) {
- emit('file', files)
- return
- }
-
- const filesFiltered = files.filter((file) => {
- const { type, name } = file
- const extension = name.includes('.') ? `.${name.split('.').pop()}` : ''
- const baseType = type.replace(/\/.*$/, '')
- return accept
- .split(',')
- .map((type) => type.trim())
- .filter((type) => type)
- .some((acceptedType) => {
- if (acceptedType.startsWith('.')) {
- return extension === acceptedType
- }
- if (/\/\*$/.test(acceptedType)) {
- return baseType === acceptedType.replace(/\/\*$/, '')
- }
- if (/^[^/]+\/[^/]+$/.test(acceptedType)) {
- return type === acceptedType
- }
- return false
- })
- })
-
- emit('file', filesFiltered)
+ emit('file', files)
}
const onDragover = () => {
diff --git a/packages/constants/date.ts b/packages/constants/date.ts
index b6976ce1fa613..03738511376e0 100644
--- a/packages/constants/date.ts
+++ b/packages/constants/date.ts
@@ -1,5 +1,6 @@
export const datePickTypes = [
'year',
+ 'years',
'month',
'date',
'dates',
diff --git a/packages/element-plus/component.ts b/packages/element-plus/component.ts
index 4da41b2c0fa94..9bd877a0383f2 100644
--- a/packages/element-plus/component.ts
+++ b/packages/element-plus/component.ts
@@ -104,6 +104,7 @@ import { ElTreeV2 } from '@element-plus/components/tree-v2'
import { ElUpload } from '@element-plus/components/upload'
import { ElWatermark } from '@element-plus/components/watermark'
import { ElTour, ElTourStep } from '@element-plus/components/tour'
+import { ElAnchor, ElAnchorLink } from '@element-plus/components/anchor'
import type { Plugin } from 'vue'
@@ -209,4 +210,6 @@ export default [
ElWatermark,
ElTour,
ElTourStep,
+ ElAnchor,
+ ElAnchorLink,
] as Plugin[]
diff --git a/packages/theme-chalk/src/alert.scss b/packages/theme-chalk/src/alert.scss
index e9630f308515a..465b9e823fa50 100644
--- a/packages/theme-chalk/src/alert.scss
+++ b/packages/theme-chalk/src/alert.scss
@@ -61,32 +61,35 @@
}
@include e(content) {
- display: table-cell;
- padding: 0 8px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
}
& .#{$namespace}-alert__icon {
font-size: getCssVar('alert', 'icon-size');
width: getCssVar('alert', 'icon-size');
+ margin-right: 8px;
@include when(big) {
font-size: getCssVar('alert', 'icon-large-size');
width: getCssVar('alert', 'icon-large-size');
+ margin-right: 12px;
}
}
@include e(title) {
font-size: getCssVar('alert', 'title-font-size');
- line-height: 18px;
- vertical-align: text-top;
- @include when(bold) {
- font-weight: bold;
+ line-height: 24px;
+
+ &.with-description {
+ font-size: getCssVar('alert', 'title-with-description-font-size');
}
}
& .#{$namespace}-alert__description {
font-size: getCssVar('alert', 'description-font-size');
- margin: 5px 0 0 0;
+ margin: 0;
}
& .#{$namespace}-alert__close-btn {
@@ -94,13 +97,14 @@
opacity: 1;
position: absolute;
top: 12px;
- right: 15px;
+ right: 16px;
cursor: pointer;
@include when(customed) {
font-style: normal;
font-size: getCssVar('alert', 'close-customed-font-size');
- top: 9px;
+ line-height: 24px;
+ top: 8px;
}
}
}
diff --git a/packages/theme-chalk/src/anchor-link.scss b/packages/theme-chalk/src/anchor-link.scss
new file mode 100644
index 0000000000000..a37180422ae86
--- /dev/null
+++ b/packages/theme-chalk/src/anchor-link.scss
@@ -0,0 +1,42 @@
+@use 'sass:map';
+
+@use 'mixins/mixins' as *;
+@use 'mixins/var' as *;
+@use 'common/var' as *;
+
+@include b(anchor) {
+
+ @include e(item) {
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ }
+
+ @include e(link) {
+ font-size: getCssVar('anchor-font-size');
+ line-height: getCssVar('anchor-line-height');
+ padding: 4px 0;
+ color: getCssVar('anchor-color');
+ transition: color getCssVar('transition-duration');
+ white-space: nowrap;
+ text-decoration: none;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ max-width: 100%;
+ outline: none;
+ cursor: pointer;
+
+ &:hover, &:focus {
+ color: getCssVar('anchor-color');
+ }
+
+ @include when(active) {
+ color: getCssVar('anchor-active-color');
+ }
+ }
+
+ & .#{$namespace}-anchor__list .#{$namespace}-anchor__item a {
+ display: inline-block;
+ }
+
+}
\ No newline at end of file
diff --git a/packages/theme-chalk/src/anchor.scss b/packages/theme-chalk/src/anchor.scss
new file mode 100644
index 0000000000000..a8dd28b2fd5e5
--- /dev/null
+++ b/packages/theme-chalk/src/anchor.scss
@@ -0,0 +1,93 @@
+@use 'sass:map';
+
+@use 'mixins/mixins' as *;
+@use 'mixins/var' as *;
+@use 'common/var' as *;
+
+@include b(anchor) {
+ @include set-component-css-var('anchor', $anchor);
+
+ position: relative;
+ background-color: getCssVar('anchor-bg-color') ;
+
+ @include e(marker) {
+ position: absolute;
+ background-color: getCssVar('anchor-marker-bg-color');
+ border-radius: 4px;
+ opacity: 0;
+ z-index: 0;
+ }
+
+ &.#{$namespace}-anchor--vertical {
+
+ @include e(marker) {
+ width: 4px;
+ height: 14px;
+ top: 8px;
+ left: 0;
+ transition: top .25s cubic-bezier(0,1,.5,1),opacity .25s;
+ }
+
+ @include e(list) {
+ padding-left: getCssVar('anchor-padding-indent');
+ }
+
+ &.#{$namespace}-anchor--underline {
+ &::before {
+ position: absolute;
+ left: 0;
+ width: 2px;
+ height: 100%;
+ background-color: rgba(5, 5, 5, 0.06);
+ content: "";
+ }
+
+ @include e(marker) {
+ width: 2px;
+ border-radius: unset;
+ }
+ }
+
+ }
+
+ &.#{$namespace}-anchor--horizontal {
+
+ @include e(marker) {
+ height: 2px;
+ width: 20px;
+ bottom: 0;
+ transition: left .25s cubic-bezier(0,1,.5,1),opacity .25s, width .25s;
+ }
+
+ @include e(list) {
+ display: flex;
+ padding-bottom: 4px;
+
+ @include e(item) {
+ padding-left: 16px;
+
+ &:first-child {
+ padding-left: 0;
+ }
+ }
+ }
+
+ &.#{$namespace}-anchor--underline {
+ &::before {
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ height: 2px;
+ background-color: rgba(5, 5, 5, 0.06);
+ content: "";
+ }
+
+ @include e(marker) {
+ height: 2px;
+ border-radius: unset;
+ }
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/packages/theme-chalk/src/button.scss b/packages/theme-chalk/src/button.scss
index 7ac07f8a3bc2e..99bcafb77fd38 100644
--- a/packages/theme-chalk/src/button.scss
+++ b/packages/theme-chalk/src/button.scss
@@ -43,8 +43,7 @@ $button-icon-span-gap: map.merge(
border: getCssVar('border');
border-color: getCssVar('button', 'border-color');
- &:hover,
- &:focus {
+ &:hover {
color: getCssVar('button', 'hover', 'text-color');
border-color: getCssVar('button', 'hover', 'border-color');
background-color: getCssVar('button', 'hover', 'bg-color');
@@ -61,6 +60,7 @@ $button-icon-span-gap: map.merge(
&:focus-visible {
outline: 2px solid getCssVar('button', 'outline-color');
outline-offset: 1px;
+ transition: outline-offset 0s, outline 0s;
}
> span {
@@ -116,8 +116,7 @@ $button-icon-span-gap: map.merge(
@include when(disabled) {
&,
- &:hover,
- &:focus {
+ &:hover {
color: getCssVar('button', 'disabled', 'text-color');
cursor: not-allowed;
background-image: none;
@@ -164,14 +163,14 @@ $button-icon-span-gap: map.merge(
}
&:not(.is-disabled) {
- &:hover,
- &:focus {
+ &:hover {
background-color: getCssVar('fill-color', 'light');
}
&:focus-visible {
outline: 2px solid getCssVar('button', 'outline-color');
outline-offset: 1px;
+ transition: outline-offset 0s, outline 0s;
}
&:active {
@@ -181,8 +180,7 @@ $button-icon-span-gap: map.merge(
@include when(has-bg) {
background-color: getCssVar('fill-color', 'light');
- &:hover,
- &:focus {
+ &:hover {
background-color: getCssVar('fill-color');
}
@@ -207,8 +205,7 @@ $button-icon-span-gap: map.merge(
padding: 2px;
height: auto;
- &:hover,
- &:focus {
+ &:hover {
color: getCssVar('button', 'hover', 'link-text-color');
}
@@ -219,8 +216,7 @@ $button-icon-span-gap: map.merge(
}
&:not(.is-disabled) {
- &:hover,
- &:focus {
+ &:hover {
border-color: transparent;
background-color: transparent;
}
@@ -246,8 +242,7 @@ $button-icon-span-gap: map.merge(
}
&:not(.is-disabled) {
- &:hover,
- &:focus {
+ &:hover {
color: getCssVar('color', 'primary', 'light-3');
border-color: transparent;
background-color: transparent;
diff --git a/packages/theme-chalk/src/carousel.scss b/packages/theme-chalk/src/carousel.scss
index 7fa5554e6670d..496c9e1420cbf 100644
--- a/packages/theme-chalk/src/carousel.scss
+++ b/packages/theme-chalk/src/carousel.scss
@@ -178,3 +178,11 @@
transform: translateY(-50%) translateX(10px);
opacity: 0;
}
+
+.#{$namespace}-transitioning {
+ filter: url('#elCarouselHorizontal');
+}
+
+.#{$namespace}-transitioning-vertical {
+ filter: url('#elCarouselVertical');
+}
diff --git a/packages/theme-chalk/src/common/var.scss b/packages/theme-chalk/src/common/var.scss
index 3603a59f9defb..1707293b69d9a 100644
--- a/packages/theme-chalk/src/common/var.scss
+++ b/packages/theme-chalk/src/common/var.scss
@@ -400,6 +400,7 @@ $select: () !default;
$select: map.merge(
(
'border-color-hover': getCssVar('border-color-hover'),
+ 'disabled-color': getCssVar('disabled-text-color'),
'disabled-border': getCssVar('disabled-border-color'),
'font-size': getCssVar('font-size-base'),
'close-hover-color': getCssVar('text-color-secondary'),
@@ -498,10 +499,11 @@ $alert: map.merge(
(
'padding': 8px 16px,
'border-radius-base': getCssVar('border-radius-base'),
- 'title-font-size': 13px,
- 'description-font-size': 12px,
- 'close-font-size': 12px,
- 'close-customed-font-size': 13px,
+ 'title-font-size': 14px,
+ 'title-with-description-font-size': 16px,
+ 'description-font-size': 14px,
+ 'close-font-size': 16px,
+ 'close-customed-font-size': 14px,
'icon-size': 16px,
'icon-large-size': 28px,
),
@@ -534,7 +536,7 @@ $message: map.merge(
(
'bg-color': getCssVar('color', 'info', 'light-9'),
'border-color': getCssVar('border-color-lighter'),
- 'padding': 15px 19px,
+ 'padding': 11px 15px,
'close-size': 16px,
'close-icon-color': getCssVar('text-color-placeholder'),
'close-hover-color': getCssVar('text-color-secondary'),
@@ -814,6 +816,22 @@ $tour: map.merge(
$tour
);
+// Anchor
+// css3 var in packages/theme-chalk/src/anchor.scss
+$anchor: () !default;
+$anchor: map.merge(
+ (
+ 'bg-color': getCssVar('bg-color'),
+ 'padding-indent': 14px,
+ 'line-height': 22px,
+ 'font-size': 12px,
+ 'color': getCssVar('text-color-secondary'),
+ 'active-color': getCssVar('color-primary'),
+ 'marker-bg-color': getCssVar('color-primary'),
+ ),
+ $anchor
+);
+
// Table
// css3 var in packages/theme-chalk/src/table.scss
$table: () !default;
diff --git a/packages/theme-chalk/src/date-picker/date-table.scss b/packages/theme-chalk/src/date-picker/date-table.scss
index 4ca42868af86f..50e4a03409cfe 100644
--- a/packages/theme-chalk/src/date-picker/date-table.scss
+++ b/packages/theme-chalk/src/date-picker/date-table.scss
@@ -130,11 +130,7 @@
&.selected .#{$namespace}-date-table-cell {
margin-left: 5px;
margin-right: 5px;
- background-color: getCssVar('datepicker-inrange-bg-color');
border-radius: 15px;
- &:hover {
- background-color: getCssVar('datepicker-inrange-hover-bg-color');
- }
}
&.selected .#{$namespace}-date-table-cell__text {
diff --git a/packages/theme-chalk/src/date-picker/month-table.scss b/packages/theme-chalk/src/date-picker/month-table.scss
index eb7f35e622096..ced74450cfb2b 100644
--- a/packages/theme-chalk/src/date-picker/month-table.scss
+++ b/packages/theme-chalk/src/date-picker/month-table.scss
@@ -7,9 +7,11 @@
border-collapse: collapse;
td {
+ width: 68px;
text-align: center;
padding: 8px 0;
cursor: pointer;
+ position: relative;
& div {
height: 48px;
padding: 6px 0;
@@ -37,13 +39,16 @@
}
.cell {
- width: 60px;
+ width: 54px;
height: 36px;
display: block;
line-height: 36px;
color: getCssVar('datepicker-text-color');
margin: 0 auto;
border-radius: 18px;
+ position: absolute;
+ left: 50%;
+ transform: translateX(-50%);
&:hover {
color: getCssVar('datepicker-hover-text-color');
}
@@ -67,23 +72,33 @@
}
&.start-date div {
+ margin-left: 3px;
border-top-left-radius: 24px;
border-bottom-left-radius: 24px;
}
&.end-date div {
+ margin-right: 3px;
border-top-right-radius: 24px;
border-bottom-right-radius: 24px;
}
+ &.current:not(.disabled) div {
+ border-radius: 24px;
+ margin-left: 3px;
+ margin-right: 3px;
+ }
+
&.current:not(.disabled) .cell {
- color: getCssVar('datepicker-active-color');
+ color: $color-white;
+ background-color: getCssVar('datepicker-active-color');
}
&:focus-visible {
outline: none;
.cell {
outline: 2px solid getCssVar('datepicker-active-color');
+ outline-offset: 1px;
}
}
}
diff --git a/packages/theme-chalk/src/date-picker/year-table.scss b/packages/theme-chalk/src/date-picker/year-table.scss
index 520ba17fa77da..e37a38f8bc257 100644
--- a/packages/theme-chalk/src/date-picker/year-table.scss
+++ b/packages/theme-chalk/src/date-picker/year-table.scss
@@ -11,9 +11,17 @@
}
td {
+ width: 68px;
text-align: center;
- padding: 20px 3px;
+ padding: 8px 0px;
cursor: pointer;
+ position: relative;
+
+ & div {
+ height: 48px;
+ padding: 6px 0;
+ box-sizing: border-box;
+ }
&.today {
.cell {
@@ -33,27 +41,38 @@
}
.cell {
- width: 48px;
+ width: 54px;
height: 36px;
display: block;
line-height: 36px;
color: getCssVar('datepicker-text-color');
border-radius: 18px;
margin: 0 auto;
+ position: absolute;
+ left: 50%;
+ transform: translateX(-50%);
&:hover {
color: getCssVar('datepicker-hover-text-color');
}
}
+ &.current:not(.disabled) div {
+ border-radius: 24px;
+ margin-left: 3px;
+ margin-right: 3px;
+ }
+
&.current:not(.disabled) .cell {
- color: getCssVar('datepicker-active-color');
+ color: $color-white;
+ background-color: getCssVar('datepicker-active-color');
}
&:focus-visible {
outline: none;
.cell {
outline: 2px solid getCssVar('datepicker-active-color');
+ outline-offset: 1px;
}
}
}
diff --git a/packages/theme-chalk/src/index.scss b/packages/theme-chalk/src/index.scss
index bbb1e7954fb9d..a4577814c5a2f 100644
--- a/packages/theme-chalk/src/index.scss
+++ b/packages/theme-chalk/src/index.scss
@@ -104,3 +104,5 @@
@use './option-group.scss';
@use './statistic.scss';
@use './tour.scss';
+@use './anchor.scss';
+@use './anchor-link.scss';
diff --git a/packages/theme-chalk/src/input.scss b/packages/theme-chalk/src/input.scss
index 60ffe977493a3..cd9f8a81a4cd4 100644
--- a/packages/theme-chalk/src/input.scss
+++ b/packages/theme-chalk/src/input.scss
@@ -464,3 +464,8 @@
}
}
}
+
+
+@include b(input-hidden) {
+ display: none !important;
+}
diff --git a/packages/theme-chalk/src/message.scss b/packages/theme-chalk/src/message.scss
index 5240595a87b6c..a271153ddc278 100644
--- a/packages/theme-chalk/src/message.scss
+++ b/packages/theme-chalk/src/message.scss
@@ -23,17 +23,12 @@
padding: getCssVar('message', 'padding');
display: flex;
align-items: center;
+ gap: 8px;
@include when(center) {
justify-content: center;
}
- @include when(closable) {
- .#{$namespace}-message__content {
- padding-right: 31px;
- }
- }
-
p {
margin: 0;
}
@@ -61,10 +56,6 @@
}
}
- @include e(icon) {
- margin-right: 10px;
- }
-
.#{$namespace}-message__badge {
position: absolute;
top: -8px;
@@ -81,10 +72,6 @@
}
& .#{$namespace}-message__closeBtn {
- position: absolute;
- top: 50%;
- right: 19px;
- transform: translateY(-50%);
cursor: pointer;
color: getCssVar('message', 'close-icon-color');
font-size: getCssVar('message', 'close-size');
diff --git a/packages/theme-chalk/src/select.scss b/packages/theme-chalk/src/select.scss
index 28910b0362d7f..bf55f20c93017 100644
--- a/packages/theme-chalk/src/select.scss
+++ b/packages/theme-chalk/src/select.scss
@@ -72,6 +72,10 @@
@include mixed-input-border(#{getCssVar('input-focus-border-color')});
}
+ @include e(selected-item) {
+ color: getCssVar('select-disabled-color');
+ }
+
@include e(caret) {
cursor: not-allowed;
}
diff --git a/packages/utils/dom/element.ts b/packages/utils/dom/element.ts
new file mode 100644
index 0000000000000..7dd1488043210
--- /dev/null
+++ b/packages/utils/dom/element.ts
@@ -0,0 +1,16 @@
+import { isString } from '../types'
+import { isClient } from '../browser'
+
+type GetElement = (
+ target: T
+) => T extends string ? HTMLElement | null : T
+
+export const getElement = ((
+ target: string | HTMLElement | Window | null | undefined
+) => {
+ if (!isClient || target === '') return null
+ if (isString(target)) {
+ return document.querySelector(target)
+ }
+ return target
+}) as GetElement
diff --git a/packages/utils/dom/index.ts b/packages/utils/dom/index.ts
index 91024cffaadb3..88d498223dc81 100644
--- a/packages/utils/dom/index.ts
+++ b/packages/utils/dom/index.ts
@@ -3,3 +3,4 @@ export * from './event'
export * from './position'
export * from './scroll'
export * from './style'
+export * from './element'
diff --git a/packages/utils/dom/scroll.ts b/packages/utils/dom/scroll.ts
index b3f7c23ce2aca..856f28d619d50 100644
--- a/packages/utils/dom/scroll.ts
+++ b/packages/utils/dom/scroll.ts
@@ -1,4 +1,7 @@
import { isClient } from '../browser'
+import { easeInOutCubic } from '../easings'
+import { isWindow } from '../types'
+import { rAF } from '../raf'
import { getStyle } from './style'
export const isScroll = (el: HTMLElement, isVertical?: boolean): boolean => {
@@ -99,3 +102,54 @@ export function scrollIntoView(
container.scrollTop = bottom - container.clientHeight
}
}
+
+export function animateScrollTo(
+ container: HTMLElement | Window,
+ from: number,
+ to: number,
+ duration: number,
+ callback?: unknown
+) {
+ const startTime = Date.now()
+
+ const scroll = () => {
+ const timestamp = Date.now()
+ const time = timestamp - startTime
+ const nextScrollTop = easeInOutCubic(
+ time > duration ? duration : time,
+ from,
+ to,
+ duration
+ )
+
+ if (isWindow(container)) {
+ container.scrollTo(window.pageXOffset, nextScrollTop)
+ } else {
+ container.scrollTop = nextScrollTop
+ }
+ if (time < duration) {
+ rAF(scroll)
+ } else if (typeof callback === 'function') {
+ callback()
+ }
+ }
+
+ scroll()
+}
+
+export const getScrollElement = (
+ target: HTMLElement,
+ container: HTMLElement | Window
+) => {
+ if (isWindow(container)) {
+ return target.ownerDocument.documentElement
+ }
+ return container
+}
+
+export const getScrollTop = (container: HTMLElement | Window) => {
+ if (isWindow(container)) {
+ return window.scrollY
+ }
+ return container.scrollTop
+}
diff --git a/packages/utils/easings.ts b/packages/utils/easings.ts
new file mode 100644
index 0000000000000..8accfa09da1a5
--- /dev/null
+++ b/packages/utils/easings.ts
@@ -0,0 +1,8 @@
+export function easeInOutCubic(t: number, b: number, c: number, d: number) {
+ const cc = c - b
+ t /= d / 2
+ if (t < 1) {
+ return (cc / 2) * t * t * t + b
+ }
+ return (cc / 2) * ((t -= 2) * t * t + 2) + b
+}
diff --git a/packages/utils/index.ts b/packages/utils/index.ts
index 17c4f21910612..944d05508e218 100644
--- a/packages/utils/index.ts
+++ b/packages/utils/index.ts
@@ -12,3 +12,5 @@ export * from './rand'
export * from './strings'
export * from './types'
export * from './typescript'
+export * from './throttleByRaf'
+export * from './easings'
diff --git a/packages/utils/throttleByRaf.ts b/packages/utils/throttleByRaf.ts
new file mode 100644
index 0000000000000..801d5f120bd53
--- /dev/null
+++ b/packages/utils/throttleByRaf.ts
@@ -0,0 +1,22 @@
+import { cAF, rAF } from './raf'
+
+export function throttleByRaf(cb: (...args: any[]) => void) {
+ let timer = 0
+
+ const throttle = (...args: any[]): void => {
+ if (timer) {
+ cAF(timer)
+ }
+ timer = rAF(() => {
+ cb(...args)
+ timer = 0
+ })
+ }
+
+ throttle.cancel = () => {
+ cAF(timer)
+ timer = 0
+ }
+
+ return throttle
+}
diff --git a/packages/utils/types.ts b/packages/utils/types.ts
index 2c9d424cf9923..15deaf3f07e61 100644
--- a/packages/utils/types.ts
+++ b/packages/utils/types.ts
@@ -36,3 +36,7 @@ export const isStringNumber = (val: string): boolean => {
}
return !Number.isNaN(Number(val))
}
+
+export const isWindow = (val: unknown): val is Window => {
+ return val === window
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d62438ab08a9d..70b9321b29ad5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -166,8 +166,8 @@ importers:
specifier: ^3.2.11
version: 3.2.11
husky:
- specifier: ^8.0.1
- version: 8.0.1
+ specifier: ^9.0.11
+ version: 9.0.11
jsdom:
specifier: 16.4.0
version: 16.4.0
@@ -7647,9 +7647,9 @@ packages:
engines: {node: '>=12.20.0'}
dev: true
- /husky@8.0.1:
- resolution: {integrity: sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw==}
- engines: {node: '>=14'}
+ /husky@9.0.11:
+ resolution: {integrity: sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==}
+ engines: {node: '>=18'}
hasBin: true
dev: true
diff --git a/typings/components.d.ts b/typings/components.d.ts
index e893cc635d72d..78607629489b3 100644
--- a/typings/components.d.ts
+++ b/typings/components.d.ts
@@ -101,6 +101,8 @@ declare module '@vue/runtime-core' {
ElWatermark: typeof import('../packages/element-plus')['ElWatermark']
ElTour: typeof import('../packages/element-plus')['ElTour']
ElTourStep: typeof import('../packages/element-plus')['ElTourStep']
+ ElAnchor: typeof import('../packages/element-plus')['ElAnchor']
+ ElAnchorLink: typeof import('../packages/element-plus')['ElAnchorLink']
}
interface ComponentCustomProperties {