From 3e61384d8be398fa1eaf8df43b25aea291b6e375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8F=B5=E4=B9=8B?= Date: Fri, 15 Mar 2024 15:50:40 +0800 Subject: [PATCH 1/4] chore(Radio): rename to ts --- components/radio/__docs__/adaptor/index.jsx | 71 ------ components/radio/__docs__/adaptor/index.tsx | 95 +++++++ components/radio/__docs__/theme/index.jsx | 201 --------------- components/radio/__docs__/theme/index.tsx | 241 ++++++++++++++++++ .../__tests__/{a11y-spec.js => a11y-spec.tsx} | 14 +- .../{group-spec.js => group-spec.tsx} | 77 ++++-- .../{index-spec.js => index-spec.tsx} | 0 components/radio/{index.jsx => index.tsx} | 0 .../radio/mobile/{index.jsx => index.tsx} | 0 .../{radio-group.jsx => radio-group.tsx} | 9 +- components/radio/{radio.jsx => radio.tsx} | 11 +- components/radio/{style.js => style.ts} | 0 components/radio/{index.d.ts => types.ts} | 0 .../{with-context.jsx => with-context.tsx} | 6 +- 14 files changed, 407 insertions(+), 318 deletions(-) delete mode 100644 components/radio/__docs__/adaptor/index.jsx create mode 100644 components/radio/__docs__/adaptor/index.tsx delete mode 100644 components/radio/__docs__/theme/index.jsx create mode 100644 components/radio/__docs__/theme/index.tsx rename components/radio/__tests__/{a11y-spec.js => a11y-spec.tsx} (92%) rename components/radio/__tests__/{group-spec.js => group-spec.tsx} (87%) rename components/radio/__tests__/{index-spec.js => index-spec.tsx} (100%) rename components/radio/{index.jsx => index.tsx} (100%) rename components/radio/mobile/{index.jsx => index.tsx} (100%) rename components/radio/{radio-group.jsx => radio-group.tsx} (97%) rename components/radio/{radio.jsx => radio.tsx} (95%) rename components/radio/{style.js => style.ts} (100%) rename components/radio/{index.d.ts => types.ts} (100%) rename components/radio/{with-context.jsx => with-context.tsx} (84%) diff --git a/components/radio/__docs__/adaptor/index.jsx b/components/radio/__docs__/adaptor/index.jsx deleted file mode 100644 index 2070c25bcd..0000000000 --- a/components/radio/__docs__/adaptor/index.jsx +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react'; -import { Types, parseData, NodeType } from '@alifd/adaptor-helper'; -import { Radio } from '@alifd/next'; - -export default { - name: 'Radio', - shape: ['normal', {value: 'button', label: 'Radio Button'}], - editor: (shape = 'normal') => { - if (shape === 'button') { - return { - props: [{ - name: 'size', - type: Types.enum, - options: ['large', 'medium', 'small'], - default: 'medium' - }, { - name: 'state', - label: 'Status', - type: Types.enum, - options: ['normal', 'disabled'], - default: 'normal' - }], - data: { - hover: true, - disable: true, - active: true, - default: `Cell 1\n*Cell 2\nCell 3` - } - }; - } - return { - props: [{ - name: 'state', - label: 'Status', - type: Types.enum, - options: ['normal', 'hover', 'disabled', 'checked', 'checkedHover', 'checkedDisabled'], - default: 'normal' - }, { - name: 'label', - type: Types.string, - default: 'label' - }], - } - }, - adaptor: ({ shape, size, state = '', label, data, ...others }) => { - if (shape === 'normal') { - return ( - - ); - } - - const list = parseData(data).filter(({ type }) => type === NodeType.node); - - return ( - item.state === 'active')}> - { - list.map((item, index) => {item.value}) - } - - ); - - } - -}; - diff --git a/components/radio/__docs__/adaptor/index.tsx b/components/radio/__docs__/adaptor/index.tsx new file mode 100644 index 0000000000..e94f6efd6c --- /dev/null +++ b/components/radio/__docs__/adaptor/index.tsx @@ -0,0 +1,95 @@ +import React from 'react'; +import { Types, parseData, NodeType } from '@alifd/adaptor-helper'; +import { Radio } from '@alifd/next'; + +export default { + name: 'Radio', + shape: ['normal', { value: 'button', label: 'Radio Button' }], + editor: (shape = 'normal') => { + if (shape === 'button') { + return { + props: [ + { + name: 'size', + type: Types.enum, + options: ['large', 'medium', 'small'], + default: 'medium', + }, + { + name: 'state', + label: 'Status', + type: Types.enum, + options: ['normal', 'disabled'], + default: 'normal', + }, + ], + data: { + hover: true, + disable: true, + active: true, + default: `Cell 1\n*Cell 2\nCell 3`, + }, + }; + } + return { + props: [ + { + name: 'state', + label: 'Status', + type: Types.enum, + options: [ + 'normal', + 'hover', + 'disabled', + 'checked', + 'checkedHover', + 'checkedDisabled', + ], + default: 'normal', + }, + { + name: 'label', + type: Types.string, + default: 'label', + }, + ], + }; + }, + adaptor: ({ shape, size, state = '', label, data, ...others }) => { + if (shape === 'normal') { + return ( + + ); + } + + const list = parseData(data).filter(({ type }) => type === NodeType.node); + + return ( + item.state === 'active')} + > + {list.map((item, index) => ( + + {item.value} + + ))} + + ); + }, +}; diff --git a/components/radio/__docs__/theme/index.jsx b/components/radio/__docs__/theme/index.jsx deleted file mode 100644 index 5d60612b6d..0000000000 --- a/components/radio/__docs__/theme/index.jsx +++ /dev/null @@ -1,201 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import PropTypes from 'prop-types'; -import '../../../demo-helper/style'; -import { Demo, DemoGroup, DemoHead, initDemo } from '../../../demo-helper'; -import ConfigProvider from '../../../config-provider'; -import zhCN from '../../../locale/zh-cn'; -import enUS from '../../../locale/en-us'; -import '../../style'; -import Radio from '../../index'; -import Field from '../../../field'; - -// import demo helper - - -// import component - - -const i18nMap = { - 'zh-cn': { - label: '文本', - withLabel: '带有 label', - number: '按钮数量', - editLabel: '编辑文本', - dataSource: [ - { - value: 'gcellone', - label: '单元格一' - }, { - value: 'gcelltwo', - label: '单元格二' - }, { - value: 'gcellthree', - label: '单元格三' - }, { - value: 'gcellfour', - label: '单元格四' - } - ] - }, - 'en-us': { - label: 'label', - withLabel: 'With Label', - number: 'Number Of Buttons', - editLabel: 'Edit Label', - dataSource: [ - { - value: 'gcellone', - label: 'Cell 1' - }, { - value: 'gcelltwo', - label: 'Cell 2' - }, { - value: 'gcellthree', - label: 'Cell 3' - }, { - value: 'gcellfour', - label: 'Cell 4' - } - ] - } -}; -const RadioGroup = Radio.Group; - -// default radio -class RadioDefaultDemo extends React.Component { - static propTypes = { - i18n: PropTypes.object, - } - constructor(props) { - super(props); - this.field = new Field(this, { - values: { - demo: { - radiolabel: { - label: '标签', - name: 'radiolabel', - value: 'true', - enum: [{label: '显示', value: 'true'}, {label: '隐藏', value: 'false'}] - } - } - } - }); - } - render() { - const {init, getValue} = this.field; - const { i18n } = this.props; - const label = (getValue('demo').radiolabel.value === 'true') ? i18n.label : null; - return ( - - - - - {label} - - - - - {label} - - - - - {label} - - - - - {label} - - - - - {label} - - - - - {label} - - - - - ); - } -} - -// button radio -/* eslint-disable */ -class RadioButtonDemo extends React.Component { - static propTypes = { - i18n: PropTypes.object, - } - render() { - const { i18n } = this.props; - //const {init, getValue} = this.field; - //const number = getValue('demo').value; - const dataSource = i18n.dataSource.slice(0, 3); - - const disabledRadio = dataSource.map((d, i) => {d.label}); - const disabledSelectedRadio = dataSource.map(d => {d.label}); - - return ( - - - - - - - - - - - - - - - - {disabledRadio} - - - {disabledRadio} - - - {disabledRadio} - - - - - {disabledSelectedRadio} - - - {disabledSelectedRadio} - - - {disabledSelectedRadio} - - - - - ); - } -} - -window.renderDemo = function(lang = 'en-us') { - const i18n = i18nMap[lang]; - ReactDOM.render(( - -
- - -
-
- ), document.getElementById('container')); -}; - -window.renderDemo(); -initDemo('radio'); diff --git a/components/radio/__docs__/theme/index.tsx b/components/radio/__docs__/theme/index.tsx new file mode 100644 index 0000000000..5522900172 --- /dev/null +++ b/components/radio/__docs__/theme/index.tsx @@ -0,0 +1,241 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; +import '../../../demo-helper/style'; +import { Demo, DemoGroup, DemoHead, initDemo } from '../../../demo-helper'; +import ConfigProvider from '../../../config-provider'; +import zhCN from '../../../locale/zh-cn'; +import enUS from '../../../locale/en-us'; +import '../../style'; +import Radio from '../../index'; +import Field from '../../../field'; + +// import demo helper + +// import component + +const i18nMap = { + 'zh-cn': { + label: '文本', + withLabel: '带有 label', + number: '按钮数量', + editLabel: '编辑文本', + dataSource: [ + { + value: 'gcellone', + label: '单元格一', + }, + { + value: 'gcelltwo', + label: '单元格二', + }, + { + value: 'gcellthree', + label: '单元格三', + }, + { + value: 'gcellfour', + label: '单元格四', + }, + ], + }, + 'en-us': { + label: 'label', + withLabel: 'With Label', + number: 'Number Of Buttons', + editLabel: 'Edit Label', + dataSource: [ + { + value: 'gcellone', + label: 'Cell 1', + }, + { + value: 'gcelltwo', + label: 'Cell 2', + }, + { + value: 'gcellthree', + label: 'Cell 3', + }, + { + value: 'gcellfour', + label: 'Cell 4', + }, + ], + }, +}; +const RadioGroup = Radio.Group; + +// default radio +class RadioDefaultDemo extends React.Component { + static propTypes = { + i18n: PropTypes.object, + }; + constructor(props) { + super(props); + this.field = new Field(this, { + values: { + demo: { + radiolabel: { + label: '标签', + name: 'radiolabel', + value: 'true', + enum: [ + { label: '显示', value: 'true' }, + { label: '隐藏', value: 'false' }, + ], + }, + }, + }, + }); + } + render() { + const { init, getValue } = this.field; + const { i18n } = this.props; + const label = getValue('demo').radiolabel.value === 'true' ? i18n.label : null; + return ( + + + + + {label} + + + + + + {label} + + + + + + {label} + + + + + + {label} + + + + + + {label} + + + + + + {label} + + + + + + ); + } +} + +// button radio +/* eslint-disable */ +class RadioButtonDemo extends React.Component { + static propTypes = { + i18n: PropTypes.object, + }; + render() { + const { i18n } = this.props; + //const {init, getValue} = this.field; + //const number = getValue('demo').value; + const dataSource = i18n.dataSource.slice(0, 3); + + const disabledRadio = dataSource.map((d, i) => ( + + {d.label} + + )); + const disabledSelectedRadio = dataSource.map(d => ( + + {d.label} + + )); + + return ( + + + + + + + + + + + + + + + + {disabledRadio} + + + {disabledRadio} + + + {disabledRadio} + + + + + {disabledSelectedRadio} + + + {disabledSelectedRadio} + + + {disabledSelectedRadio} + + + + + ); + } +} + +window.renderDemo = function (lang = 'en-us') { + const i18n = i18nMap[lang]; + ReactDOM.render( + +
+ + +
+
, + document.getElementById('container') + ); +}; + +window.renderDemo(); +initDemo('radio'); diff --git a/components/radio/__tests__/a11y-spec.js b/components/radio/__tests__/a11y-spec.tsx similarity index 92% rename from components/radio/__tests__/a11y-spec.js rename to components/radio/__tests__/a11y-spec.tsx index dd12e9e8b3..a74329adc7 100644 --- a/components/radio/__tests__/a11y-spec.js +++ b/components/radio/__tests__/a11y-spec.tsx @@ -111,12 +111,7 @@ describe('Radio A11y', () => { ); wrapper.update(); - assert( - wrapper - .find('input#pear') - .at(0) - .getDOMNode().tabIndex === 0 - ); + assert(wrapper.find('input#pear').at(0).getDOMNode().tabIndex === 0); }); it('should not add tabIndex for non Radio Item', async () => { @@ -134,11 +129,6 @@ describe('Radio A11y', () => { ); - assert( - wrapper - .find('div#mywrapper') - .at(0) - .getDOMNode().tabIndex === -1 - ); + assert(wrapper.find('div#mywrapper').at(0).getDOMNode().tabIndex === -1); }); }); diff --git a/components/radio/__tests__/group-spec.js b/components/radio/__tests__/group-spec.tsx similarity index 87% rename from components/radio/__tests__/group-spec.js rename to components/radio/__tests__/group-spec.tsx index 2b1ef84647..ea46a4e2f2 100644 --- a/components/radio/__tests__/group-spec.js +++ b/components/radio/__tests__/group-spec.tsx @@ -120,7 +120,12 @@ describe('Radio.Group', () => { document.body.appendChild(container); before(done => { ReactDOM.render( - , + , container, function init() { done(); @@ -128,7 +133,10 @@ describe('Radio.Group', () => { ); }); it('should be 20px height', () => { - assert(container.querySelector('.next-radio-wrapper').getBoundingClientRect().height === 20); + assert( + container.querySelector('.next-radio-wrapper').getBoundingClientRect() + .height === 20 + ); }); }); @@ -138,7 +146,12 @@ describe('Radio.Group', () => { document.body.appendChild(container); before(done => { ReactDOM.render( - , + , container, function init() { done(); @@ -156,7 +169,12 @@ describe('Radio.Group', () => { document.body.appendChild(container); before(done => { ReactDOM.render( - , + , container, function init() { done(); @@ -165,8 +183,10 @@ describe('Radio.Group', () => { }); it('should be 40px height', () => { assert( - Math.abs(container.querySelector('.next-radio-wrapper').getBoundingClientRect().height - 40) < - 0.0001 + Math.abs( + container.querySelector('.next-radio-wrapper').getBoundingClientRect() + .height - 40 + ) < 0.0001 ); }); }); @@ -177,7 +197,12 @@ describe('Radio.Group', () => { document.body.appendChild(container); before(done => { ReactDOM.render( - , + , container, function init() { done(); @@ -242,11 +267,10 @@ describe('Radio.Group', () => { describe('[events] simulate change', () => { it('should call `onChange`', () => { const onChange = sinon.spy(); - const wrapper1 = mount(); - wrapper1 - .find('input') - .first() - .simulate('change'); + const wrapper1 = mount( + + ); + wrapper1.find('input').first().simulate('change'); assert(onChange.calledOnce); }); }); @@ -354,14 +378,18 @@ describe('Radio.Group', () => { }); describe('string array ', () => { it('should support string array', () => { - const wrapper = mount(); + const wrapper = mount( + + ); assert(wrapper.find('.next-radio-group').children().length === 3); }); }); describe('Radio shape', () => { it('shape = button', () => { - const wrapper = mount(); + const wrapper = mount( + + ); assert(wrapper.find('.next-radio-button').children().length === 3); }); }); @@ -420,10 +448,7 @@ describe('Radio.Group', () => { ); const group = wrapper.instance().getInstance(); group.focus(); - const inputElement = wrapper - .find('input') - .at(0) - .getDOMNode(); + const inputElement = wrapper.find('input').at(0).getDOMNode(); assert(document.activeElement === inputElement); }); it('should focus the checked radio', () => { @@ -438,10 +463,7 @@ describe('Radio.Group', () => { ); const group = wrapper.instance().getInstance(); group.focus(); - const inputElement = wrapper - .find('input') - .at(1) - .getDOMNode(); + const inputElement = wrapper.find('input').at(1).getDOMNode(); assert(document.activeElement === inputElement); }); it('should focus in datasource mode', () => { @@ -450,10 +472,7 @@ describe('Radio.Group', () => { }); const group = wrapper.instance().getInstance(); group.focus(); - const inputElement = wrapper - .find('input') - .at(1) - .getDOMNode(); + const inputElement = wrapper.find('input').at(1).getDOMNode(); assert(document.activeElement === inputElement); }); }); @@ -466,7 +485,11 @@ describe('Radio.Group', () => { it('should renderPreview', () => { const wrapper = mount( - 'render preivew'} dataSource={numberList} /> + 'render preivew'} + dataSource={numberList} + /> ); assert(wrapper.getDOMNode().innerText === 'render preivew'); }); diff --git a/components/radio/__tests__/index-spec.js b/components/radio/__tests__/index-spec.tsx similarity index 100% rename from components/radio/__tests__/index-spec.js rename to components/radio/__tests__/index-spec.tsx diff --git a/components/radio/index.jsx b/components/radio/index.tsx similarity index 100% rename from components/radio/index.jsx rename to components/radio/index.tsx diff --git a/components/radio/mobile/index.jsx b/components/radio/mobile/index.tsx similarity index 100% rename from components/radio/mobile/index.jsx rename to components/radio/mobile/index.tsx diff --git a/components/radio/radio-group.jsx b/components/radio/radio-group.tsx similarity index 97% rename from components/radio/radio-group.jsx rename to components/radio/radio-group.tsx index bdacfb8521..9b9615a0a4 100644 --- a/components/radio/radio-group.jsx +++ b/components/radio/radio-group.tsx @@ -68,7 +68,10 @@ class RadioGroup extends Component { /** * 可选项列表, 数据项可为 String 或者 Object, 如 `['apple', 'pear', 'orange']` `[{label: 'apply', value: 'apple'}]` */ - dataSource: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.string), PropTypes.arrayOf(PropTypes.object)]), + dataSource: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.string), + PropTypes.arrayOf(PropTypes.object), + ]), /** * 通过子元素方式设置内部radio */ @@ -316,13 +319,13 @@ class RadioGroup extends Component { className={cls} style={style} onFocus={makeChain( - function() { + function () { this.hasFocus = true; }.bind(this), this.props.onFocus )} onBlur={makeChain( - function() { + function () { this.hasFocus = false; }.bind(this), this.props.onBlur diff --git a/components/radio/radio.jsx b/components/radio/radio.tsx similarity index 95% rename from components/radio/radio.jsx rename to components/radio/radio.tsx index e855f4187c..5185d2996c 100644 --- a/components/radio/radio.jsx +++ b/components/radio/radio.tsx @@ -137,7 +137,8 @@ class Radio extends UIState { const { props } = this; const { context } = props; - const disabled = props.disabled || (context.__group__ && 'disabled' in context && context.disabled); + const disabled = + props.disabled || (context.__group__ && 'disabled' in context && context.disabled); return disabled; } @@ -286,8 +287,12 @@ class Radio extends UIState { aria-checked={checked} aria-disabled={disabled} className={clsWrapper} - onMouseEnter={disabled ? onMouseEnter : makeChain(this._onUIMouseEnter, onMouseEnter)} - onMouseLeave={disabled ? onMouseLeave : makeChain(this._onUIMouseLeave, onMouseLeave)} + onMouseEnter={ + disabled ? onMouseEnter : makeChain(this._onUIMouseEnter, onMouseEnter) + } + onMouseLeave={ + disabled ? onMouseLeave : makeChain(this._onUIMouseLeave, onMouseLeave) + } > {radioComp} {[children, label].map((d, i) => diff --git a/components/radio/style.js b/components/radio/style.ts similarity index 100% rename from components/radio/style.js rename to components/radio/style.ts diff --git a/components/radio/index.d.ts b/components/radio/types.ts similarity index 100% rename from components/radio/index.d.ts rename to components/radio/types.ts diff --git a/components/radio/with-context.jsx b/components/radio/with-context.tsx similarity index 84% rename from components/radio/with-context.jsx rename to components/radio/with-context.tsx index fde81dbed9..ccca131fe4 100644 --- a/components/radio/with-context.jsx +++ b/components/radio/with-context.tsx @@ -8,7 +8,11 @@ export default function withContext(Radio) { onChange: PropTypes.func, __group__: PropTypes.bool, isButton: PropTypes.bool, - selectedValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]), + selectedValue: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.bool, + ]), disabled: PropTypes.bool, }; From 1fc5e53c42f86e0b938be3df1951e3462e3005fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8F=B5=E4=B9=8B?= Date: Fri, 22 Mar 2024 15:36:01 +0800 Subject: [PATCH 2/4] refactor(Radio): convert to TypeScript, impove docs and tests, close #4556 --- components/radio/__docs__/adaptor/index.tsx | 12 +- .../__docs__/demo/accessibility/index.md | 2 +- .../__docs__/demo/accessibility/index.tsx | 2 +- .../radio/__docs__/demo/button/index.tsx | 30 +- .../radio/__docs__/demo/control/index.tsx | 20 +- .../radio/__docs__/demo/dataSource/index.md | 2 +- .../radio/__docs__/demo/dataSource/index.tsx | 16 +- .../radio/__docs__/demo/direction/index.md | 2 +- .../radio/__docs__/demo/direction/index.tsx | 4 +- .../radio/__docs__/demo/group/index.tsx | 16 +- .../radio/__docs__/demo/isPreview/index.tsx | 5 +- .../radio/__docs__/demo/useWithGrid/index.md | 2 +- .../radio/__docs__/demo/useWithGrid/index.tsx | 22 +- components/radio/__docs__/index.en-us.md | 72 +-- components/radio/__docs__/index.md | 78 ++-- components/radio/__docs__/theme/index.tsx | 30 +- components/radio/__tests__/a11y-spec.tsx | 59 +-- components/radio/__tests__/group-spec.tsx | 411 +++++++----------- components/radio/__tests__/index-spec.tsx | 143 +++--- components/radio/index.tsx | 25 +- components/radio/radio-group.tsx | 141 ++---- components/radio/radio.tsx | 127 ++---- components/radio/types.ts | 204 ++++++--- components/radio/with-context.tsx | 23 +- 24 files changed, 645 insertions(+), 803 deletions(-) diff --git a/components/radio/__docs__/adaptor/index.tsx b/components/radio/__docs__/adaptor/index.tsx index e94f6efd6c..1bcfa6f805 100644 --- a/components/radio/__docs__/adaptor/index.tsx +++ b/components/radio/__docs__/adaptor/index.tsx @@ -2,6 +2,14 @@ import React from 'react'; import { Types, parseData, NodeType } from '@alifd/adaptor-helper'; import { Radio } from '@alifd/next'; +interface Data { + size: string; + state: string; + label: string; + type: string; + value: string; +} + export default { name: 'Radio', shape: ['normal', { value: 'button', label: 'Radio Button' }], @@ -55,7 +63,7 @@ export default { ], }; }, - adaptor: ({ shape, size, state = '', label, data, ...others }) => { + adaptor: ({ shape, size, state = '', label, data, ...others }: any) => { if (shape === 'normal') { return ( type === NodeType.node); + const list = (parseData(data) as Data[]).filter(({ type }) => type === NodeType.node); return ( - Programming language : + Programming language : python diff --git a/components/radio/__docs__/demo/button/index.tsx b/components/radio/__docs__/demo/button/index.tsx index a46fbf44d3..2e037ec859 100644 --- a/components/radio/__docs__/demo/button/index.tsx +++ b/components/radio/__docs__/demo/button/index.tsx @@ -22,37 +22,29 @@ const list = [ ]; class ControlApp extends React.Component { - constructor(props) { - super(props); + state = { + value1: 'apple', + value2: 'apple', + value3: '', + }; - this.state = { - value1: 'apple', - value2: 'apple', - value3: '', - }; - - this.onNestChange = this.onNestChange.bind(this); - this.onSmallChange = this.onSmallChange.bind(this); - this.onMediumChange = this.onMediumChange.bind(this); - } - - onSmallChange(value) { + onSmallChange = (value: string) => { this.setState({ value1: value, }); - } + }; - onMediumChange(value) { + onMediumChange = (value: string) => { this.setState({ value2: value, }); - } + }; - onNestChange(value) { + onNestChange = (value: string) => { this.setState({ value3: value, }); - } + }; render() { return ( diff --git a/components/radio/__docs__/demo/control/index.tsx b/components/radio/__docs__/demo/control/index.tsx index 5509977e89..19d70a0831 100644 --- a/components/radio/__docs__/demo/control/index.tsx +++ b/components/radio/__docs__/demo/control/index.tsx @@ -20,26 +20,16 @@ const list = [ ]; class ControlApp extends React.Component { - constructor(props) { - super(props); - - this.state = { - value: 'orange', - }; - - this.onChange = this.onChange.bind(this); - } + state = { + value: 'orange', + }; - onChange(value) { + onChange = (value: string) => { this.setState({ value: value, }); console.log('onChange', value); - } - - onClick(e) { - console.log('onClick', e); - } + }; render() { return ( diff --git a/components/radio/__docs__/demo/dataSource/index.md b/components/radio/__docs__/demo/dataSource/index.md index f89784ddcf..67d42bd8ef 100644 --- a/components/radio/__docs__/demo/dataSource/index.md +++ b/components/radio/__docs__/demo/dataSource/index.md @@ -2,7 +2,7 @@ # 传入数组配置 -通过配置 `dataSource` 参数来渲染单选框分组,数组中对象元素支持配置radio属性。 +通过配置 `dataSource` 参数来渲染单选框分组,数组中对象元素支持配置 radio 属性。 # en-US order=2 diff --git a/components/radio/__docs__/demo/dataSource/index.tsx b/components/radio/__docs__/demo/dataSource/index.tsx index 3ec576de05..651e1abfac 100644 --- a/components/radio/__docs__/demo/dataSource/index.tsx +++ b/components/radio/__docs__/demo/dataSource/index.tsx @@ -22,22 +22,18 @@ const list = [ ]; class App extends React.Component { - constructor(props) { - super(props); - - this.state = { - value: 'apple', - buttonValue: 'pear', - }; - } + state = { + value: 'apple', + buttonValue: 'pear', + }; - onChange = value => { + onChange = (value: string) => { this.setState({ value: value, }); }; - onButtonChange = value => { + onButtonChange = (value: string) => { this.setState({ buttonValue: value, }); diff --git a/components/radio/__docs__/demo/direction/index.md b/components/radio/__docs__/demo/direction/index.md index 2bf8486f80..4844845922 100644 --- a/components/radio/__docs__/demo/direction/index.md +++ b/components/radio/__docs__/demo/direction/index.md @@ -2,7 +2,7 @@ # 垂直展示 -垂直展示``,配合更多输入框。仅适用于非Button样式的``。 +垂直展示``,配合更多输入框。仅适用于非 Button 样式的``。 # en-US order=5 diff --git a/components/radio/__docs__/demo/direction/index.tsx b/components/radio/__docs__/demo/direction/index.tsx index 891754b2d7..c41ff896b9 100644 --- a/components/radio/__docs__/demo/direction/index.tsx +++ b/components/radio/__docs__/demo/direction/index.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { type ChangeEvent } from 'react'; import ReactDOM from 'react-dom'; import { Radio, Input, Switch } from '@alifd/next'; @@ -8,7 +8,7 @@ class App extends React.Component { dir: true, }; - onChange = (value, e) => { + onChange = (value: number, e: ChangeEvent) => { console.log('radio checked', value, e); this.setState({ value, diff --git a/components/radio/__docs__/demo/group/index.tsx b/components/radio/__docs__/demo/group/index.tsx index ae0a2f9947..bda7d0ce03 100644 --- a/components/radio/__docs__/demo/group/index.tsx +++ b/components/radio/__docs__/demo/group/index.tsx @@ -5,21 +5,15 @@ import { Radio } from '@alifd/next'; const RadioGroup = Radio.Group; class App extends React.Component { - constructor(props) { - super(props); + state = { + value: 'orange', + }; - this.state = { - value: 'orange', - }; - - this.onChange = this.onChange.bind(this); - } - - onChange(value) { + onChange = (value: string) => { this.setState({ value: value, }); - } + }; render() { return ( diff --git a/components/radio/__docs__/demo/isPreview/index.tsx b/components/radio/__docs__/demo/isPreview/index.tsx index 97f1e95e47..410c25e41f 100644 --- a/components/radio/__docs__/demo/isPreview/index.tsx +++ b/components/radio/__docs__/demo/isPreview/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Radio, Switch } from '@alifd/next'; +import type { RadioProps, GroupProps } from '@alifd/next/lib/radio'; class App extends React.Component { state = { @@ -20,10 +21,10 @@ class App extends React.Component { }); }; - renderChecked = (checked, props) => + renderChecked: RadioProps['renderPreview'] = (checked, props) => checked ? {props.children} : null; - renderPreview = (previewed, props) => {previewed.label}; + renderPreview: GroupProps['renderPreview'] = previewed => {previewed.label}; render() { return ( diff --git a/components/radio/__docs__/demo/useWithGrid/index.md b/components/radio/__docs__/demo/useWithGrid/index.md index d50a2c6d28..470efb0345 100644 --- a/components/radio/__docs__/demo/useWithGrid/index.md +++ b/components/radio/__docs__/demo/useWithGrid/index.md @@ -1,6 +1,6 @@ # zh-CN order=7 -# 使用Grid快速布局 +# 使用 Grid 快速布局 使用 `Grid` 布局 `RadioGroup` 中的选项。 diff --git a/components/radio/__docs__/demo/useWithGrid/index.tsx b/components/radio/__docs__/demo/useWithGrid/index.tsx index 89c00d3097..2ac1e48ca1 100644 --- a/components/radio/__docs__/demo/useWithGrid/index.tsx +++ b/components/radio/__docs__/demo/useWithGrid/index.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { type MouseEvent } from 'react'; import ReactDOM from 'react-dom'; import { Radio, Grid } from '@alifd/next'; @@ -6,25 +6,19 @@ const { Row, Col } = Grid; const RadioGroup = Radio.Group; class ControlApp extends React.Component { - constructor(props) { - super(props); + state = { + value: 'orange', + other: 0, + }; - this.state = { - value: 'orange', - other: 0, - }; - - this.onChange = this.onChange.bind(this); - } - - onChange(value) { + onChange = (value: string) => { this.setState({ value: value, }); console.log('onChange', value); - } + }; - onClick(e) { + onClick(e: MouseEvent) { console.log('onClick', e); } diff --git a/components/radio/__docs__/index.en-us.md b/components/radio/__docs__/index.en-us.md index aaefa2a70c..69f44ad890 100644 --- a/components/radio/__docs__/index.en-us.md +++ b/components/radio/__docs__/index.en-us.md @@ -18,37 +18,55 @@ Radio buttons allow the user to select a single option from data-set. User can u ### Radio -| Param | Descripiton | Type | Default Value | -| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -------- | --------- | -| id | Input`s id in component | String | - | -| checked | To set radio button is checked | Boolean | - | -| defaultChecked | To set radio button default to be checked | Boolean | - | -| label | To set the radio label | String | - | -| onChange | Callback on state change

**signatures**:
Function(checked: Boolean, e: Event) => void
**params**:
_checked_: {Boolean} Is checked
_e_: {Event} Dom Event | Function | func.noop | -| onMouseEnter | Callback on mouse enter

**signatures**:
Function(e: Event) => void
**params**:
_e_: {Event} Dom Event | Function | func.noop | -| onMouseLeave | Callback on mouse leave

**signatures**:
Function(e: Event) => void
**params**:
_e_: {Event} Dom Event | Function | func.noop | -| disabled | Set radio button disabel to be used | Boolean | - | -| value | value | String/Number/Boolean | - | -| name | name | String | - | +| Param | Description | Type | Default Value | Required | +| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------- | -------- | +| id | Id of the input | string | - | | +| checked | To set radio button is checked | boolean | - | | +| defaultChecked | To set radio button default to be checked | boolean | - | | +| label | To set the radio label | React.ReactNode | - | | +| onChange | Callback on check state change

**signature**:
**params**:
_checked_: Is checked
_event_: DOM event | (checked: boolean, event: React.ChangeEvent\) => void | - | | +| onMouseEnter | Callback on mouse enter | (e: React.MouseEvent\) => void | - | | +| onMouseLeave | Callback on mouse leave | (e: React.MouseEvent\) => void | - | | +| disabled | Set radio button disable to be used | boolean | - | | +| value | Value of radio | RadioValue | - | | +| name | Form item name | string | - | | +| isPreview | Set radio to preview state | boolean | - | | +| renderPreview | Customized rendering content function in preview mode

**signature**:
**params**:
_checked_: Is checked
_props_: The props of the radio
**return**:
The content of item | (checked: boolean, props: RadioProps) => React.ReactNode | - | | ### Radio.Group -| Param | Descripiton | Type | Default Value | -| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | --------- | -| name | name | String | - | -| value | The value of the Item witch is selected in radio group | String/Number/Boolean | - | -| size | Used with `shape` prop,valid when shape is set to button

**option**:
'large'
'medium'
'small' | Enum | 'medium' | -| shape | Make radio shape like button, when it set value to 'button'

**option**:
'button' | Enum | - | -| defaultValue | The value of the Item witch is default selected in radio group | String/Number/Boolean | - | -| onChange | Callback on state change

**signatures**:
Function(value: String/Number, e: Event) => void
**params**:
_value_: {String/Number} The selected value
_e_: {Event} Dom Event | Function | () => { } | -| disabled | All the radios in group are disable to be used | Boolean | - | -| dataSource | The data of radio buttons, it can be a String or a Object. For example: `['apple', 'pear', 'orange']` | Array<any> | \[] | -| children | To set radio button by setting children components | Array<ReactElement>/ReactElement | - | -| direction | The direction of item's aligning
- hoz: horizonal (default)
- ver: vertical

**Allowed values**:
'hoz', 'ver' | Enum | 'hoz' | +| Param | Description | Type | Default Value | Required | +| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | ------------- | -------- | +| name | Form name | string | - | | +| value | The value of the Item witch is selected in radio group (controlled) | RadioValue | - | | +| defaultValue | The value of the Item witch is default selected in radio group (uncontrolled) | RadioValue | - | | +| component | Specify jsx tag name | React.ElementType | 'div' | | +| onChange | Callback on value change

**signature**:
**params**:
_value_: The selected value
_event_: Dom Event | (value: RadioValue, event: React.ChangeEvent\) => void | - | | +| disabled | All the radios in group are disable to be used | boolean | - | | +| shape | Shape type | 'normal' \| 'button' | - | | +| size | Used with `shape` prop,valid when shape is set to button | 'large' \| 'medium' \| 'small' | 'medium' | | +| dataSource | List of options | Array\ \| Array\ | - | | +| children | To set radio button by setting children components | React.ReactNode | - | | +| direction | How items are arranged | 'hoz' \| 'ver' | - | | +| isPreview | - | boolean | - | | +| renderPreview | Customized rendering content function in preview mode

**signature**:
**params**:
_previewed_: Previewed item data,
_props_: The props of the previewed item
**return**:
The content of item | (previewed: RadioValueItem \| object, props: GroupProps) => React.ReactNode | - | | +### RadioValueItem + +| Param | Description | Type | Default Value | Required | +| -------- | ----------- | --------------- | ------------- | -------- | +| label | - | React.ReactNode | - | | +| value | - | RadioValue | - | yes | +| disabled | - | boolean | - | | + +### RadioValue + +```typescript +export type RadioValue = string | number | boolean; +``` ## ARIA and KeyBoard -| KeyBoard | Descripiton | -| :---------- | :------------------------------ | -| Tab | Get the focus, if there is no selection, it is the first one, then you can select it with a space. If it is selected, it will focus on the selected item, and then left-right will navigate and select radio. | +| KeyBoard | Descripiton | +| :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Tab | Get the focus, if there is no selection, it is the first one, then you can select it with a space. If it is selected, it will focus on the selected item, and then left-right will navigate and select radio. | diff --git a/components/radio/__docs__/index.md b/components/radio/__docs__/index.md index 91b11cab8b..d606725856 100644 --- a/components/radio/__docs__/index.md +++ b/components/radio/__docs__/index.md @@ -19,41 +19,55 @@ ### Radio -| 参数 | 说明 | 类型 | 默认值 | -| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | --------- | -| id | 组件input的id | String | - | -| checked | 设置radio是否选中 | Boolean | - | -| defaultChecked | 设置radio是否默认选中 | Boolean | - | -| label | 通过属性配置label | ReactNode | - | -| onChange | 状态变化时触发的事件

**签名**:
Function(checked: Boolean, e: Event) => void
**参数**:
_checked_: {Boolean} 是否选中
_e_: {Event} Dom 事件对象 | Function | func.noop | -| onMouseEnter | 鼠标进入enter事件

**签名**:
Function(e: Event) => void
**参数**:
_e_: {Event} Dom 事件对象 | Function | func.noop | -| onMouseLeave | 鼠标离开事件

**签名**:
Function(e: Event) => void
**参数**:
_e_: {Event} Dom 事件对象 | Function | func.noop | -| disabled | radio是否被禁用 | Boolean | - | -| value | radio 的value | String/Number/Boolean | - | -| name | name | String | - | -| isPreview | 是否为预览态 | Boolean | false | -| renderPreview | 预览态模式下渲染的内容

**签名**:
Function(checked: Boolean, props: Object) => reactNode
**参数**:
_checked_: {Boolean} 是否选中
_props_: {Object} 所有传入的参数
**返回值**:
{reactNode} Element 渲染内容
| Function | - | +| 参数 | 说明 | 类型 | 默认值 | 是否必填 | +| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------ | -------- | +| id | 组件 input 的 id | string | - | | +| checked | 设置 radio 是否选中 | boolean | - | | +| defaultChecked | 设置 radio 是否默认选中 | boolean | - | | +| label | 通过属性配置 label | React.ReactNode | - | | +| onChange | 选中状态变化时触发的事件

**签名**:
**参数**:
_checked_: 是否选中
_event_: DOM 事件 | (checked: boolean, event: React.ChangeEvent\) => void | - | | +| onMouseEnter | 鼠标进入 enter 事件 | (e: React.MouseEvent\) => void | - | | +| onMouseLeave | 鼠标离开事件 | (e: React.MouseEvent\) => void | - | | +| disabled | radio 是否被禁用 | boolean | - | | +| value | radio 的 value | RadioValue | - | | +| name | 表单项 name | string | - | | +| isPreview | 是否开启预览态 | boolean | - | | +| renderPreview | 自定义预览态模式下渲染的内容

**签名**:
**参数**:
_checked_: 是否选中
_props_: 所有传入的参数
**返回值**:
渲染内容 | (checked: boolean, props: RadioProps) => React.ReactNode | - | | ### Radio.Group -| 参数 | 说明 | 类型 | 默认值 | -| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | -------- | -| name | name | String | - | -| size | 与 `shape` 属性配套使用,shape设为button时有效

**可选值**:
'large'(大)
'medium'(中)
'small'(小) | Enum | 'medium' | -| shape | 可以设置成 button 展示形状

**可选值**:
'normal'(按钮状)
'button' | Enum | - | -| value | radio group的选中项的值 | String/Number/Boolean | - | -| defaultValue | radio group的默认值 | String/Number/Boolean | - | -| component | 设置标签类型 | String/Function | 'div' | -| onChange | 选中值改变时的事件

**签名**:
Function(value: String/Number, e: Event) => void
**参数**:
_value_: {String/Number} 选中项的值
_e_: {Event} Dom 事件对象 | Function | () => {} | -| disabled | 表示radio被禁用 | Boolean | - | -| dataSource | 可选项列表, 数据项可为 String 或者 Object, 如 `['apple', 'pear', 'orange']` `[{label: 'apply', value: 'apple'}]` | Array<String>/Array<Object> | \[] | -| children | 通过子元素方式设置内部radio | Array<ReactElement>/ReactElement | - | -| direction | 子项目的排列方式
- hoz: 水平排列 (default)
- ver: 垂直排列

**可选值**:
'hoz', 'ver' | Enum | 'hoz' | -| isPreview | 是否为预览态 | Boolean | false | -| renderPreview | 预览态模式下渲染的内容

**签名**:
Function(previewed: Object, props: Object) => reactNode
**参数**:
_previewed_: {Object} 预览值:{label: "", value: ""}
_props_: {Object} 所有传入的参数
**返回值**:
{reactNode} Element 渲染内容
| Function | - | +| 参数 | 说明 | 类型 | 默认值 | 是否必填 | +| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | -------- | -------- | +| name | 表单 name | string | - | | +| value | radio group 的选中项的值(受控) | RadioValue | - | | +| defaultValue | radio group 的默认值(非受控) | RadioValue | - | | +| component | 设置标签类型 | React.ElementType | 'div' | | +| onChange | 选中值改变时的事件

**签名**:
**参数**:
_value_: 选中的值
_event_: Dom 事件对象 | (value: RadioValue, event: React.ChangeEvent\) => void | - | | +| disabled | 表示 radio 被禁用 | boolean | - | | +| shape | 展示类型 | 'normal' \| 'button' | - | | +| size | 与 `shape` 属性配套使用,shape 设为 button 时有效 | 'large' \| 'medium' \| 'small' | 'medium' | | +| dataSource | 可选项列表 | Array\ \| Array\ | - | | +| children | 通过子元素方式设置内部 radio | React.ReactNode | - | | +| direction | 子项目的排列方式 | 'hoz' \| 'ver' | - | | +| isPreview | 是否开启预览态 | boolean | - | | +| renderPreview | 自定义预览态模式下渲染的内容

**签名**:
**参数**:
_previewed_: 预览的数据项
_props_: 预览项的参数
**返回值**:
渲染内容 | (previewed: RadioValueItem \| object, props: GroupProps) => React.ReactNode | - | | + +### RadioValueItem + +| 参数 | 说明 | 类型 | 默认值 | 是否必填 | +| -------- | ---- | --------------- | ------ | -------- | +| label | - | React.ReactNode | - | | +| value | - | RadioValue | - | 是 | +| disabled | - | boolean | - | | + +### RadioValue + +```typescript +export type RadioValue = string | number | boolean; +``` ## 无障碍键盘操作指南 -| 按键 | 说明 | -| :-- | :----------------------------------------------------- | -| Tab | 获取焦点,如果没有任何选中就是第一个,之后可以空格选中。如果有选中的就聚焦到选中项,然后通过左右键直接选中。 | +| 按键 | 说明 | +| :--- | :----------------------------------------------------------------------------------------------------------- | +| Tab | 获取焦点,如果没有任何选中就是第一个,之后可以空格选中。如果有选中的就聚焦到选中项,然后通过左右键直接选中。 | diff --git a/components/radio/__docs__/theme/index.tsx b/components/radio/__docs__/theme/index.tsx index 5522900172..dddc5e600c 100644 --- a/components/radio/__docs__/theme/index.tsx +++ b/components/radio/__docs__/theme/index.tsx @@ -2,7 +2,13 @@ import React from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import '../../../demo-helper/style'; -import { Demo, DemoGroup, DemoHead, initDemo } from '../../../demo-helper'; +import { + Demo, + DemoGroup, + DemoHead, + initDemo, + type DemoFunctionDefineForObject, +} from '../../../demo-helper'; import ConfigProvider from '../../../config-provider'; import zhCN from '../../../locale/zh-cn'; import enUS from '../../../locale/en-us'; @@ -10,10 +16,6 @@ import '../../style'; import Radio from '../../index'; import Field from '../../../field'; -// import demo helper - -// import component - const i18nMap = { 'zh-cn': { label: '文本', @@ -66,12 +68,17 @@ const i18nMap = { }; const RadioGroup = Radio.Group; +interface DemoProps { + i18n: (typeof i18nMap)[keyof typeof i18nMap]; +} + // default radio -class RadioDefaultDemo extends React.Component { +class RadioDefaultDemo extends React.Component { static propTypes = { i18n: PropTypes.object, }; - constructor(props) { + field: Field; + constructor(props: DemoProps) { super(props); this.field = new Field(this, { values: { @@ -92,7 +99,11 @@ class RadioDefaultDemo extends React.Component { render() { const { init, getValue } = this.field; const { i18n } = this.props; - const label = getValue('demo').radiolabel.value === 'true' ? i18n.label : null; + const label = + getValue<{ radiolabel: DemoFunctionDefineForObject }>('demo')!.radiolabel.value === + 'true' + ? i18n.label + : null; return ( { static propTypes = { i18n: PropTypes.object, }; diff --git a/components/radio/__tests__/a11y-spec.tsx b/components/radio/__tests__/a11y-spec.tsx index a74329adc7..f6dfafa394 100644 --- a/components/radio/__tests__/a11y-spec.tsx +++ b/components/radio/__tests__/a11y-spec.tsx @@ -1,44 +1,11 @@ import React from 'react'; -import Enzyme, { mount } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; -import assert from 'power-assert'; import Radio from '../index'; import '../style'; -import { unmount, testReact } from '../../util/__tests__/legacy/a11y/validate'; +import { testReact } from '../../util/__tests__/a11y/validate'; -Enzyme.configure({ adapter: new Adapter() }); - -const list = [ - { - value: 'apple', - label: 'Apple', - disabled: false, - }, - { - value: 'pear', - label: 'Pear', - }, - { - value: 'orange', - label: 'Orange', - disabled: true, - }, -]; - -/* eslint-disable no-undef, react/jsx-filename-extension */ describe('Radio A11y', () => { - let wrapper; - - afterEach(() => { - if (wrapper) { - wrapper.unmount(); - wrapper = null; - } - unmount(); - }); - it('should not have any violations for different states', async () => { - wrapper = await testReact( + await testReact(
test 1 @@ -58,11 +25,10 @@ describe('Radio A11y', () => {  
); - return wrapper; }); it('should not have any violations for Radio Group with children', async () => { - wrapper = await testReact( + await testReact( 苹果 @@ -75,7 +41,6 @@ describe('Radio A11y', () => { ); - return wrapper; }); it('should not have any violations for Radio Group with datasource', async () => { @@ -94,12 +59,11 @@ describe('Radio A11y', () => { label: '橙子', }, ]; - wrapper = await testReact(); - return wrapper; + await testReact(); }); - it('should add tabIndex for first Radio Item', async () => { - const wrapper = mount( + it('should add tabIndex for first Radio Item', () => { + cy.mount( ={true} @@ -110,25 +74,24 @@ describe('Radio A11y', () => { ); - wrapper.update(); - assert(wrapper.find('input#pear').at(0).getDOMNode().tabIndex === 0); + cy.get('input#pear').should('have.prop', 'tabIndex', 0); }); - it('should not add tabIndex for non Radio Item', async () => { - const wrapper = mount( + it('should not add tabIndex for non Radio Item', () => { + cy.mount(
梨子
-
+
苹果
); - assert(wrapper.find('div#mywrapper').at(0).getDOMNode().tabIndex === -1); + cy.get('div#mywrapper').should('have.prop', 'tabIndex', -1); }); }); diff --git a/components/radio/__tests__/group-spec.tsx b/components/radio/__tests__/group-spec.tsx index ea46a4e2f2..4e20043191 100644 --- a/components/radio/__tests__/group-spec.tsx +++ b/components/radio/__tests__/group-spec.tsx @@ -1,19 +1,12 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import Enzyme, { shallow, mount } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; -import assert from 'power-assert'; -import sinon from 'sinon'; -import Radio from '../index'; +import React, { cloneElement, createRef, type FC, type ReactElement } from 'react'; +import type { MountReturn } from 'cypress/react'; +import Radio, { type RadioValueItem } from '../index'; import '../style'; -/* eslint-disable */ -Enzyme.configure({ adapter: new Adapter() }); - const RadioGroup = Radio.Group; describe('Radio.Group', () => { - let list, numberList; + let list: RadioValueItem[], numberList: RadioValueItem[]; beforeEach('mock data', () => { list = [ { @@ -47,53 +40,48 @@ describe('Radio.Group', () => { ]; }); describe('[render] control', () => { - const wrapper1 = shallow(); - it('should contain `apple`', () => { - assert(wrapper1.dive().state().value === 'apple'); + cy.mount(); + cy.get('.next-radio-wrapper.checked .next-radio-label').should('have.text', '苹果'); }); it('should have three children with mock data', () => { - const wrapper2 = mount(); - assert(wrapper2.find('.next-radio').length === 3); - assert(wrapper2.find('.next-radio.disabled').length === 1); + cy.mount(); + cy.get('.next-radio').should('have.length', 3); + cy.get('.next-radio.disabled').should('have.length', 1); }); }); describe('[render] uncontrol', () => { it('should have three children with mock data', () => { - const wrapper = mount(); - wrapper - .find('input') - .first() - .simulate('change', { - target: { checked: true }, - }); - assert(wrapper.find('.next-radio-group').children().length === 3); + cy.mount(); + cy.get('.next-radio-wrapper.checked .next-radio-label').should('have.text', '梨'); + cy.get('.next-radio-group').children().should('have.length', 3); }); }); describe('[render] nest', () => { - const wrapper = shallow( - - - 苹果 - - - 梨子 - - - 西瓜 - - - ); - it('should contain `pear` and `watermelon`', () => { - assert(wrapper.dive().state().value === 'watermelon'); + cy.mount( + + + 苹果 + + + 梨子 + + + 西瓜 + + + ); + cy.get('input#pear').should('exist'); + cy.get('input#watermelon').should('exist'); + cy.get('.next-radio.checked input#watermelon').should('exist'); }); it('should have two children with nest ', () => { - const wrapper = mount( + cy.mount( 苹果 @@ -106,219 +94,145 @@ describe('Radio.Group', () => { ); - const target = wrapper.find('.next-radio-group'); - assert(target.children().length === 3); - assert(target.find('.next-radio.disabled').length === 1); + cy.get('.next-radio-group').children().should('have.length', 3); + cy.get('.next-radio.disabled').should('have.length', 1); }); }); describe('[render] button shape', () => { - describe('small button', () => { - let wrapper; - const container = document.createElement('div'); - container.style.visibility = 'hidden'; - document.body.appendChild(container); - before(done => { - ReactDOM.render( + describe('button size', () => { + it('should be 20px height when small', () => { + cy.mount( , - container, - function init() { - done(); - } - ); - }); - it('should be 20px height', () => { - assert( - container.querySelector('.next-radio-wrapper').getBoundingClientRect() - .height === 20 + /> ); + cy.get('.next-radio-wrapper').eq(0).should('have.prop', 'offsetHeight', 20); }); - }); - - describe('medium button', () => { - const container = document.createElement('div'); - container.style.visibility = 'hidden'; - document.body.appendChild(container); - before(done => { - ReactDOM.render( + it('should be 28px height when medium', () => { + cy.mount( , - container, - function init() { - done(); - } + /> ); + cy.get('.next-radio-wrapper').eq(0).should('have.prop', 'offsetHeight', 28); }); - it('should be 28px height', () => { - assert(container.getBoundingClientRect().height === 28); - }); - }); - - describe('large button', () => { - const container = document.createElement('div'); - container.style.visibility = 'hidden'; - document.body.appendChild(container); - before(done => { - ReactDOM.render( + it('should be 40px height when large', () => { + cy.mount( , - container, - function init() { - done(); - } - ); - }); - it('should be 40px height', () => { - assert( - Math.abs( - container.querySelector('.next-radio-wrapper').getBoundingClientRect() - .height - 40 - ) < 0.0001 + /> ); + cy.get('.next-radio-wrapper').eq(0).should('have.prop', 'offsetHeight', 40); }); }); - describe('default tagName', () => { - const container = document.createElement('div'); - container.style.visibility = 'hidden'; - document.body.appendChild(container); - before(done => { - ReactDOM.render( + describe('tagName', () => { + it('should be div when default', () => { + cy.mount( , - container, - function init() { - done(); - } + /> ); + cy.get('div.next-radio-group').should('exist'); }); - it('should be div', () => { - assert(container.querySelector('div.next-radio-group')); - }); - }); - - describe('customer tagName(String)', () => { - const container = document.createElement('div'); - container.style.visibility = 'hidden'; - document.body.appendChild(container); - before(done => { - ReactDOM.render( + it('should be footer', () => { + cy.mount( , - container, - function init() { - done(); - } + /> ); + cy.get('footer.next-radio-group').should('exist'); }); - it('should be footer', () => { - assert(container.querySelector('footer.next-radio-group')); - }); - }); - - describe('customer tagName(Func)', () => { - const container = document.createElement('div'); - container.style.visibility = 'hidden'; - document.body.appendChild(container); - const Footer = props =>
; - before(done => { - ReactDOM.render( + it('should be special-name', () => { + const Footer: FC = ({ children }) =>
{children}
; + cy.mount( , - container, - function init() { - done(); - } + /> ); - }); - it('should be special-name', () => { - assert(container.querySelector('.special-name')); + cy.get('.special-name').should('exist'); }); }); }); describe('[events] simulate change', () => { it('should call `onChange`', () => { - const onChange = sinon.spy(); - const wrapper1 = mount( - - ); - wrapper1.find('input').first().simulate('change'); - assert(onChange.calledOnce); + const onChange = cy.spy(); + cy.mount(); + cy.get('input').eq(2).check(); + cy.then(() => { + cy.wrap(onChange).should('be.calledOnce'); + cy.wrap(onChange.firstCall.args[0]).should('eq', 'orange'); + }); }); }); describe('disabled && value === undefined', () => { it('should support disabled', () => { - const wrapper = mount(); - wrapper.setProps({ - disabled: true, - }); - assert(wrapper.find('.next-radio-group.disabled').length === 1); + cy.mount(); + cy.get('.next-radio-group.disabled').should('exist'); + // click will change nothing when disabled + cy.get('.next-radio-wrapper.checked .next-radio-label').should('have.text', '0'); + cy.get('.next-radio-wrapper').eq(1).click(); + cy.get('.next-radio-wrapper.checked .next-radio-label').should('have.text', '0'); }); it('should support value === undefined', () => { - const wrapper = shallow(); - assert(wrapper.dive().state().value === 0); - wrapper.setProps({ - value: undefined, + cy.mount().as('radio'); + cy.get('.next-radio-wrapper.checked .next-radio-label').should('have.text', '0'); + cy.get('@radio').then(({ rerender, component }) => { + return rerender(cloneElement(component as ReactElement, { value: undefined })); }); - assert(wrapper.dive().state().value === undefined); + cy.get('.next-radio-wrapper.checked').should('not.exist'); }); }); describe('[behavior] controlled', () => { it('should support controlled `value`', () => { - const wrapper = shallow(); - assert(wrapper.dive().state().value === 'pear'); + cy.mount().as('radio'); + cy.get('.next-radio-wrapper.checked .next-radio-label').should('have.text', '梨'); - wrapper.setProps({ - value: 'apple', + cy.get('@radio').then(({ component, rerender }) => { + return rerender(cloneElement(component as ReactElement, { value: 'apple' })); }); - assert(wrapper.dive().state().value === 'apple'); + cy.get('.next-radio-wrapper.checked .next-radio-label').should('have.text', '苹果'); }); it('should support controlled `disabled`', () => { - const wrapper = mount(); - assert(!wrapper.props().disabled); - assert(wrapper.find('.next-radio.disabled').length === 1); - - wrapper.setProps({ - disabled: true, + cy.mount().as('radio'); + cy.get('.next-radio-group.disabled').should('not.exist'); + cy.get('.next-radio.disabled').should('have.length', 1); + cy.get('@radio').then(({ component, rerender }) => { + return rerender(cloneElement(component as ReactElement, { disabled: true })); }); - assert(wrapper.props().disabled); - assert(wrapper.find('.next-radio-group.disabled').length === 1); - wrapper.setProps({ - disabled: true, - value: undefined, + cy.get('.next-radio-group.disabled').should('exist'); + + cy.get('@radio').then(({ component, rerender }) => { + return rerender( + cloneElement(component as ReactElement, { disabled: true, value: undefined }) + ); }); - assert(wrapper.props().disabled); - assert(wrapper.find('.next-radio-group.disabled').length === 1); - assert(wrapper.find('.next-radio.disabled').length === list.length); + cy.get('.next-radio-group.disabled').should('exist'); + cy.get('.next-radio.disabled').should('have.length', list.length); + cy.mount(); }); // for issue https://github.com/facebook/react/issues/8727 it('change 3 times for react@15.6.1', () => { @@ -327,7 +241,7 @@ describe('Radio.Group', () => { value: 'orange', }; - onChange = value => { + onChange = (value: string) => { this.setState({ value, }); @@ -349,149 +263,130 @@ describe('Radio.Group', () => { ); } } - const wrapper = mount(); - - wrapper.find('input#apple').simulate('change'); - assert(wrapper.find('input#apple').prop('checked')); - assert(wrapper.state().value === 'apple'); - - wrapper.find('input#watermelon').simulate('change'); - assert(!wrapper.find('input#apple').prop('checked')); - assert(wrapper.find('input#watermelon').prop('checked')); - assert(wrapper.state().value === 'watermelon'); - - wrapper.find('input#apple').simulate('change'); - assert(wrapper.find('input#apple').prop('checked')); - assert(wrapper.state().value === 'apple'); + cy.mount(); + cy.get('input#apple').check(); + cy.get('input#apple').should('have.prop', 'checked', true); + cy.get('.next-radio-wrapper.checked .next-radio-label').should('have.text', '苹果'); + + cy.get('input#watermelon').check(); + cy.get('input#watermelon').should('have.prop', 'checked', true); + cy.get('.next-radio-wrapper.checked .next-radio-label').should('have.text', '西瓜'); + + cy.get('input#apple').check(); + cy.get('input#apple').should('have.prop', 'checked', true); + cy.get('.next-radio-wrapper.checked .next-radio-label').should('have.text', '苹果'); }); }); describe('value === 0', () => { it('should support value === 0', () => { - const wrapper = shallow().dive(); - assert(wrapper.state().value === 0); - wrapper.setProps({ - value: 1, + cy.mount().as('radio'); + cy.get('.next-radio-wrapper.checked .next-radio-label').should('have.text', '0'); + + cy.get('@radio').then(({ component, rerender }) => { + return rerender(cloneElement(component as ReactElement, { value: 1 })); }); - assert(wrapper.state().value === 1); + + cy.get('.next-radio-wrapper.checked .next-radio-label').should('have.text', '1'); }); }); describe('string array ', () => { it('should support string array', () => { - const wrapper = mount( - - ); - assert(wrapper.find('.next-radio-group').children().length === 3); + cy.mount(); + cy.get('.next-radio-group').children().should('have.length', 3); }); }); describe('Radio shape', () => { it('shape = button', () => { - const wrapper = mount( - - ); - assert(wrapper.find('.next-radio-button').children().length === 3); + cy.mount(); + cy.get('.next-radio-button').children().should('have.length', 3); }); }); describe('Children over datasource', () => { it('should render children over datasource', () => { - const wrapper = mount( + cy.mount( Apple - + Orange ); - assert(wrapper.find('.next-radio-button').children().length === 2); + cy.get('.next-radio-button').children().should('have.length', 2); }); it('should support null children', () => { - const wrapper = mount( + cy.mount( {null} - + Orange HelloWorld ); - assert(wrapper.find('label').length === 1); + cy.get('label').should('have.length', 1); }); }); describe('[focus] call focus()', () => { - let wrapper, target; - - beforeEach(() => { - target = document.createElement('div'); - document.body.appendChild(target); - }); - - afterEach(() => { - target = null; - if (wrapper) { - wrapper.unmount(); - } - }); it('should focus', () => { - wrapper = mount( - + const ref = createRef>(); + cy.mount( + 1 2 - , - { - attachTo: target, - } + ); - const group = wrapper.instance().getInstance(); - group.focus(); - const inputElement = wrapper.find('input').at(0).getDOMNode(); - assert(document.activeElement === inputElement); + cy.get('input').eq(0).should('not.be.focused'); + cy.then(() => { + ref.current?.getInstance().focus(); + }); + cy.get('input').eq(0).should('be.focused'); }); it('should focus the checked radio', () => { - wrapper = mount( - + const ref = createRef>(); + cy.mount( + 1 2 - , - { - attachTo: target, - } + ); - const group = wrapper.instance().getInstance(); - group.focus(); - const inputElement = wrapper.find('input').at(1).getDOMNode(); - assert(document.activeElement === inputElement); + cy.get('input').eq(1).should('not.be.focused'); + cy.then(() => { + ref.current?.getInstance().focus(); + }); + cy.get('input').eq(1).should('be.focused'); }); it('should focus in datasource mode', () => { - wrapper = mount(, { - attachTo: target, + const ref = createRef>(); + cy.mount(); + cy.get('input').eq(1).should('not.be.focused'); + cy.then(() => { + ref.current?.getInstance().focus(); }); - const group = wrapper.instance().getInstance(); - group.focus(); - const inputElement = wrapper.find('input').at(1).getDOMNode(); - assert(document.activeElement === inputElement); + cy.get('input').eq(1).should('be.focused'); }); }); describe('should render preview', () => { it('should isPreview', () => { - const wrapper = mount(); - assert(wrapper.getDOMNode().innerText === '苹果'); + cy.mount(); + cy.get('.next-form-preview').should('have.text', '苹果'); }); it('should renderPreview', () => { - const wrapper = mount( + cy.mount( 'render preivew'} dataSource={numberList} /> ); - assert(wrapper.getDOMNode().innerText === 'render preivew'); + cy.get('.next-form-preview').should('have.text', 'render preivew'); }); }); }); diff --git a/components/radio/__tests__/index-spec.tsx b/components/radio/__tests__/index-spec.tsx index 2a185984ba..320d220ac6 100644 --- a/components/radio/__tests__/index-spec.tsx +++ b/components/radio/__tests__/index-spec.tsx @@ -1,125 +1,104 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import Enzyme, { mount } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; -import assert from 'power-assert'; -import ReactTestUtils from 'react-dom/test-utils'; +import React, { cloneElement, createRef, type ReactElement } from 'react'; +import type { MountReturn } from 'cypress/react'; import Radio from '../index'; - -/* eslint-disable */ -Enzyme.configure({ adapter: new Adapter() }); +import '../style'; describe('Radio', () => { describe('[render] normal', () => { it('should get a normal radio', () => { - const wrapper1 = mount(); - const wrapper2 = mount(香蕉); - assert(wrapper1.find('.next-radio').length === 1); - assert(wrapper2.find('.next-radio').length === 1); + cy.mount(); + cy.get('.next-radio').should('have.length', 1); + cy.mount(香蕉); + cy.get('.next-radio').should('have.length', 1); + cy.get('#banana').should('have.length', 1).and('have.prop', 'tagName', 'INPUT'); }); it('with checked && defaultChecked', () => { - const wrapper1 = mount(); - const wrapper2 = mount(); - assert(wrapper1.find('.next-radio.checked').length === 0); - assert(wrapper2.find('.next-radio.checked').length === 0); + cy.mount(); + cy.get('.next-radio.checked').should('not.exist'); + cy.mount(); + cy.get('.next-radio.checked').should('not.exist'); }); it('disabled', () => { - const wrapper = mount(); - assert(wrapper.find('.next-radio.disabled').length === 1); + cy.mount(); + cy.get('.next-radio.disabled').should('have.length', 1); }); it('should support className', () => { - const wrapper = mount(); - assert(wrapper.find('.next-radio-wrapper.custom-name').length === 1); + cy.mount(); + cy.get('.next-radio-wrapper.custom-name').should('have.length', 1); }); it('should support name', () => { - const wrapper = mount(); - assert(wrapper.find('input[name="customer"]').length === 1); + cy.mount(); + cy.get('input[name="customer"]').should('have.length', 1); }); it('should isPreview', () => { - const wrapper = mount(); - assert(wrapper.getDOMNode().innerText === 'apple'); + cy.mount(); + cy.get('.next-form-preview').should('have.text', 'apple'); }); it('should renderPreview', () => { - const wrapper = mount( 'render preivew'} />); - assert(wrapper.getDOMNode().innerText === 'render preivew'); + cy.mount( 'render preivew'} />); + cy.get('.next-form-preview').should('have.text', 'render preivew'); }); }); describe('[focus] call focus()', () => { - let wrapper, target; - - beforeEach(() => { - target = document.createElement('div'); - document.body.appendChild(target); - }); - - afterEach(() => { - target = null; - if (wrapper) { - wrapper.unmount(); - } - }); it('should focus', () => { - wrapper = mount(1, { - attachTo: target, + const ref = createRef>(); + cy.mount(1); + cy.get('input').should('not.be.focused'); + cy.then(() => { + ref.current?.getInstance().focus(); }); - const group = wrapper.instance().getInstance(); - group.focus(); - const inputElement = wrapper.find('input').getDOMNode(); - assert(document.activeElement === inputElement); + cy.get('input').should('be.focused'); }); it('should not focus when disabled', () => { - wrapper = mount(1, { - attachTo: target, + const ref = createRef>(); + cy.mount( + + 1 + + ); + cy.get('input').should('not.be.focused'); + cy.then(() => { + ref.current?.getInstance().focus(); }); - const group = wrapper.instance().getInstance(); - group.focus(); - const inputElement = wrapper.find('input').getDOMNode(); - assert(document.activeElement !== inputElement); + cy.get('input').should('not.be.focused'); }); }); describe('behavior', () => { it('simulate click', () => { - let wrapper; - const container = document.createElement('div'); - container.style.display = 'none'; - document.body.appendChild(container); - - before(done => { - ReactDOM.render(, container, function init() { - wrapper = this; - done(); - }); - }); it('should checked after click', () => { - assert(!wrapper.state.checked); - ReactTestUtils.scryRenderedDOMComponentsWithTag(wrapper, 'input')[0].click(); - assert(!!wrapper.state.checked); + cy.mount(); + cy.get('.next-radio.checked').should('not.exist'); + cy.get('input').check(); + cy.get('.next-radio.checked').should('exist'); }); it('should call `onChange`', () => { - const onChange = sinon.spy(); - const wrapper1 = mount(); - - assert(!wrapper.find('input').prop('checked')); - wrapper1.find('input').simulate('change', { target: { checked: true } }); - - assert(onChange.calledOnce); - assert(wrapper.find('input').prop('checked')); + const onChange = cy.spy(); + cy.mount(); + cy.get('input').should('have.prop', 'checked', false); + cy.get('input').check(); + cy.then(() => { + cy.wrap(onChange).should('be.calledOnce'); + }); + cy.get('input').should('have.prop', 'checked', true); }); }); it('should support controlled `checked`', () => { - const wrapper = mount(); - assert(wrapper.find('input').props().checked); - assert(wrapper.find('.checked').length !== 0); - - wrapper.setProps({ - checked: false, + cy.mount().as('radio'); + cy.get('input').should('have.prop', 'checked', true); + cy.get('.checked').should('exist'); + cy.get('@radio').then(({ component, rerender }) => { + return rerender( + cloneElement(component as ReactElement, { + checked: false, + }) + ); }); - assert(!wrapper.find('input').props().checked); - assert(wrapper.find('.checked').length === 0); + cy.get('input').should('have.prop', 'checked', false); + cy.get('.checked').should('not.exist'); }); }); }); diff --git a/components/radio/index.tsx b/components/radio/index.tsx index 0ca454967b..cca38a5a6e 100644 --- a/components/radio/index.tsx +++ b/components/radio/index.tsx @@ -1,18 +1,23 @@ import Radio from './radio'; import RadioGroup from './radio-group'; import ConfigProvider from '../config-provider'; +import { assignSubComponent } from '../util/component'; -Radio.Group = ConfigProvider.config(RadioGroup, { - transform: /* istanbul ignore next */ (props, deprecated) => { - if ('itemDirection' in props) { - deprecated('itemDirection', 'direction', 'Radio'); - const { itemDirection, ...others } = props; +const RadioWithSub = assignSubComponent(Radio, { + Group: ConfigProvider.config(RadioGroup, { + transform: (props, deprecated) => { + if ('itemDirection' in props) { + deprecated('itemDirection', 'direction', 'Radio'); + const { itemDirection, ...others } = props; - props = { direction: itemDirection, ...others }; - } + props = { direction: itemDirection, ...others }; + } - return props; - }, + return props; + }, + }), }); -export default Radio; +export type { RadioProps, GroupProps, RadioValue, RadioValueItem } from './types'; + +export default RadioWithSub; diff --git a/components/radio/radio-group.tsx b/components/radio/radio-group.tsx index 9b9615a0a4..c211abd0b8 100644 --- a/components/radio/radio-group.tsx +++ b/components/radio/radio-group.tsx @@ -1,98 +1,48 @@ -import React, { Component } from 'react'; +import React, { Component, type ChangeEvent, type ReactNode } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import { polyfill } from 'react-lifecycles-compat'; import ConfigProvider from '../config-provider'; import { obj, func, focus } from '../util'; import Radio from './radio'; +import type { + GroupChildProps, + GroupProps, + RadioValue, + RadioValueItem, + WrappedRadio, +} from './types'; +import type { ConfiguredComponent } from '../config-provider/types'; const { makeChain } = func; const { focusRef } = focus; const { pickOthers } = obj; -/** - * Radio.Group - * @order 2 - */ -class RadioGroup extends Component { +interface GroupState { + value: RadioValue | undefined; +} + +class RadioGroup extends Component { static propTypes = { ...ConfigProvider.propTypes, - /** - * 样式类名的品牌前缀 - */ prefix: PropTypes.string, - /** - * 自定义类名 - */ className: PropTypes.string, - /** - * 自定义内敛样式 - */ style: PropTypes.object, - /** - * name - */ name: PropTypes.string, - /** - * radio group的选中项的值 - */ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]), - /** - * radio group的默认值 - */ defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]), - /** - * 设置标签类型 - */ component: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), - /** - * 选中值改变时的事件 - * @param {String/Number} value 选中项的值 - * @param {Event} e Dom 事件对象 - */ onChange: PropTypes.func, - /** - * 表示radio被禁用 - */ disabled: PropTypes.bool, - /** - * 可以设置成 button 展示形状 - * @enumdesc 按钮状 - */ shape: PropTypes.oneOf(['normal', 'button']), - /** - * 与 `shape` 属性配套使用,shape设为button时有效 - * @enumdesc 大, 中, 小 - */ size: PropTypes.oneOf(['large', 'medium', 'small']), - /** - * 可选项列表, 数据项可为 String 或者 Object, 如 `['apple', 'pear', 'orange']` `[{label: 'apply', value: 'apple'}]` - */ dataSource: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.string), PropTypes.arrayOf(PropTypes.object), ]), - /** - * 通过子元素方式设置内部radio - */ children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.element), PropTypes.element]), - - /** - * 子项目的排列方式 - * - hoz: 水平排列 (default) - * - ver: 垂直排列 - */ direction: PropTypes.oneOf(['hoz', 'ver']), - /** - * 是否为预览态 - */ isPreview: PropTypes.bool, - /** - * 预览态模式下渲染的内容 - * @param {Object} previewed 预览值:{label: "", value: ""} - * @param {Object} props 所有传入的参数 - * @returns {reactNode} Element 渲染内容 - */ renderPreview: PropTypes.func, }; @@ -114,9 +64,12 @@ class RadioGroup extends Component { disabled: PropTypes.bool, }; - constructor(props) { + radioRefs: unknown[]; + hasFocus: boolean; + + constructor(props: GroupProps) { super(props); - let value = ''; + let value: RadioValue | undefined = ''; if ('value' in props) { value = props.value; } else if ('defaultValue' in props) { @@ -131,7 +84,7 @@ class RadioGroup extends Component { this.onChange = this.onChange.bind(this); } - static getDerivedStateFromProps(props, state) { + static getDerivedStateFromProps(props: GroupProps, state: GroupState) { if ('value' in props && props.value !== state.value) { return { value: props.value, @@ -153,12 +106,12 @@ class RadioGroup extends Component { }; } - onChange(currentValue, e) { + onChange(currentValue: RadioValue, e: ChangeEvent) { if (!('value' in this.props)) { this.setState({ value: currentValue }); } if (currentValue !== this.state.value) { - this.props.onChange(currentValue, e); + this.props.onChange!(currentValue, e); } } @@ -166,13 +119,13 @@ class RadioGroup extends Component { if (!this.hasFocus) { const availableRef = this.radioRefs.filter(ref => { if (ref) { - return !ref.props.disabled; + return !(ref as WrappedRadio).props.disabled; } return false; - }); + }) as WrappedRadio[]; const radioRef = availableRef.find(ref => { if (ref) { - return ref.props.checked; + return (ref as WrappedRadio).props.checked; } return false; }); @@ -184,9 +137,9 @@ class RadioGroup extends Component { } } - saveRadioRef = (el, index) => { - if (el && typeof el.getInstance === 'function') { - const radio = el.getInstance(); + saveRadioRef = (el: unknown, index: number) => { + if (el && typeof (el as ConfiguredComponent).getInstance === 'function') { + const radio = (el as ConfiguredComponent).getInstance(); this.radioRefs[index] = radio; } }; @@ -205,18 +158,18 @@ class RadioGroup extends Component { isPreview, renderPreview, } = this.props; - const others = pickOthers(Object.keys(RadioGroup.propTypes), this.props); + const others = pickOthers(RadioGroup.propTypes, this.props); if (rtl) { others.dir = 'rtl'; } let children; - const previewed = {}; + const previewed: { label?: ReactNode; value?: RadioValue } = {}; this.radioRefs = []; if (this.props.children) { children = React.Children.map(this.props.children, (child, index) => { - if (!React.isValidElement(child)) { + if (!React.isValidElement(child)) { return child; } const checked = this.state.value === child.props.value; @@ -226,12 +179,12 @@ class RadioGroup extends Component { } const tabIndex = (index === 0 && !this.state.value) || checked ? 0 : -1; const childrtl = child.props.rtl === undefined ? rtl : child.props.rtl; - if (child.type && child.type.displayName === 'Config(Radio)') { + if (child.type && (child.type as typeof Radio).displayName === 'Config(Radio)') { return React.cloneElement(child, { checked, tabIndex, rtl: childrtl, - ref: e => { + ref: (e: InstanceType) => { this.saveRadioRef(e, index); }, }); @@ -239,20 +192,22 @@ class RadioGroup extends Component { return React.cloneElement(child, { checked, rtl: childrtl, - ref: e => { + ref: (e: unknown) => { this.saveRadioRef(e, index); }, }); }); } else { - children = this.props.dataSource.map((item, index) => { - let option = item; + children = this.props.dataSource!.map((item, index) => { + let option: RadioValueItem; if (typeof item !== 'object') { option = { label: item, value: item, disabled, }; + } else { + option = item; } const checked = this.state.value === option.value; if (checked) { @@ -274,20 +229,21 @@ class RadioGroup extends Component { ); }); } + // Useless code // Reset the ref if children is unavailable. - if (children.length === 0) { - this.firstRef = null; - } - if (!previewed.value) { - this.selectedRef = null; - } + // if (children.length === 0) { + // this.firstRef = null; + // } + // if (!previewed.value) { + // this.selectedRef = null; + // } if (isPreview) { const previewCls = classnames(className, `${prefix}form-preview`); if ('renderPreview' in this.props) { return (
- {renderPreview(previewed, this.props)} + {renderPreview!(previewed, this.props)}
); } @@ -301,16 +257,15 @@ class RadioGroup extends Component { const isButtonShape = shape === 'button'; - const cls = classnames({ + const cls = classnames(className, { [`${prefix}radio-group`]: true, [`${prefix}radio-group-${direction}`]: !isButtonShape, [`${prefix}radio-button`]: isButtonShape, [`${prefix}radio-button-${size}`]: isButtonShape, - [className]: !!className, disabled, }); - const TagName = component; + const TagName = component!; return ( { static displayName = 'Radio'; static propTypes = { ...ConfigProvider.propTypes, - /** - * 自定义类名 - */ className: PropTypes.string, - /** - * 组件input的id - */ id: PropTypes.string, - /** - * 自定义内敛样式 - */ style: PropTypes.object, - /** - * 设置radio是否选中 - */ checked: PropTypes.bool, - /** - * 设置radio是否默认选中 - */ defaultChecked: PropTypes.bool, - /** - * 通过属性配置label - */ label: PropTypes.node, - /** - * 状态变化时触发的事件 - * @param {Boolean} checked 是否选中 - * @param {Event} e Dom 事件对象 - */ onChange: PropTypes.func, - /** - * 鼠标进入enter事件 - * @param {Event} e Dom 事件对象 - */ onMouseEnter: PropTypes.func, - /** - * 鼠标离开事件 - * @param {Event} e Dom 事件对象 - */ onMouseLeave: PropTypes.func, - /** - * radio是否被禁用 - */ disabled: PropTypes.bool, - /** - * radio 的value - */ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]), - /** - * name - */ name: PropTypes.string, - /** - * 是否为预览态 - */ isPreview: PropTypes.bool, - /** - * 预览态模式下渲染的内容 - * @param {Boolean} checked 是否选中 - * @param {Object} props 所有传入的参数 - * @returns {reactNode} Element 渲染内容 - */ renderPreview: PropTypes.func, }; @@ -98,7 +51,25 @@ class Radio extends UIState { disabled: PropTypes.bool, }; - constructor(props) { + static getDerivedStateFromProps(nextProps: RadioWithContextProps) { + const { context: nextContext } = nextProps; + + if (nextContext.__group__ && 'selectedValue' in nextContext) { + return { + checked: nextContext.selectedValue === nextProps.value, + }; + } else if ('checked' in nextProps) { + return { + checked: nextProps.checked, + }; + } + + return null; + } + + radioRef: HTMLInputElement | null; + + constructor(props: RadioWithContextProps) { super(props); const { context } = props; let checked; @@ -117,22 +88,6 @@ class Radio extends UIState { this.onChange = this.onChange.bind(this); } - static getDerivedStateFromProps(nextProps) { - const { context: nextContext } = nextProps; - - if (nextContext.__group__ && 'selectedValue' in nextContext) { - return { - checked: nextContext.selectedValue === nextProps.value, - }; - } else if ('checked' in nextProps) { - return { - checked: nextProps.checked, - }; - } - - return null; - } - get disabled() { const { props } = this; const { context } = props; @@ -143,7 +98,11 @@ class Radio extends UIState { return disabled; } - shouldComponentUpdate(nextProps, nextState, nextContext) { + shouldComponentUpdate( + nextProps: RadioWithContextProps, + nextState: RadioState, + nextContext: RadioContext + ) { const { shallowEqual } = obj; return ( !shallowEqual(this.props, nextProps) || @@ -160,19 +119,19 @@ class Radio extends UIState { } } - onChange(e) { + onChange(e: ChangeEvent) { const checked = e.target.checked; const { context, value } = this.props; if (context.__group__) { - context.onChange(value, e); + context.onChange(value!, e); } else if (this.state.checked !== checked) { if (!('checked' in this.props)) { this.setState({ checked: checked, }); } - this.props.onChange(checked, e); + this.props.onChange!(checked, e); } } @@ -184,7 +143,6 @@ class Radio extends UIState { } render() { - /* eslint-disable no-unused-vars */ const { id, className, @@ -205,10 +163,10 @@ class Radio extends UIState { const checked = !!this.state.checked; const disabled = this.disabled; const isButton = context.isButton; - const prefix = context.prefix || this.props.prefix; + const prefix = this.props.prefix; const others = obj.pickOthers(Radio.propTypes, otherProps); - const othersData = obj.pickAttrsWith(others, 'data-'); + const othersData = obj.pickAttrsWith(others, 'data-') as Record<`data-${string}`, unknown>; if (isPreview) { const previewCls = classnames(className, `${prefix}form-preview`); @@ -216,7 +174,7 @@ class Radio extends UIState { if ('renderPreview' in this.props) { return (
- {renderPreview(checked, this.props)} + {renderPreview!(checked, this.props)}
); } @@ -261,9 +219,8 @@ class Radio extends UIState { press: checked, unpress: !checked, }); - const clsWrapper = classnames({ + const clsWrapper = classnames(className, { [`${prefix}radio-wrapper`]: true, - [className]: !!className, checked, disabled, [this.getStateClassName()]: true, @@ -288,9 +245,11 @@ class Radio extends UIState { aria-disabled={disabled} className={clsWrapper} onMouseEnter={ + // @ts-expect-error _onUIMouseEnter is not defined disabled ? onMouseEnter : makeChain(this._onUIMouseEnter, onMouseEnter) } onMouseLeave={ + // @ts-expect-error _onUIMouseLeave is not defined disabled ? onMouseLeave : makeChain(this._onUIMouseLeave, onMouseLeave) } > @@ -307,4 +266,6 @@ class Radio extends UIState { } } +export type { Radio }; + export default ConfigProvider.config(withContext(polyfill(Radio))); diff --git a/components/radio/types.ts b/components/radio/types.ts index 464854672a..2014fd2c29 100644 --- a/components/radio/types.ts +++ b/components/radio/types.ts @@ -1,81 +1,115 @@ -/// - -import React from 'react'; -import { data } from '../checkbox'; -import { CommonProps } from '../util'; -interface HTMLAttributesWeak extends React.HTMLAttributes { - defaultValue?: any; - onChange?: any; +import React, { type RefAttributes } from 'react'; +import type { CommonProps } from '../util'; +import type { Radio } from './radio'; + +export type HTMLAttributesWeak = Omit< + React.HTMLAttributes, + 'defaultValue' | 'onChange' | 'onMouseEnter' | 'onMouseLeave' +>; + +/** + * @api + */ +export type RadioValue = string | number | boolean; + +/** + * @api + */ +export interface RadioValueItem { + label?: React.ReactNode; + value: RadioValue; + disabled?: boolean; } -export interface GroupProps extends HTMLAttributesWeak, CommonProps { - /** - * 样式类名的品牌前缀 - */ - prefix?: string; - - /** - * 自定义类名 - */ - className?: string; +export interface GroupChildProps extends RefAttributes { + checked: boolean; + rtl?: boolean; + tabIndex?: number; + children?: React.ReactNode; + value?: RadioValue; +} - /** - * 自定义内敛样式 - */ - style?: React.CSSProperties; +export type RadioContext = { + __group__: boolean; + isButton: boolean; + onChange: (value: RadioValue, event: React.ChangeEvent) => void; + selectedValue?: RadioValue; + disabled?: boolean; +}; +/** + * @api Radio.Group + * @order 2 + */ +export interface GroupProps extends HTMLAttributesWeak, CommonProps { /** - * name + * 表单 name + * @en form name */ name?: string; /** - * radio group的选中项的值 + * radio group 的选中项的值(受控) + * @en The value of the Item witch is selected in radio group (controlled) */ - value?: string | number | boolean; + value?: RadioValue; /** - * radio group的默认值 + * radio group 的默认值(非受控) + * @en The value of the Item witch is default selected in radio group (uncontrolled) */ - defaultValue?: string | number | boolean; + defaultValue?: RadioValue; /** * 设置标签类型 + * @en Specify jsx tag name + * @defaultValue 'div' */ - component?: React.ReactHTML | (() => void); + component?: React.ElementType; /** * 选中值改变时的事件 + * @en Callback on value change + * @param value - 选中的值 - The selected value + * @param event - Dom 事件对象 - Dom Event */ - onChange?: (value: string | number | boolean, e: any) => void; + onChange?: (value: RadioValue, event: React.ChangeEvent) => void; /** - * 表示radio被禁用 + * 表示 radio 被禁用 + * @en All the radios in group are disable to be used */ disabled?: boolean; /** - * 可以设置成 button 展示形状 + * 展示类型 + * @en Shape type */ - shape?: 'button'; + shape?: 'normal' | 'button'; /** - * 与 `shape` 属性配套使用,shape设为button时有效 + * 与 `shape` 属性配套使用,shape 设为 button 时有效 + * @en Used with `shape` prop, valid when shape is set to button + * @defaultValue 'medium' */ size?: 'large' | 'medium' | 'small'; /** - * 可选项列表, 数据项可为 String 或者 Object, 如 `['apple', 'pear', 'orange']` + * 可选项列表 + * @en List of options */ - dataSource?: Array | Array | Array; + dataSource?: Array | Array; /** - * 通过子元素方式设置内部radio + * 通过子元素方式设置内部 radio + * @en To set radio button by setting children components */ - children?: Array | React.ReactElement; + children?: React.ReactNode; /** * 子项目的排列方式 + * @en How items are arranged + * @example * - hoz: 水平排列 (default) * - ver: 垂直排列 */ @@ -87,94 +121,120 @@ export interface GroupProps extends HTMLAttributesWeak, CommonProps { isPreview?: boolean; /** - * 预览态模式下渲染的内容 + * 自定义预览态模式下渲染的内容 + * @en Customized rendering content function in preview mode + * @param previewed - 预览的数据项 - Previewed item data, + * @param props - 预览项的参数 - The props of the previewed item + * @returns 渲染内容 - The content of item + * @remarks previewed 为空对象时代表没有选中的项 - When `previewed` is an empty object, it means there is no selected item. */ renderPreview?: ( - previewed: { label: string | React.ReactNode; value: string | number | boolean }, - props: any + previewed: RadioValueItem | Partial, + props: GroupProps ) => React.ReactNode; - itemDirection?: 'hoz' | 'ver'; -} -export class Group extends React.Component {} -interface HTMLAttributesWeak extends React.HTMLAttributes { - onChange?: any; - onMouseEnter?: any; - onMouseLeave?: any; -} - -export interface RadioProps extends HTMLAttributesWeak, CommonProps { /** - * 自定义类名 + * @deprecated Use direction instead + * @skip */ - className?: string; + itemDirection?: 'hoz' | 'ver'; +} +/** + * @api + * @order 1 + */ +export interface RadioProps extends HTMLAttributesWeak, CommonProps { /** - * 组件input的id + * 组件 input 的 id + * @en Id of the input */ id?: string; /** - * 自定义内敛样式 - */ - style?: React.CSSProperties; - - /** - * 设置radio是否选中 + * 设置 radio 是否选中 + * @en To set radio button is checked */ checked?: boolean; /** - * 设置radio是否默认选中 + * 设置 radio 是否默认选中 + * @en To set radio button default to be checked */ defaultChecked?: boolean; /** - * 通过属性配置label + * 通过属性配置 label + * @en To set the radio label */ label?: React.ReactNode; /** - * 状态变化时触发的事件 + * 选中状态变化时触发的事件 + * @en Callback on check state change + * @param checked - 是否选中 - Is checked + * @param event - DOM 事件 - DOM event */ - onChange?: (checked: boolean, e: any) => void; + onChange?: (checked: boolean, event: React.ChangeEvent) => void; /** - * 鼠标进入enter事件 + * 鼠标进入 enter 事件 + * @en Callback on mouse enter */ onMouseEnter?: (e: React.MouseEvent) => void; /** * 鼠标离开事件 + * @en Callback on mouse leave */ onMouseLeave?: (e: React.MouseEvent) => void; /** - * radio是否被禁用 + * radio 是否被禁用 + * @en Set radio button disable to be used */ disabled?: boolean; /** - * radio 的value + * radio 的 value + * @en Value of radio */ - value?: string | number | boolean; + value?: RadioValue; /** - * name + * 表单项 name + * @en Form item name */ name?: string; /** * 是否开启预览态 + * @en Set radio to preview state */ isPreview?: boolean; /** - * 预览态模式下渲染的内容 + * 自定义预览态模式下渲染的内容 + * @en Customized rendering content function in preview mode + * @param checked - 是否选中 - Is checked + * @param props - 所有传入的参数 - The props of the radio + * @returns 渲染内容 - The content of item */ - renderPreview?: (values: string | number | boolean, props: any) => React.ReactNode; + renderPreview?: (checked: boolean, props: RadioProps) => React.ReactNode; + + /** + * Radio.Group 传递给 Radio 的私有属性 + * @skip + */ + context?: RadioContext; +} + +export interface RadioWithContextProps extends RadioProps { + context: RadioContext; } -export default class Radio extends React.Component { - static Group: typeof Group; +export declare class WrappedRadio extends React.Component { + static displayName: 'Radio'; + radioRef: Radio | null; + focus(): void; } diff --git a/components/radio/with-context.tsx b/components/radio/with-context.tsx index ccca131fe4..ece81b4e7e 100644 --- a/components/radio/with-context.tsx +++ b/components/radio/with-context.tsx @@ -1,8 +1,10 @@ -import React from 'react'; +import React, { type ComponentRef, type ComponentType } from 'react'; import PropTypes from 'prop-types'; +import type { RadioContext, RadioProps, WrappedRadio } from './types'; -export default function withContext(Radio) { - return class WrappedComp extends React.Component { +export default function withContext(Radio: C) { + type Ref = ComponentRef & { focus: () => void }; + class WrappedComp extends React.Component { static displayName = 'Radio'; static contextTypes = { onChange: PropTypes.func, @@ -16,7 +18,9 @@ export default function withContext(Radio) { disabled: PropTypes.bool, }; - constructor(props) { + radioRef: Ref | null; + + constructor(props: RadioProps) { super(props); this.radioRef = null; } @@ -30,13 +34,16 @@ export default function withContext(Radio) { render() { return ( { + ref={(el: Ref | null) => { this.radioRef = el; }} - {...this.props} - context={this.context} + // eslint-disable-next-line @typescript-eslint/no-explicit-any + {...(this.props as any)} + context={this.context as RadioContext} /> ); } - }; + } + + return WrappedComp as unknown as typeof WrappedRadio; } From 94ef7a6b2e168f992cf0b1a9d3d684761020d6da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8F=B5=E4=B9=8B?= Date: Wed, 3 Apr 2024 10:29:31 +0800 Subject: [PATCH 3/4] chore(Radio): improve by codereview --- components/radio/radio-group.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/components/radio/radio-group.tsx b/components/radio/radio-group.tsx index c211abd0b8..525558d601 100644 --- a/components/radio/radio-group.tsx +++ b/components/radio/radio-group.tsx @@ -18,7 +18,7 @@ const { makeChain } = func; const { focusRef } = focus; const { pickOthers } = obj; -interface GroupState { +export interface GroupState { value: RadioValue | undefined; } @@ -125,7 +125,7 @@ class RadioGroup extends Component { }) as WrappedRadio[]; const radioRef = availableRef.find(ref => { if (ref) { - return (ref as WrappedRadio).props.checked; + return ref.props.checked; } return false; }); @@ -229,14 +229,6 @@ class RadioGroup extends Component { ); }); } - // Useless code - // Reset the ref if children is unavailable. - // if (children.length === 0) { - // this.firstRef = null; - // } - // if (!previewed.value) { - // this.selectedRef = null; - // } if (isPreview) { const previewCls = classnames(className, `${prefix}form-preview`); From 850b3196d6db63b5f3119843f9bfdc0b766c9edf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8F=B5=E4=B9=8B?= Date: Tue, 9 Apr 2024 16:58:02 +0800 Subject: [PATCH 4/4] chore(Radio): improve types --- components/radio/radio-group.tsx | 2 ++ components/radio/types.ts | 2 ++ components/radio/with-context.tsx | 18 ++++++++---------- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/components/radio/radio-group.tsx b/components/radio/radio-group.tsx index 525558d601..33e0b587bf 100644 --- a/components/radio/radio-group.tsx +++ b/components/radio/radio-group.tsx @@ -284,4 +284,6 @@ class RadioGroup extends Component { } } +export type { RadioGroup }; + export default polyfill(RadioGroup); diff --git a/components/radio/types.ts b/components/radio/types.ts index 2014fd2c29..e5646d2430 100644 --- a/components/radio/types.ts +++ b/components/radio/types.ts @@ -2,6 +2,8 @@ import React, { type RefAttributes } from 'react'; import type { CommonProps } from '../util'; import type { Radio } from './radio'; +export type { Radio }; + export type HTMLAttributesWeak = Omit< React.HTMLAttributes, 'defaultValue' | 'onChange' | 'onMouseEnter' | 'onMouseLeave' diff --git a/components/radio/with-context.tsx b/components/radio/with-context.tsx index ece81b4e7e..04fdc2f6c3 100644 --- a/components/radio/with-context.tsx +++ b/components/radio/with-context.tsx @@ -1,10 +1,9 @@ -import React, { type ComponentRef, type ComponentType } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; -import type { RadioContext, RadioProps, WrappedRadio } from './types'; +import type { RadioContext, RadioProps, WrappedRadio, Radio as RadioClass } from './types'; -export default function withContext(Radio: C) { - type Ref = ComponentRef & { focus: () => void }; - class WrappedComp extends React.Component { +export default function withContext(Radio: typeof RadioClass) { + class WrappedComp extends React.Component implements WrappedRadio { static displayName = 'Radio'; static contextTypes = { onChange: PropTypes.func, @@ -18,7 +17,7 @@ export default function withContext(Radio: C) { disabled: PropTypes.bool, }; - radioRef: Ref | null; + radioRef: RadioClass | null; constructor(props: RadioProps) { super(props); @@ -34,16 +33,15 @@ export default function withContext(Radio: C) { render() { return ( { + ref={el => { this.radioRef = el; }} - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {...(this.props as any)} + {...this.props} context={this.context as RadioContext} /> ); } } - return WrappedComp as unknown as typeof WrappedRadio; + return WrappedComp as typeof WrappedRadio; }