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