refactor(components): refactor button (#5933)

* refactor(components): refactor button

* refactor: rename

* test: apply jsx

* feat: expose

* test: fix
This commit is contained in:
三咲智子 2022-02-12 18:37:16 +08:00 committed by GitHub
parent 0000686bbf
commit ea812ae622
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 141 additions and 178 deletions

View File

@ -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(

View File

@ -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
}

View File

@ -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>

View File

@ -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: {

View File

@ -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)
}) })
}) })