feat(switch): add switch component

fix #82
This commit is contained in:
bastarder 2020-07-31 14:53:25 +08:00 committed by jeremywu
parent 77c9d91eea
commit a73dacc92b
8 changed files with 513 additions and 1 deletions

View File

@ -12,6 +12,7 @@ import ElBreadcrumb from '@element-plus/breadcrumb'
import ElIcon from '@element-plus/icon'
import ElLink from '@element-plus/link'
import ElRate from '@element-plus/rate'
import ElSwitch from '@element-plus/switch'
export {
ElAvatar,
@ -27,6 +28,7 @@ export {
ElIcon,
ElLink,
ElRate,
ElSwitch,
}
export default function install(app: App): void {
@ -43,4 +45,5 @@ export default function install(app: App): void {
ElIcon(app)
ElLink(app)
ElRate(app)
ElSwitch(app)
}

View File

@ -26,6 +26,9 @@
"@element-plus/progress": "^0.0.0",
"@element-plus/tag": "^0.0.0",
"@element-plus/time-line": "^0.0.0",
"@element-plus/rate": "^0.0.0"
"@element-plus/rate": "^0.0.0",
"@element-plus/breadcrumb": "^0.0.0",
"@element-plus/icon": "^0.0.0",
"@element-plus/switch": "^0.0.0"
}
}

View File

@ -0,0 +1,231 @@
import { mount } from '@vue/test-utils'
import Switch from '../src/index.vue'
describe('Switch.vue', () => {
test('create', () => {
const wrapper = mount(Switch, {
props: {
activeText: 'on',
inactiveText: 'off',
activeColor: '#0f0',
inactiveColor: '#f00',
width: 100,
},
})
const vm = wrapper.vm
const coreEl = vm.$el.querySelector('.el-switch__core')
expect(coreEl.style.backgroundColor).toEqual('rgb(255, 0, 0)')
expect(coreEl.style.width).toEqual('100px')
const leftLabelWrapper = wrapper.find('.el-switch__label--left span')
expect(leftLabelWrapper.text()).toEqual('off')
})
test('switch with icons', () => {
const wrapper = mount(Switch, {
props: {
activeIconClass: 'el-icon-check',
inactiveIconClass: 'el-icon-close',
},
})
const iconWrapper = wrapper.find('.el-switch__label--left i')
expect(iconWrapper.classes('el-icon-close')).toBe(true)
})
test('value correctly update', async () => {
const wrapper = mount({
components: {
'el-switch': Switch,
},
template: `
<div>
<el-switch
v-model="value"
activeColor="#0f0"
inactiveColor="#f00">
</el-switch>
</div>
`,
data() {
return {
value: true,
}
},
})
const vm = wrapper.vm
const coreEl = vm.$el.querySelector('.el-switch__core')
expect(coreEl.style.backgroundColor).toEqual('rgb(0, 255, 0)')
const coreWrapper = wrapper.find('.el-switch__core')
await coreWrapper.trigger('click')
expect(coreEl.style.backgroundColor).toEqual('rgb(255, 0, 0)')
expect(vm.value).toEqual(false)
await coreWrapper.trigger('click')
expect(vm.value).toEqual(true)
})
test('change event', async () => {
const wrapper = mount({
components: {
'el-switch': Switch,
},
template: `
<div>
<el-switch
v-model="value"
@update:modelValue="handleChange">
</el-switch>
</div>
`,
methods: {
handleChange(val) {
this.target = val
},
},
data() {
return {
target: 1,
value: true,
}
},
})
const vm = wrapper.vm
expect(vm.target).toEqual(1)
const coreWrapper = wrapper.find('.el-switch__core')
await coreWrapper.trigger('click')
const switchWrapper = wrapper.findComponent(Switch)
expect(switchWrapper.emitted()['update:modelValue']).toBeTruthy()
expect(vm.target).toEqual(false)
})
test('disabled switch should not respond to user click', async () => {
const wrapper = mount({
components: {
'el-switch': Switch,
},
template: `
<div>
<el-switch disabled v-model="value"></el-switch>
</div>
`,
data() {
return {
value: true,
}
},
})
const vm = wrapper.vm
expect(vm.value).toEqual(true)
const coreWrapper = wrapper.find('.el-switch__core')
await coreWrapper.trigger('click')
expect(vm.value).toEqual(true)
})
test('expand switch value', async () => {
const wrapper = mount({
components: {
'el-switch': Switch,
},
template: `
<div>
<el-switch v-model="value" :active-value="onValue" :inactive-value="offValue"></el-switch>
</div>
`,
data() {
return {
value: '100',
onValue: '100',
offValue: '0',
}
},
})
const vm = wrapper.vm
const coreWrapper = wrapper.find('.el-switch__core')
await coreWrapper.trigger('click')
expect(vm.value).toEqual('0')
await coreWrapper.trigger('click')
expect(vm.value).toEqual('100')
})
test('value is the single source of truth', async () => {
const wrapper = mount({
components: {
'el-switch': Switch,
},
template: `
<div>
<el-switch :value="true"></el-switch>
</div>
`,
})
const vm = wrapper.vm
const coreWrapper = wrapper.find('.el-switch__core')
const switchWrapper = wrapper.findComponent(Switch)
const switchVm = switchWrapper.vm
const inputEl = vm.$el.querySelector('input')
expect(switchVm.checked).toBe(true)
expect(switchWrapper.classes('is-checked')).toEqual(true)
expect(inputEl.checked).toEqual(true)
await coreWrapper.trigger('click')
expect(switchVm.checked).toBe(true)
expect(switchWrapper.classes('is-checked')).toEqual(true)
expect(inputEl.checked).toEqual(true)
})
test('model-value is the single source of truth', async () => {
const wrapper = mount({
components: {
'el-switch': Switch,
},
template: `
<div>
<el-switch :model-value="true"></el-switch>
</div>
`,
})
const vm = wrapper.vm
const coreWrapper = wrapper.find('.el-switch__core')
const switchWrapper = wrapper.findComponent(Switch)
const switchVm = switchWrapper.vm
const inputEl = vm.$el.querySelector('input')
expect(switchVm.checked).toBe(true)
expect(switchWrapper.classes('is-checked')).toEqual(true)
expect(inputEl.checked).toEqual(true)
await coreWrapper.trigger('click')
expect(switchVm.checked).toBe(true)
expect(switchWrapper.classes('is-checked')).toEqual(true)
expect(inputEl.checked).toEqual(true)
})
test('sets checkbox value', async () => {
const wrapper = mount({
components: {
'el-switch': Switch,
},
template: `
<div>
<el-switch v-model="value"></el-switch>
</div>
`,
data() {
return {
value: false,
}
},
})
const vm = wrapper.vm
const inputEl = vm.$el.querySelector('input')
vm.value = true
await vm.$nextTick()
expect(inputEl.checked).toEqual(true)
vm.value = false
await vm.$nextTick()
expect(inputEl.checked).toEqual(false)
})
})

