From bf6e99113dc2ab2fddc2c7e8480607f2d1734451 Mon Sep 17 00:00:00 2001 From: "X.Q. Chen" <31237954+brenner8023@users.noreply.github.com> Date: Wed, 7 Jul 2021 20:26:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20add=20toggle=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 描述: - 补充相关的动画样式,toggle组件要用到 - 新增toggle组件 自测: - 单测、eslint、demo均符合预期 --- devui/style/core/_animation.scss | 1 + devui/style/theme/_animation.scss | 9 ++ devui/toggle/__tests__/toggle.spec.ts | 112 +++++++++++++++++ devui/toggle/demo/demo-basic.tsx | 35 ++++++ devui/toggle/demo/demo-custom.tsx | 42 +++++++ devui/toggle/demo/toggle-demo.tsx | 31 +++-- devui/toggle/src/toggle.scss | 170 ++++++++++++++++++++++++++ devui/toggle/src/toggle.tsx | 97 +++++++++++++++ devui/toggle/toggle.tsx | 12 -- 9 files changed, 488 insertions(+), 21 deletions(-) create mode 100644 devui/style/core/_animation.scss create mode 100644 devui/style/theme/_animation.scss create mode 100644 devui/toggle/__tests__/toggle.spec.ts create mode 100644 devui/toggle/demo/demo-basic.tsx create mode 100644 devui/toggle/demo/demo-custom.tsx create mode 100644 devui/toggle/src/toggle.scss create mode 100644 devui/toggle/src/toggle.tsx delete mode 100644 devui/toggle/toggle.tsx diff --git a/devui/style/core/_animation.scss b/devui/style/core/_animation.scss new file mode 100644 index 00000000..0ea1756e --- /dev/null +++ b/devui/style/core/_animation.scss @@ -0,0 +1 @@ +@import '../../style/theme/animation' \ No newline at end of file diff --git a/devui/style/theme/_animation.scss b/devui/style/theme/_animation.scss new file mode 100644 index 00000000..7604080d --- /dev/null +++ b/devui/style/theme/_animation.scss @@ -0,0 +1,9 @@ +$devui-animation-duration-slow: var(--devui-animation-duration-slow, 300ms); +$devui-animation-duration-base: var(--devui-animation-duration-base, 200ms); +$devui-animation-duration-fast: var(--devui-animation-duration-fast, 100ms); + +$devui-animation-ease-in: var(--devui-animation-ease-in, cubic-bezier(0.5, 0, 0.84, 0.25)); +$devui-animation-ease-out: var(--devui-animation-ease-out, cubic-bezier(0.16, 0.75, 0.5, 1)); +$devui-animation-ease-in-out: var(--devui-animation-ease-in-out, cubic-bezier(0.5, 0.05, 0.5, 0.95)); +$devui-animation-ease-in-smooth: var(--devui-animation-ease-in-smooth, cubic-bezier(0.645, 0.045, 0.355, 1)); +$devui-animation-linear: var(--devui-animation-linear, cubic-bezier(0, 0, 1, 1)); diff --git a/devui/toggle/__tests__/toggle.spec.ts b/devui/toggle/__tests__/toggle.spec.ts new file mode 100644 index 00000000..d6eed6b9 --- /dev/null +++ b/devui/toggle/__tests__/toggle.spec.ts @@ -0,0 +1,112 @@ +import { mount } from '@vue/test-utils'; +import { ref, nextTick } from 'vue'; +import DToggle from '../src/toggle'; + +describe('d-toggle', () => { + it('toggle render work', async () => { + const checked = ref(false); + const wrapper = mount({ + components: { DToggle }, + template: ` + + `, + setup () { + return { + checked + }; + } + }); + + expect(wrapper.classes()).toContain('devui-toggle'); + expect(wrapper.classes()).not.toContain('devui-checked'); + + checked.value = true; + await nextTick(); + + expect(wrapper.classes()).toContain('devui-checked'); + }); + + it('toggle disabled work', async () => { + const onChange = jest.fn(); + const wrapper = mount(DToggle, { + props: { + disabled: true, + onChange + } + }); + + expect(wrapper.classes()).toContain('devui-disabled'); + + await wrapper.trigger('click'); + expect(onChange).toBeCalledTimes(0); + + await wrapper.setProps({ + disabled: false + }); + await wrapper.trigger('click'); + + expect(wrapper.classes()).not.toContain('devui-disabled'); + expect(onChange).toBeCalledTimes(1); + }); + + it('toggle size work', async () => { + const wrapper = mount(DToggle, { + props: { + size: 'sm' + } + }); + + expect(wrapper.classes()).toContain('devui-toggle-sm'); + + await wrapper.setProps({ + size: 'lg' + }); + expect(wrapper.classes()).not.toContain('devui-toggle-sm'); + expect(wrapper.classes()).toContain('devui-toggle-lg'); + }); + + it('toggle beforeChange work', async () => { + const beforeChange = jest.fn(() => false); + const onChange = jest.fn(); + const wrapper = mount(DToggle, { + props: { + beforeChange, + onChange + } + }); + + await wrapper.trigger('click'); + expect(beforeChange).toBeCalledTimes(1); + expect(onChange).toBeCalledTimes(0); + + beforeChange.mockReturnValue(true); + await wrapper.trigger('click'); + expect(beforeChange).toBeCalledTimes(2); + expect(onChange).toBeCalledTimes(1); + }); + + it('toggle slot work', async () => { + const isChecked = ref(false); + const wrapper = mount({ + components: { DToggle }, + template: ` + + + + + `, + setup () { + return { + isChecked + }; + } + }); + + expect(wrapper.text()).toBe('关'); + + isChecked.value = true; + await nextTick(); + + expect(wrapper.text()).toBe('开'); + }); +}); diff --git a/devui/toggle/demo/demo-basic.tsx b/devui/toggle/demo/demo-basic.tsx new file mode 100644 index 00000000..bcb39d71 --- /dev/null +++ b/devui/toggle/demo/demo-basic.tsx @@ -0,0 +1,35 @@ +import { defineComponent, ref } from 'vue'; +import DToggle from '../src/toggle'; + +export default defineComponent({ + name: 'DemoBasic', + + setup () { + const checked1 = ref(false); + const checked2 = ref(false); + const checked3 = ref(true); + const checked4 = ref(false); + const doUpdate1 = (v: boolean) => checked1.value = v; + const doUpdate2 = (v: boolean) => checked2.value = v; + const doUpdate3 = (v: boolean) => checked3.value = v; + const doUpdate4 = (v: boolean) => checked4.value = v; + + return () => { + return (
+
中杯拿铁
+ + +
大杯拿铁
+ + + +
特大杯拿铁
+ + +
别这样
+ + +
); + }; + } +}); diff --git a/devui/toggle/demo/demo-custom.tsx b/devui/toggle/demo/demo-custom.tsx new file mode 100644 index 00000000..f6bd92b7 --- /dev/null +++ b/devui/toggle/demo/demo-custom.tsx @@ -0,0 +1,42 @@ +import { defineComponent, ref } from 'vue'; +import DToggle from '../src/toggle'; + +export default defineComponent({ + name: 'DemoCustom', + setup () { + const checked = ref(true); + const doUpdate = (v: boolean) => checked.value = v; + const checked2 = ref(true); + const doUpdate2 = (v: boolean) => checked2.value = v; + + return { + checked, + doUpdate, + checked2, + doUpdate2 + }; + }, + render () { + const { + checked, + doUpdate, + checked2, + doUpdate2 + } = this; + + return ( +
+ +
+ '开', + uncheckedContent: () => '关' + }}> + +
+ ); + } +}); diff --git a/devui/toggle/demo/toggle-demo.tsx b/devui/toggle/demo/toggle-demo.tsx index 4c35eae9..a3cec733 100644 --- a/devui/toggle/demo/toggle-demo.tsx +++ b/devui/toggle/demo/toggle-demo.tsx @@ -1,12 +1,25 @@ -import { defineComponent } from 'vue' +import { defineComponent } from 'vue'; +import { useDemo } from 'hooks/use-demo'; +import DemoBasic from './demo-basic'; +import DemoBasicCode from './demo-basic?raw'; +import DemoCustom from './demo-custom'; +import DemoCustomCode from './demo-custom?raw'; export default defineComponent({ - name: 'd-toggle-demo', - props: { - }, - setup(props, ctx) { - return () => { - return
devui-toggle-demo
- } + name: 'ToggleDemo', + render () { + return useDemo([ + { + id: 'demo-basic', + title: '基本用法', + code: DemoBasicCode, + content: + }, { + id: 'demo-custom', + title: '自定义', + code: DemoCustomCode, + content: + } + ]); } -}) \ No newline at end of file +}); diff --git a/devui/toggle/src/toggle.scss b/devui/toggle/src/toggle.scss new file mode 100644 index 00000000..14212c08 --- /dev/null +++ b/devui/toggle/src/toggle.scss @@ -0,0 +1,170 @@ +@import '../../style/theme/color'; +@import '../../style/theme/font'; +@import '../../style/core/animation'; + +:host { + display: inline-block; + font-size: 0; + vertical-align: middle; +} + +.devui-toggle { + width: 36px; + height: 18px; + border-radius: (20px/2); + background: $devui-line; + border: 1px solid $devui-line; + position: relative; + display: inline-block; + box-sizing: content-box; + overflow: visible; + padding: 0; + margin: 0; + margin-right: 5px; + cursor: pointer; + transition: $devui-animation-duration-slow $devui-animation-ease-in-smooth all; + + &:not(.devui-checked):hover { + border-color: $devui-line; + } + + &:active { + border-color: $devui-brand-active-focus; + } + + &.devui-checked:hover { + border-color: $devui-brand-active; + } + + .devui-toggle-inner-wrapper { + display: inline-block; + width: 100%; + height: 100%; + padding-left: 16px; + font-size: $devui-font-size; + + .devui-toggle-inner { + width: 100%; + height: 100%; + text-align: center; + } + } + + &.devui-checked .devui-toggle-inner-wrapper { + padding-left: unset; + padding-right: 16px; + } + + small { + width: 16px; + height: 16px; + background: $devui-light-text; + border-radius: 100%; + position: absolute; + top: 1px; + left: 1px; + transition: $devui-animation-duration-slow $devui-animation-ease-in-smooth all; + } + + &.devui-checked small { + left: 19px; + } + + &.devui-toggle-lg { + width: 58px; + height: 30px; + border-radius: (32px/2); + + .devui-toggle-inner-wrapper { + padding-left: 28px; + font-size: $devui-font-size-modal-title; + } + + &.devui-checked .devui-toggle-inner-wrapper { + padding-left: unset; + padding-right: 28px; + } + + & small { + width: 28px; + height: 28px; + top: 1px; + left: 1px; + } + + &.devui-checked small { + background: $devui-light-text; + left: 29px; + } + } + + &.devui-toggle-sm { + width: 30px; + height: 14px; + border-radius: (16px/2); + + .devui-toggle-inner-wrapper { + padding-left: 12px; + font-size: $devui-font-size; + } + + &.devui-checked .devui-toggle-inner-wrapper { + padding-left: unset; + padding-right: 12px; + } + + & small { + width: 12px; + height: 12px; + position: absolute; + } + + &.devui-checked small { + left: 17px; + } + } + + &.devui-checked { + background: $devui-brand; + border-color: $devui-brand; + + &:hover { + background: $devui-brand-active; + border-color: $devui-brand-active; + } + + &:active { + background: $devui-brand-active-focus; + border-color: $devui-brand-active-focus; + } + } + + &.devui-disabled { + &, + &:hover, + &:active, + &.devui-checked { + cursor: not-allowed; + } + + &, + &:hover, + &:active { + background-color: $devui-disabled-line; + border-color: $devui-disabled-line; + + small { + background-color: $devui-unavailable; + } + } + + &.devui-checked { + background-color: $devui-icon-fill-active-disabled; + border-color: $devui-icon-fill-active-disabled; + + small { + background-color: $devui-light-text; + } + } + } +} diff --git a/devui/toggle/src/toggle.tsx b/devui/toggle/src/toggle.tsx new file mode 100644 index 00000000..b7a3998a --- /dev/null +++ b/devui/toggle/src/toggle.tsx @@ -0,0 +1,97 @@ +import { defineComponent, PropType } from 'vue'; +import './toggle.scss'; + +const toggleProps = { + size: { + type: String as PropType<'sm' | '' | 'lg'>, + default: '' + }, + color: { + type: String, + default: undefined + }, + checked: { + type: Boolean, + default: false + }, + disabled: { + type: Boolean, + default: false + }, + beforeChange: { + type: Function as PropType<(v: boolean) => boolean | Promise>, + default: undefined + }, + change: { + type: Function as PropType<(v: boolean) => void>, + default: undefined + }, + 'onUpdate:checked': { + type: Function as PropType<(v: boolean) => void>, + default: undefined + } +} as const; + +export default defineComponent({ + name: 'DToggle', + props: toggleProps, + emits: ['change', 'update:checked'], + setup(props, ctx) { + const canChange = () => { + if (props.disabled) { + return Promise.resolve(false); + } + if (props.beforeChange) { + const res = props.beforeChange(!props.checked); + return typeof res === 'boolean' ? Promise.resolve(res) : res; + } + + return Promise.resolve(true); + }; + const toggle = () => { + canChange().then(res => { + if (!res) { + return; + } + ctx.emit('update:checked', !props.checked); + ctx.emit('change', !props.checked); + }); + }; + + return { + toggle + }; + }, + + render () { + const { + size, + checked, + disabled, + color, + toggle + } = this; + + const outerCls = { + 'devui-toggle': true, + [`devui-toggle-${size}`]: size !== '', + 'devui-checked': checked, + 'devui-disabled': disabled + }; + const outerStyle = [ + `background: ${checked && !disabled ? color : ''}`, + `border-color: ${checked && !disabled ? color : ''}` + ]; + + return ( + + +
+ { checked ? this.$slots.checkedContent?.() : this.$slots.uncheckedContent?.() } +
+
+ +
+ ); + } +}); \ No newline at end of file diff --git a/devui/toggle/toggle.tsx b/devui/toggle/toggle.tsx deleted file mode 100644 index d998a0f6..00000000 --- a/devui/toggle/toggle.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { defineComponent } from 'vue' - -export default defineComponent({ - name: 'd-toggle', - props: { - }, - setup(props, ctx) { - return () => { - return
devui-toggle
- } - } -}) \ No newline at end of file -- Gitee