src/radio.vue
<template> <label class="el-radio" :class="[ border && radioSize ? 'el-radio--' + radioSize : '', { 'is-disabled': isDisabled }, { 'is-focus': focus }, { 'is-bordered': border }, { 'is-checked': model === label } ]" role="radio" :aria-checked="model === label" :aria-disabled="isDisabled" :tabindex="tabIndex" @keydown.space.stop.prevent="model = isDisabled ? model : label" > <span class="el-radio__input" :class="{ 'is-disabled': isDisabled, 'is-checked': model === label }" > <span class="el-radio__inner"></span> <input class="el-radio__original" :value="label" type="radio" aria-hidden="true" v-model="model" @focus="focus = true" @blur="focus = false" @change="handleChange" :name="name" :disabled="isDisabled" tabindex="-1" > </span> <span class="el-radio__label" @keydown.stop> <slot></slot> <template v-if="!$slots.default">{{label}}</template> </span> </label> </template> <script> import Emitter from 'element-ui/src/mixins/emitter'; export default { name: 'ElRadio', mixins: [Emitter], inject: { elForm: { default: '' }, elFormItem: { default: '' } }, componentName: 'ElRadio', props: { // value / v-model 绑定值 string / number / boolean value: {}, // Radio 的 value string / number / boolean label: {}, // 是否禁用 boolean — false disabled: Boolean, // 原生 name 属性 string name: String, // Radio 的尺寸,仅在 border 为真时有效 string medium / small / mini border: Boolean, // 是否显示边框 boolean — false size: String }, data() { return { focus: false }; }, computed: { // 是否是radioGroup isGroup() { let parent = this.$parent; while (parent) { if (parent.$options.componentName !== 'ElRadioGroup') { parent = parent.$parent; } else { this._radioGroup = parent; return true; } } return false; }, model: { // 取值 get() { return this.isGroup ? this._radioGroup.value : this.value; }, // 设置值 set(val) { if (this.isGroup) { this.dispatch('ElRadioGroup', 'input', [val]); } else { this.$emit('input', val); } } }, _elFormItemSize() { return (this.elFormItem || {}).elFormItemSize; }, // 大小 radioSize() { const temRadioSize = this.size || this._elFormItemSize || (this.$ELEMENT || {}).size; return this.isGroup ? this._radioGroup.radioGroupSize || temRadioSize : temRadioSize; }, // 是否禁用 isDisabled() { return this.isGroup ? this._radioGroup.disabled || this.disabled || (this.elForm || {}).disabled : this.disabled || (this.elForm || {}).disabled; }, tabIndex() { return (this.isDisabled || (this.isGroup && this.model !== this.label)) ? -1 : 0; } }, methods: { handleChange() { this.$nextTick(() => { // 向外抛出chang事件并传值 this.$emit('change', this.model); // 如果是group,通知ElRadioGroup,执行handleChange事件,并传值this.model this.isGroup && this.dispatch('ElRadioGroup', 'handleChange', this.model); }); } } }; </script>
src/radio-group.vue
<template> <div class="el-radio-group" role="radiogroup" @keydown="handleKeydown" > <slot></slot> </div> </template> <script> import Emitter from 'element-ui/src/mixins/emitter'; // Object.freeze,冻结对象 const keyCode = Object.freeze({ LEFT: 37, UP: 38, RIGHT: 39, DOWN: 40 }); export default { name: 'ElRadioGroup', componentName: 'ElRadioGroup', inject: { elFormItem: { default: '' } }, mixins: [Emitter], props: { // 绑定值 string / number / boolean value: {}, // 单选框组尺寸,仅对按钮形式的 Radio 或带有边框的 Radio 有效 string medium / small / mini size: String, // 按钮形式的 Radio 激活时的填充色和边框色 string — #409EFF fill: String, // 按钮形式的 Radio 激活时的文本颜色 string — #ffffff textColor: String, // 是否禁用 disabled: Boolean }, computed: { _elFormItemSize() { return (this.elFormItem || {}).elFormItemSize; }, // 大小 radioGroupSize() { return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size; } }, created() { // 接收handleChange事件 this.$on('handleChange', value => { // 向外抛出change并传值 this.$emit('change', value); }); }, mounted() { // 当radioGroup没有默认选项时,第一个可以选中Tab导航 const radios = this.$el.querySelectorAll('[type=radio]'); const firstLabel = this.$el.querySelectorAll('[role=radio]')[0]; if (![].some.call(radios, radio => radio.checked) && firstLabel) { firstLabel.tabIndex = 0; } }, methods: { handleKeydown(e) { // 左右上下按键 可以在radio组内切换不同选项 const target = e.target; const className = target.nodeName === 'INPUT' ? '[type=radio]' : '[role=radio]'; const radios = this.$el.querySelectorAll(className); const length = radios.length; const index = [].indexOf.call(radios, target); const roleRadios = this.$el.querySelectorAll('[role=radio]'); switch (e.keyCode) { // 点击左箭头和上箭头 case keyCode.LEFT: case keyCode.UP: e.stopPropagation(); e.preventDefault(); // 如果是第一个,选中最后一个兵聚焦 if (index === 0) { roleRadios[length - 1].click(); roleRadios[length - 1].focus(); } else { // 向前选 roleRadios[index - 1].click(); roleRadios[index - 1].focus(); } break; // 点击下箭头和右箭头 case keyCode.RIGHT: case keyCode.DOWN: // 如果是最后一个 if (index === (length - 1)) { e.stopPropagation(); e.preventDefault(); // 选中第一个并聚焦 roleRadios[0].click(); roleRadios[0].focus(); } else { // 向后选 roleRadios[index + 1].click(); roleRadios[index + 1].focus(); } break; default: break; } } }, watch: { value(value) { // 通知ElFormItem组件,执行el.form.change方法,并传值[this.value] this.dispatch('ElFormItem', 'el.form.change', [this.value]); } } }; </script>
src/radio-button.vue
<template> <label class="el-radio-button" :class="[ size ? 'el-radio-button--' + size : '', { 'is-active': value === label }, { 'is-disabled': isDisabled }, { 'is-focus': focus } ]" role="radio" :aria-checked="value === label" :aria-disabled="isDisabled" :tabindex="tabIndex" @keydown.space.stop.prevent="value = isDisabled ? value : label" > <input class="el-radio-button__orig-radio" :value="label" type="radio" v-model="value" :name="name" @change="handleChange" :disabled="isDisabled" tabindex="-1" @focus="focus = true" @blur="focus = false" > <span class="el-radio-button__inner" :style="value === label ? activeStyle : null" @keydown.stop> <slot></slot> <template v-if="!$slots.default">{{label}}</template> </span> </label> </template> <script> import Emitter from 'element-ui/src/mixins/emitter'; export default { name: 'ElRadioButton', mixins: [Emitter], inject: { elForm: { default: '' }, elFormItem: { default: '' } }, props: { // Radio 的 value string / number label: {}, // 是否禁用 boolean — false disabled: Boolean, // 原生 name 属性 string name: String }, data() { return { focus: false }; }, computed: { // 动态计算value value: { // 取值 get() { return this._radioGroup.value; }, // 设置值 set(value) { this._radioGroup.$emit('input', value); } }, // radioGroup组件 _radioGroup() { let parent = this.$parent; while (parent) { if (parent.$options.componentName !== 'ElRadioGroup') { parent = parent.$parent; } else { return parent; } } return false; }, // 激活的样式 activeStyle() { return { backgroundColor: this._radioGroup.fill || '', borderColor: this._radioGroup.fill || '', boxShadow: this._radioGroup.fill ? `-1px 0 0 0 ${this._radioGroup.fill}` : '', color: this._radioGroup.textColor || '' }; }, // elFormItem大小 _elFormItemSize() { return (this.elFormItem || {}).elFormItemSize; }, // 大小 size() { return this._radioGroup.radioGroupSize || this._elFormItemSize || (this.$ELEMENT || {}).size; }, // 是否禁用 isDisabled() { return this.disabled || this._radioGroup.disabled || (this.elForm || {}).disabled; }, tabIndex() { return (this.isDisabled || (this._radioGroup && this.value !== this.label)) ? -1 : 0; } }, methods: { // 改变事件 handleChange() { this.$nextTick(() => { // 通知ElRadioGroup组件触发handleChange事件,传值this.value this.dispatch('ElRadioGroup', 'handleChange', this.value); }); } } }; </script>