View File

@ -0,0 +1,28 @@
<template>
<div>
<div>
use v-model
<el-switch v-model="v" />
</div>
<div>
use :value
<el-switch :value="true" />
</div>
<div>
use :model-value
<el-switch :model-value="true" />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
data() {
return {
v: true,
}
},
})
</script>

View File

@ -0,0 +1,6 @@
export { default as BasicUsage } from './basic.vue'
export default {
title: 'Switch',
}

5
packages/switch/index.ts Normal file
View File

@ -0,0 +1,5 @@
import { App } from 'vue'
import Switch from './src/index.vue'
export default (app: App): void => {
app.component(Switch.name, Switch)
}

View File

@ -0,0 +1,12 @@
{
"name": "@element-plus/switch",
"version": "0.0.0",
"main": "dist/index.js",
"license": "MIT",
"peerDependencies": {
"vue": "^3.0.0-rc.1"
},
"devDependencies": {
"@vue/test-utils": "^2.0.0-beta.0"
}
}

View File

@ -0,0 +1,224 @@
<template>
<div
class="el-switch"
:class="{ 'is-disabled': switchDisabled, 'is-checked': checked }"
role="switch"
:aria-checked="checked"
:aria-disabled="switchDisabled"
@click.prevent="switchValue"
>
<input
:id="id"
ref="input"
class="el-switch__input"
type="checkbox"
:name="name"
:true-value="activeValue"
:false-value="inactiveValue"
:disabled="switchDisabled"
@change="handleChange"
@keydown.enter="switchValue"
>
<span
v-if="inactiveIconClass || inactiveText"
:class="['el-switch__label', 'el-switch__label--left', !checked ? 'is-active' : '']"
>
<i v-if="inactiveIconClass" :class="[inactiveIconClass]"></i>
<span v-if="!inactiveIconClass && inactiveText" :aria-hidden="checked">{{ inactiveText }}</span>
</span>
<span ref="core" class="el-switch__core" :style="{ 'width': coreWidth + 'px' }">
</span>
<span
v-if="activeIconClass || activeText"
:class="['el-switch__label', 'el-switch__label--right', checked ? 'is-active' : '']"
>
<i v-if="activeIconClass" :class="[activeIconClass]"></i>
<span v-if="!activeIconClass && activeText" :aria-hidden="!checked">{{ activeText }}</span>
</span>
</div>
</template>
<script lang='ts'>
import { defineComponent, computed, onMounted, ref, inject, nextTick, watch } from 'vue'
// TODOS: replace these interface definition with actual ElForm interface
interface ElForm {
disabled: boolean;
}
type ValueType = boolean | string | number;
interface ISwitchProps {
modelValue: ValueType,
value: ValueType,
disabled: boolean,
width: number,
activeIconClass: string,
inactiveIconClass: string,
activeText: string,
inactiveText: string,
activeColor: string,
inactiveColor: string,
activeValue: ValueType,
inactiveValue: ValueType,
name: string,
validateEvent: boolean,
id: string,
}
export default defineComponent({
name: 'ElSwitch',
props: {
modelValue: {
type: [Boolean, String, Number],
default: false,
},
value: {
type: [Boolean, String, Number],
default: false,
},
disabled: {
type: Boolean,
default: false,
},
width: {
type: Number,
default: 40,
},
activeIconClass: {
type: String,
default: '',
},
inactiveIconClass: {
type: String,
default: '',
},
activeText: {
type: String,
default: '',
},
inactiveText: {
type: String,
default: '',
},
activeColor: {
type: String,
default: '',
},
inactiveColor: {
type: String,
default: '',
},
activeValue: {
type: [Boolean, String, Number],
default: true,
},
inactiveValue: {
type: [Boolean, String, Number],
default: false,
},
name: {
type: String,
default: '',
},
validateEvent: {
type: Boolean,
default: true,
},
id: {
type: String,
default: '',
},
},
emits: ['update:modelValue', 'change', 'input'],
setup(props: ISwitchProps, ctx) {
const elForm = inject<ElForm>('elForm', {})
const coreWidth = ref(props.width)
const isModelValue = ref(props.modelValue !== false)
const input = ref(null)
const core = ref(null)
watch(() => props.modelValue, () => {
isModelValue.value = true
})
watch(() => props.value, () => {
isModelValue.value = false
})
const actualValue = computed((): ValueType => {
return isModelValue.value ? props.modelValue : props.value
})
const checked = computed((): boolean => {
return actualValue.value === props.activeValue
})
if (!~[props.activeValue, props.inactiveValue].indexOf(actualValue.value)) {
ctx.emit('update:modelValue', props.inactiveValue)
ctx.emit('change', props.inactiveValue)
ctx.emit('input', props.inactiveValue)
}
watch(checked, () => {
input.value.checked = checked.value
if (props.activeColor || props.inactiveColor) {
setBackgroundColor()
}
if (props.validateEvent) {
// TODO: should dispatch event to parent component <el-form-item>;
// dispatch('ElFormItem', 'el.form.change', [this.value]);
}
})
const switchDisabled = computed((): boolean => {
return props.disabled || (elForm || {}).disabled
})
const handleChange = (): void => {
const val = checked.value ? props.inactiveValue : props.activeValue
ctx.emit('update:modelValue', val)
ctx.emit('change', val)
ctx.emit('input', val)
nextTick(() => {
input.value.checked = checked.value
})
}
const switchValue = (): void => {
!switchDisabled.value && handleChange()
}
const setBackgroundColor = (): void => {
const newColor = checked.value ? props.activeColor : props.inactiveColor
const coreEl = core.value
coreEl.style.borderColor = newColor
coreEl.style.backgroundColor = newColor
}
onMounted(() => {
coreWidth.value = coreWidth.value || 40
if (props.activeValue || props.inactiveValue) {
setBackgroundColor()
}
input.value.checked = checked.value
})
return {
input,
core,
coreWidth,
switchDisabled,
checked,
handleChange,
switchValue,
}
},
})
</script>
<style scoped>
</style>