diff --git a/demo/documentation/components/radio/zhCN/basic.demo.md b/demo/documentation/components/radio/zhCN/basic.demo.md index c5b2c2adb..461039f7d 100644 --- a/demo/documentation/components/radio/zhCN/basic.demo.md +++ b/demo/documentation/components/radio/zhCN/basic.demo.md @@ -1,25 +1,25 @@ # 基础用法 ```html Definitely Maybe Be Here Now Be Here Now - + ``` ```js diff --git a/demo/documentation/components/radio/zhCN/button-group.demo.md b/demo/documentation/components/radio/zhCN/button-group.demo.md index cc8b646c9..dd31926c4 100644 --- a/demo/documentation/components/radio/zhCN/button-group.demo.md +++ b/demo/documentation/components/radio/zhCN/button-group.demo.md @@ -2,7 +2,7 @@ 有的时候用按钮显得更优雅一点。 ```html
- +
禁用 Shakemaker 禁用 Live Forever diff --git a/demo/documentation/components/radio/zhCN/group.demo.md b/demo/documentation/components/radio/zhCN/group.demo.md index aea2d8a0f..9a4ba2c7f 100644 --- a/demo/documentation/components/radio/zhCN/group.demo.md +++ b/demo/documentation/components/radio/zhCN/group.demo.md @@ -1,7 +1,10 @@ # 选项组 一个选项组看起来就挺舒服。 ```html - + any`|`undefined`|| ### Radio Group Props |名称|类型|默认值|说明| |-|-|-|-| -|theme|`'light' \| 'dark' \| null \| string`|`null`|| -|name|`string`|`null`|选项组内部 radio 元素的 name 属性| -|size|`'small' \| 'medium' \| 'large'`|`small`|| -|value|`string \| number \| boolean`|`null`|| |disabled|`boolean`|`false`|| - -## Events -### Radio, Radio Button Events -|名称|参数|说明| -|-|-|-| -|change|`(checkedValue: string \| number \| boolean)`|| - -### Radio Group Events -|名称|参数|说明| -|-|-|-| -|change|`(checkedValue: string \| number \| boolean)`|| +|name|`string`|`null`|选项组内部 radio 元素的 name 属性| +|size|`'small' \| 'medium' \| 'large'`|`medium`|| +|theme|`'light' \| 'dark' \| null \| string`|`null`|| +|value|`string \| number \| boolean`|`null`|| +|on-update:value|`(checkedValue: string \| number \| boolean) => any`|`undefined`|| diff --git a/demo/documentation/components/radio/zhCN/radio-focus-debug.demo.md b/demo/documentation/components/radio/zhCN/radio-focus-debug.demo.md index 6b285d4c6..d4749a7ef 100644 --- a/demo/documentation/components/radio/zhCN/radio-focus-debug.demo.md +++ b/demo/documentation/components/radio/zhCN/radio-focus-debug.demo.md @@ -2,7 +2,7 @@ ```html diff --git a/demo/documentation/components/radio/zhCN/size.demo.md b/demo/documentation/components/radio/zhCN/size.demo.md index 607083e62..dab710d42 100644 --- a/demo/documentation/components/radio/zhCN/size.demo.md +++ b/demo/documentation/components/radio/zhCN/size.demo.md @@ -2,7 +2,7 @@ 任君挑选。 ```html
- +
- +
禁用 Shakemaker 禁用 Live Forever diff --git a/src/_utils/composition/index.js b/src/_utils/composition/index.js index c8793a2ee..c018be045 100644 --- a/src/_utils/composition/index.js +++ b/src/_utils/composition/index.js @@ -79,7 +79,7 @@ export function useDisabledUntilMounted (durationTickCount = 0) { export function useMemo (valueGenerator, deps) { const valueRef = ref(valueGenerator()) - watch(deps, () => { + watch(deps.filter(dep => dep), () => { valueRef.value = valueGenerator() }) return valueRef diff --git a/src/_utils/vue/index.js b/src/_utils/vue/index.js index c4d0c1b63..a79aa7025 100644 --- a/src/_utils/vue/index.js +++ b/src/_utils/vue/index.js @@ -3,3 +3,4 @@ export { getVNodeChildren } from './src/get-v-node-children' export { createId } from './src/create-id' export { keep } from './src/keep' export { omit } from './omit' +export { flatten } from './src/flatten' diff --git a/src/_utils/vue/src/flatten.js b/src/_utils/vue/src/flatten.js new file mode 100644 index 000000000..d59a470b9 --- /dev/null +++ b/src/_utils/vue/src/flatten.js @@ -0,0 +1,13 @@ +import { Fragment } from 'vue' + +export function flatten (vNodes) { + let result = [] + vNodes.forEach(vNode => { + if (vNode.type === Fragment) { + result = result.concat(flatten(vNode.children)) + } else { + result.push(vNode) + } + }) + return result +} diff --git a/src/radio/src/Radio.vue b/src/radio/src/Radio.vue index b66321151..a2648ee60 100644 --- a/src/radio/src/Radio.vue +++ b/src/radio/src/Radio.vue @@ -3,7 +3,7 @@ class="n-radio" :class="{ 'n-radio--disabled': syntheticDisabled, - 'n-radio--checked': syntheticChecked, + 'n-radio--checked': renderSafeChecked, 'n-radio--focus': focus, [`n-radio--${syntheticSize}-size`]: true, [`n-${syntheticTheme}-theme`]: syntheticTheme @@ -17,7 +17,7 @@ type="radio" class="n-radio__radio-input" :name="syntheticName" - :checked="syntheticChecked" + :checked="renderSafeChecked" :disabled="syntheticDisabled" @change="handleRadioInputChange" @focus="handleRadioInputFocus" @@ -26,7 +26,7 @@
@@ -42,6 +42,7 @@ import themeable from '../../_mixins/themeable' import radioMixin from './radio-mixin' import usecssr from '../../_mixins/usecssr' import styles from './styles/radio/index.js' +import setup from './radio-setup' export default { name: 'Radio', @@ -49,14 +50,9 @@ export default { withapp, themeable, usecssr(styles), - asformitem( - { - change: 'change', - blur: 'blur', - focus: 'focus' - }, - 'medium', - function () { + asformitem({ + defaultSize: 'medium', + syntheticSize () { const size = this.size if (size) return size const NRadioGroup = this.NRadioGroup @@ -66,14 +62,14 @@ export default { const NFormItem = this.NFormItem if ( NFormItem && - NFormItem !== '__FORM_ITEM_INNER__' && - NFormItem.syntheticSize + NFormItem !== '__FORM_ITEM_INNER__' && + NFormItem.syntheticSize ) { return NFormItem.syntheticSize } return 'medium' } - ), + }), radioMixin ], props: { @@ -81,9 +77,10 @@ export default { validator (value) { return ['small', 'medium', 'large'].includes(value) }, - default: null + default: undefined } }, + setup, computed: { syntheticTheme () { const theme = this.theme diff --git a/src/radio/src/RadioButton.vue b/src/radio/src/RadioButton.vue index c25086df4..b51d88536 100644 --- a/src/radio/src/RadioButton.vue +++ b/src/radio/src/RadioButton.vue @@ -3,11 +3,11 @@ class="n-radio-button" :class="{ 'n-radio-button--disabled': syntheticDisabled, - 'n-radio-button--checked': syntheticChecked, + 'n-radio-button--checked': renderSafeChecked, 'n-radio-button--focus': focus }" :style="{ - color: syntheticChecked ? syntheticAscendantBackgroundColor : null + color: renderSafeChecked ? syntheticAscendantBackgroundColor : null }" @keyup.enter="handleKeyUpEnter" @click="handleClick" @@ -18,7 +18,7 @@ type="radio" class="n-radio-button__radio-input" :name="syntheticName" - :checked="syntheticChecked" + :checked="renderSafeChecked" :disabled="syntheticDisabled" @change="handleRadioInputChange" @focus="handleRadioInputFocus" @@ -31,6 +31,7 @@ diff --git a/src/radio/src/RadioGroup.js b/src/radio/src/RadioGroup.js index 2fb1a9971..4a5fccb42 100644 --- a/src/radio/src/RadioGroup.js +++ b/src/radio/src/RadioGroup.js @@ -3,31 +3,35 @@ import withapp from '../../_mixins/withapp' import themeable from '../../_mixins/themeable' import hollowoutable from '../../_mixins/hollowoutable' import asformitem from '../../_mixins/asformitem' -import getDefaultSlot from '../../_utils/vue/getDefaultSlot' +import { getSlot, flatten } from '../../_utils/vue' +import { warn } from '../../_utils/naive/warn' import usecssr from '../../_mixins/usecssr' import styles from './styles/radio-group/index.js' -import { warn } from '../../_utils/naive/warn' function mapSlot (h, defaultSlot, groupInstance) { - const mappedSlot = [] - defaultSlot = defaultSlot || [] + const children = [] + let isButtonGroup = false for (let i = 0; i < defaultSlot.length; ++i) { const wrappedInstance = defaultSlot[i] const instanceOptions = wrappedInstance.type + const name = instanceOptions.name if ( __DEV__ && ( !instanceOptions || - !['Radio', 'RadioButton'].includes(instanceOptions.name) + !['Radio', 'RadioButton'].includes(name) ) ) { warn('radio-group', '`n-radio-group` only taks `n-radio` and `n-radio-button` as children.') continue } const instanceProps = wrappedInstance.props - if (i === 0 || instanceOptions.name === 'Radio') { - mappedSlot.push(wrappedInstance) + if (name === 'RadioButton') { + isButtonGroup = true + } + if (i === 0 || name === 'Radio') { + children.push(wrappedInstance) } else { - const lastInstanceProps = mappedSlot[mappedSlot.length - 1].props + const lastInstanceProps = children[children.length - 1].props const lastInstanceChecked = groupInstance.$props.value === lastInstanceProps.value const lastInstanceDisabled = lastInstanceProps.disabled const currentInstanceChecked = groupInstance.$props.value === instanceProps.value @@ -63,16 +67,21 @@ function mapSlot (h, defaultSlot, groupInstance) { 'n-radio-group__splitor--disabled': currentInstanceDisabled, 'n-radio-group__splitor--checked': currentInstanceChecked } - let splitorClass - if (lastInstancePriority < currentInstancePriority) splitorClass = currentInstanceClass - else splitorClass = lastInstanceClass - mappedSlot.push(h('div', { - staticClass: 'n-radio-group__splitor', - class: splitorClass + const splitorClass = lastInstancePriority < currentInstancePriority + ? currentInstanceClass + : lastInstanceClass + children.push(h('div', { + class: [ + 'n-radio-group__splitor', + splitorClass + ] }), wrappedInstance) } } - return mappedSlot + return { + children, + isButtonGroup + } } export default { @@ -85,13 +94,9 @@ export default { hollowoutable, usecssr(styles), asformitem({ - change: 'change' - }, 'small') + defaultSize: 'medium' + }) ], - model: { - prop: 'value', - event: 'change' - }, props: { name: { type: String, @@ -102,24 +107,35 @@ export default { default: null }, size: { - default: undefined, validator (value) { return ['small', 'medium', 'large'].includes(value) - } + }, + default: undefined }, disabled: { type: Boolean, default: false + }, + 'onUpdate:value': { + type: Function, + default: undefined + }, + // deprecated + onChange: { + validator () { + if (__DEV__) warn('radio-group', '`on-change` is deprecated, please use `on-update:value` instead.') + return true + }, + default: undefined } }, data () { return { - radioButtonCount: 0, transitionDisabled: true } }, mounted () { - if (this.radioButtonCount > 0) { + if (this.isButtonGroup) { this.$nextTick().then(() => { this.transitionDisabled = false }) @@ -127,12 +143,15 @@ export default { }, provide () { return { - NRadioGroup: this, - NFormItem: null + NRadioGroup: this } }, render () { - const isButtonGroup = this.radioButtonCount > 0 + const { + children, + isButtonGroup + } = mapSlot(h, flatten(getSlot(this)), this) + this.isButtonGroup = isButtonGroup return h('div', { class: [ 'n-radio-group', @@ -143,6 +162,6 @@ export default { [`n-radio-group--transition-disabled`]: isButtonGroup && this.transitionDisabled } ] - }, mapSlot(h, getDefaultSlot(this), this)) + }, children) } } diff --git a/src/radio/src/radio-mixin.js b/src/radio/src/radio-mixin.js index 62c34a6a3..38bc7610a 100644 --- a/src/radio/src/radio-mixin.js +++ b/src/radio/src/radio-mixin.js @@ -1,3 +1,5 @@ +import { warn } from '../../_utils/naive/warn' + export default { props: { name: { @@ -15,15 +17,22 @@ export default { disabled: { type: Boolean, default: false - } - }, - model: { - prop: 'checkedValue', - event: 'change' - }, - inject: { - NRadioGroup: { - default: null + }, + onClick: { + type: Function, + default: undefined + }, + 'onUpdate:checkedValue': { + type: Function, + default: undefined + }, + // deprecated + onChange: { + validator () { + if (__DEV__) warn('radio', '`on-change` is deprecated, please use `on-update:checked-value` instead.') + return true + }, + default: undefined } }, data () { @@ -36,13 +45,6 @@ export default { if (this.name !== undefined) return this.name if (this.NRadioGroup) return this.NRadioGroup.name }, - syntheticChecked () { - if (this.NRadioGroup) { - return this.NRadioGroup.value === this.value - } else { - return this.checkedValue === this.value - } - }, syntheticDisabled () { if (this.NRadioGroup && this.NRadioGroup.disabled) return true if (this.disabled) return true @@ -84,14 +86,38 @@ export default { }, 0) }, handleClick (e) { - this.$emit('click', e) + const { + onClick + } = this + if (onClick) onClick(e) this.toggle() }, emitChangeEvent () { + const { + value + } = this if (this.NRadioGroup) { - this.NRadioGroup.$emit('change', this.value) + const { + onChange, + 'onUpdate:value': updateValue, + __triggerFormInput, + __triggerFormChange + } = this.NRadioGroup + if (updateValue) updateValue(value) + if (onChange) onChange(value) // deprecated + __triggerFormInput() + __triggerFormChange() } else { - this.$emit('change', this.value) + const { + onChange, + 'onUpdate:checkedValue': updateCheckedValue, + __triggerFormInput, + __triggerFormChange + } = this + if (updateCheckedValue) updateCheckedValue(value) + if (onChange) onChange(value) // deprecated + __triggerFormInput() + __triggerFormChange() } } } diff --git a/src/radio/src/radio-setup.js b/src/radio/src/radio-setup.js new file mode 100644 index 000000000..b8383350c --- /dev/null +++ b/src/radio/src/radio-setup.js @@ -0,0 +1,17 @@ +import { inject, toRef } from 'vue' +import { useMemo } from '../../_utils/composition' + +export default function setup (props) { + const NRadioGroup = inject('NRadioGroup', null) + return { + NRadioGroup, + renderSafeChecked: useMemo(() => { + if (NRadioGroup) return NRadioGroup.value === props.value + return props.checkedValue === props.value + }, [ + NRadioGroup ? toRef(NRadioGroup, 'value') : null, + toRef(props, 'value'), + toRef(props, 'checkedValue') + ]) + } +} diff --git a/vue3.md b/vue3.md index 20f55e7d8..8b1920205 100644 --- a/vue3.md +++ b/vue3.md @@ -155,7 +155,14 @@ placeable 进行了大调整 - set default trigger to `null` - [ ] popselect - [x] progress -- [ ] radio +- [x] radio + - radio-group + - break + - default `size` `'small'` => `'medium'` + - deprecate + - `on-change` => `on-update:value` + - radio & radio-button + - `on-change` => `on-update:checked-value` - [x] result - [ ] scrollbar - [ ] select @@ -167,7 +174,7 @@ placeable 进行了大调整 - remove - `value` => `model-value` - `change` => `on-update:model-value` -- [ ] table +- [x] table - [x] tabs - deprecate - `active-name` => `value`