From b4c51a090bb2251357b6c02a9d03a3eaeec81993 Mon Sep 17 00:00:00 2001 From: Jay Date: Sun, 22 Aug 2021 11:38:33 +0800 Subject: [PATCH 1/2] =?UTF-8?q?refactor:=20radio=E3=80=81radio-group?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E5=8A=9F=E8=83=BD=E5=AE=8C=E5=96=84=E4=B8=8E?= =?UTF-8?q?=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devui/radio/__tests__/radio-group.spec.ts | 58 ++-- devui/radio/__tests__/radio.spec.ts | 109 ++++--- devui/radio/index.ts | 8 +- devui/radio/src/radio-group.scss | 21 +- devui/radio/src/radio-group.tsx | 61 +++- .../src/{use-radio.ts => radio-types.ts} | 118 ++++---- devui/radio/src/radio.scss | 143 ++++----- devui/radio/src/radio.tsx | 76 +++-- sites/components/radio/index.md | 284 +++++++++++++++++- 9 files changed, 590 insertions(+), 288 deletions(-) rename devui/radio/src/{use-radio.ts => radio-types.ts} (37%) diff --git a/devui/radio/__tests__/radio-group.spec.ts b/devui/radio/__tests__/radio-group.spec.ts index 8949a5d6..c28a920c 100644 --- a/devui/radio/__tests__/radio-group.spec.ts +++ b/devui/radio/__tests__/radio-group.spec.ts @@ -12,13 +12,13 @@ describe('RadioGroup', () => { DRadio }, template: ` - - AB - CD + + Item1 + Item2 `, setup () { - const radioVal = ref('ab'); + const radioVal = ref('Item1'); return { radioVal, onChange @@ -44,20 +44,20 @@ describe('RadioGroup', () => { it('radioGroup cssStyle work', async () => { const wrapper = mount(DRadioGroup, { props: { - value: 'AA' + value: 'Item1' } }); - expect(wrapper.html()).not.toMatch('devui-radio-horizontal'); + expect(wrapper.html()).not.toMatch('is-row'); await wrapper.setProps({ - value: 'AA', + value: 'Item1', cssStyle: 'row' }); - expect(wrapper.html()).toMatch('devui-radio-horizontal'); + expect(wrapper.html()).toMatch('is-row'); }); it('radioGroup beforeChange work', async () => { - const beforeChange = jest.fn(v => v !== 'bb'); + const beforeChange = jest.fn(v => v !== 'Item2'); const onChange = jest.fn(); const wrapper = mount({ components: { @@ -65,14 +65,14 @@ describe('RadioGroup', () => { DRadio }, template: ` - - AA - BB - CC + + Item1 + Item2 + Item3 `, setup () { - const radioVal = ref('aa'); + const radioVal = ref('Item1'); return { radioVal, onChange, @@ -81,18 +81,18 @@ describe('RadioGroup', () => { } }); - const [radioA, radioB, radioC] = wrapper.findAllComponents({ name: 'DRadio' }); - expect(radioA.classes()).toContain('active'); + const [radio1, radio2, radio3] = wrapper.findAllComponents({ name: 'DRadio' }); + expect(radio1.classes()).toContain('active'); - await radioB.find('input').trigger('change'); - expect(radioA.classes()).toContain('active'); + await radio2.find('input').trigger('change'); + expect(radio1.classes()).toContain('active'); expect(beforeChange).toBeCalledTimes(1); expect(onChange).toBeCalledTimes(0); beforeChange.mockReset(); - await radioC.find('input').trigger('change'); - expect(radioA.classes()).not.toContain('active'); - expect(radioC.classes()).toContain('active'); + await radio3.find('input').trigger('change'); + expect(radio1.classes()).not.toContain('active'); + expect(radio3.classes()).toContain('active'); expect(beforeChange).toBeCalledTimes(1); expect(onChange).toBeCalledTimes(1); }); @@ -105,13 +105,13 @@ describe('RadioGroup', () => { DRadio }, template: ` - - EE - FF + + Item1 + Item2 `, setup () { - const radioVal = ref('ee'); + const radioVal = ref('Item1'); return { radioVal, onChange @@ -119,11 +119,11 @@ describe('RadioGroup', () => { } }); - const radioF = wrapper.findAllComponents({ name: 'DRadio' })[1]; - const inputF = wrapper.findAll('input')[1]; + const radio2 = wrapper.findAllComponents({ name: 'DRadio' })[1]; + const input2 = wrapper.findAll('input')[1]; - await inputF.trigger('change'); - expect(radioF.classes()).not.toContain('active'); + await input2.trigger('change'); + expect(radio2.classes()).not.toContain('active'); expect(onChange).toHaveBeenCalledTimes(0); }); }); diff --git a/devui/radio/__tests__/radio.spec.ts b/devui/radio/__tests__/radio.spec.ts index 5e1e93c6..a9d38f88 100644 --- a/devui/radio/__tests__/radio.spec.ts +++ b/devui/radio/__tests__/radio.spec.ts @@ -1,81 +1,80 @@ -import { mount } from '@vue/test-utils'; -import { ref } from 'vue'; -import DRadio from '../src/radio'; +import { mount } from '@vue/test-utils' +import { ref } from 'vue' +import DRadio from '../src/radio' describe('Radio', () => { it('radio render work', async () => { - const onChange = jest.fn(); + const onChange = jest.fn() const wrapper = mount({ components: { DRadio }, - template: `ItemA`, - setup () { - const checked = ref(false); + template: `ItemA`, + setup() { + const modelValue = ref('Item1') return { - checked, - onChange - }; + modelValue, + onChange, + } }, - }); + }) - expect(wrapper.classes()).toContain('devui-radio'); - expect(wrapper.classes()).not.toContain('active'); - expect(wrapper.text()).toEqual('ItemA'); + expect(wrapper.classes()).toContain('devui-radio') + expect(wrapper.classes()).not.toContain('active') + expect(wrapper.text()).toEqual('ItemA') - const input = wrapper.find('input'); - await input.trigger('change'); - expect(onChange).toBeCalledTimes(1); - }); + const input = wrapper.find('input') + await input.trigger('change') + expect(onChange).toBeCalledTimes(1) + }) it('radio value work', () => { const wrapper = mount(DRadio, { props: { - value: 'ABC' - } - }); - const input = wrapper.find('input'); - expect(input.attributes()['value']).toEqual('ABC'); - }); + value: 'Item1', + }, + }) + const input = wrapper.find('input') + expect(input.attributes()['value']).toEqual('Item1') + }) it('radio disabled work', async () => { - const onChange = jest.fn(); + const onChange = jest.fn() const wrapper = mount(DRadio, { props: { - value: 'CD', + value: 'Item1', disabled: true, - onChange - } - }); - const circle = wrapper.find('circle'); - expect(circle.classes()).toContain('disabled'); - const input = wrapper.find('input'); - await input.trigger('change'); - expect(onChange).toBeCalledTimes(0); - }); + onChange, + }, + }) + expect(wrapper.classes()).toContain('disabled') + const input = wrapper.find('input') + await input.trigger('change') + expect(onChange).toBeCalledTimes(0) + }) it('radio beforeChange work', async () => { - const beforeChange = jest.fn(() => true); - const onChange = jest.fn(); + const beforeChange = jest.fn(() => true) + const onChange = jest.fn() const wrapper = mount(DRadio, { props: { - value: 'EF', + value: 'Item1', onChange, - beforeChange - } - }); - const input = wrapper.find('input'); - await input.trigger('change'); - expect(beforeChange).toHaveBeenCalledTimes(1); - expect(onChange).toHaveBeenCalledTimes(1); + beforeChange, + }, + }) + const input = wrapper.find('input') + await input.trigger('change') + expect(beforeChange).toHaveBeenCalledTimes(1) + expect(onChange).toHaveBeenCalledTimes(1) - const beforeChangeFalse = jest.fn(() => false); - onChange.mockReset(); + const beforeChangeFalse = jest.fn(() => false) + onChange.mockReset() await wrapper.setProps({ - value: 'CD', + value: 'Item2', beforeChange: beforeChangeFalse, - onChange - }); - await input.trigger('change'); - expect(beforeChangeFalse).toHaveBeenCalledTimes(1); - expect(onChange).toHaveBeenCalledTimes(0); - }); -}); + onChange, + }) + await input.trigger('change') + expect(beforeChangeFalse).toHaveBeenCalledTimes(1) + expect(onChange).toHaveBeenCalledTimes(0) + }) +}) diff --git a/devui/radio/index.ts b/devui/radio/index.ts index cfe383dd..00ab6857 100644 --- a/devui/radio/index.ts +++ b/devui/radio/index.ts @@ -1,16 +1,22 @@ import type { App } from 'vue' import Radio from './src/radio' +import RadioGroup from './src/radio-group' Radio.install = function(app: App) { app.component(Radio.name, Radio) } -export { Radio } +RadioGroup.install = function(app: App) { + app.component(RadioGroup.name, RadioGroup) +} + +export { Radio, RadioGroup } export default { title: 'Radio 单选框', category: '数据录入', install(app: App): void { app.use(Radio as any) + app.use(RadioGroup as any) } } diff --git a/devui/radio/src/radio-group.scss b/devui/radio/src/radio-group.scss index 936b05b7..0b0cbaf0 100644 --- a/devui/radio/src/radio-group.scss +++ b/devui/radio/src/radio-group.scss @@ -1,16 +1,23 @@ .devui-radio-group { - .devui-radio { - display: block; - height: 28px; + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + align-items: flex-start; + + &.is-row { + flex-direction: row; + } + + &.is-column { + flex-direction: column; } -} -.devui-radio-horizontal { .devui-radio { - display: inline-block; + line-height: 28px; &:not(:last-child) { - padding-right: 20px; + margin-right: 20px; + margin-bottom: 0; } } } diff --git a/devui/radio/src/radio-group.tsx b/devui/radio/src/radio-group.tsx index ea5aaac1..f636bd06 100644 --- a/devui/radio/src/radio-group.tsx +++ b/devui/radio/src/radio-group.tsx @@ -1,34 +1,65 @@ import { defineComponent, provide, toRef, ExtractPropTypes } from 'vue'; -import { radioGroupProps } from './use-radio'; -import { radioGroupInjectionKey } from './use-radio'; +import DRadio from './radio' +import { radioGroupProps, radioGroupInjectionKey } from './radio-types'; import './radio-group.scss'; export default defineComponent({ name: 'DRadioGroup', props: radioGroupProps, - emits: ['change', 'update:value'], - setup (props: ExtractPropTypes, ctx) { - const { emit } = ctx; - const doChange = (radioValue: string) => { - emit('update:value', radioValue); + emits: ['change', 'update:modelValue'], + setup(props: ExtractPropTypes, { emit }) { + /** change 事件 */ + const emitChange = (radioValue: string) => { + emit('update:modelValue', radioValue); emit('change', radioValue); }; + + // 注入给子组件 provide(radioGroupInjectionKey, { - value: toRef(props, 'value'), + modelValue: toRef(props, 'modelValue'), name: toRef(props, 'name'), + disabled: toRef(props, 'disabled'), beforeChange: props.beforeChange, - doChange + emitChange }); }, - render () { + render() { const { - cssStyle + cssStyle, + values } = this; + /** 获取展示内容 */ + const getContent = () => { + const defaultSlot = this.$slots.default; + // 有默认插槽则使用默认插槽 + if (defaultSlot) { + return defaultSlot() + } + // 有数据列表则使用数据列表 + else if (Array.isArray(values)) { + return values.map(item => { + return ( + + {item} + + ) + }) + } + // 什么都没有则返回空 + else { + return '' + } + } + return ( -
-
- {this.$slots.default?.()} -
+
+ {getContent()}
); } diff --git a/devui/radio/src/use-radio.ts b/devui/radio/src/radio-types.ts similarity index 37% rename from devui/radio/src/use-radio.ts rename to devui/radio/src/radio-types.ts index c265897c..f4aa59b7 100644 --- a/devui/radio/src/use-radio.ts +++ b/devui/radio/src/radio-types.ts @@ -1,55 +1,63 @@ -import type { InjectionKey, PropType, Ref } from 'vue'; - -const radioCommonProps = { - name: { - type: String as PropType, - default: undefined - }, - value: { - type: String, - required: true - }, - beforeChange: { - type: Function as PropType<(value: string) => boolean | Promise>, - default: undefined - }, - onChange: { - type: Function as PropType<(v: string) => void>, - default: undefined - } -} as const; - -export const radioProps = { - ...radioCommonProps, - 'onUpdate:checked': Function as PropType<(v: boolean) => void>, - checked: { - type: Boolean, - default: false - }, - disabled: { - type: Boolean, - default: false - } -} as const; - -export const radioGroupProps = { - ...radioCommonProps, - name: { - type: String as PropType, - default: undefined - }, - 'onUpdate:value': Function as PropType<(value: string) => void>, - cssStyle: { - type: String as PropType<'row' | 'column'>, - default: 'column' - } -} as const; - -interface RadioGroupInjection { - value: Ref - name: Ref - beforeChange: (value: string) => boolean | Promise - doChange: (value: string) => void -} - -export const radioGroupInjectionKey: InjectionKey = Symbol('DRadioGroup'); +import type { InjectionKey, PropType, Ref } from 'vue' + +/** radio、radio-group 共用 props */ +const radioCommonProps = { + /** 双向绑定的值 */ + modelValue: { + type: String, + default: null, + }, + /** 单选框的名称 */ + name: { + type: String as PropType, + default: null, + }, + /** 值改变之前触发的事件 */ + beforeChange: { + type: Function as PropType<(value: string) => boolean | Promise>, + default: null, + }, + /** 是否禁用 */ + disabled: { + type: Boolean, + default: false, + }, +} + +/** radio 的 props */ +export const radioProps = { + ...radioCommonProps, + /** 单选框的值 */ + value: { + type: String, + required: true, + default: null, + }, +} as const + +/** radio-group 的 props */ +export const radioGroupProps = { + ...radioCommonProps, + /** 选项列表 */ + values: { + type: Array as PropType, + default: null, + }, + /** 展示方式,横向/竖向 */ + cssStyle: { + type: String as PropType<'row' | 'column'>, + default: 'column', + }, +} as const + +/** radio-group 注入字段的接口 */ +interface RadioGroupInjection { + modelValue: Ref + name: Ref + disabled: Ref + beforeChange: (value: string) => boolean | Promise + emitChange: (value: string) => void +} + +/** radio-group 注入 radio 的 key 值 */ +export const radioGroupInjectionKey: InjectionKey = Symbol('DRadioGroup') diff --git a/devui/radio/src/radio.scss b/devui/radio/src/radio.scss index 4cf62e7a..0238ac77 100644 --- a/devui/radio/src/radio.scss +++ b/devui/radio/src/radio.scss @@ -2,129 +2,106 @@ @import '../../style/core/_font'; .devui-radio { - &.disabled { - cursor: not-allowed; - - .devui-radio-label { - color: $devui-disabled-text; - } - } - + display: flex; + justify-content: flex-start; + align-items: center; font-size: $devui-font-size; line-height: 1.5; font-weight: normal; cursor: pointer; color: $devui-text; - margin: 0 auto; - - svg { - .devui-outer { - stroke: $devui-line; - fill: transparent; - - &.disabled { - stroke: $devui-disabled-line; - fill: $devui-disabled-bg; - } - } - .devui-inner { - fill: $devui-icon-fill-active; - } + &:not(:last-child) { + margin-bottom: 20px; } - &:hover:not(.disabled) { - svg .devui-outer { - stroke: $devui-form-control-line-active; - } - + &:hover { .devui-radio-label { color: $devui-primary-hover; } } - &:active:not(.disabled), - &:focus:not(.disabled) { - svg .devui-outer { - stroke: $devui-form-control-line-active; + &:active, + &:focus, + &:hover { + .devui-radio-material-outer { + stroke: $devui-form-control-line-active-hover; + } + + .devui-radio-material-inner { + fill: $devui-icon-fill-active-hover; } } &.active { - svg { - .devui-outer { - opacity: 1; - stroke: $devui-form-control-line-active; - transition: stroke 50ms cubic-bezier(0.755, 0.05, 0.855, 0.06); - - &.disabled { - stroke: $devui-icon-fill-active-disabled; - fill: transparent; - } - } - - .devui-inner { - opacity: 1; - - &.disabled { - fill: $devui-icon-fill-active-disabled; - } - - transform: scale(1); - transition: transform 200ms cubic-bezier(0.23, 1, 0.32, 1), opacity 200ms cubic-bezier(0.23, 1, 0.32, 1); - } + .devui-radio-material-inner { + opacity: 1; + transform: scale(1); + transition: + transform 200ms cubic-bezier(0.23, 1, 0.32, 1), + opacity 200ms cubic-bezier(0.23, 1, 0.32, 1); } + } - &:not(.disabled) { - &:active, - &:focus, - &:hover { - .devui-radio-material { - svg { - .devui-outer { - stroke: $devui-form-control-line-active-hover; - } + &.disabled { + cursor: not-allowed; - .devui-inner { - fill: $devui-icon-fill-active-hover; - } - } - } - } + /* 选择图标-外圈 */ + .devui-radio-material-outer { + stroke: $devui-disabled-line; + fill: $devui-disabled-bg; } - } - .devui-outer { - opacity: 1; - transition: stroke 50ms cubic-bezier(0.755, 0.05, 0.855, 0.06); - } + /* 选择图标-内圈 */ + .devui-radio-material-inner { + fill: $devui-icon-fill-active-disabled; + } - .devui-inner { - opacity: 0; - transform: scale(0); - transform-origin: 50% 50%; - transition: transform 200ms cubic-bezier(0.755, 0.05, 0.855, 0.06), opacity 200ms cubic-bezier(0.755, 0.05, 0.855, 0.06); + .devui-radio-label { + color: $devui-disabled-text; + } } - .devui-radio-material { + /* 选择图标-容器 */ + &-material { vertical-align: middle; position: relative; display: inline-block; overflow: hidden; height: 16px; width: 16px; + line-height: 16px; user-select: none; - transform: translateY(-1px); } - .devui-radio-label { + /* 选择图标-外圈 */ + &-material-outer { + opacity: 1; + transition: stroke 50ms cubic-bezier(0.755, 0.05, 0.855, 0.06); + stroke: $devui-line; + fill: transparent; + } + + /* 选择图标-内圈 */ + &-material-inner { + opacity: 0; + transform: scale(0); + transform-origin: 50% 50%; + transition: + transform 200ms cubic-bezier(0.755, 0.05, 0.855, 0.06), + opacity 200ms cubic-bezier(0.755, 0.05, 0.855, 0.06); + fill: $devui-icon-fill-active; + } + + /* 内容 */ + &-label { color: $devui-text; margin-left: 8px; font-size: $devui-font-size; transition: color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); } - .devui-radio-input { + &-input { opacity: 0; z-index: -1; width: 0; diff --git a/devui/radio/src/radio.tsx b/devui/radio/src/radio.tsx index e325c91a..1bc8d7c0 100644 --- a/devui/radio/src/radio.tsx +++ b/devui/radio/src/radio.tsx @@ -1,31 +1,39 @@ import { defineComponent, ExtractPropTypes, inject, computed } from 'vue'; -import { radioProps, radioGroupInjectionKey } from './use-radio'; +import { radioProps, radioGroupInjectionKey } from './radio-types'; import './radio.scss'; export default defineComponent({ name: 'DRadio', props: radioProps, - emits: ['change', 'update:checked'], - setup(props: ExtractPropTypes, ctx) { + emits: ['change', 'update:modelValue'], + setup(props: ExtractPropTypes, { emit }) { const radioGroupConf = inject(radioGroupInjectionKey, null); + /** 是否禁用 */ + const _disabled = props.disabled || radioGroupConf?.disabled; + + /** 判断是否勾选 */ const isChecked = computed(() => { - return radioGroupConf ? radioGroupConf.value.value === props.value : props.checked; + const _value = radioGroupConf ? radioGroupConf.modelValue.value : props.modelValue; + + return props.value === _value; }); const radioName = computed(() => { return radioGroupConf ? radioGroupConf.name.value : props.name; }); - const canChange = (targetVal: string) => { - const beforeChange = props.beforeChange || (radioGroupConf ? radioGroupConf.beforeChange : undefined); + /** 判断是否允许切换 */ + const judgeCanChange = (_value: string) => { + const beforeChange = props.beforeChange || (radioGroupConf ? radioGroupConf.beforeChange : null); + let flag = Promise.resolve(true); if (beforeChange) { - const res = beforeChange(targetVal); - if (typeof res === 'undefined') { + const canChange = beforeChange(_value); + if (typeof canChange === 'undefined') { return flag; } - if (typeof res === 'boolean') { - flag = Promise.resolve(res); + if (typeof canChange === 'boolean') { + flag = Promise.resolve(canChange); } else { - flag = res; + flag = canChange; } } return flag; @@ -34,20 +42,23 @@ export default defineComponent({ return { isChecked, radioName, - handleChange: (event: Event) => { - canChange((event.target as HTMLInputElement).value).then(res => { - if (!res) { - event.preventDefault(); - return; - } - radioGroupConf?.doChange(props.value); - ctx.emit('update:checked', (event.target as HTMLInputElement).checked); - ctx.emit('change', props.value); - }); + disabled: _disabled, + handleChange: async (event: Event) => { + const _value = props.value; + const canChange = await judgeCanChange(_value); + + // 不可以切换 + if (!canChange) { + event.preventDefault(); + return; + } + radioGroupConf?.emitChange(_value); // 触发父组件的change + emit('update:modelValue', _value); + emit('change', _value); } }; }, - render () { + render() { const { disabled, radioName, @@ -56,12 +67,15 @@ export default defineComponent({ $slots, handleChange } = this; - const labelCls = { - 'devui-radio': true, - active: isChecked, - disabled - }; - + console.log('--this', this) + const labelCls = [ + 'devui-radio', + { + active: isChecked, + disabled + } + ]; + return (