diff --git a/devui/style/core/_animation.scss b/devui/style/core/_animation.scss new file mode 100644 index 0000000000000000000000000000000000000000..0ea1756e1a3da865120c7615bbb33071324d2b76 --- /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 0000000000000000000000000000000000000000..7604080d66a3fa903dc644d4f15af3e5e3154523 --- /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 0000000000000000000000000000000000000000..d6eed6b99d06ed0f8bed100800bc74c1ca044aec --- /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 0000000000000000000000000000000000000000..bcb39d7141ddb8803cc600485028e8da20d05f70 --- /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 0000000000000000000000000000000000000000..f6bd92b70ebf0732182aa6c1d3c11e0cd678f9e3 --- /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 4c35eae9de91880093d13a268e8700d01278b7d2..a3cec7335606d21cb1b444af7663917d9e95102a 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 0000000000000000000000000000000000000000..14212c0854959b9e003d4eb34394f189b6a186db --- /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 0000000000000000000000000000000000000000..b7a3998a616ea043ac7ef1478379e1195fa4f861 --- /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 d998a0f68d76e911fb8a92eb9e910758d59f0efc..0000000000000000000000000000000000000000 --- 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