mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-12-03 19:58:09 +08:00
refactor(components): refactor button (#5933)
* refactor(components): refactor button * refactor: rename * test: apply jsx * feat: expose * test: fix
This commit is contained in:
parent
0000686bbf
commit
ea812ae622
@ -1,8 +1,9 @@
|
|||||||
import { ref, h, nextTick, defineComponent } from 'vue'
|
import { ref, nextTick, defineComponent } from 'vue'
|
||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import { Loading, Search } from '@element-plus/icons-vue'
|
import { Loading, Search } from '@element-plus/icons-vue'
|
||||||
import Button from '../src/button.vue'
|
import Button from '../src/button.vue'
|
||||||
import ButtonGroup from '../src/button-group.vue'
|
import ButtonGroup from '../src/button-group.vue'
|
||||||
|
import type { ComponentSize } from '@element-plus/constants'
|
||||||
|
|
||||||
const AXIOM = 'Rem is the best girl'
|
const AXIOM = 'Rem is the best girl'
|
||||||
|
|
||||||
@ -83,7 +84,7 @@ describe('Button.vue', () => {
|
|||||||
default: '<span class="inner-slot"></span>',
|
default: '<span class="inner-slot"></span>',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
await (<HTMLElement>wrapper.element.querySelector('.inner-slot')).click()
|
wrapper.element.querySelector<HTMLElement>('.inner-slot')!.click()
|
||||||
expect(wrapper.emitted()).toBeDefined()
|
expect(wrapper.emitted()).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -119,25 +120,15 @@ describe('Button.vue', () => {
|
|||||||
|
|
||||||
it('loading slot', () => {
|
it('loading slot', () => {
|
||||||
const App = defineComponent({
|
const App = defineComponent({
|
||||||
setup() {
|
setup: () => () =>
|
||||||
return () =>
|
(
|
||||||
h(
|
<Button
|
||||||
Button,
|
v-slots={{ loading: <span class="custom-loading">111</span> }}
|
||||||
{
|
loading={true}
|
||||||
loading: true,
|
>
|
||||||
},
|
Loading
|
||||||
{
|
</Button>
|
||||||
default: 'Loading',
|
),
|
||||||
loading: h(
|
|
||||||
'span',
|
|
||||||
{
|
|
||||||
class: 'custom-loading',
|
|
||||||
},
|
|
||||||
['111']
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
const wrapper = mount(App)
|
const wrapper = mount(App)
|
||||||
expect(wrapper.find('.custom-loading').exists()).toBeTruthy()
|
expect(wrapper.find('.custom-loading').exists()).toBeTruthy()
|
||||||
@ -146,30 +137,28 @@ describe('Button.vue', () => {
|
|||||||
describe('Button Group', () => {
|
describe('Button Group', () => {
|
||||||
it('create', () => {
|
it('create', () => {
|
||||||
const wrapper = mount({
|
const wrapper = mount({
|
||||||
template: `
|
setup: () => () =>
|
||||||
<el-button-group>
|
(
|
||||||
<el-button type="primary">Prev</el-button>
|
<ButtonGroup>
|
||||||
<el-button type="primary">Next</el-button>
|
<Button type="primary">Prev</Button>
|
||||||
</el-button-group>`,
|
<Button type="primary">Next</Button>
|
||||||
components: {
|
</ButtonGroup>
|
||||||
'el-button-group': ButtonGroup,
|
),
|
||||||
'el-button': Button,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
expect(wrapper.classes()).toContain('el-button-group')
|
expect(wrapper.classes()).toContain('el-button-group')
|
||||||
expect(wrapper.findAll('button').length).toBe(2)
|
expect(wrapper.findAll('button').length).toBe(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('button group reactive size', async () => {
|
it('button group reactive size', async () => {
|
||||||
const size = ref('small')
|
const size = ref<ComponentSize>('small')
|
||||||
const wrapper = mount({
|
const wrapper = mount({
|
||||||
setup() {
|
setup: () => () =>
|
||||||
return () =>
|
(
|
||||||
h(ButtonGroup, { size: size.value }, () => [
|
<ButtonGroup size={size.value}>
|
||||||
h(Button, { type: 'primary' }, () => 'Prev'),
|
<Button type="primary">Prev</Button>
|
||||||
h(Button, { type: 'primary' }, () => 'Next'),
|
<Button type="primary">Next</Button>
|
||||||
])
|
</ButtonGroup>
|
||||||
},
|
),
|
||||||
})
|
})
|
||||||
expect(wrapper.classes()).toContain('el-button-group')
|
expect(wrapper.classes()).toContain('el-button-group')
|
||||||
expect(
|
expect(
|
||||||
@ -186,13 +175,13 @@ describe('Button Group', () => {
|
|||||||
|
|
||||||
it('button group type', async () => {
|
it('button group type', async () => {
|
||||||
const wrapper = mount({
|
const wrapper = mount({
|
||||||
setup() {
|
setup: () => () =>
|
||||||
return () =>
|
(
|
||||||
h(ButtonGroup, { type: 'warning' }, () => [
|
<ButtonGroup type="warning">
|
||||||
h(Button, { type: 'primary' }, () => 'Prev'),
|
<Button type="primary">Prev</Button>
|
||||||
h(Button, {}, () => 'Next'),
|
<Button>Next</Button>
|
||||||
])
|
</ButtonGroup>
|
||||||
},
|
),
|
||||||
})
|
})
|
||||||
expect(wrapper.classes()).toContain('el-button-group')
|
expect(wrapper.classes()).toContain('el-button-group')
|
||||||
expect(
|
expect(
|
@ -4,7 +4,7 @@ import { Loading } from '@element-plus/icons-vue'
|
|||||||
import type { ExtractPropTypes } from 'vue'
|
import type { ExtractPropTypes } from 'vue'
|
||||||
import type button from './button.vue'
|
import type button from './button.vue'
|
||||||
|
|
||||||
export const buttonType = [
|
export const buttonTypes = [
|
||||||
'default',
|
'default',
|
||||||
'primary',
|
'primary',
|
||||||
'success',
|
'success',
|
||||||
@ -14,14 +14,14 @@ export const buttonType = [
|
|||||||
'text',
|
'text',
|
||||||
'',
|
'',
|
||||||
] as const
|
] as const
|
||||||
export const buttonNativeType = ['button', 'submit', 'reset'] as const
|
export const buttonNativeTypes = ['button', 'submit', 'reset'] as const
|
||||||
|
|
||||||
export const buttonProps = buildProps({
|
export const buttonProps = buildProps({
|
||||||
size: useSizeProp,
|
size: useSizeProp,
|
||||||
disabled: Boolean,
|
disabled: Boolean,
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
values: buttonType,
|
values: buttonTypes,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
@ -30,7 +30,7 @@ export const buttonProps = buildProps({
|
|||||||
},
|
},
|
||||||
nativeType: {
|
nativeType: {
|
||||||
type: String,
|
type: String,
|
||||||
values: buttonNativeType,
|
values: buttonNativeTypes,
|
||||||
default: 'button',
|
default: 'button',
|
||||||
},
|
},
|
||||||
loading: Boolean,
|
loading: Boolean,
|
||||||
@ -48,11 +48,6 @@ export const buttonProps = buildProps({
|
|||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
} as const)
|
} as const)
|
||||||
|
|
||||||
export interface ButtonConfigContext {
|
|
||||||
autoInsertSpace?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const buttonEmits = {
|
export const buttonEmits = {
|
||||||
click: (evt: MouseEvent) => evt instanceof MouseEvent,
|
click: (evt: MouseEvent) => evt instanceof MouseEvent,
|
||||||
}
|
}
|
||||||
@ -64,3 +59,7 @@ export type ButtonType = ButtonProps['type']
|
|||||||
export type ButtonNativeType = ButtonProps['nativeType']
|
export type ButtonNativeType = ButtonProps['nativeType']
|
||||||
|
|
||||||
export type ButtonInstance = InstanceType<typeof button>
|
export type ButtonInstance = InstanceType<typeof button>
|
||||||
|
|
||||||
|
export interface ButtonConfigContext {
|
||||||
|
autoInsertSpace?: boolean
|
||||||
|
}
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
ref="buttonRef"
|
ref="_ref"
|
||||||
:class="[
|
:class="[
|
||||||
ns.b(),
|
ns.b(),
|
||||||
ns.m(buttonType),
|
ns.m(_type),
|
||||||
ns.m(buttonSize),
|
ns.m(_size),
|
||||||
ns.is('disabled', buttonDisabled),
|
ns.is('disabled', _disabled),
|
||||||
ns.is('loading', loading),
|
ns.is('loading', loading),
|
||||||
ns.is('plain', plain),
|
ns.is('plain', plain),
|
||||||
ns.is('round', round),
|
ns.is('round', round),
|
||||||
ns.is('circle', circle),
|
ns.is('circle', circle),
|
||||||
]"
|
]"
|
||||||
:disabled="buttonDisabled || loading"
|
:disabled="_disabled || loading"
|
||||||
:autofocus="autofocus"
|
:autofocus="autofocus"
|
||||||
:type="nativeType"
|
:type="nativeType"
|
||||||
:style="buttonStyle"
|
:style="buttonStyle"
|
||||||
@ -35,8 +35,8 @@
|
|||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { computed, inject, defineComponent, Text, ref } from 'vue'
|
import { computed, inject, Text, ref, useSlots } from 'vue'
|
||||||
import { useCssVar } from '@vueuse/core'
|
import { useCssVar } from '@vueuse/core'
|
||||||
import { TinyColor } from '@ctrl/tinycolor'
|
import { TinyColor } from '@ctrl/tinycolor'
|
||||||
import { ElIcon } from '@element-plus/components/icon'
|
import { ElIcon } from '@element-plus/components/icon'
|
||||||
@ -48,120 +48,102 @@ import {
|
|||||||
useSize,
|
useSize,
|
||||||
} from '@element-plus/hooks'
|
} from '@element-plus/hooks'
|
||||||
import { buttonGroupContextKey } from '@element-plus/tokens'
|
import { buttonGroupContextKey } from '@element-plus/tokens'
|
||||||
import { Loading } from '@element-plus/icons-vue'
|
|
||||||
|
|
||||||
import { buttonEmits, buttonProps } from './button'
|
import { buttonEmits, buttonProps } from './button'
|
||||||
|
|
||||||
export default defineComponent({
|
defineOptions({
|
||||||
name: 'ElButton',
|
name: 'ElButton',
|
||||||
|
})
|
||||||
|
|
||||||
components: {
|
const props = defineProps(buttonProps)
|
||||||
ElIcon,
|
const emit = defineEmits(buttonEmits)
|
||||||
Loading,
|
const slots = useSlots()
|
||||||
},
|
|
||||||
|
|
||||||
props: buttonProps,
|
const buttonGroupContext = inject(buttonGroupContextKey, undefined)
|
||||||
emits: buttonEmits,
|
const globalConfig = useGlobalConfig('button')
|
||||||
|
const ns = useNamespace('button')
|
||||||
|
const { form } = useFormItem()
|
||||||
|
const _size = useSize(computed(() => buttonGroupContext?.size))
|
||||||
|
const _disabled = useDisabled()
|
||||||
|
const _ref = ref<HTMLButtonElement>()
|
||||||
|
|
||||||
setup(props, { emit, slots }) {
|
const _type = computed(() => props.type || buttonGroupContext?.type || '')
|
||||||
const buttonRef = ref()
|
const autoInsertSpace = computed(
|
||||||
const buttonGroupContext = inject(buttonGroupContextKey, undefined)
|
() => props.autoInsertSpace ?? globalConfig.value?.autoInsertSpace ?? false
|
||||||
const globalConfig = useGlobalConfig('button')
|
)
|
||||||
const ns = useNamespace('button')
|
|
||||||
const autoInsertSpace = computed(
|
|
||||||
() =>
|
|
||||||
props.autoInsertSpace ?? globalConfig.value?.autoInsertSpace ?? false
|
|
||||||
)
|
|
||||||
|
|
||||||
// add space between two characters in Chinese
|
// add space between two characters in Chinese
|
||||||
const shouldAddSpace = computed(() => {
|
const shouldAddSpace = computed(() => {
|
||||||
const defaultSlot = slots.default?.()
|
const defaultSlot = slots.default?.()
|
||||||
if (autoInsertSpace.value && defaultSlot?.length === 1) {
|
if (autoInsertSpace.value && defaultSlot?.length === 1) {
|
||||||
const slot = defaultSlot[0]
|
const slot = defaultSlot[0]
|
||||||
if (slot?.type === Text) {
|
if (slot?.type === Text) {
|
||||||
const text = slot.children
|
const text = slot.children
|
||||||
return /^\p{Unified_Ideograph}{2}$/u.test(text as string)
|
return /^\p{Unified_Ideograph}{2}$/u.test(text as string)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
// calculate hover & active color by color
|
||||||
|
const typeColor = computed(() => useCssVar(`--el-color-${props.type}`).value)
|
||||||
|
const buttonStyle = computed(() => {
|
||||||
|
let styles: Record<string, string> = {}
|
||||||
|
|
||||||
|
const buttonColor = props.color || typeColor.value
|
||||||
|
|
||||||
|
if (buttonColor) {
|
||||||
|
const color = new TinyColor(buttonColor)
|
||||||
|
const shadeBgColor = color.shade(10).toString()
|
||||||
|
if (props.plain) {
|
||||||
|
styles = {
|
||||||
|
'--el-button-bg-color': color.tint(90).toString(),
|
||||||
|
'--el-button-text-color': buttonColor,
|
||||||
|
'--el-button-hover-text-color': 'var(--el-color-white)',
|
||||||
|
'--el-button-hover-bg-color': buttonColor,
|
||||||
|
'--el-button-hover-border-color': buttonColor,
|
||||||
|
'--el-button-active-bg-color': shadeBgColor,
|
||||||
|
'--el-button-active-text-color': 'var(--el-color-white)',
|
||||||
|
'--el-button-active-border-color': shadeBgColor,
|
||||||
}
|
}
|
||||||
return false
|
} else {
|
||||||
})
|
const tintBgColor = color.tint(20).toString()
|
||||||
|
styles = {
|
||||||
const { form } = useFormItem()
|
'--el-button-bg-color': buttonColor,
|
||||||
const buttonSize = useSize(computed(() => buttonGroupContext?.size))
|
'--el-button-border-color': buttonColor,
|
||||||
const buttonDisabled = useDisabled()
|
'--el-button-hover-bg-color': tintBgColor,
|
||||||
const buttonType = computed(
|
'--el-button-hover-border-color': tintBgColor,
|
||||||
() => props.type || buttonGroupContext?.type || ''
|
'--el-button-active-bg-color': shadeBgColor,
|
||||||
)
|
'--el-button-active-border-color': shadeBgColor,
|
||||||
|
|
||||||
// calculate hover & active color by color
|
|
||||||
const typeColor = computed(
|
|
||||||
() => useCssVar(`--el-color-${props.type}`).value
|
|
||||||
)
|
|
||||||
const buttonStyle = computed(() => {
|
|
||||||
let styles = {}
|
|
||||||
|
|
||||||
const buttonColor = props.color || typeColor.value
|
|
||||||
|
|
||||||
if (buttonColor) {
|
|
||||||
const shadeBgColor = new TinyColor(buttonColor).shade(10).toString()
|
|
||||||
if (props.plain) {
|
|
||||||
styles = {
|
|
||||||
'--el-button-bg-color': new TinyColor(buttonColor)
|
|
||||||
.tint(90)
|
|
||||||
.toString(),
|
|
||||||
'--el-button-text-color': buttonColor,
|
|
||||||
'--el-button-hover-text-color': 'var(--el-color-white)',
|
|
||||||
'--el-button-hover-bg-color': buttonColor,
|
|
||||||
'--el-button-hover-border-color': buttonColor,
|
|
||||||
'--el-button-active-bg-color': shadeBgColor,
|
|
||||||
'--el-button-active-text-color': 'var(--el-color-white)',
|
|
||||||
'--el-button-active-border-color': shadeBgColor,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const tintBgColor = new TinyColor(buttonColor).tint(20).toString()
|
|
||||||
styles = {
|
|
||||||
'--el-button-bg-color': buttonColor,
|
|
||||||
'--el-button-border-color': buttonColor,
|
|
||||||
'--el-button-hover-bg-color': tintBgColor,
|
|
||||||
'--el-button-hover-border-color': tintBgColor,
|
|
||||||
'--el-button-active-bg-color': shadeBgColor,
|
|
||||||
'--el-button-active-border-color': shadeBgColor,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (buttonDisabled.value) {
|
|
||||||
const disabledButtonColor = new TinyColor(buttonColor)
|
|
||||||
.tint(50)
|
|
||||||
.toString()
|
|
||||||
styles['--el-button-disabled-bg-color'] = disabledButtonColor
|
|
||||||
styles['--el-button-disabled-border-color'] = disabledButtonColor
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return styles
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleClick = (evt: MouseEvent) => {
|
|
||||||
if (props.nativeType === 'reset') {
|
|
||||||
form?.resetFields()
|
|
||||||
}
|
|
||||||
emit('click', evt)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
if (_disabled.value) {
|
||||||
buttonRef,
|
const disabledButtonColor = color.tint(50).toString()
|
||||||
buttonStyle,
|
styles['--el-button-disabled-bg-color'] = disabledButtonColor
|
||||||
|
styles['--el-button-disabled-border-color'] = disabledButtonColor
|
||||||
buttonSize,
|
|
||||||
buttonType,
|
|
||||||
buttonDisabled,
|
|
||||||
|
|
||||||
shouldAddSpace,
|
|
||||||
|
|
||||||
handleClick,
|
|
||||||
|
|
||||||
ns,
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
|
return styles
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleClick = (evt: MouseEvent) => {
|
||||||
|
if (props.nativeType === 'reset') {
|
||||||
|
form?.resetFields()
|
||||||
|
}
|
||||||
|
emit('click', evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
/** @description button html element */
|
||||||
|
ref: _ref,
|
||||||
|
/** @description button size */
|
||||||
|
size: _size,
|
||||||
|
/** @description button type */
|
||||||
|
type: _type,
|
||||||
|
/** @description button disabled */
|
||||||
|
disabled: _disabled,
|
||||||
|
/** @description whether adding space */
|
||||||
|
shouldAddSpace,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { buttonType } from '@element-plus/components/button'
|
import { buttonTypes } from '@element-plus/components/button'
|
||||||
import { QuestionFilled } from '@element-plus/icons-vue'
|
import { QuestionFilled } from '@element-plus/icons-vue'
|
||||||
import { buildProps, definePropType, iconPropType } from '@element-plus/utils'
|
import { buildProps, definePropType, iconPropType } from '@element-plus/utils'
|
||||||
import { useTooltipContentProps } from '@element-plus/components/tooltip'
|
import { useTooltipContentProps } from '@element-plus/components/tooltip'
|
||||||
@ -16,12 +16,12 @@ export const popconfirmProps = buildProps({
|
|||||||
},
|
},
|
||||||
confirmButtonType: {
|
confirmButtonType: {
|
||||||
type: String,
|
type: String,
|
||||||
values: buttonType,
|
values: buttonTypes,
|
||||||
default: 'primary',
|
default: 'primary',
|
||||||
},
|
},
|
||||||
cancelButtonType: {
|
cancelButtonType: {
|
||||||
type: String,
|
type: String,
|
||||||
values: buttonType,
|
values: buttonTypes,
|
||||||
default: 'text',
|
default: 'text',
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
|
@ -34,17 +34,10 @@ const mountComponent = (setup = NOOP, options = {}) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getButtonVm = (wrapper: ReturnType<typeof mountComponent>) => {
|
|
||||||
return wrapper.findComponent(ElButton).vm as any as {
|
|
||||||
buttonSize: string
|
|
||||||
buttonDisabled: boolean
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('use-form-item', () => {
|
describe('use-form-item', () => {
|
||||||
it('should return local value', () => {
|
it('should return local value', () => {
|
||||||
const wrapper = mountComponent()
|
const wrapper = mountComponent()
|
||||||
expect(getButtonVm(wrapper).buttonSize).toBe('default')
|
expect(wrapper.find('.el-button--default').exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return props.size instead of injected.size', () => {
|
it('should return props.size instead of injected.size', () => {
|
||||||
@ -62,7 +55,7 @@ describe('use-form-item', () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(getButtonVm(wrapper).buttonSize).toBe(propSize)
|
expect(wrapper.find(`.el-button--${propSize}`).exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return fallback.size instead inject.size', () => {
|
it('should return fallback.size instead inject.size', () => {
|
||||||
@ -77,7 +70,7 @@ describe('use-form-item', () => {
|
|||||||
} as ElFormItemContext)
|
} as ElFormItemContext)
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(getButtonVm(wrapper).buttonSize).toBe(fallbackSize)
|
expect(wrapper.find(`.el-button--${fallbackSize}`).exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return formItem.size instead form.size', () => {
|
it('should return formItem.size instead form.size', () => {
|
||||||
@ -92,6 +85,6 @@ describe('use-form-item', () => {
|
|||||||
} as ElFormContext)
|
} as ElFormContext)
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(getButtonVm(wrapper).buttonSize).toBe(itemSize)
|
expect(wrapper.find(`.el-button--${itemSize}`).exists()).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user