mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-12-02 19:28:14 +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 { Loading, Search } from '@element-plus/icons-vue'
|
||||
import Button from '../src/button.vue'
|
||||
import ButtonGroup from '../src/button-group.vue'
|
||||
import type { ComponentSize } from '@element-plus/constants'
|
||||
|
||||
const AXIOM = 'Rem is the best girl'
|
||||
|
||||
@ -83,7 +84,7 @@ describe('Button.vue', () => {
|
||||
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()
|
||||
})
|
||||
|
||||
@ -119,25 +120,15 @@ describe('Button.vue', () => {
|
||||
|
||||
it('loading slot', () => {
|
||||
const App = defineComponent({
|
||||
setup() {
|
||||
return () =>
|
||||
h(
|
||||
Button,
|
||||
{
|
||||
loading: true,
|
||||
},
|
||||
{
|
||||
default: 'Loading',
|
||||
loading: h(
|
||||
'span',
|
||||
{
|
||||
class: 'custom-loading',
|
||||
},
|
||||
['111']
|
||||
),
|
||||
}
|
||||
)
|
||||
},
|
||||
setup: () => () =>
|
||||
(
|
||||
<Button
|
||||
v-slots={{ loading: <span class="custom-loading">111</span> }}
|
||||
loading={true}
|
||||
>
|
||||
Loading
|
||||
</Button>
|
||||
),
|
||||
})
|
||||
const wrapper = mount(App)
|
||||
expect(wrapper.find('.custom-loading').exists()).toBeTruthy()
|
||||
@ -146,30 +137,28 @@ describe('Button.vue', () => {
|
||||
describe('Button Group', () => {
|
||||
it('create', () => {
|
||||
const wrapper = mount({
|
||||
template: `
|
||||
<el-button-group>
|
||||
<el-button type="primary">Prev</el-button>
|
||||
<el-button type="primary">Next</el-button>
|
||||
</el-button-group>`,
|
||||
components: {
|
||||
'el-button-group': ButtonGroup,
|
||||
'el-button': Button,
|
||||
},
|
||||
setup: () => () =>
|
||||
(
|
||||
<ButtonGroup>
|
||||
<Button type="primary">Prev</Button>
|
||||
<Button type="primary">Next</Button>
|
||||
</ButtonGroup>
|
||||
),
|
||||
})
|
||||
expect(wrapper.classes()).toContain('el-button-group')
|
||||
expect(wrapper.findAll('button').length).toBe(2)
|
||||
})
|
||||
|
||||
it('button group reactive size', async () => {
|
||||
const size = ref('small')
|
||||
const size = ref<ComponentSize>('small')
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
return () =>
|
||||
h(ButtonGroup, { size: size.value }, () => [
|
||||
h(Button, { type: 'primary' }, () => 'Prev'),
|
||||
h(Button, { type: 'primary' }, () => 'Next'),
|
||||
])
|
||||
},
|
||||
setup: () => () =>
|
||||
(
|
||||
<ButtonGroup size={size.value}>
|
||||
<Button type="primary">Prev</Button>
|
||||
<Button type="primary">Next</Button>
|
||||
</ButtonGroup>
|
||||
),
|
||||
})
|
||||
expect(wrapper.classes()).toContain('el-button-group')
|
||||
expect(
|
||||
@ -186,13 +175,13 @@ describe('Button Group', () => {
|
||||
|
||||
it('button group type', async () => {
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
return () =>
|
||||
h(ButtonGroup, { type: 'warning' }, () => [
|
||||
h(Button, { type: 'primary' }, () => 'Prev'),
|
||||
h(Button, {}, () => 'Next'),
|
||||
])
|
||||
},
|
||||
setup: () => () =>
|
||||
(
|
||||
<ButtonGroup type="warning">
|
||||
<Button type="primary">Prev</Button>
|
||||
<Button>Next</Button>
|
||||
</ButtonGroup>
|
||||
),
|
||||
})
|
||||
expect(wrapper.classes()).toContain('el-button-group')
|
||||
expect(
|
@ -4,7 +4,7 @@ import { Loading } from '@element-plus/icons-vue'
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import type button from './button.vue'
|
||||
|
||||
export const buttonType = [
|
||||
export const buttonTypes = [
|
||||
'default',
|
||||
'primary',
|
||||
'success',
|
||||
@ -14,14 +14,14 @@ export const buttonType = [
|
||||
'text',
|
||||
'',
|
||||
] as const
|
||||
export const buttonNativeType = ['button', 'submit', 'reset'] as const
|
||||
export const buttonNativeTypes = ['button', 'submit', 'reset'] as const
|
||||
|
||||
export const buttonProps = buildProps({
|
||||
size: useSizeProp,
|
||||
disabled: Boolean,
|
||||
type: {
|
||||
type: String,
|
||||
values: buttonType,
|
||||
values: buttonTypes,
|
||||
default: '',
|
||||
},
|
||||
icon: {
|
||||
@ -30,7 +30,7 @@ export const buttonProps = buildProps({
|
||||
},
|
||||
nativeType: {
|
||||
type: String,
|
||||
values: buttonNativeType,
|
||||
values: buttonNativeTypes,
|
||||
default: 'button',
|
||||
},
|
||||
loading: Boolean,
|
||||
@ -48,11 +48,6 @@ export const buttonProps = buildProps({
|
||||
default: undefined,
|
||||
},
|
||||
} as const)
|
||||
|
||||
export interface ButtonConfigContext {
|
||||
autoInsertSpace?: boolean
|
||||
}
|
||||
|
||||
export const buttonEmits = {
|
||||
click: (evt: MouseEvent) => evt instanceof MouseEvent,
|
||||
}
|
||||
@ -64,3 +59,7 @@ export type ButtonType = ButtonProps['type']
|
||||
export type ButtonNativeType = ButtonProps['nativeType']
|
||||
|
||||
export type ButtonInstance = InstanceType<typeof button>
|
||||
|
||||
export interface ButtonConfigContext {
|
||||
autoInsertSpace?: boolean
|
||||
}
|
||||
|
@ -1,17 +1,17 @@
|
||||
<template>
|
||||
<button
|
||||
ref="buttonRef"
|
||||
ref="_ref"
|
||||
:class="[
|
||||
ns.b(),
|
||||
ns.m(buttonType),
|
||||
ns.m(buttonSize),
|
||||
ns.is('disabled', buttonDisabled),
|
||||
ns.m(_type),
|
||||
ns.m(_size),
|
||||
ns.is('disabled', _disabled),
|
||||
ns.is('loading', loading),
|
||||
ns.is('plain', plain),
|
||||
ns.is('round', round),
|
||||
ns.is('circle', circle),
|
||||
]"
|
||||
:disabled="buttonDisabled || loading"
|
||||
:disabled="_disabled || loading"
|
||||
:autofocus="autofocus"
|
||||
:type="nativeType"
|
||||
:style="buttonStyle"
|
||||
@ -35,8 +35,8 @@
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, inject, defineComponent, Text, ref } from 'vue'
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, Text, ref, useSlots } from 'vue'
|
||||
import { useCssVar } from '@vueuse/core'
|
||||
import { TinyColor } from '@ctrl/tinycolor'
|
||||
import { ElIcon } from '@element-plus/components/icon'
|
||||
@ -48,120 +48,102 @@ import {
|
||||
useSize,
|
||||
} from '@element-plus/hooks'
|
||||
import { buttonGroupContextKey } from '@element-plus/tokens'
|
||||
import { Loading } from '@element-plus/icons-vue'
|
||||
|
||||
import { buttonEmits, buttonProps } from './button'
|
||||
|
||||
export default defineComponent({
|
||||
defineOptions({
|
||||
name: 'ElButton',
|
||||
})
|
||||
|
||||
components: {
|
||||
ElIcon,
|
||||
Loading,
|
||||
},
|
||||
const props = defineProps(buttonProps)
|
||||
const emit = defineEmits(buttonEmits)
|
||||
const slots = useSlots()
|
||||
|
||||
props: buttonProps,
|
||||
emits: buttonEmits,
|
||||
const buttonGroupContext = inject(buttonGroupContextKey, undefined)
|
||||
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 buttonRef = ref()
|
||||
const buttonGroupContext = inject(buttonGroupContextKey, undefined)
|
||||
const globalConfig = useGlobalConfig('button')
|
||||
const ns = useNamespace('button')
|
||||
const autoInsertSpace = computed(
|
||||
() =>
|
||||
props.autoInsertSpace ?? globalConfig.value?.autoInsertSpace ?? false
|
||||
)
|
||||
const _type = computed(() => props.type || buttonGroupContext?.type || '')
|
||||
const autoInsertSpace = computed(
|
||||
() => props.autoInsertSpace ?? globalConfig.value?.autoInsertSpace ?? false
|
||||
)
|
||||
|
||||
// add space between two characters in Chinese
|
||||
const shouldAddSpace = computed(() => {
|
||||
const defaultSlot = slots.default?.()
|
||||
if (autoInsertSpace.value && defaultSlot?.length === 1) {
|
||||
const slot = defaultSlot[0]
|
||||
if (slot?.type === Text) {
|
||||
const text = slot.children
|
||||
return /^\p{Unified_Ideograph}{2}$/u.test(text as string)
|
||||
}
|
||||
// add space between two characters in Chinese
|
||||
const shouldAddSpace = computed(() => {
|
||||
const defaultSlot = slots.default?.()
|
||||
if (autoInsertSpace.value && defaultSlot?.length === 1) {
|
||||
const slot = defaultSlot[0]
|
||||
if (slot?.type === Text) {
|
||||
const text = slot.children
|
||||
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
|
||||
})
|
||||
|
||||
const { form } = useFormItem()
|
||||
const buttonSize = useSize(computed(() => buttonGroupContext?.size))
|
||||
const buttonDisabled = useDisabled()
|
||||
const buttonType = computed(
|
||||
() => props.type || buttonGroupContext?.type || ''
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
} else {
|
||||
const tintBgColor = color.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,
|
||||
}
|
||||
|
||||
return styles
|
||||
})
|
||||
|
||||
const handleClick = (evt: MouseEvent) => {
|
||||
if (props.nativeType === 'reset') {
|
||||
form?.resetFields()
|
||||
}
|
||||
emit('click', evt)
|
||||
}
|
||||
|
||||
return {
|
||||
buttonRef,
|
||||
buttonStyle,
|
||||
|
||||
buttonSize,
|
||||
buttonType,
|
||||
buttonDisabled,
|
||||
|
||||
shouldAddSpace,
|
||||
|
||||
handleClick,
|
||||
|
||||
ns,
|
||||
if (_disabled.value) {
|
||||
const disabledButtonColor = color.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)
|
||||
}
|
||||
|
||||
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>
|
||||
|
@ -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 { buildProps, definePropType, iconPropType } from '@element-plus/utils'
|
||||
import { useTooltipContentProps } from '@element-plus/components/tooltip'
|
||||
@ -16,12 +16,12 @@ export const popconfirmProps = buildProps({
|
||||
},
|
||||
confirmButtonType: {
|
||||
type: String,
|
||||
values: buttonType,
|
||||
values: buttonTypes,
|
||||
default: 'primary',
|
||||
},
|
||||
cancelButtonType: {
|
||||
type: String,
|
||||
values: buttonType,
|
||||
values: buttonTypes,
|
||||
default: 'text',
|
||||
},
|
||||
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', () => {
|
||||
it('should return local value', () => {
|
||||
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', () => {
|
||||
@ -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', () => {
|
||||
@ -77,7 +70,7 @@ describe('use-form-item', () => {
|
||||
} 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', () => {
|
||||
@ -92,6 +85,6 @@ describe('use-form-item', () => {
|
||||
} 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