From 3fdc17d15033c3f0f61c34c94e2aca1ac11e0c4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AA=86=E6=9E=97?= Date: Wed, 13 Mar 2024 17:36:42 +0800 Subject: [PATCH] =?UTF-8?q?parent=20142d089989465d9128cfd998cf5795ab53ded1?= =?UTF-8?q?2a=20author=20=E9=AA=86=E6=9E=97=20?= =?UTF-8?q?=201710322602=20+0800=20committer=20=E9=AA=86=E6=9E=97=20=201710930339=20+0800?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parent 142d089989465d9128cfd998cf5795ab53ded12a author 骆林 1710322602 +0800 committer 骆林 1710930294 +0800 parent 142d089989465d9128cfd998cf5795ab53ded12a author 骆林 1710322602 +0800 committer 骆林 1710930251 +0800 parent 142d089989465d9128cfd998cf5795ab53ded12a author 骆林 1710322602 +0800 committer 骆林 1710930016 +0800 parent 142d089989465d9128cfd998cf5795ab53ded12a author 骆林 1710322602 +0800 committer 骆林 1710929391 +0800 parent 142d089989465d9128cfd998cf5795ab53ded12a author 骆林 1710322602 +0800 committer 骆林 1710929075 +0800 parent 142d089989465d9128cfd998cf5795ab53ded12a author 骆林 1710322602 +0800 committer 骆林 1710929054 +0800 parent 142d089989465d9128cfd998cf5795ab53ded12a author 骆林 1710322602 +0800 committer 骆林 1710929008 +0800 parent 142d089989465d9128cfd998cf5795ab53ded12a author 骆林 1710322602 +0800 committer 骆林 1710928916 +0800 fix(Drawer): 优化ts,新增tsdoc chore(Notification): adjust ts types chore(Notification): doc & test chore(Notification): adjust ts types chore(Notification): lint eg docs(Search): remove unnecessary symbol "," fix(Drawer): 异步执行cy.get 与 hide执行顺序修正 fix(Drawer): package-lock.json no change fix(Overlay): solve problems caused by numerical floating, close #4740 fix(*): rollback #4746 and fix textarea clear spec Update stale.yml chore(BuildTool): 支持指定主题包调试`run start --theme xxx` chore(DatePicker): fix theme demo margin, close #3627 chore(Shell): fix spell error "palceholder -> placeholder", close #3564 docs(Field): improve document description of parseName, close #3453 chore(Pagination): improve document of pageSizeSelector, fix pageJump runtime error, close #3306 docs(Calendar2): remove legacy api, close #3100 fix(Table): fix merging cell width in locked columns, close #4716 (#4752) * fix(Table): 修复合并单元格的锁列滑动问题 * fix(Table): should support for merging cells in locked columns, close #4716 * fix(Table): 修复测试用例 fix(Shell): phone shell should hidden when collapsed, close #3886 (#4766) Co-authored-by: lancely refactor(Timeline): fix cr comments fix(Upload): should hide trigger when limit is reached for Upload.Dragger, close #3951 (#4761) Co-authored-by: lancely feat(DatePicker): improve focus logic, close #3998 (#4769) * feat(DatePicker): interactive optimization of date selection box close #3998 * feat(DatePicker): improve focus logic --------- Co-authored-by: WB01081293 Co-authored-by: 珵之 refactor(Field): convert to TypeScript, impove docs and tests refactor(Field): convert to TypeScript, impove docs and tests chore(Field): improve for codereview chore(Field): fix dependency version feat(TreeSelect): support useDetailValue, close #3531 (#4771) * feat(TreeSelect): support useDetailValue, close #3531 * chore(TreeSelect): improve useDetailValue tc and demos by codereview * test(TreeSelect): add control mode spec for useDetailValue refactor(Collapse): convert to TypeScript, impove docs and tests refactor(Collapse): imporve docs and types chore(Collapse): improve by codereview chore(Collapse): remove useless propTypes comments fix(Collapse): hotfix panel className missing chore(*): Release-1.27.7 chore(Divider): adjust ts & docs & test chore(Notification): doc & test chore(Notification): adjust ts types chore(Notification): lint eg docs(Search): remove unnecessary symbol "," chore(Step): rename to ts refactor(Step): convert to TypeScript, impove docs and tests chore(Step): improve for codereview feat(Checkbox): upgrade checkbox ts refactor(Checkbox): improve ts & tc refactor(Checkbox): improve doc refactor(Checkbox): fix comment issues fix(Drawer): 添加v2 tsdoc & fix onToggleHighlightItem & fix ariaProps fix(Drawer): docs basic type from drawer/index --- .../__docs__/demo/all-check/index.tsx | 5 +- .../checkbox/__docs__/demo/control/index.tsx | 21 +- .../__docs__/demo/dataSource/index.tsx | 20 +- .../checkbox/__docs__/demo/group/index.tsx | 17 +- .../__docs__/demo/indeterminate/index.tsx | 14 +- .../__docs__/demo/isPreview/index.tsx | 5 +- .../__docs__/demo/uncontrol/index.tsx | 11 +- components/checkbox/__docs__/index.en-us.md | 76 ++- components/checkbox/__docs__/index.md | 81 +-- .../__tests__/{a11y-spec.js => a11y-spec.tsx} | 23 +- components/checkbox/__tests__/group-spec.js | 236 --------- components/checkbox/__tests__/group-spec.tsx | 236 +++++++++ components/checkbox/__tests__/index-spec.js | 126 ----- components/checkbox/__tests__/index-spec.tsx | 132 +++++ ...{checkbox-group.jsx => checkbox-group.tsx} | 118 ++--- .../checkbox/{checkbox.jsx => checkbox.tsx} | 126 ++--- components/checkbox/index.d.ts | 168 ------- components/checkbox/index.jsx | 18 - components/checkbox/index.tsx | 23 + .../checkbox/mobile/{index.jsx => index.tsx} | 0 components/checkbox/{style.js => style.ts} | 0 components/checkbox/types.ts | 237 +++++++++ components/checkbox/with-context.jsx | 19 - components/checkbox/with-context.tsx | 34 ++ .../drawer/__docs__/demo/basic/index.tsx | 3 +- .../drawer/__docs__/demo/placement/index.tsx | 6 +- .../drawer/__docs__/demo/quick/index.tsx | 3 +- .../drawer/__docs__/demo/select/index.tsx | 19 +- components/drawer/__docs__/index.en-us.md | 80 ++- components/drawer/__docs__/index.md | 76 ++- components/drawer/__docs__/theme/index.tsx | 21 +- components/drawer/__tests__/index-spec.tsx | 3 +- components/drawer/__tests__/index-v2-spec.tsx | 7 +- components/drawer/drawer-v2.tsx | 24 +- components/drawer/drawer.tsx | 111 +---- components/drawer/index.tsx | 8 +- components/drawer/inner.tsx | 25 +- components/drawer/show.tsx | 68 +-- components/drawer/types.ts | 280 +++++++++-- components/field/__tests__/index-spec.tsx | 15 + components/field/__tests__/options-spec.tsx | 8 + components/mixin-ui-state/index.tsx | 24 +- .../{index-spec.js => index-spec.ts} | 14 +- .../notification/{config.js => config.ts} | 0 components/notification/index.d.ts | 35 -- .../notification/{index.jsx => index.tsx} | 130 +++-- .../mobile/{index.jsx => index.tsx} | 1 + .../notification/{style.js => style.ts} | 0 components/notification/types.ts | 124 +++++ components/overlay/index.d.ts | 4 +- .../search/__docs__/demo/base/index.tsx | 14 +- components/select/index.d.ts | 2 +- components/step/__docs__/adaptor/index.jsx | 139 ------ components/step/__docs__/adaptor/index.tsx | 178 +++++++ components/step/__docs__/demo/basic/index.tsx | 2 +- .../step/__docs__/demo/controlled/index.tsx | 27 +- .../__docs__/demo/custom-step-item/index.tsx | 4 +- .../step/__docs__/demo/direction/index.tsx | 38 +- .../step/__docs__/demo/read-only/index.tsx | 11 +- .../demo/step-process-failed/index.tsx | 4 +- components/step/__docs__/index.en-us.md | 57 ++- components/step/__docs__/index.md | 60 ++- components/step/__docs__/theme/index.jsx | 327 ------------ components/step/__docs__/theme/index.tsx | 418 ++++++++++++++++ .../__tests__/{a11y-spec.js => a11y-spec.tsx} | 51 +- components/step/__tests__/index-spec.js | 466 ------------------ components/step/__tests__/index-spec.tsx | 427 ++++++++++++++++ components/step/index.d.ts | 106 ---- components/step/index.jsx | 21 - components/step/index.tsx | 34 ++ .../step/mobile/{index.jsx => index.tsx} | 0 components/step/{style.js => style.ts} | 0 components/step/types.ts | 201 ++++++++ .../view/{step-item.jsx => step-item.tsx} | 201 ++++---- components/step/view/{step.jsx => step.tsx} | 148 ++---- package-lock.json | 2 +- 76 files changed, 3145 insertions(+), 2628 deletions(-) rename components/checkbox/__tests__/{a11y-spec.js => a11y-spec.tsx} (63%) delete mode 100644 components/checkbox/__tests__/group-spec.js create mode 100644 components/checkbox/__tests__/group-spec.tsx delete mode 100644 components/checkbox/__tests__/index-spec.js create mode 100644 components/checkbox/__tests__/index-spec.tsx rename components/checkbox/{checkbox-group.jsx => checkbox-group.tsx} (64%) rename components/checkbox/{checkbox.jsx => checkbox.tsx} (73%) delete mode 100644 components/checkbox/index.d.ts delete mode 100644 components/checkbox/index.jsx create mode 100644 components/checkbox/index.tsx rename components/checkbox/mobile/{index.jsx => index.tsx} (100%) rename components/checkbox/{style.js => style.ts} (100%) create mode 100644 components/checkbox/types.ts delete mode 100644 components/checkbox/with-context.jsx create mode 100644 components/checkbox/with-context.tsx rename components/notification/__tests__/{index-spec.js => index-spec.ts} (69%) rename components/notification/{config.js => config.ts} (100%) delete mode 100644 components/notification/index.d.ts rename components/notification/{index.jsx => index.tsx} (65%) rename components/notification/mobile/{index.jsx => index.tsx} (82%) rename components/notification/{style.js => style.ts} (100%) create mode 100644 components/notification/types.ts delete mode 100644 components/step/__docs__/adaptor/index.jsx create mode 100644 components/step/__docs__/adaptor/index.tsx delete mode 100644 components/step/__docs__/theme/index.jsx create mode 100644 components/step/__docs__/theme/index.tsx rename components/step/__tests__/{a11y-spec.js => a11y-spec.tsx} (82%) delete mode 100644 components/step/__tests__/index-spec.js create mode 100644 components/step/__tests__/index-spec.tsx delete mode 100644 components/step/index.d.ts delete mode 100644 components/step/index.jsx create mode 100644 components/step/index.tsx rename components/step/mobile/{index.jsx => index.tsx} (100%) rename components/step/{style.js => style.ts} (100%) create mode 100644 components/step/types.ts rename components/step/view/{step-item.jsx => step-item.tsx} (68%) rename components/step/view/{step.jsx => step.tsx} (52%) diff --git a/components/checkbox/__docs__/demo/all-check/index.tsx b/components/checkbox/__docs__/demo/all-check/index.tsx index 1b6bcd378f..6d2b6dd4fd 100644 --- a/components/checkbox/__docs__/demo/all-check/index.tsx +++ b/components/checkbox/__docs__/demo/all-check/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Checkbox, Divider } from '@alifd/next'; +import type { CheckboxProps, GroupProps } from '@alifd/next/lib/checkbox'; const CheckboxGroup = Checkbox.Group; @@ -12,13 +13,13 @@ const App = () => { const [indeterminate, setIndeterminate] = React.useState(true); const [checkAll, setCheckAll] = React.useState(false); - const onChange = list => { + const onChange: GroupProps['onChange'] = (list: string[]) => { setCheckedList(list); setIndeterminate(!!list.length && list.length < plainOptions.length); setCheckAll(list.length === plainOptions.length); }; - const onCheckAllChange = (checked, e) => { + const onCheckAllChange: CheckboxProps['onChange'] = (checked, e) => { setCheckedList(e.target.checked ? plainOptions : []); setIndeterminate(false); setCheckAll(e.target.checked); diff --git a/components/checkbox/__docs__/demo/control/index.tsx b/components/checkbox/__docs__/demo/control/index.tsx index 986b656c67..5d7a9974a2 100644 --- a/components/checkbox/__docs__/demo/control/index.tsx +++ b/components/checkbox/__docs__/demo/control/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Checkbox } from '@alifd/next'; +import { type GroupProps } from '@alifd/next/lib/checkbox'; const list = [ { @@ -18,26 +19,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: GroupProps['onChange'] = value => { this.setState({ value: value, }); console.log('onChange', value); - } - - onClick(e) { - console.log('onClick', e); - } + }; render() { return ( diff --git a/components/checkbox/__docs__/demo/dataSource/index.tsx b/components/checkbox/__docs__/demo/dataSource/index.tsx index 3a0192677c..33c8112540 100644 --- a/components/checkbox/__docs__/demo/dataSource/index.tsx +++ b/components/checkbox/__docs__/demo/dataSource/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Checkbox } from '@alifd/next'; +import { type GroupProps } from '@alifd/next/lib/checkbox'; const list = [ { @@ -20,15 +21,11 @@ const list = [ ]; class App extends React.Component { - constructor(props) { - super(props); - - this.state = { - value: 'apple', - }; - } + state = { + value: 'apple', + }; - onChange = value => { + onChange: GroupProps['onChange'] = value => { this.setState({ value: value, }); @@ -36,12 +33,7 @@ class App extends React.Component { render() { return ( - + ); } } diff --git a/components/checkbox/__docs__/demo/group/index.tsx b/components/checkbox/__docs__/demo/group/index.tsx index af18e679a4..ce3df04a38 100644 --- a/components/checkbox/__docs__/demo/group/index.tsx +++ b/components/checkbox/__docs__/demo/group/index.tsx @@ -1,23 +1,18 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Checkbox } from '@alifd/next'; +import { type GroupProps } from '@alifd/next/lib/checkbox'; 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: GroupProps['onChange'] = value => { this.setState({ value: value, }); - } + }; render() { return ( diff --git a/components/checkbox/__docs__/demo/indeterminate/index.tsx b/components/checkbox/__docs__/demo/indeterminate/index.tsx index 8af9451534..c7069f50f0 100644 --- a/components/checkbox/__docs__/demo/indeterminate/index.tsx +++ b/components/checkbox/__docs__/demo/indeterminate/index.tsx @@ -3,15 +3,11 @@ import ReactDOM from 'react-dom'; import { Checkbox, Button } from '@alifd/next'; class IndeterminateApp extends React.Component { - constructor(props) { - super(props); - - this.state = { - checked: false, - indeterminate: true, - disabled: false, - }; - } + state = { + checked: false, + indeterminate: true, + disabled: false, + }; toggle = () => { if (this.state.indeterminate) { diff --git a/components/checkbox/__docs__/demo/isPreview/index.tsx b/components/checkbox/__docs__/demo/isPreview/index.tsx index 54372ad59c..11b62cec06 100644 --- a/components/checkbox/__docs__/demo/isPreview/index.tsx +++ b/components/checkbox/__docs__/demo/isPreview/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Checkbox, Switch } from '@alifd/next'; +import { type CheckboxProps, type GroupProps } from '@alifd/next/lib/checkbox'; class App extends React.Component { state = { @@ -20,10 +21,10 @@ class App extends React.Component { }); }; - renderChecked = (checked, props) => + renderChecked: CheckboxProps['renderPreview'] = (checked, props) => checked ? {props.children} : null; - renderPreview = (previewed, props) => + renderPreview: GroupProps['renderPreview'] = previewed => previewed.length ? previewed.map((Item, index) => ( diff --git a/components/checkbox/__docs__/demo/uncontrol/index.tsx b/components/checkbox/__docs__/demo/uncontrol/index.tsx index f4cbdc7677..14eb902b08 100644 --- a/components/checkbox/__docs__/demo/uncontrol/index.tsx +++ b/components/checkbox/__docs__/demo/uncontrol/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Checkbox } from '@alifd/next'; +import { type GroupProps } from '@alifd/next/lib/checkbox'; const { Group: CheckboxGroup } = Checkbox; const list = [ @@ -21,15 +22,9 @@ const list = [ ]; class UnControlApp extends React.Component { - constructor(props) { - super(props); - - this.onChange = this.onChange.bind(this); - } - - onChange(selectedItems) { + onChange: GroupProps['onChange'] = selectedItems => { console.log('onChange callback', selectedItems); - } + }; render() { return ( diff --git a/components/checkbox/__docs__/index.en-us.md b/components/checkbox/__docs__/index.en-us.md index 7defeb1fd2..6c5c70286b 100644 --- a/components/checkbox/__docs__/index.en-us.md +++ b/components/checkbox/__docs__/index.en-us.md @@ -13,41 +13,67 @@ Checkbox ### When to Use Checkbox is used to verify which options you want selected from a group. If you have only a single option, do not use the checkbox, use the on/off switch. + ## API ### Checkbox -| Param | Description | Type | Default Value | -| ------------------------ |---------------------------- | ------------ | ------------- | -| id | checkbox id, mounted on input | String | - | -| checked | Set the status to be checked | Boolean | - | -| defaultChecked | Set the default status to be checked | Boolean | false | -| disabled | Set the status to be disabled | Boolean | - | -| label | Set the label by property | String | - | -| indeterminate | The intermediate state of the Checkbox will only affect the style of the Checkbox and does not affect its checked property. | Boolean | - | -| defaultIndeterminate | Set the default status to intermediate, it will only affect the style of the Checkbox and does not affect its checked property. | Boolean | false | -| onChange | Callback function triggered when the state changes

**signatures**:
Function(checked: Boolean, e: Event) => void
**params**:
_checked_: {Boolean} The checked value of the underlying checkbox input
_e_: {Event} Dom event object | Function | func.noop | -| onMouseEnter | Callback function triggered when the mouse pointer enters the element.

**signatures**:
Function(e: Event) => void
**params**:
_e_: {Event} Dom event object | Function | func.noop | -| onMouseLeave | Callback function triggered when the mouse pointer leaves the element.

**signatures**:
Function(e: Event) => void
**params**:
_e_: {Event} Dom event object | Function | func.noop | -|value | The value of the Checkbox | String/Number/Boolean | - | +| Param | Description | Type | Default Value | Required | Supported Version | +| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------- | -------- | ----------------- | +| className | ClassName | string | - | | - | +| id | Checkbox id, mounted on the input | string | - | | - | +| style | Custom inline style | React.CSSProperties | - | | - | +| checked | Checked status | boolean | - | | - | +| value | Checkbox value | IValue | - | | - | +| name | Name | string | - | | - | +| defaultChecked | Default checked status | boolean | false | | - | +| disabled | Disabled | boolean | - | | - | +| label | Label | React.ReactNode | - | | - | +| indeterminate | Checkbox middle status, only affects the style of Checkbox, and does not affect its checked property | boolean | - | | - | +| defaultIndeterminate | Checkbox default middle status, only affects the style of Checkbox, and does not affect its checked property | boolean | false | | - | +| onChange | Status change event | (checked: boolean, e: React.ChangeEvent) => void | - | | - | +| onMouseEnter | Mouse enter event | (e: React.MouseEvent) => void | - | | - | +| onMouseLeave | Mouse leave event | (e: React.MouseEvent) => void | - | | - | +| isPreview | Is preview | boolean | false | | 1.19 | +| renderPreview | Custom rendering content

**signature**:
**params**:
_checked_: Is checked
_props_: All props
**return**:
Custom rendering content | (checked: boolean, props: CheckboxProps) => React.ReactNode | - | | 1.19 | ### Checkbox.Group -| params | desc | type | default | -| ---------------- | --------------------------------------------------- | -------- | ------------- | -| disabled | Set the status of all checkbox in group to be checked | Boolean | - | -| dataSource | Optional list, data item can be String or Object, for example `['apple', 'pear', 'orange']` or `[{value: 'apple', label: 'Apple',}, {value: 'pear', label: 'Pear'}, {value: 'orange', label: 'Orange'}]` | Array<any> | \[] | -| value | The values of selected optional list | Array/String/Number/Boolean | - | -| defaultValue | The values of default selected optional list | Array/String/Number/Boolean | - | -| children | To set nested checkbox by children components | Array<ReactElement> | - | -| onChange | Callback function triggered when the selected value changes

**signatures**:
Function(value: Array, e: Event) => void
**params**:
_value_: {Array} values of selected optional list
_e_: {Event} Dom event object | Function | () => { } | -| direction | The direction of item's aligning
- hoz: horizontal (default)
- ver: vertical

**Allowed values**:
'hoz', 'ver' | Enum | 'hoz' | +| Param | Description | Type | Default Value | Required | Supported Version | +| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -------- | ----------------- | +| className | Custom className | string | - | | - | +| style | Custom inline style | React.CSSProperties | - | | - | +| disabled | Entirely disabled | boolean | - | | - | +| dataSource | Option list | Array \| Array | - | | - | +| value | Selected value list | IValue[] \| IValue | - | | - | +| defaultValue | Default selected value list | IValue[] \| IValue | - | | - | +| name | Name | string | - | | - | +| children | Set internal checkbox through child elements | React.ReactNode | - | | - | +| onChange | Selected value change event | (value: IValue[], e: React.ChangeEvent) => void | - | | - | +| direction | Arrangement of subitems | 'hoz' \| 'ver' | - | | - | +| itemDirection | [Deprecated] Arrangement of subitems | 'hoz' \| 'ver' | - | | - | +| isPreview | Is preview | boolean | - | | 1.19 | +| renderPreview | Custom rendering content

**signature**:
**params**:
_previewed_: Previewed value [\{label: '', value:''\},...]
_props_: All props
**return**:
Custom rendering content | (
previewed: {
label: string \| React.ReactNode;
value: string \| React.ReactNode;
}[],
props: object
) => React.ReactNode | - | | 1.19 | + +### IValue +```typescript +export type IValue = string | number | boolean; +``` +### CheckboxData +```typescript +export type CheckboxData = { + value: IValue; + label?: React.ReactNode; + disabled?: boolean; + [propName: string]: unknown; +}; +``` ## ARIA and KeyBoard -| KeyBoard | Descripiton | -| :---------- | :------------------------------ | -| SPACE | Select or cancel the current item | +| KeyBoard | Descripiton | +| :------- | :-------------------------------- | +| SPACE | Select or cancel the current item | diff --git a/components/checkbox/__docs__/index.md b/components/checkbox/__docs__/index.md index a8b1db55ab..46969e002d 100644 --- a/components/checkbox/__docs__/index.md +++ b/components/checkbox/__docs__/index.md @@ -18,39 +18,62 @@ ### Checkbox -| 参数 | 说明 | 类型 | 默认值 | 版本支持 | -| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | --------- | ---- | -| id | checkbox id, 挂载在input上 | String | - | | -| checked | 选中状态 | Boolean | - | | -| defaultChecked | 默认选中状态 | Boolean | false | | -| disabled | 禁用 | Boolean | - | | -| label | 通过属性配置label, | ReactNode | - | | -| indeterminate | Checkbox 的中间状态,只会影响到 Checkbox 的样式,并不影响其 checked 属性 | Boolean | - | | -| defaultIndeterminate | Checkbox 的默认中间态,只会影响到 Checkbox 的样式,并不影响其 checked 属性 | Boolean | false | | -| 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 | 鼠标离开Leave事件

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

**签名**:
Function(checked: Boolean, props: Object) => reactNode
**参数**:
_checked_: {Boolean} 是否选中
_props_: {Object} 所有传入的参数
**返回值**:
{reactNode} Element 渲染内容
| Function | - | 1.19 | +| 参数 | 说明 | 类型 | 默认值 | 是否必填 | 支持版本 | +| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | ------ | -------- | -------- | +| className | 自定义类名 | string | - | | - | +| id | checkbox id, 挂载在 input 上 | string | - | | - | +| style | 自定义内联样式 | React.CSSProperties | - | | - | +| checked | 选中状态 | boolean | - | | - | +| value | checkbox 的 value | IValue | - | | - | +| name | name | string | - | | - | +| defaultChecked | 默认选中状态 | boolean | false | | - | +| disabled | 禁用 | boolean | - | | - | +| label | 通过属性配置 label, | React.ReactNode | - | | - | +| indeterminate | Checkbox 的中间状态,只会影响到 Checkbox 的样式,并不影响其 checked 属性 | boolean | - | | - | +| defaultIndeterminate | Checkbox 的默认中间态,只会影响到 Checkbox 的样式,并不影响其 checked 属性 | boolean | false | | - | +| onChange | 状态变化时触发的事件 | (checked: boolean, e: React.ChangeEvent) => void | - | | - | +| onMouseEnter | 鼠标进入 enter 事件 | (e: React.MouseEvent) => void | - | | - | +| onMouseLeave | 鼠标离开 Leave 事件 | (e: React.MouseEvent) => void | - | | - | +| isPreview | 是否为预览态 | boolean | false | | 1.19 | +| renderPreview | 预览态模式下渲染的内容

**签名**:
**参数**:
_checked_: 是否选中
_props_: 所有传入的参数
**返回值**:
定制渲染内容 | (checked: boolean, props: CheckboxProps) => React.ReactNode | - | | 1.19 | ### Checkbox.Group -| 参数 | 说明 | 类型 | 默认值 | 版本支持 | -| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | -------- | ---- | -| disabled | 整体禁用 | Boolean | - | | -| dataSource | 可选项列表, 数据项可为 String 或者 Object, 如 `['apple', 'pear', 'orange']` 或者 `[{value: 'apple', label: '苹果',}, {value: 'pear', label: '梨'}, {value: 'orange', label: '橙子'}]` | Array<String>/Array<Object> | \[] | | -| value | 被选中的值列表 | Array/String/Number/Boolean | - | | -| defaultValue | 默认被选中的值列表 | Array/String/Number/Boolean | - | | -| children | 通过子元素方式设置内部 checkbox | Array<ReactElement> | - | | -| onChange | 选中值改变时的事件

**签名**:
Function(value: Array, e: Event) => void
**参数**:
_value_: {Array} 选中项列表
_e_: {Event} Dom 事件对象 | Function | () => {} | | -| direction | 子项目的排列方式
- hoz: 水平排列 (default)
- ver: 垂直排列

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

**签名**:
Function(previewed: Array, props: Object) => reactNode
**参数**:
_previewed_: {Array} 预览值 [{label: '', value:''},...]
_props_: {Object} 所有传入的参数
**返回值**:
{reactNode} Element 渲染内容
| Function | - | 1.19 | +| 参数 | 说明 | 类型 | 默认值 | 是否必填 | 支持版本 | +| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -------- | -------- | +| className | 自定义类名 | string | - | | - | +| style | 自定义内联样式 | React.CSSProperties | - | | - | +| disabled | 整体禁用 | boolean | - | | - | +| dataSource | 可选项列表 | Array \| Array | - | | - | +| value | 被选中的值列表 | IValue[] \| IValue | - | | - | +| defaultValue | 默认被选中的值列表 | IValue[] \| IValue | - | | - | +| name | name | string | - | | - | +| children | 通过子元素方式设置内部 checkbox | React.ReactNode | - | | - | +| onChange | 选中值改变时的事件 | (value: IValue[], e: React.ChangeEvent) => void | - | | - | +| direction | 子项目的排列方式 | 'hoz' \| 'ver' | - | | - | +| itemDirection | [废弃] 子项目的排列方式 | 'hoz' \| 'ver' | - | | - | +| isPreview | 是否为预览态 | boolean | - | | 1.19 | +| renderPreview | 预览态模式下渲染的内容

**签名**:
**参数**:
_previewed_: 预览值 [\{label: '', value:''\},...]
_props_: 所有传入的参数
**返回值**:
定制渲染内容 | (
previewed: {
label: string \| React.ReactNode;
value: string \| React.ReactNode;
}[],
props: object
) => React.ReactNode | - | | 1.19 | + +### IValue + +```typescript +export type IValue = string | number | boolean; +``` + +### CheckboxData + +```typescript +export type CheckboxData = { + value: IValue; + label?: React.ReactNode; + disabled?: boolean; + [propName: string]: unknown; +}; +``` ## 无障碍键盘操作指南 -| 按键 | 说明 | -| :---- | :------- | +| 按键 | 说明 | +| :---- | :--------------- | | SPACE | 选择或取消当前项 | diff --git a/components/checkbox/__tests__/a11y-spec.js b/components/checkbox/__tests__/a11y-spec.tsx similarity index 63% rename from components/checkbox/__tests__/a11y-spec.js rename to components/checkbox/__tests__/a11y-spec.tsx index 56cf65c678..9836823891 100644 --- a/components/checkbox/__tests__/a11y-spec.js +++ b/components/checkbox/__tests__/a11y-spec.tsx @@ -1,25 +1,10 @@ import React from 'react'; -import Enzyme from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; import Checkbox from '../index'; -import { unmount, testReact } from '../../util/__tests__/legacy/a11y/validate'; +import { testReact } from '../../util/__tests__/a11y/validate'; -Enzyme.configure({ adapter: new Adapter() }); - -/* eslint-disable no-undef, react/jsx-filename-extension */ describe('Checkbox A11y', () => { - let wrapper; - - afterEach(() => { - if (wrapper) { - wrapper.unmount(); - wrapper = null; - } - unmount(); - }); - it('should not have any violations for grouped checkbox with children', async () => { - wrapper = await testReact( + await testReact( 苹果 @@ -32,7 +17,6 @@ describe('Checkbox A11y', () => { ); - return wrapper; }); it('should not have any violations for grouped checkbox with datasource', async () => { @@ -50,7 +34,6 @@ describe('Checkbox A11y', () => { label: '橙子', }, ]; - wrapper = await testReact(); - return wrapper; + await testReact(); }); }); diff --git a/components/checkbox/__tests__/group-spec.js b/components/checkbox/__tests__/group-spec.js deleted file mode 100644 index 6fcd49c884..0000000000 --- a/components/checkbox/__tests__/group-spec.js +++ /dev/null @@ -1,236 +0,0 @@ -import React from 'react'; -import Enzyme, { shallow, mount } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; -import sinon from 'sinon'; -import assert from 'power-assert'; -import Checkbox from '../index'; - -/* eslint-disable */ -Enzyme.configure({ adapter: new Adapter() }); - -const CheckboxGroup = Checkbox.Group; - -describe('Checkbox.Group', () => { - let list; - beforeEach('mock data', () => { - list = [ - { - value: 'apple', - label: '苹果', - }, - { - value: 'pear', - label: '梨', - }, - { - value: 'orange', - label: '橙子', - }, - ]; - }); - describe('[render] control', () => { - it('should contain `pear`', () => { - const wrapper = shallow().dive(); - assert(wrapper.state().value.indexOf('pear') !== -1); - }); - - it('should have three children with mock data', () => { - const wrapper = mount(); - assert(wrapper.find('.next-checkbox-group').children().length === 3); - }); - - it('should support null child', () => { - const wrapper = mount( - - 1 - 2 - {null} - - ); - assert(wrapper.find('.next-checkbox-group').children().length === 2); - }); - }); - - describe('[render] uncontrol', () => { - it('should have three children with mock data', () => { - const wrapper = mount(); - assert(wrapper.find('.next-checkbox-group').children().length === 3); - }); - }); - - describe('[render] nest', () => { - const wrapper = shallow( - - - 苹果 - - - 梨子 - - - 西瓜 - - - ).dive(); - - it('should contain `pear` and `watermelon`', () => { - assert(wrapper.state().value.indexOf('pear') !== -1); - assert(wrapper.state().value.indexOf('watermelon') !== -1); - }); - - it('should have two children with nest ', () => { - const wrapper = mount( - - - 苹果 - - - 梨子 - - - 西瓜 - - - ); - const target = wrapper.find('.next-checkbox-group'); - assert(target.children().length === 3); - assert(target.find('.disabled').length === 1); - }); - }); - - describe('[events] simulate change', () => { - it('should call `onChange`', () => { - const onChange = sinon.spy(); - const wrapper = mount(); - wrapper - .find('input') - .first() - .simulate('change'); - assert(onChange.calledOnce); - - const onChange1 = sinon.spy(); - const wrapper1 = mount(); - wrapper1 - .find('input') - .first() - .simulate('change'); - assert(onChange.calledOnce); - }); - }); - - describe('[behavior] controlled', () => { - it('should support controlled `value`', () => { - const wrapper = shallow().dive(); - assert(wrapper.state().value[0] === 'pear'); - - wrapper.setProps({ - value: ['apple'], - }); - assert(wrapper.state().value[0] === 'apple'); - wrapper.setProps({ - value: 'orange', - }); - assert(wrapper.state().value[0] === 'orange'); - - wrapper.setProps({ - value: null, - }); - assert(wrapper.state().value.length === 0); - }); - - it('should support controlled `disabled`', () => { - const wrapper = mount(); - assert(!wrapper.props().disabled); - assert(!wrapper.find('.next-checkbox-group').hasClass('disabled')); - - wrapper.setProps({ - disabled: true, - }); - assert(wrapper.find('.next-checkbox-group').hasClass('disabled')); - }); - }); - describe('value === undefined', () => { - it('should support value === undefined', () => { - const wrapper = shallow(); - const wrapper1 = shallow(); - wrapper.setProps({ - value: undefined, - }); - assert.deepEqual(wrapper.dive().state().value, []); - assert.deepEqual(wrapper1.dive().state().value, []); - }); - }); - describe('value === 0', () => { - it('should support value === 0', () => { - const wrapper = shallow(); - assert.deepEqual(wrapper.dive().state().value, [0]); - wrapper.setProps({ - value: 1, - }); - assert.deepEqual(wrapper.dive().state().value, [1]); - }); - }); - - describe("should respect children's indeternimate state", () => { - it('should support value === 0', () => { - const wrapper1 = mount( - - 1 - - ); - const wrapper2 = mount( - - 1 - - ); - - assert(wrapper1.find('.indeterminate').length === 1); - assert(wrapper2.find('.indeterminate').length === 1); - }); - }); - - describe('render in preview mode', () => { - it('should isPreview', () => { - const wrapper = mount(); - assert(wrapper.getDOMNode().innerText === '苹果'); - }); - - it('should renderPreview', () => { - const wrapper = mount( - 'checkbox preview'} defaultValue={0} dataSource={list} /> - ); - assert(wrapper.getDOMNode().innerText === 'checkbox preview'); - }); - }); - it('value support bool`', () => { - let value = null; - const wrapper = mount( - { - value = v; - }} - dataSource={[ - { - value: false, - label: '苹果', - }, - { - value: true, - label: '橙子', - }, - ]} - /> - ); - - wrapper - .find('input') - .at(1) - .simulate('change'); - assert.deepEqual(value, [true]); - wrapper - .find('input') - .at(0) - .simulate('change'); - assert.deepEqual(value, [true, false]); - }); -}); diff --git a/components/checkbox/__tests__/group-spec.tsx b/components/checkbox/__tests__/group-spec.tsx new file mode 100644 index 0000000000..79a034cb63 --- /dev/null +++ b/components/checkbox/__tests__/group-spec.tsx @@ -0,0 +1,236 @@ +import React, { type ReactElement } from 'react'; +import { type MountReturn } from 'cypress/react'; +import Checkbox from '../index'; + +const CheckboxGroup = Checkbox.Group; + +const list = [ + { + value: 'apple', + label: '苹果', + }, + { + value: 'pear', + label: '梨', + }, + { + value: 'orange', + label: '橙子', + }, +]; + +describe('Checkbox.Group', () => { + describe('[render] control', () => { + it('should contain `pear`', () => { + cy.mount(); + cy.get('.next-checkbox-wrapper.checked').should('have.text', '梨'); + }); + + it('should have three children with mock data', () => { + cy.mount(); + cy.get('.next-checkbox-wrapper').should('have.length', 3); + }); + + it('should support null child', () => { + cy.mount( + + 1 + 2 + {null} + + ); + cy.get('.next-checkbox-wrapper').should('have.length', 2); + }); + }); + + describe('[render] uncontrolled', () => { + it('should have three children with mock data', () => { + cy.mount(); + cy.get('.next-checkbox-wrapper').should('have.length', 3); + }); + }); + + describe('[render] nest', () => { + it('should contain `pear` and `watermelon`', () => { + cy.mount( + + + 苹果 + + + 梨子 + + + 西瓜 + + + ); + cy.get('.next-checkbox-wrapper.checked').eq(0).should('have.text', '梨子'); + cy.get('.next-checkbox-wrapper.checked').eq(1).should('have.text', '西瓜'); + }); + + it('should have two children with nest ', () => { + cy.mount( + + + 苹果 + + + 梨子 + + + 西瓜 + + + ); + cy.get('.next-checkbox-wrapper').should('have.length', 3); + cy.get('.next-checkbox-wrapper.disabled').should('have.length', 1); + }); + }); + + describe('[events] simulate change', () => { + it('should call `onChange`', () => { + // const onChange = sinon.spy(); + const onChange = cy.spy().as('onChange'); + cy.mount(); + cy.get('input').eq(0).click(); + cy.get('@onChange').should('be.calledOnce'); + + const onChange1 = cy.spy().as('onChange1'); + cy.mount(); + cy.get('input').eq(0).click(); + cy.get('@onChange1').should('be.calledOnce'); + }); + }); + + describe('[behavior] controlled', () => { + it('should support controlled `value`', () => { + cy.mount().as('Demo'); + + cy.get('.next-checkbox-wrapper.checked').should('have.text', '梨'); + + cy.get('@Demo').then(({ component, rerender }) => { + return rerender( + React.cloneElement(component as ReactElement, { value: ['apple'] }) + ); + }); + + cy.get('.next-checkbox-wrapper.checked').should('have.text', '苹果'); + + cy.get('@Demo').then(({ component, rerender }) => { + return rerender(React.cloneElement(component as ReactElement, { value: 'orange' })); + }); + + cy.get('.next-checkbox-wrapper.checked').should('have.text', '橙子'); + + cy.get('@Demo').then(({ component, rerender }) => { + return rerender(React.cloneElement(component as ReactElement, { value: null })); + }); + + cy.get('.next-checkbox-wrapper.checked').should('have.length', 0); + }); + + it('should support controlled `disabled`', () => { + cy.mount().as( + 'Demo' + ); + + cy.get('.next-checkbox-group').should('not.have.class', 'disabled'); + + cy.get('@Demo').then(({ component, rerender }) => { + return rerender(React.cloneElement(component as ReactElement, { disabled: true })); + }); + + cy.get('.next-checkbox-group').should('have.class', 'disabled'); + }); + }); + describe('value === undefined', () => { + it('should support value === undefined', () => { + cy.mount().as('Demo'); + cy.get('@Demo').then(({ component, rerender }) => { + return rerender( + React.cloneElement(component as ReactElement, { value: undefined }) + ); + }); + cy.get('.next-checkbox-wrapper.checked').should('have.length', 0); + cy.mount(); + cy.get('.next-checkbox-wrapper.checked').should('have.length', 0); + }); + }); + describe('value === 0', () => { + it('should support value === 0', () => { + cy.mount( + + ).as('Demo'); + cy.get('.next-checkbox-wrapper.checked').should('have.text', 0); + cy.get('@Demo').then(({ component, rerender }) => { + return rerender(React.cloneElement(component as ReactElement, { value: 1 })); + }); + cy.get('.next-checkbox-wrapper.checked').should('have.text', 1); + }); + }); + + describe("should respect children's indeterminate state", () => { + it('should support value === 0', () => { + cy.mount( + + 1 + + ); + cy.get('.indeterminate').should('have.length', 1); + cy.mount( + + 1 + + ); + cy.get('.indeterminate').should('have.length', 1); + }); + }); + + describe('render in preview mode', () => { + it('should isPreview', () => { + cy.mount(); + cy.get('.next-form-preview').should('have.text', '苹果'); + }); + + it('should renderPreview', () => { + cy.mount( + 'checkbox preview'} + defaultValue={0} + dataSource={list} + /> + ); + cy.get('.next-form-preview').should('have.text', 'checkbox preview'); + }); + }); + it('value support bool`', () => { + const handleChange = cy.spy().as('handleChange'); + cy.mount( + + ); + cy.get('input').eq(1).click(); + cy.get('@handleChange').should('be.calledWith', [true]); + cy.get('input').eq(0).click(); + cy.get('@handleChange').should('be.calledWith', [true, false]); + }); +}); diff --git a/components/checkbox/__tests__/index-spec.js b/components/checkbox/__tests__/index-spec.js deleted file mode 100644 index 8674ff7722..0000000000 --- a/components/checkbox/__tests__/index-spec.js +++ /dev/null @@ -1,126 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import Enzyme, { mount } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; -import sinon from 'sinon'; -import assert from 'power-assert'; -import Checkbox from '../index'; - -/* eslint-disable */ -Enzyme.configure({ adapter: new Adapter() }); - -describe('Checkbox', () => { - describe('[render] normal', () => { - it('should get a normal checkbox', () => { - const wrapper1 = mount(); - const wrapper2 = mount(香蕉); - assert(wrapper1.find('.next-checkbox').length === 1); - assert(wrapper2.find('.next-checkbox').length === 1); - assert(wrapper2.find('input#banana').length === 1); - }); - }); - - describe('[render] checked', () => { - it('should get a checked checkbox', () => { - const wrapper1 = mount(); - const wrapper2 = mount(); - assert(wrapper1.find('.checked').length === 1); - assert(wrapper2.find('.checked').length === 1); - }); - }); - - describe('[render] indeterminate', () => { - it('should get a indeterminate checkbox', () => { - const wrapper1 = mount(); - const wrapper2 = mount(); - - assert(wrapper1.find('.indeterminate').length === 1); - assert(wrapper2.find('.indeterminate').length === 1); - }); - }); - - describe('[render] disabled', () => { - it('should get a disabled checkbox', () => { - const wrapper = mount(); - assert(wrapper.find('.disabled').length === 1); - }); - }); - - describe('[render] label', () => { - it('should get a checkbox with label', () => { - const wrapper = mount(); - assert(wrapper.find('.next-checkbox-label').length === 1); - }); - }); - - describe('[attribute] set custom `className`', () => { - it('should has className `cumstom-name`', () => { - const wrapper = mount(); - assert(wrapper.props().className === 'cumstom-name'); - assert(wrapper.find('.next-checkbox-wrapper.cumstom-name').length === 1); - }); - }); - - describe('[events] simulate click', () => { - const wrapper = mount(); - - it('should checked after click', () => { - wrapper.find('input').simulate('change', { target: { checked: true } }); - assert(wrapper.find('input').prop('checked')); - }); - it('should call `onChange`', () => { - const onChange = sinon.spy(); - const wrapper = mount(); - wrapper.find('input').simulate('change'); - assert(onChange.calledOnce); - }); - it('should return the passed value', () => { - const onChange = sinon.spy(); - const wrapper = mount(); - wrapper.find('input').simulate('change'); - assert(onChange.getCalls()[0].args[1].target.value === 'banana'); - }); - it('should call `onMouseEnter`', () => { - const onMouseEnter = sinon.spy(); - const wrapper1 = mount(); - wrapper1.find('.next-checkbox-wrapper').simulate('mouseEnter'); - assert(onMouseEnter.calledOnce); - }); - it('should call `onMouseLeave`', () => { - const onMouseLeave = sinon.spy(); - const wrapper1 = mount(); - wrapper1.find('.next-checkbox-wrapper').simulate('mouseLeave'); - assert(onMouseLeave.calledOnce); - }); - }); - - describe('[behavior] controlled', () => { - it('should support controlled `checked` and `indeterminate`', () => { - const wrapper = mount(); - assert(wrapper.find('input').props().checked); - assert(wrapper.find('.checked').length === 1); - - wrapper.setProps({ - checked: false, - }); - assert(!wrapper.find('input').props().checked); - assert(wrapper.find('.checked').length === 0); - wrapper.setProps({ - indeterminate: true, - }); - assert(wrapper.find('.indeterminate').length === 1); - }); - }); - - describe('render in preview mode', () => { - it('should isPreview', () => { - const wrapper = mount(); - assert(wrapper.getDOMNode().innerText === 'apple'); - }); - - it('should renderPreview', () => { - const wrapper = mount( 'checked'} />); - assert(wrapper.getDOMNode().innerText === 'checked'); - }); - }); -}); diff --git a/components/checkbox/__tests__/index-spec.tsx b/components/checkbox/__tests__/index-spec.tsx new file mode 100644 index 0000000000..e49e3b7781 --- /dev/null +++ b/components/checkbox/__tests__/index-spec.tsx @@ -0,0 +1,132 @@ +import React, { type ReactElement } from 'react'; +import { type MountReturn } from 'cypress/react'; +import Checkbox from '../index'; + +describe('Checkbox', () => { + describe('[render] normal', () => { + it('should get a normal checkbox', () => { + cy.mount(); + cy.get('.next-checkbox').should('have.length', 1); + cy.mount(香蕉); + cy.get('.next-checkbox').should('have.length', 1); + cy.get('input#banana').should('have.length', 1); + }); + }); + + describe('[render] checked', () => { + it('should get a checked checkbox', () => { + cy.mount(); + cy.get('.checked').should('have.length', 1); + cy.mount(); + cy.get('.checked').should('have.length', 1); + }); + }); + + describe('[render] indeterminate', () => { + it('should get a indeterminate checkbox', () => { + cy.mount(); + cy.get('.indeterminate').should('have.length', 1); + cy.mount(); + cy.get('.indeterminate').should('have.length', 1); + }); + }); + + describe('[render] disabled', () => { + it('should get a disabled checkbox', () => { + cy.mount(); + cy.get('.disabled').should('have.length', 1); + }); + }); + + describe('[render] label', () => { + it('should get a checkbox with label', () => { + cy.mount(); + cy.get('.next-checkbox-label').should('have.length', 1); + }); + }); + + describe('[attribute] set custom `className`', () => { + it('should has className `custom-name`', () => { + cy.mount(); + cy.get('.next-checkbox-wrapper.custom-name').should('exist'); + }); + }); + + describe('[events] simulate click', () => { + it('should checked after click', () => { + cy.mount(); + cy.get('input').eq(0).check(); + cy.get('input').should('have.prop', 'checked', true); + }); + it('should call `onChange`', () => { + const onChange = cy.spy().as('onChange'); + cy.mount(); + cy.get('input').eq(0).check(); + cy.get('@onChange').should('be.called'); + }); + it('should return the passed value', () => { + const onChange = cy.spy().as('onChange'); + cy.mount( + { + e.persist(); + onChange(e); + }} + value="banana" + /> + ); + cy.get('input').eq(0).check(); + cy.get('@onChange').should( + 'be.calledWithMatch', + (e: React.ChangeEvent) => { + return e.target.value === 'banana'; + } + ); + }); + it('should call `onMouseEnter`', () => { + const onMouseEnter = cy.spy().as('onMouseEnter'); + cy.mount(); + // React 的 mouseEnter 事件是通过监听 mouseover 实现的 + cy.get('.next-checkbox-wrapper').trigger('mouseover'); + cy.get('@onMouseEnter').should('be.calledOnce'); + }); + it('should call `onMouseLeave`', () => { + const onMouseLeave = cy.spy().as('onMouseLeave'); + cy.mount(); + // React 的 mouseLeave 事件是通过监听 mouseout 实现的 + cy.get('.next-checkbox-wrapper').trigger('mouseout'); + cy.get('@onMouseLeave').should('be.calledOnce'); + }); + }); + + describe('[behavior] controlled', () => { + it('should support controlled `checked` and `indeterminate`', () => { + cy.mount().as('Demo'); + cy.get('input').should('be.checked'); + cy.get('.checked').should('have.length', 1); + cy.get('@Demo').then(({ component, rerender }) => { + return rerender(React.cloneElement(component as ReactElement, { checked: false })); + }); + cy.get('input').should('not.be.checked'); + cy.get('.checked').should('have.length', 0); + cy.get('@Demo').then(({ component, rerender }) => { + return rerender( + React.cloneElement(component as ReactElement, { indeterminate: true }) + ); + }); + cy.get('.indeterminate').should('have.length', 1); + }); + }); + + describe('render in preview mode', () => { + it('should isPreview', () => { + cy.mount(); + cy.get('.next-form-preview').should('have.text', 'apple'); + }); + + it('should renderPreview', () => { + cy.mount( 'checked'} />); + cy.get('.next-form-preview').should('have.text', 'checked'); + }); + }); +}); diff --git a/components/checkbox/checkbox-group.jsx b/components/checkbox/checkbox-group.tsx similarity index 64% rename from components/checkbox/checkbox-group.jsx rename to components/checkbox/checkbox-group.tsx index ea2f56a964..0bcde1986e 100644 --- a/components/checkbox/checkbox-group.jsx +++ b/components/checkbox/checkbox-group.tsx @@ -1,70 +1,41 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import * as React from 'react'; +import * as PropTypes from 'prop-types'; import classnames from 'classnames'; import { polyfill } from 'react-lifecycles-compat'; import { obj } from '../util'; import Checkbox from './checkbox'; +import type { CheckboxData, GroupProps, GroupState, ValueItem } from './types'; const { pickOthers } = obj; /** Checkbox.Group */ -class CheckboxGroup extends Component { +class CheckboxGroup extends React.Component { static propTypes = { prefix: PropTypes.string, rtl: PropTypes.bool, - /** - * 自定义类名 - */ className: PropTypes.string, - /** - * 自定义内敛样式 - */ style: PropTypes.object, - /** - * 整体禁用 - */ disabled: PropTypes.bool, - /** - * 可选项列表, 数据项可为 String 或者 Object, 如 `['apple', 'pear', 'orange']` 或者 `[{value: 'apple', label: '苹果',}, {value: 'pear', label: '梨'}, {value: 'orange', label: '橙子'}]` - */ - dataSource: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.string), PropTypes.arrayOf(PropTypes.object)]), - /** - * 被选中的值列表 - */ - value: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.number, PropTypes.bool]), - /** - * 默认被选中的值列表 - */ - defaultValue: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.number, PropTypes.bool]), - /** - * 通过子元素方式设置内部 checkbox - */ + dataSource: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.string), + PropTypes.arrayOf(PropTypes.object), + ]), + value: PropTypes.oneOfType([ + PropTypes.array, + PropTypes.string, + PropTypes.number, + PropTypes.bool, + ]), + defaultValue: PropTypes.oneOfType([ + PropTypes.array, + PropTypes.string, + PropTypes.number, + PropTypes.bool, + ]), children: PropTypes.arrayOf(PropTypes.element), - /** - * 选中值改变时的事件 - * @param {Array} value 选中项列表 - * @param {Event} e Dom 事件对象 - */ onChange: PropTypes.func, - - /** - * 子项目的排列方式 - * - hoz: 水平排列 (default) - * - ver: 垂直排列 - */ direction: PropTypes.oneOf(['hoz', 'ver']), - /** - * 是否为预览态 - * @version 1.19 - */ isPreview: PropTypes.bool, - /** - * 预览态模式下渲染的内容 - * @param {Array} previewed 预览值 [{label: '', value:''},...] - * @param {Object} props 所有传入的参数 - * @returns {reactNode} Element 渲染内容 - * @version 1.19 - */ renderPreview: PropTypes.func, }; @@ -83,10 +54,10 @@ class CheckboxGroup extends Component { disabled: PropTypes.bool, }; - constructor(props) { + constructor(props: GroupProps) { super(props); - let value = []; + let value: GroupProps['value'] = []; if ('value' in props) { value = props.value; } else if ('defaultValue' in props) { @@ -115,7 +86,7 @@ class CheckboxGroup extends Component { }; } - static getDerivedStateFromProps(nextProps) { + static getDerivedStateFromProps(nextProps: GroupProps) { if ('value' in nextProps) { let { value } = nextProps; if (!Array.isArray(value)) { @@ -125,14 +96,13 @@ class CheckboxGroup extends Component { value = [value]; } } - return { value }; } return null; } - onChange(currentValue, e) { + onChange(currentValue: ValueItem, event: React.ChangeEvent) { const { value } = this.state; const index = value.indexOf(currentValue); const valTemp = [...value]; @@ -146,41 +116,54 @@ class CheckboxGroup extends Component { if (!('value' in this.props)) { this.setState({ value: valTemp }); } - this.props.onChange(valTemp, e); + this.props.onChange?.(valTemp, event); } render() { - const { className, style, prefix, disabled, direction, rtl, isPreview, renderPreview } = this.props; + const { className, style, prefix, disabled, direction, rtl, isPreview, renderPreview } = + this.props; const others = pickOthers(CheckboxGroup.propTypes, this.props); - // 如果内嵌标签跟dataSource同时存在,以内嵌标签为主 + // 如果内嵌标签跟 dataSource 同时存在,以内嵌标签为主 let children; - const previewed = []; + const previewed: { + label: string | React.ReactNode; + value: string | React.ReactNode; + }[] = []; if (this.props.children) { children = React.Children.map(this.props.children, child => { - if (!React.isValidElement(child)) { + if ( + !React.isValidElement<{ + value: ValueItem; + children?: string; + rtl?: boolean; + }>(child) + ) { return child; } - const checked = this.state.value && this.state.value.indexOf(child.props.value) > -1; + const checked = + this.state.value && this.state.value.indexOf(child.props?.value) > -1; if (checked) { previewed.push({ - label: child.props.children, - value: child.props.value, + label: child.props?.children, + value: child.props?.value, }); } - return React.cloneElement(child, child.props.rtl === undefined ? { rtl } : null); + return React.cloneElement(child, child.props?.rtl === undefined ? { rtl } : {}); }); } else { - children = this.props.dataSource.map((item, index) => { - let option = item; + children = this.props.dataSource?.map((item, index) => { + let option: CheckboxData; if (typeof item !== 'object') { option = { label: item, value: item, disabled, }; + } else { + option = item; } const checked = this.state.value && this.state.value.indexOf(option.value) > -1; @@ -210,7 +193,7 @@ class CheckboxGroup extends Component { if ('renderPreview' in this.props) { return (
- {renderPreview(previewed, this.props)} + {renderPreview?.(previewed, this.props)}
); } @@ -222,10 +205,9 @@ class CheckboxGroup extends Component { ); } - const cls = classnames({ + const cls = classnames(className, { [`${prefix}checkbox-group`]: true, [`${prefix}checkbox-group-${direction}`]: true, - [className]: !!className, disabled, }); diff --git a/components/checkbox/checkbox.jsx b/components/checkbox/checkbox.tsx similarity index 73% rename from components/checkbox/checkbox.jsx rename to components/checkbox/checkbox.tsx index 241cb30e47..1e5e615e79 100644 --- a/components/checkbox/checkbox.jsx +++ b/components/checkbox/checkbox.tsx @@ -1,99 +1,57 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import * as React from 'react'; +import * as PropTypes from 'prop-types'; import classnames from 'classnames'; import { polyfill } from 'react-lifecycles-compat'; -import UIState from '../mixin-ui-state'; +import UIState, { type UIStateState } from '../mixin-ui-state'; import ConfigProvider from '../config-provider'; import Icon from '../icon'; -import withContext from './with-context'; +import withCheckboxContext, { type CheckboxContext } from './with-context'; import { obj, func } from '../util'; +import type { CheckboxProps } from './types'; const noop = func.noop; -function isChecked(selectedValue, value) { +function isChecked( + selectedValue: CheckboxContext['selectedValue'], + value: CheckboxProps['value'] +): boolean { return selectedValue.indexOf(value) > -1; } + +interface CheckboxState extends UIStateState { + value?: CheckboxProps['value']; + checked?: boolean; + indeterminate?: boolean; +} + +export interface PrivateCheckboxProps extends CheckboxProps { + context: CheckboxContext; +} + /** * Checkbox * @order 1 */ -class Checkbox extends UIState { +class Checkbox extends UIState { static displayName = 'Checkbox'; static propTypes = { ...ConfigProvider.propTypes, prefix: PropTypes.string, rtl: PropTypes.bool, - /** - * 自定义类名 - */ className: PropTypes.string, - /** - * checkbox id, 挂载在input上 - */ id: PropTypes.string, - /** - * 自定义内敛样式 - */ style: PropTypes.object, - /** - * 选中状态 - */ checked: PropTypes.bool, - /** - * 默认选中状态 - */ defaultChecked: PropTypes.bool, - /** - * 禁用 - */ disabled: PropTypes.bool, - /** - * 通过属性配置label, - */ label: PropTypes.node, - /** - * Checkbox 的中间状态,只会影响到 Checkbox 的样式,并不影响其 checked 属性 - */ indeterminate: PropTypes.bool, - /** - * Checkbox 的默认中间态,只会影响到 Checkbox 的样式,并不影响其 checked 属性 - */ defaultIndeterminate: PropTypes.bool, - /** - * 状态变化时触发的事件 - * @param {Boolean} checked 是否选中 - * @param {Event} e Dom 事件对象 - */ onChange: PropTypes.func, - /** - * 鼠标进入enter事件 - * @param {Event} e Dom 事件对象 - */ onMouseEnter: PropTypes.func, - /** - * 鼠标离开Leave事件 - * @param {Event} e Dom 事件对象 - */ onMouseLeave: PropTypes.func, - /** - * checkbox 的value - */ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]), - /** - * name - */ name: PropTypes.string, - /** - * 是否为预览态 - * @version 1.19 - */ isPreview: PropTypes.bool, - /** - * 预览态模式下渲染的内容 - * @param {Boolean} checked 是否选中 - * @param {Object} props 所有传入的参数 - * @returns {reactNode} Element 渲染内容 - * @version 1.19 - */ renderPreview: PropTypes.func, }; @@ -107,7 +65,7 @@ class Checkbox extends UIState { isPreview: false, }; - constructor(props) { + constructor(props: PrivateCheckboxProps) { super(props); const { context } = props; let checked, indeterminate; @@ -134,9 +92,9 @@ class Checkbox extends UIState { this.onChange = this.onChange.bind(this); } - static getDerivedStateFromProps(nextProps) { + static getDerivedStateFromProps(nextProps: PrivateCheckboxProps) { const { context: nextContext } = nextProps; - const state = {}; + const state: CheckboxState = {}; if (nextContext.__group__) { if ('selectedValue' in nextContext) { state.checked = isChecked(nextContext.selectedValue, nextProps.value); @@ -159,7 +117,11 @@ class Checkbox extends UIState { return props.disabled || ('disabled' in context && context.disabled); } - shouldComponentUpdate(nextProps, nextState, nextContext) { + shouldComponentUpdate( + nextProps: PrivateCheckboxProps, + nextState: CheckboxState, + nextContext: CheckboxContext + ) { const { shallowEqual } = obj; return ( !shallowEqual(this.props, nextProps) || @@ -168,15 +130,15 @@ class Checkbox extends UIState { ); } - onChange(e) { + onChange(event: React.ChangeEvent) { const { context, value } = this.props; - const checked = e.target.checked; + const checked = event.target.checked; if (this.disabled) { return; } if (context.__group__) { - context.onChange(value, e); + context.onChange(value, event); } else { if (!('checked' in this.props)) { this.setState({ @@ -189,7 +151,7 @@ class Checkbox extends UIState { indeterminate: false, }); } - this.props.onChange(checked, e); + this.props.onChange?.(checked, event); } } @@ -226,7 +188,7 @@ class Checkbox extends UIState { - {renderPreview(checked, this.props)} +
+ {renderPreview?.(checked, this.props)}
); } @@ -290,8 +256,8 @@ class Checkbox extends UIState {
{childInput} - {[label, children].map((item, i) => - [undefined, null].indexOf(item) === -1 ? ( + {[label, children].map((item: React.ReactNode | undefined | null, i) => + item !== undefined && item !== null ? ( {item} @@ -302,4 +268,6 @@ class Checkbox extends UIState { } } -export default ConfigProvider.config(withContext(polyfill(Checkbox))); +export default ConfigProvider.config( + withCheckboxContext(polyfill(Checkbox) as React.ComponentType) +); diff --git a/components/checkbox/index.d.ts b/components/checkbox/index.d.ts deleted file mode 100644 index 950a4df4fa..0000000000 --- a/components/checkbox/index.d.ts +++ /dev/null @@ -1,168 +0,0 @@ -/// - -import React from 'react'; -import { CommonProps } from '../util'; - -interface HTMLAttributesWeak extends React.HTMLAttributes { - defaultValue?: any; - onChange?: any; -} - -type data = { - value?: string | number | boolean; - label?: React.ReactNode; - disabled?: boolean; - [propName: string]: any; -}; - -export type CheckboxData = data; - -export interface GroupProps extends HTMLAttributesWeak, CommonProps { - /** - * 自定义类名 - */ - className?: string; - - /** - * 自定义内敛样式 - */ - style?: React.CSSProperties; - - /** - * 整体禁用 - */ - disabled?: boolean; - - /** - * 是否为预览态 - */ - isPreview?: boolean; - - renderPreview?: (checked: boolean, props: object) => React.ReactNode; - - /** - * 可选项列表, 数据项可为 String 或者 Object, 如 `['apple', 'pear', 'orange']` 或者 `[{value: 'apple', label: '苹果',}, {value: 'pear', label: '梨'}, {value: 'orange', label: '橙子'}]` - */ - dataSource?: Array | Array | Array; - - /** - * 被选中的值列表 - */ - value?: Array | Array | Array | string | number | boolean; - - /** - * 默认被选中的值列表 - */ - defaultValue?: Array | Array | Array | string | number | boolean; - - /** - * name - */ - name?: string; - - /** - * 通过子元素方式设置内部 checkbox - */ - children?: Array; - - /** - * 选中值改变时的事件 - */ - onChange?: (value: Array | Array | Array, e: any) => void; - - /** - * 子项目的排列方式 - * - hoz: 水平排列 (default) - * - ver: 垂直排列 - */ - direction?: 'hoz' | 'ver'; - itemDirection?: 'hoz' | 'ver'; -} - -export class Group extends React.Component {} -interface HTMLAttributesWeak extends React.HTMLAttributes { - onChange?: any; - onMouseEnter?: any; - onMouseLeave?: any; -} - -export interface CheckboxProps extends HTMLAttributesWeak, CommonProps { - /** - * 自定义类名 - */ - className?: string; - - /** - * checkbox id, 挂载在input上 - */ - id?: string; - - /** - * 自定义内敛样式 - */ - style?: React.CSSProperties; - - /** - * 选中状态 - */ - checked?: boolean; - - /** - * checkbox 的value - */ - value?: string | number | boolean; - - /** - * name - */ - name?: string; - - /** - * 默认选中状态 - */ - defaultChecked?: boolean; - - /** - * 禁用 - */ - disabled?: boolean; - - /** - * 通过属性配置label, - */ - label?: React.ReactNode; - - /** - * Checkbox 的中间状态,只会影响到 Checkbox 的样式,并不影响其 checked 属性 - */ - indeterminate?: boolean; - - /** - * Checkbox 的默认中间态,只会影响到 Checkbox 的样式,并不影响其 checked 属性 - */ - defaultIndeterminate?: boolean; - - /** - * 是否为预览态 - */ - isPreview?: boolean; - - /** - * 状态变化时触发的事件 - */ - onChange?: (checked: boolean, e: any) => void; - - /** - * 鼠标进入enter事件 - */ - onMouseEnter?: (e: React.MouseEvent) => void; - - /** - * 鼠标离开Leave事件 - */ - onMouseLeave?: (e: React.MouseEvent) => void; -} - -export default class Checkbox extends React.Component { - static Group: typeof Group; -} diff --git a/components/checkbox/index.jsx b/components/checkbox/index.jsx deleted file mode 100644 index 37c0c44f0b..0000000000 --- a/components/checkbox/index.jsx +++ /dev/null @@ -1,18 +0,0 @@ -import Checkbox from './checkbox'; -import Group from './checkbox-group'; -import ConfigProvider from '../config-provider'; - -Checkbox.Group = ConfigProvider.config(Group, { - transform: /* istanbul ignore next */ (props, deprecated) => { - if ('itemDirection' in props) { - deprecated('itemDirection', 'direction', 'Checkbox'); - const { itemDirection, ...others } = props; - - props = { direction: itemDirection, ...others }; - } - - return props; - }, -}); - -export default Checkbox; diff --git a/components/checkbox/index.tsx b/components/checkbox/index.tsx new file mode 100644 index 0000000000..afb8076fe9 --- /dev/null +++ b/components/checkbox/index.tsx @@ -0,0 +1,23 @@ +import Checkbox from './checkbox'; +import Group from './checkbox-group'; +import ConfigProvider from '../config-provider'; +import { assignSubComponent } from '../util/component'; + +const CheckboxWithGroup = assignSubComponent(Checkbox, { + Group: ConfigProvider.config(Group, { + transform: /* istanbul ignore next */ (props, deprecated) => { + if ('itemDirection' in props) { + deprecated('itemDirection', 'direction', 'Checkbox'); + const { itemDirection, ...others } = props; + + props = { direction: itemDirection, ...others }; + } + + return props; + }, + }), +}); + +export type { CheckboxProps, GroupProps, CheckboxData, ValueItem } from './types'; + +export default CheckboxWithGroup; diff --git a/components/checkbox/mobile/index.jsx b/components/checkbox/mobile/index.tsx similarity index 100% rename from components/checkbox/mobile/index.jsx rename to components/checkbox/mobile/index.tsx diff --git a/components/checkbox/style.js b/components/checkbox/style.ts similarity index 100% rename from components/checkbox/style.js rename to components/checkbox/style.ts diff --git a/components/checkbox/types.ts b/components/checkbox/types.ts new file mode 100644 index 0000000000..c6cc61e414 --- /dev/null +++ b/components/checkbox/types.ts @@ -0,0 +1,237 @@ +import type * as React from 'react'; +import { type CommonProps } from '../util'; + +interface HTMLAttributesWeak + extends Omit, 'onChange' | 'defaultValue'> {} + +/** + * @api + */ +export type ValueItem = string | number | boolean; + +/** + * @api + */ +export type CheckboxData = { + value: ValueItem; + label?: React.ReactNode; + disabled?: boolean; + [propName: string]: unknown; +}; + +/** + * @api Checkbox.Group + */ +export interface GroupProps extends HTMLAttributesWeak, CommonProps { + /** + * 自定义类名 + * @en Custom className + */ + className?: string; + + /** + * 自定义内联样式 + * @en Custom inline style + */ + style?: React.CSSProperties; + + /** + * 整体禁用 + * @en Entirely disabled + */ + disabled?: boolean; + + /** + * 可选项列表 + * @en Option list + * @remarks + * 数据项可为 String 或者 Object, 如 `['apple', 'pear', 'orange']` 或者 `[{value: 'apple', label: '苹果',}, {value: 'pear', label: '梨'}, {value: 'orange', label: '橙子'}]` + * - + * Data item can be String or Object, such as `['apple', 'pear', 'orange']` or `[{value: 'apple', label: 'Apple',}, {value: 'pear', label: 'Pear'}, {value: 'orange', label: 'Orange'}]` + */ + dataSource?: Array | Array; + + /** + * 被选中的值列表 + * @en Selected value list + */ + value?: ValueItem[] | ValueItem; + + /** + * 默认被选中的值列表 + * @en Default selected value list + */ + defaultValue?: ValueItem[] | ValueItem; + + /** + * name + * @en name + */ + name?: string; + + /** + * 通过子元素方式设置内部 checkbox + * @en Set internal checkbox through child elements + */ + children?: React.ReactNode; + + /** + * 选中值改变时的事件 + * @en Selected value change event + */ + onChange?: (value: ValueItem[], e: React.ChangeEvent) => void; + + /** + * 子项目的排列方式 + * @en Arrangement of subitems + * @remarks + * hoz: 水平排列 (default), + * ver: 垂直排列 + * - + * hoz: Horizontal arrangement (default), + * ver: Vertical arrangement + */ + direction?: 'hoz' | 'ver'; + /** + * [废弃] 子项目的排列方式 + * @en [Deprecated] Arrangement of subitems + * @deprecated Use `direction` instead + */ + itemDirection?: 'hoz' | 'ver'; + + /** + * 是否为预览态 + * @en Is preview + * @version 1.19 + */ + isPreview?: boolean; + + /** + * 预览态模式下渲染的内容 + * @en Custom rendering content + * @version 1.19 + * @param previewed - 预览值 [\{label: '', value:''\},...] - Previewed value [\{label: '', value:''\},...] + * @param props - 所有传入的参数 - All props + * @returns 定制渲染内容 - Custom rendering content + */ + renderPreview?: ( + previewed: { + label: string | React.ReactNode; + value: string | React.ReactNode; + }[], + props: object + ) => React.ReactNode; +} + +export interface GroupState { + value: ValueItem[]; +} + +/** + * @api Checkbox + */ +export interface CheckboxProps extends HTMLAttributesWeak, CommonProps { + /** + * 自定义类名 + * @en className + */ + className?: string; + + /** + * checkbox id, 挂载在 input 上 + * @en Checkbox id, mounted on the input + */ + id?: string; + + /** + * 自定义内联样式 + * @en Custom inline style + */ + style?: React.CSSProperties; + + /** + * 选中状态 + * @en Checked status + */ + checked?: boolean; + + /** + * checkbox 的 value + * @en Checkbox value + */ + value?: ValueItem; + + /** + * name + * @en name + */ + name?: string; + + /** + * 默认选中状态 + * @en Default checked status + * @defaultValue false + */ + defaultChecked?: boolean; + + /** + * 禁用 + * @en Disabled + */ + disabled?: boolean; + + /** + * 通过属性配置 label, + * @en Label + */ + label?: React.ReactNode; + + /** + * Checkbox 的中间状态,只会影响到 Checkbox 的样式,并不影响其 checked 属性 + * @en Checkbox middle status, only affects the style of Checkbox, and does not affect its checked property + */ + indeterminate?: boolean; + + /** + * Checkbox 的默认中间态,只会影响到 Checkbox 的样式,并不影响其 checked 属性 + * @en Checkbox default middle status, only affects the style of Checkbox, and does not affect its checked property + * @defaultValue false + */ + defaultIndeterminate?: boolean; + + /** + * 状态变化时触发的事件 + * @en Status change event + */ + onChange?: (checked: boolean, e: React.ChangeEvent) => void; + + /** + * 鼠标进入 enter 事件 + * @en Mouse enter event + */ + onMouseEnter?: (e: React.MouseEvent) => void; + + /** + * 鼠标离开 Leave 事件 + * @en Mouse leave event + */ + onMouseLeave?: (e: React.MouseEvent) => void; + + /** + * 是否为预览态 + * @en Is preview + * @defaultValue false + * @version 1.19 + */ + isPreview?: boolean; + + /** + * 预览态模式下渲染的内容 + * @en Custom rendering content + * @version 1.19 + * @param checked - 是否选中 - Is checked + * @param props - 所有传入的参数 - All props + * @returns 定制渲染内容 - Custom rendering content + */ + renderPreview?: (checked: boolean, props: CheckboxProps) => React.ReactNode; +} diff --git a/components/checkbox/with-context.jsx b/components/checkbox/with-context.jsx deleted file mode 100644 index a5b5857bec..0000000000 --- a/components/checkbox/with-context.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -export default function withContext(Checkbox) { - return class WrappedComp extends React.Component { - static displayName = 'Checkbox'; - static contextTypes = { - onChange: PropTypes.func, - __group__: PropTypes.bool, - selectedValue: PropTypes.array, - disabled: PropTypes.bool, - prefix: PropTypes.string, - }; - - render() { - return ; - } - }; -} diff --git a/components/checkbox/with-context.tsx b/components/checkbox/with-context.tsx new file mode 100644 index 0000000000..261229f91d --- /dev/null +++ b/components/checkbox/with-context.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import * as PropTypes from 'prop-types'; +import { type PrivateCheckboxProps } from './checkbox'; +import { type CheckboxProps } from './types'; + +export interface CheckboxContext { + onChange: ( + value: string | number | boolean | undefined, + event: React.ChangeEvent + ) => void; + __group__: boolean; + selectedValue: CheckboxProps['value'][]; + disabled: boolean; + prefix: string; +} + +export default function withCheckboxContext( + Checkbox: React.ComponentType +): React.ComponentType { + return class WrappedComp extends React.Component { + static displayName = 'Checkbox'; + static contextTypes = { + onChange: PropTypes.func, + __group__: PropTypes.bool, + selectedValue: PropTypes.array, + disabled: PropTypes.bool, + prefix: PropTypes.string, + }; + + render() { + return ; + } + }; +} diff --git a/components/drawer/__docs__/demo/basic/index.tsx b/components/drawer/__docs__/demo/basic/index.tsx index e9f21ec266..3b721e6c21 100644 --- a/components/drawer/__docs__/demo/basic/index.tsx +++ b/components/drawer/__docs__/demo/basic/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Button, Drawer } from '@alifd/next'; +import type { DrawerProps } from '@alifd/next/lib/drawer'; class Demo extends React.Component { state = { @@ -13,7 +14,7 @@ class Demo extends React.Component { }); }; - onClose = (reason: string, e: React.MouseEvent | KeyboardEvent) => { + onClose: DrawerProps['onClose'] = (reason, e) => { console.log('onClose: ', reason, e); this.setState({ visible: false, diff --git a/components/drawer/__docs__/demo/placement/index.tsx b/components/drawer/__docs__/demo/placement/index.tsx index 6b4f31c553..aece9b3df6 100644 --- a/components/drawer/__docs__/demo/placement/index.tsx +++ b/components/drawer/__docs__/demo/placement/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Radio, Button, Drawer } from '@alifd/next'; +import type { RadioProps } from '@alifd/next/lib/radio'; class Demo extends React.Component { state = { @@ -14,12 +15,13 @@ class Demo extends React.Component { }); }; - onClose = (reason: string, e: React.MouseEvent | KeyboardEvent) => { + onClose = () => { this.setState({ visible: false, }); }; - onPlacementChange = (dir: string | number | boolean, e: any) => { + + onPlacementChange: RadioProps['onChange'] = dir => { this.setState({ placement: dir, }); diff --git a/components/drawer/__docs__/demo/quick/index.tsx b/components/drawer/__docs__/demo/quick/index.tsx index 6b4e4ae478..2b77ab2012 100644 --- a/components/drawer/__docs__/demo/quick/index.tsx +++ b/components/drawer/__docs__/demo/quick/index.tsx @@ -1,10 +1,11 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Button, Drawer } from '@alifd/next'; -import { QuickShowRet } from '../../../types'; +import type { QuickShowRet } from '@alifd/next/lib/drawer'; let instance: QuickShowRet | null = null; const show = () => { + instance && instance.hide(); instance = Drawer.show({ title: 'quick', hasMask: false, diff --git a/components/drawer/__docs__/demo/select/index.tsx b/components/drawer/__docs__/demo/select/index.tsx index 64784d45df..23d3b72397 100644 --- a/components/drawer/__docs__/demo/select/index.tsx +++ b/components/drawer/__docs__/demo/select/index.tsx @@ -1,17 +1,26 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Radio, Drawer, Select } from '@alifd/next'; +import type { SelectProps } from '@alifd/next/lib/select'; +import type { RadioProps } from '@alifd/next/lib/radio'; + +interface onToggleHighlightItemProps { + deep: number; + value: string; + label: string; +} const Option = Select.Option; -const onChange = function (value: any) { +const onChange: SelectProps['onChange'] = value => { console.log(value); }; -const onBlur = function (e: any) { + +const onBlur: SelectProps['onBlur'] = e => { console.log(/onblur/, e); }; -const onToggleHighlightItem = function (item: any, type: any) { +const onToggleHighlightItem = (item: onToggleHighlightItemProps, type: 'up' | 'down') => { console.log(item, type); }; @@ -20,7 +29,7 @@ class Demo extends React.Component { placement: 'right', }; - onPlacementChange = (dir: any) => { + onPlacementChange: RadioProps['onChange'] = dir => { this.setState({ placement: dir, }); @@ -48,7 +57,7 @@ class Demo extends React.Component { autoWidth={false} onChange={onChange} onBlur={onBlur} - onToggleHighlightItem={onToggleHighlightItem as any} + onToggleHighlightItem={onToggleHighlightItem} defaultValue="jack" aria-label="name is" hasClear diff --git a/components/drawer/__docs__/index.en-us.md b/components/drawer/__docs__/index.en-us.md index f1e6b39ca2..b990dd89b4 100644 --- a/components/drawer/__docs__/index.en-us.md +++ b/components/drawer/__docs__/index.en-us.md @@ -19,47 +19,73 @@ version 1.25 add api `v2` to support open new version Dialog, feature as list: feature: -- use css (not js) to compute position, will easier +- use css (not js) to compute position, will easier - support `width/height` to fix width/height of drawer, or you can set `auto` to follow content width/height ## API ### Drawer -> Inherited Overlay.Popup's API unless otherwise specified - -| Param | Descripiton | Type | Default Value | -| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | --------------------------------------------------------------------------------- | -| trigger | trigger the overlay to show or hide elements | ReactElement | - | -| triggerType | trigger the overlay to show or hide operations, either 'click', 'hover', 'focus', or an array of them, such as ['hover', 'focus'] | String/Array | 'hover' | -| visible | whether the overlay is visiible currently | Boolean | - | -| animation | configure animation, support the {in: 'enter-class', out: 'leave-class' } object parameters, if set to false, do not play the animation. Refer to `Animate` component documentation for available animations. | Object/Boolean | { in: 'expandInDown', out: 'expandOutUp' } | -| hasMask | whether to show the mask | Boolean | false | -| closeable | [deprecated]controls how the dialog is closed. The value can be either a String or Boolean, where the string consists of the following values:
**close** clicking the close button can close the dialog
**mask** clicking the mask can close the dialog
**esc** pressing the esc key can close the dialog
such as 'close' or 'close,esc,mask'
If set to true, all of the above close methods take effect
If set to false, all of the above close methods will fail | String/Boolean | 'esc,close' | -| closeMode | [recommand]controls how the dialog is closed. The value can be either a String or Array:
**close** clicking the close button can close the dialog
**mask** clicking the mask can close the dialog
**esc** pressing the esc key can close the dialog
for example 'close' or ['close','esc','mask'] | Array<Enum>/Enum | - | -| onVisibleChange | callback function triggered when the ovlery is visible or hidden

**signatures**:
Function(visible: Boolean, type: String, e: Object) => void
**params**:
_visible_: {Boolean} whether the overlay is visible
_type_: {String} the reason that triggers the overlay to show or hide
_e_: {Object} DOM event | Function | func.noop | -| placement | placement of the drawer

**options**:
'top', 'right', 'bottom', 'left' | Enum | 'right' | -| v2 | v2 version | Boolean | - | | -| afterClose | [v2] callback after Drawer close

**signatures**:
Function() => void | Function | - | | - +| Param | Description | Type | Default Value | Required | Supported Version | +| --------------- | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------- | -------- | ----------------- | +| closeable | [Deprecated] Control the way the drawer is closed | 'close' \| 'mask' \| 'esc' \| boolean \| 'close,mask' \| 'close,esc' \| 'mask,esc' | true | | - | +| closeMode | Control the way the dialog is closed | CloseMode[] \| 'close' \| 'mask' \| 'esc' | - | | 1.21 | +| cache | Whether to retain the child node when hiding | boolean | - | | - | +| title | Title | React.ReactNode | - | | - | +| bodyStyle | Style on body | React.CSSProperties | - | | - | +| headerStyle | Style on header | React.CSSProperties | - | | - | +| animation | Animation playback method when showing and hiding

**signature**:
**params**:
_animation_: animation | { in: string; out: string } \| false | \{ in: 'expandInDown', out: 'expandOutUp' \} | | - | +| visible | Whether to show | boolean | - | | - | +| width | Width, only effective when placement is left right | number \| string | - | | - | +| height | Height, only effective when placement is the top bottom | number \| string | - | | - | +| onVisibleChange | [v2 Deprecated] Controlled mode (without trigger), only triggered when closed, equivalent to onClose | (visible: boolean, reason: string, e?: React.MouseEvent) => void | - | | - | +| onClose | Callback when the dialog is closed | (reason: string, e: React.MouseEvent \| KeyboardEvent) => void | `() => {}` | | - | +| placement | The position of the page | 'top' \| 'right' \| 'bottom' \| 'left' | 'right' | | - | +| v2 | Enable v2 version | false \| undefined | false | | - | +| content | Content | React.ReactNode | - | | - | +| popupContainer | Render component container | string \| HTMLElement \| null | - | | - | +| hasMask | Whether there is a mask | boolean | true | | - | +| afterOpen | Callback after the dialog is opened | () => void | - | | - | + +### Drawer V2 + +| Param | Description | Type | Default Value | Required | Supported Version | +| -------------- | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------- | -------- | ----------------- | +| closeable | [Deprecated] Control the way the drawer is closed | 'close' \| 'mask' \| 'esc' \| boolean \| 'close,mask' \| 'close,esc' \| 'mask,esc' | true | | - | +| closeMode | Control the way the dialog is closed | CloseMode[] \| 'close' \| 'mask' \| 'esc' | - | | 1.21 | +| cache | Whether to retain the child node when hiding | boolean | - | | - | +| title | Title | React.ReactNode | - | | - | +| bodyStyle | Style on body | React.CSSProperties | - | | - | +| headerStyle | Style on header | React.CSSProperties | - | | - | +| animation | Animation playback method when showing and hiding

**signature**:
**params**:
_animation_: animation | { in: string; out: string } \| false | \{ in: 'expandInDown', out: 'expandOutUp' \} | | - | +| visible | Whether to show | boolean | - | | - | +| width | Width, only effective when placement is left right | number \| string | - | | - | +| height | Height, only effective when placement is the top bottom | number \| string | - | | - | +| afterClose | Callback after the dialog is closed | () => void | - | | - | +| onClose | Callback when the dialog is closed | (reason: string, e: React.MouseEvent \| KeyboardEvent) => void | `() => {}` | | - | +| placement | The position of the page | 'top' \| 'right' \| 'bottom' \| 'left' | 'right' | | - | +| v2 | Enable v2 version | boolean | false | | - | +| content | Content | React.ReactNode | - | | - | +| popupContainer | Render component container | string \| HTMLElement \| null | - | | - | +| hasMask | Whether there is a mask | boolean | true | | - | + ### Drawer.show The following only list common properties that config can pass, and other properties of the Dialog can also be passed in. -| Param | Descripiton | Type | Default Value | -| :------- | :-------------- | :-------- | :------- | -| title | title of drawer | ReactNode | '' | -| content | content of drawer | ReactNode | '' | +| Param | Descripiton | Type | Default Value | +| :------ | :---------------- | :-------- | :------------ | +| title | title of drawer | ReactNode | '' | +| content | content of drawer | ReactNode | '' | - ## ARIA and Keyboard -| Keyboard | Descripiton | -| :-------- | :--------------------------------------- | -| esc | pressing ESC will close dialog | -| tab | focus on any element that can be focused, the focus remains in the dialog when the dialog is displayed | -| shift+tab | back focus on any element that can be focused, the focus remains in the dialog when the dialog is displayed | +| Keyboard | Descripiton | +| :-------- | :---------------------------------------------------------------------------------------------------------- | +| esc | pressing ESC will close dialog | +| tab | focus on any element that can be focused, the focus remains in the dialog when the dialog is displayed | +| shift+tab | back focus on any element that can be focused, the focus remains in the dialog when the dialog is displayed | diff --git a/components/drawer/__docs__/index.md b/components/drawer/__docs__/index.md index d48e069378..cc07994f83 100644 --- a/components/drawer/__docs__/index.md +++ b/components/drawer/__docs__/index.md @@ -27,26 +27,48 @@ ### Drawer -> 继承 Overlay.Popup 的 API,除非特别说明 - -| 参数 | 说明 | 类型 | 默认值 | 版本支持 | -| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | ------------------------------------------ | ---- | -| width | 宽度,仅在 placement是 left right 的时候生效 | Number/String | - | | -| height | 高度,仅在 placement是 top bottom 的时候生效 | Number/String | - | | -| closeable | [废弃]同closeMode, 控制对话框关闭的方式,值可以为字符串或者布尔值,其中字符串是由以下值组成:
**close** 表示点击关闭按钮可以关闭对话框
**mask** 表示点击遮罩区域可以关闭对话框
**esc** 表示按下 esc 键可以关闭对话框
如 'close' 或 'close,esc,mask'
如果设置为 true,则以上关闭方式全部生效
如果设置为 false,则以上关闭方式全部失效 | String/Boolean | true | | -| cache | 隐藏时是否保留子节点,不销毁 | Boolean | - | | -| closeMode | [推荐]控制对话框关闭的方式,值可以为字符串或者数组,其中字符串、数组均为以下值的枚举:
**close** 表示点击关闭按钮可以关闭对话框
**mask** 表示点击遮罩区域可以关闭对话框
**esc** 表示按下 esc 键可以关闭对话框
如 'close' 或 ['close','esc','mask'], \[] | Array<Enum>/Enum | - | 1.21 | -| onClose | 对话框关闭时触发的回调函数

**签名**:
Function(trigger: String, event: Object) => void
**参数**:
_trigger_: {String} 关闭触发行为的描述字符串
_event_: {Object} 关闭时事件对象 | Function | () => {} | | -| afterOpen | [v2废弃]对话框打开后的回调函数

**签名**:
Function() => void | Function | - | | -| placement | 位于页面的位置

**可选值**:
'top', 'right', 'bottom', 'left' | Enum | 'right' | | -| title | 标题 | ReactNode | - | | -| headerStyle | header上的样式 | Object | - | | -| bodyStyle | body上的样式 | Object | - | | -| visible | 是否显示 | Boolean | - | | -| hasMask | 是否显示遮罩 | Boolean | true | | -| animation | 显示隐藏时动画的播放方式,支持 { in: 'enter-class', out: 'leave-class' } 的对象参数,如果设置为 false,则不播放动画。 请参考 Animate 组件的文档获取可用的动画名 | Object/Boolean | { in: 'expandInDown', out: 'expandOutUp' } | | -| v2 | 开启 v2 | Boolean | - | | -| afterClose | [v2] 弹窗关闭后的回调

**签名**:
Function() => void | Function | - | | +| 参数 | 说明 | 类型 | 默认值 | 是否必填 | 支持版本 | +| --------------- | ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------- | -------- | -------- | +| closeable | [废弃]同closeMode, 控制对话框关闭的方式, | 'close' \| 'mask' \| 'esc' \| boolean \| 'close,mask' \| 'close,esc' \| 'mask,esc' | true | | - | +| closeMode | [推荐]控制对话框关闭的方式 | CloseMode[] \| 'close' \| 'mask' \| 'esc' | - | | 1.21 | +| cache | 隐藏时是否保留子节点,不销毁 | boolean | - | | - | +| title | 标题 | React.ReactNode | - | | - | +| bodyStyle | body上的样式 | React.CSSProperties | - | | - | +| headerStyle | header上的样式 | React.CSSProperties | - | | - | +| animation | 显示隐藏时动画的播放方式

**签名**:
**参数**:
_animation_: 指定进场和出场动画的对象。 | { in: string; out: string } \| false | \{ in: 'expandInDown', out: 'expandOutUp' \} | | - | +| visible | 是否显示 | boolean | - | | - | +| width | 宽度,仅在 placement是 left right 的时候生效 | number \| string | - | | - | +| height | 高度,仅在 placement是 top bottom 的时候生效 | number \| string | - | | - | +| onVisibleChange | [v2 废弃] 受控模式下(没有 trigger 的时候),只会在关闭时触发,相当于onClose | (visible: boolean, reason: string, e?: React.MouseEvent) => void | - | | - | +| onClose | 对话框关闭时触发的回调函数 | (reason: string, e: React.MouseEvent \| KeyboardEvent) => void | `() => {}` | | - | +| placement | 位于页面的位置 | 'top' \| 'right' \| 'bottom' \| 'left' | 'right' | | - | +| v2 | 开启v2 | false \| undefined | false | | - | +| content | 内容 | React.ReactNode | - | | - | +| popupContainer | 渲染组件的容器 | string \| HTMLElement \| null | - | | - | +| hasMask | 是否显示遮罩 | boolean | true | | - | +| afterOpen | [v2 废弃]对话框打开后的回调函数 | () => void | - | | - | + +### Drawer V2 + +| 参数 | 说明 | 类型 | 默认值 | 是否必填 | 支持版本 | +| -------------- | ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------- | -------- | -------- | +| closeable | [废弃]同closeMode, 控制对话框关闭的方式, | 'close' \| 'mask' \| 'esc' \| boolean \| 'close,mask' \| 'close,esc' \| 'mask,esc' | true | | - | +| closeMode | [推荐]控制对话框关闭的方式 | CloseMode[] \| 'close' \| 'mask' \| 'esc' | - | | 1.21 | +| cache | 隐藏时是否保留子节点,不销毁 | boolean | - | | - | +| title | 标题 | React.ReactNode | - | | - | +| bodyStyle | body上的样式 | React.CSSProperties | - | | - | +| headerStyle | header上的样式 | React.CSSProperties | - | | - | +| animation | 显示隐藏时动画的播放方式

**签名**:
**参数**:
_animation_: 指定进场和出场动画的对象。 | { in: string; out: string } \| false | \{ in: 'expandInDown', out: 'expandOutUp' \} | | - | +| visible | 是否显示 | boolean | - | | - | +| width | 宽度,仅在 placement是 left right 的时候生效 | number \| string | - | | - | +| height | 高度,仅在 placement是 top bottom 的时候生效 | number \| string | - | | - | +| afterClose | [v2] 弹窗关闭后的回调 | () => void | - | | - | +| onClose | 对话框关闭时触发的回调函数 | (reason: string, e: React.MouseEvent \| KeyboardEvent) => void | `() => {}` | | - | +| placement | 位于页面的位置 | 'top' \| 'right' \| 'bottom' \| 'left' | 'right' | | - | +| v2 | 开启v2 | boolean | false | | - | +| content | 内容 | React.ReactNode | - | | - | +| popupContainer | 渲染组件的容器 | string \| HTMLElement \| null | - | | - | +| hasMask | 是否显示遮罩 | boolean | true | | - | @@ -54,10 +76,10 @@ 以下只列举 config 可以传入的常用属性,Drawer 组件其他属性也可以传入 -| 属性 | 说明 | 类型 | 默认值 | -| :------ | :-- | :-------- | :-- | -| title | 标题 | ReactNode | '' | -| content | 内容 | ReactNode | '' | +| 属性 | 说明 | 类型 | 默认值 | +| :------ | :--- | :-------- | :----- | +| title | 标题 | ReactNode | '' | +| content | 内容 | ReactNode | '' | ### Drawer.withContext @@ -70,8 +92,8 @@ ## 无障碍键盘操作指南 -| 键盘 | 说明 | -| :-------- | :--------------------------------------- | -| esc | 按下ESC键将会关闭dialog而不触发任何的动作 | +| 键盘 | 说明 | +| :-------- | :------------------------------------------------------------------------ | +| esc | 按下ESC键将会关闭dialog而不触发任何的动作 | | tab | 正向聚焦到任何可以被聚焦的元素, 在Dialog显示的时候,焦点始终保持在框体内 | | shift+tab | 反向聚焦到任何可以被聚焦的元素,在Dialog显示的时候,焦点始终保持在框体内 | diff --git a/components/drawer/__docs__/theme/index.tsx b/components/drawer/__docs__/theme/index.tsx index 26ba948ac3..c5769e31d2 100644 --- a/components/drawer/__docs__/theme/index.tsx +++ b/components/drawer/__docs__/theme/index.tsx @@ -2,12 +2,17 @@ import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import '../../../demo-helper/style'; import '../../style'; -import { Demo, DemoGroup, initDemo } from '../../../demo-helper'; +import { + Demo, + DemoGroup, + initDemo, + type DemoFunctionDefineForArray, + type DemoFunctionDefineForObject, +} from '../../../demo-helper'; import Drawer from '../../index'; import zhCN from '../../../locale/zh-cn'; import enUS from '../../../locale/en-us'; -import { InnerProps } from '../../types'; interface FunctionProps { lang: string; @@ -84,7 +89,9 @@ class FunctionDemo extends Component { }, }, }; - onFunctionChange = (demoFunction: any) => { + onFunctionChange = ( + demoFunction: Record | DemoFunctionDefineForArray[] + ) => { this.setState({ demoFunction, }); @@ -110,7 +117,11 @@ class FunctionDemo extends Component { const hasTitle = this.state.demoFunction.hasTitle.value === 'true'; const hasCloseIcon = this.state.demoFunction.hasCloseIcon.value === 'true'; - const placement = this.state.demoFunction.placement.value; + const placement = this.state.demoFunction.placement.value as + | 'top' + | 'bottom' + | 'left' + | 'right'; const style: React.CSSProperties = { position: 'absolute', top: placement === 'bottom' ? 'auto' : 0, @@ -120,7 +131,7 @@ class FunctionDemo extends Component { const normalContent = ( { it('should show and hide', () => { - class DrawerDemo extends React.Component { + class DrawerDemo extends React.Component<{ animation: DrawerProps['animation'] }> { state = { visible: false, }; diff --git a/components/drawer/__tests__/index-v2-spec.tsx b/components/drawer/__tests__/index-v2-spec.tsx index d1af4efd99..d3205f255a 100644 --- a/components/drawer/__tests__/index-v2-spec.tsx +++ b/components/drawer/__tests__/index-v2-spec.tsx @@ -2,10 +2,11 @@ import React from 'react'; import Drawer from '../index'; import ConfigProvider from '../../config-provider'; import '../style'; +import type { DrawerProps } from '../types'; describe('Drawer v2', () => { it('should show and hide', () => { - class DrawerDemo extends React.Component { + class DrawerDemo extends React.Component<{ animation: DrawerProps['animation'] }> { state = { visible: false, }; @@ -100,6 +101,8 @@ describe('Drawer v2', () => { cy.get('.test-closeable').should('exist'); cy.get('.drawer-quick-content').should('exist'); - hide(); + cy.then(() => { + hide(); + }); }); }); diff --git a/components/drawer/drawer-v2.tsx b/components/drawer/drawer-v2.tsx index baf3016b48..b9dcbd35e0 100644 --- a/components/drawer/drawer-v2.tsx +++ b/components/drawer/drawer-v2.tsx @@ -9,7 +9,7 @@ import Animate from '../animate'; import zhCN from '../locale/zh-cn'; import { log, func, dom, focus, guid } from '../util'; import scrollLocker from '../dialog/scroll-locker'; -import { DrawerProps } from './types'; +import type { DrawerV2Props } from './types'; const { OverlayContext } = Overlay; const noop = func.noop; @@ -51,7 +51,7 @@ const getAnimation = (placement: string) => { return animation; }; -const Drawer = (props: DrawerProps) => { +const Drawer = (props: DrawerV2Props) => { if (!useState || !useRef || !useEffect) { log.warning('need react version > 16.8.0'); return null; @@ -196,7 +196,7 @@ const Drawer = (props: DrawerProps) => { const handleExited = () => { if (!isAnimationEnd.current) { markAnimationEnd(true); - dom.setStyle(wrapperRef.current as HTMLDivElement, 'display', 'none'); + dom.setStyle(wrapperRef.current!, 'display', 'none'); scrollLocker.unlock(document.body, locker.current); if (autoFocus && lastFocus.current) { @@ -236,7 +236,7 @@ const Drawer = (props: DrawerProps) => { const handleEnter = () => { markAnimationEnd(false); - dom.setStyle(wrapperRef.current as HTMLDivElement, 'display', ''); + dom.setStyle(wrapperRef.current!, 'display', ''); }; const handleEntered = () => { if (autoFocus && drawerRef.current && drawerRef.current.bodyNode) { @@ -258,16 +258,16 @@ const Drawer = (props: DrawerProps) => { [`${prefix}overlay-inner`]: true, [`${prefix}drawer-wrapper`]: true, [`${prefix}drawer-${placement}`]: true, - [className as string]: !!className, + [className!]: !!className, }); const drawerCls = classNames({ [`${prefix}drawer-v2`]: true, - [className as string]: !!className, + [className!]: !!className, }); - const newAnimation: { in: string; out: string } | boolean | null = + const newAnimation: DrawerV2Props['animation'] = animation === null || animation === false - ? null + ? undefined : animation ? animation : getAnimation(placement); @@ -320,13 +320,7 @@ const Drawer = (props: DrawerProps) => {
- | undefined - } + animation={newAnimation} timeout={timeout} onEnter={handleEnter} onEntered={handleEntered} diff --git a/components/drawer/drawer.tsx b/components/drawer/drawer.tsx index 070372b455..62353a86ae 100644 --- a/components/drawer/drawer.tsx +++ b/components/drawer/drawer.tsx @@ -1,12 +1,12 @@ import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Overlay, { PopupProps } from '../overlay'; +import React, { Component, type ComponentType } from 'react'; +import Overlay from '../overlay'; import Inner from './inner'; import zhCN from '../locale/zh-cn'; import { obj } from '../util'; -import { DrawerProps } from './types'; +import type { DrawerProps, InnerProps } from './types'; -const noop = () => {}; +const noop: InnerProps['onClose'] = () => {}; const { Popup } = Overlay; const { pickOthers } = obj; @@ -24,100 +24,33 @@ export default class Drawer extends Component { static displayName = 'Drawer'; static propTypes = { - ...(Popup.propTypes || {}), + ...((Popup as ComponentType).propTypes || {}), prefix: PropTypes.string, pure: PropTypes.bool, rtl: PropTypes.bool, - // 不建议使用trigger trigger: PropTypes.element, triggerType: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), - /** - * 宽度,仅在 placement是 left right 的时候生效 - */ width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - /** - * 高度,仅在 placement是 top bottom 的时候生效 - */ height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - /** - * [废弃]同closeMode, 控制对话框关闭的方式,值可以为字符串或者布尔值,其中字符串是由以下值组成: - * **close** 表示点击关闭按钮可以关闭对话框 - * **mask** 表示点击遮罩区域可以关闭对话框 - * **esc** 表示按下 esc 键可以关闭对话框 - * 如 'close' 或 'close,esc,mask' - * 如果设置为 true,则以上关闭方式全部生效 - * 如果设置为 false,则以上关闭方式全部失效 - */ closeable: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), - /** - * 隐藏时是否保留子节点,不销毁 - */ cache: PropTypes.bool, - /** - * [推荐]控制对话框关闭的方式,值可以为字符串或者数组,其中字符串、数组均为以下值的枚举: - * **close** 表示点击关闭按钮可以关闭对话框 - * **mask** 表示点击遮罩区域可以关闭对话框 - * **esc** 表示按下 esc 键可以关闭对话框 - * 如 'close' 或 ['close','esc','mask'], [] - * @version 1.21 - */ closeMode: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.oneOf(['close', 'mask', 'esc'])), PropTypes.oneOf(['close', 'mask', 'esc']), ]), - /** - * 对话框关闭时触发的回调函数 - * @param trigger - 关闭触发行为的描述字符串 - * @param event - 关闭时事件对象 - */ onClose: PropTypes.func, - /** - * [v2废弃]对话框打开后的回调函数 - */ afterOpen: PropTypes.func, - /** - * 位于页面的位置 - */ placement: PropTypes.oneOf(['top', 'right', 'bottom', 'left']), - /** - * 标题 - */ title: PropTypes.node, - /** - * header上的样式 - */ headerStyle: PropTypes.object, - /** - * body上的样式 - */ bodyStyle: PropTypes.object, - /** - * 是否显示 - */ visible: PropTypes.bool, - /** - * 是否显示遮罩 - */ hasMask: PropTypes.bool, - // 受控模式下(没有 trigger 的时候),只会在关闭时触发,相当于onClose onVisibleChange: PropTypes.func, - /** - * 显示隐藏时动画的播放方式,支持 `{@link in: 'enter-class', out: 'leave-class'}` 的对象参数, - * 如果设置为 `false`,则不播放动画。请参考 Animate 组件的文档获取可用的动画名。 - * - * @defaultValue `{@link in: 'expandInDown', out: 'expandOutUp'}` - */ animation: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]), locale: PropTypes.object, - // for ConfigProvider popupContainer: PropTypes.any, - /** - * 开启 v2 - */ v2: PropTypes.bool, - /** - * [v2] 弹窗关闭后的回调 - */ afterClose: PropTypes.func, }; @@ -134,7 +67,7 @@ export default class Drawer extends Component { private overlay: Overlay | null = null; - getAlign = (placement: string) => { + getAlign = (placement: string | undefined) => { let align; switch (placement) { case 'top': @@ -155,7 +88,7 @@ export default class Drawer extends Component { return align; }; - getAnimation = (placement: string) => { + getAnimation = (placement: string | undefined) => { if ('animation' in this.props) { return this.props.animation; } @@ -224,17 +157,7 @@ export default class Drawer extends Component { onVisibleChange && onVisibleChange(visible, reason, e); }; - renderInner( - closeable: - | boolean - | 'esc' - | 'close' - | 'mask' - | 'close,mask' - | 'close,esc' - | 'mask,esc' - | undefined - ) { + renderInner(closeable: InnerProps['closeable']) { const { prefix, className, @@ -247,8 +170,7 @@ export default class Drawer extends Component { placement, rtl, } = this.props; - type OthersProps = Omit; - const others: OthersProps = pickOthers(Object.keys(Drawer.propTypes), this.props); + const others = pickOthers(Drawer.propTypes, this.props); return ( { headerStyle={headerStyle} bodyStyle={bodyStyle} placement={placement} - onClose={( - onClose as (reason: string, e: React.MouseEvent) => void - ).bind(this, 'closeClick')} + onClose={onClose!.bind(this, 'closeClick')} {...others} > {children} @@ -317,9 +237,9 @@ export default class Drawer extends Component { trigger, triggerType, onVisibleChange: this.handleVisibleChange, - animation: this.getAnimation(placement as string), + animation: this.getAnimation(placement), hasMask, - align: this.getAlign(placement as string), + align: this.getAlign(placement), ...closeConfig, canCloseByOutSideClick: false, disableScroll: true, @@ -334,12 +254,7 @@ export default class Drawer extends Component { const inner = this.renderInner(canCloseByCloseClick); return ( - + {inner} ); diff --git a/components/drawer/index.tsx b/components/drawer/index.tsx index 6f880287f3..6c782ee926 100644 --- a/components/drawer/index.tsx +++ b/components/drawer/index.tsx @@ -7,7 +7,13 @@ import Drawer2 from './drawer-v2'; import Inner from './inner'; import { show, withContext } from './show'; -import { DrawerProps, QuickShowRet } from './types'; +import type { DrawerV2Props, DrawerV1Props } from './types'; + +export interface QuickShowRet { + hide: () => void; +} + +export type DrawerProps = DrawerV2Props | DrawerV1Props; class Drawer extends React.Component { static Inner: typeof Inner; diff --git a/components/drawer/inner.tsx b/components/drawer/inner.tsx index c3ad1f3d99..67f837b91d 100644 --- a/components/drawer/inner.tsx +++ b/components/drawer/inner.tsx @@ -4,11 +4,18 @@ import cx from 'classnames'; import Icon from '../icon'; import zhCN from '../locale/zh-cn'; import { obj } from '../util'; -import { InnerProps } from './types'; +import type { InnerProps } from './types'; const noop = () => {}; const { pickOthers } = obj; +interface ariaRoleProps { + role?: string; + 'aria-modal'?: boolean | 'true' | 'false'; + 'aria-level'?: number; + 'aria-label'?: string; +} + export default class Inner extends Component { static propTypes = { prefix: PropTypes.string, @@ -45,7 +52,7 @@ export default class Inner extends Component { [`${prefix}drawer-header`]: true, [`${prefix}drawer-no-title`]: !title, }); - const ariaProps: object = { + const ariaProps: ariaRoleProps = { role: 'heading', 'aria-level': 1, }; @@ -72,18 +79,14 @@ export default class Inner extends Component { renderCloseLink() { const { prefix, closeable, onClose, locale } = this.props; - const ariaProps: object = { + const ariaProps: ariaRoleProps = { role: 'button', - 'aria-label': locale ? locale.close : 'close-icon', + 'aria-label': locale?.close as string, }; if (closeable) { return ( - + ); @@ -100,10 +103,10 @@ export default class Inner extends Component { [`${prefix}drawer`]: true, [`${prefix}drawer-${placement}`]: !v2, [`${prefix}closeable`]: closeable, - [className as string]: !!className, + [className!]: !!className, }); - const ariaProps: object = { + const ariaProps: ariaRoleProps = { role, 'aria-modal': 'true', }; diff --git a/components/drawer/show.tsx b/components/drawer/show.tsx index f22b8fbc9a..276eba5d3e 100644 --- a/components/drawer/show.tsx +++ b/components/drawer/show.tsx @@ -1,28 +1,17 @@ -import React, { Component } from 'react'; +import React, { type JSXElementConstructor } from 'react'; import ReactDOM from 'react-dom'; import ConfigProvider from '../config-provider'; import Drawer from './drawer-v2'; -import { DrawerProps } from './types'; +import type { DrawerProps } from './types'; +import type { AnyProps } from '../config-provider/config'; +import type { ConsumerState } from '../config-provider/consumer'; -interface ModalProps extends DrawerProps { - content?: React.ReactNode; +interface ModalState { + visible?: boolean; + loading?: boolean; } -interface ShowConfig extends DrawerProps { - afterClose?: () => void; - onClose?: () => void; - contextConfig?: object; // 用适当的类型替换 any,如果可能的话 -} - -export interface ShowReturn { - hide: () => void; -} - -export interface ModalInstance { - getInstance: () => Modal; -} - -class Modal extends React.Component { +class Modal extends React.Component { state = { visible: true, loading: false, @@ -46,6 +35,12 @@ class Modal extends React.Component void; + onClose?: () => void; + contextConfig?: ConsumerState; +}; + /** * 创建对话框。 * @@ -55,14 +50,14 @@ const ConfigModal = ConfigProvider.config(Modal, { componentName: 'Drawer' }); * @param config - 配置项。 * @returns 返回一个对象,该对象包含有 `hide` 方法,可用来关闭对话框。 */ -export const show = (config: ShowConfig = {}): ShowReturn => { +export const show = (config: Config = {}) => { const container: HTMLDivElement = document.createElement('div'); const unmount = () => { if (config.afterClose) { config.afterClose(); } - // eslint-disable-next-line + // eslint-disable-next-line react/no-deprecated ReactDOM.unmountComponentAtNode(container); container.parentNode?.removeChild(container); }; @@ -71,8 +66,8 @@ export const show = (config: ShowConfig = {}): ShowReturn => { let newContext = config.contextConfig; if (!newContext) newContext = ConfigProvider.getContext(); - let instance: ModalInstance | null = null, - myRef: Modal | null = null; + let instance: InstanceType | null, + myRef: InstanceType | null; const handleClose = () => { const inc = instance && instance.getInstance(); @@ -81,7 +76,8 @@ export const show = (config: ShowConfig = {}): ShowReturn => { config.onClose(); } }; - // eslint-disable-next-line + + // eslint-disable-next-line react/no-deprecated ReactDOM.render( { onClose={handleClose} afterClose={unmount} ref={ref => { - myRef = ref as unknown as Modal; + myRef = ref; }} /> , container, function () { - instance = myRef as unknown as ModalInstance; + instance = myRef; } ); return { @@ -103,15 +99,25 @@ export const show = (config: ShowConfig = {}): ShowReturn => { }; }; -export const withContext =

( - WrappedComponent: React.ComponentType

-): React.ComponentType

=> { - const HOC: React.FC

= (props: P) => { +export interface ContextDialog { + show: (config?: Config) => { hide: () => void }; +} + +export interface WithContextDrawerProps { + contextDialog: ContextDialog; +} + +export const withContext =

( + WrappedComponent: JSXElementConstructor

& C +) => { + type Props = React.JSX.LibraryManagedAttributes>; + const HOC = (props: Props) => { return ( {contextConfig => ( show({ ...config, contextConfig }), }} diff --git a/components/drawer/types.ts b/components/drawer/types.ts index 333f3f45dc..60ff31a272 100644 --- a/components/drawer/types.ts +++ b/components/drawer/types.ts @@ -1,114 +1,318 @@ -import React, { ReactNode } from 'react'; -import { PopupProps } from '../overlay'; -import { CloseMode } from '../dialog'; -import { CommonProps } from '../util'; -import { ComponentLocaleObject } from '../locale/types'; +import type React from 'react'; +import type { PopupProps } from '../overlay'; +import type { CloseMode } from '../dialog'; +import type { CommonProps } from '../util'; +import type { ComponentLocaleObject } from '../locale/types'; interface HTMLAttributesWeak extends PopupProps {} -export interface DrawerProps +/** + * @api Drawer + */ +export interface DrawerV1Props extends Omit, CommonProps { /** - * [废弃]同closeMode, 控制对话框关闭的方式,值可以为字符串或者布尔值,其中字符串是由以下值组成: - * **mask** 表示点击遮罩区域可以关闭对话框 - * **esc** 表示按下 esc 键可以关闭对话框 - * 如 'mask' 或 'esc,mask' - * 如果设置为 true,则以上关闭方式全部生效 - * 如果设置为 false,则以上关闭方式全部失效 - * + * [废弃]同closeMode, 控制对话框关闭的方式, + * @en [Deprecated] Control the way the drawer is closed * @deprecated 由于设计变更,该属性已被弃用。请使用 `closeMode` 属性来控制对话框关闭的方式。 + * @defaultValue true + * @remarks + * 值可以为字符串或者布尔值,其中字符串是由以下值组成: + * **close** 表示点击关闭按钮可以关闭对话框, + * **mask** 表示点击遮罩区域可以关闭对话框, + * **esc** 表示按下 esc 键可以关闭对话框, + * 如 'mask' 或 'esc,mask', + * 如果设置为 true,则以上关闭方式全部生效, + * 如果设置为 false,则以上关闭方式全部失效。 + * - + * The value can be a string or a Boolean value, where the string is composed of the following values: + * **close** (Click the close button to close the drawer), + * **mask** (Click the mask area to close the drawer), + * **esc** (Press the esc key to close the drawer), + * For example: 'close' or 'close,esc,mask', [], + * If set to true, the above close modes are all effective, + * If set to false, the above close modes are all invalid. */ closeable?: 'close' | 'mask' | 'esc' | boolean | 'close,mask' | 'close,esc' | 'mask,esc'; /** - * [推荐]控制对话框关闭的方式,值可以为字符串或者数组,其中字符串、数组均为以下值的枚举: - * **close** 表示点击关闭按钮可以关闭对话框 - * **mask** 表示点击遮罩区域可以关闭对话框 - * **esc** 表示按下 esc 键可以关闭对话框 - * 如 'close' 或 ['close','esc','mask'], [] + * [推荐]控制对话框关闭的方式 + * @en Control the way the dialog is closed + * @version 1.21 + * @remarks + * 值可以为字符串或者数组,其中字符串、数组均为以下值的枚举: + * **close** 表示点击关闭按钮可以关闭对话框, + * **mask** 表示点击遮罩区域可以关闭对话框, + * **esc** 表示按下 esc 键可以关闭对话框, + * 如 'close' 或 ['close','esc','mask'], []。 + * - + * The value can be a string or array, where the string and array are enumerated values of the following: + * **close** (Click the close button to close the dialog), + * **mask** (Click the mask area to close the dialog), + * **esc** (Press the esc key to close the dialog), + * For example: 'close' or ['close','esc','mask'], []. */ closeMode?: CloseMode[] | 'close' | 'mask' | 'esc'; /** * 隐藏时是否保留子节点,不销毁 + * @en Whether to retain the child node when hiding */ cache?: boolean; /** * 标题 + * @en Title */ title?: React.ReactNode; /** * body上的样式 + * @en Style on body */ bodyStyle?: React.CSSProperties; + /** + * header上的样式 + * @en Style on header + */ headerStyle?: React.CSSProperties; /** - * 显示隐藏时动画的播放方式。`animation` 对象包含两个属性: `in` 和 `out`。 + * 显示隐藏时动画的播放方式 + * @en Animation playback method when showing and hiding + * @defaultValue \{ in: 'expandInDown', out: 'expandOutUp' \} + * @remarks + * `animation` 对象包含两个属性: `in` 和 `out`。 * - `in`: 进场动画 * - `out`: 出场动画 - * * @param animation - 指定进场和出场动画的对象。 */ - animation?: { in: string; out: string } | boolean; + animation?: { in: string; out: string } | false; + /** + * 是否显示 + * @en Whether to show + */ visible?: boolean; - /** * 宽度,仅在 placement是 left right 的时候生效 + * @en Width, only effective when placement is left right */ width?: number | string; - /** * 高度,仅在 placement是 top bottom 的时候生效 + * @en Height, only effective when placement is the top bottom */ height?: number | string; /** * [v2 废弃] 受控模式下(没有 trigger 的时候),只会在关闭时触发,相当于onClose - * @deprecated 该属性在v2版本已被废弃,不再推荐使用。请改用 `onClose` 事件处理器来处理关闭事件。 + * @en [v2 Deprecated] Controlled mode (without trigger), only triggered when closed, equivalent to onClose + * @remarks + * 该属性在v2版本已被废弃,不再推荐使用。 + * 请改用 `onClose` 事件处理器来处理关闭事件。 + * - + * This attribute has been deprecated in version v2 and is no longer recommended for use. + * Please use the 'onClose' event handler to handle the shutdown event instead. + * @deprecated v2 废弃 - v2 deprecated */ onVisibleChange?: (visible: boolean, reason: string, e?: React.MouseEvent) => void; + /** + * 对话框关闭时触发的回调函数 + * @en Callback when the dialog is closed + * @defaultValue `() => {}` + */ + onClose?: (reason: string, e: React.MouseEvent | KeyboardEvent) => void; + /** + * 位于页面的位置 + * @en The position of the page + * @defaultValue 'right' + */ + placement?: 'top' | 'right' | 'bottom' | 'left'; + /** + * 开启v2 + * @en Enable v2 version + * @defaultValue false + */ + v2?: false | undefined; + /** + * 内容 + * @en Content + */ + content?: React.ReactNode; + /** + * 渲染组件的容器 + * @en Render component container + * @remarks + * 如果是函数需要返回 ref, + * 如果是字符串则是该 DOM 的 id, + * 也可以直接传入 DOM 节点。 + * - + * If it is a function, it needs to return ref, + * if it is a string, it is the id of the DOM, + * or you can directly pass in DOM nodes + */ + popupContainer?: string | HTMLElement | null; + /** + * 是否显示遮罩 + * @en Whether there is a mask + * @defaultValue true + */ + hasMask?: boolean; + /** + * [v2 废弃]对话框打开后的回调函数 + * @en Callback after the dialog is opened + * @deprecated v2 废弃 - v2 deprecated + */ + afterOpen?: () => void; +} + +/** + * @api Drawer V2 + */ +export interface DrawerV2Props + extends Omit, + CommonProps { + /** + * [废弃]同closeMode, 控制对话框关闭的方式, + * @en [Deprecated] Control the way the drawer is closed + * @deprecated 由于设计变更,该属性已被弃用。请使用 `closeMode` 属性来控制对话框关闭的方式。 + * @defaultValue true + * @remarks + * 值可以为字符串或者布尔值,其中字符串是由以下值组成: + * **close** 表示点击关闭按钮可以关闭对话框, + * **mask** 表示点击遮罩区域可以关闭对话框, + * **esc** 表示按下 esc 键可以关闭对话框, + * 如 'mask' 或 'esc,mask', + * 如果设置为 true,则以上关闭方式全部生效, + * 如果设置为 false,则以上关闭方式全部失效。 + * - + * The value can be a string or a Boolean value, where the string is composed of the following values: + * **close** (Click the close button to close the drawer), + * **mask** (Click the mask area to close the drawer), + * **esc** (Press the esc key to close the drawer), + * For example: 'close' or 'close,esc,mask', [], + * If set to true, the above close modes are all effective, + * If set to false, the above close modes are all invalid. + */ + closeable?: 'close' | 'mask' | 'esc' | boolean | 'close,mask' | 'close,esc' | 'mask,esc'; + /** + * [推荐]控制对话框关闭的方式 + * @en Control the way the dialog is closed + * @version 1.21 + * @remarks + * 值可以为字符串或者数组,其中字符串、数组均为以下值的枚举: + * **close** 表示点击关闭按钮可以关闭对话框, + * **mask** 表示点击遮罩区域可以关闭对话框, + * **esc** 表示按下 esc 键可以关闭对话框, + * 如 'close' 或 ['close','esc','mask'], []。 + * - + * The value can be a string or array, where the string and array are enumerated values of the following: + * **close** (Click the close button to close the dialog), + * **mask** (Click the mask area to close the dialog), + * **esc** (Press the esc key to close the dialog), + * For example: 'close' or ['close','esc','mask'], []. + */ + closeMode?: CloseMode[] | 'close' | 'mask' | 'esc'; + /** + * 隐藏时是否保留子节点,不销毁 + * @en Whether to retain the child node when hiding + */ + cache?: boolean; + /** + * 标题 + * @en Title + */ + title?: React.ReactNode; + /** + * body上的样式 + * @en Style on body + */ + bodyStyle?: React.CSSProperties; + /** + * header上的样式 + * @en Style on header + */ + headerStyle?: React.CSSProperties; + /** + * 显示隐藏时动画的播放方式 + * @en Animation playback method when showing and hiding + * @defaultValue \{ in: 'expandInDown', out: 'expandOutUp' \} + * @remarks + * `animation` 对象包含两个属性: `in` 和 `out`。 + * - `in`: 进场动画 + * - `out`: 出场动画 + * @param animation - 指定进场和出场动画的对象。 + */ + animation?: { in: string; out: string } | false; + /** + * 是否显示 + * @en Whether to show + */ + visible?: boolean; + /** + * 宽度,仅在 placement是 left right 的时候生效 + * @en Width, only effective when placement is left right + */ + width?: number | string; + /** + * 高度,仅在 placement是 top bottom 的时候生效 + * @en Height, only effective when placement is the top bottom + */ + height?: number | string; /** * [v2] 弹窗关闭后的回调 + * @en Callback after the dialog is closed */ afterClose?: () => void; + /** + * 对话框关闭时触发的回调函数 + * @en Callback when the dialog is closed + * @defaultValue `() => {}` + */ onClose?: (reason: string, e: React.MouseEvent | KeyboardEvent) => void; /** * 位于页面的位置 + * @en The position of the page + * @defaultValue 'right' */ placement?: 'top' | 'right' | 'bottom' | 'left'; /** - * 开启v2版本 + * 开启v2 + * @en Enable v2 version + * @defaultValue false */ v2?: boolean; - /** * 内容 + * @en Content */ content?: React.ReactNode; /** - * 渲染组件的容器,如果是函数需要返回 ref,如果是字符串则是该 DOM 的 id,也可以直接传入 DOM 节点 + * 渲染组件的容器 + * @en Render component container + * @remarks + * 如果是函数需要返回 ref, + * 如果是字符串则是该 DOM 的 id, + * 也可以直接传入 DOM 节点。 + * - + * If it is a function, it needs to return ref, + * if it is a string, it is the id of the DOM, + * or you can directly pass in DOM nodes */ popupContainer?: string | HTMLElement | null; + /** + * 是否显示遮罩 + * @en Whether there is a mask + * @defaultValue true + */ + hasMask?: boolean; } -export interface QuickShowRet { - hide: () => void; -} +export type DrawerProps = DrawerV2Props | DrawerV1Props; export interface InnerProps extends Omit { prefix?: string; - closeable?: 'close' | 'mask' | 'esc' | boolean | 'close,mask' | 'close,esc' | 'mask,esc'; className?: string | undefined; role?: string; - title?: ReactNode | undefined; - placement?: 'top' | 'right' | 'bottom' | 'left'; rtl?: boolean | undefined; onClose?: (e: React.MouseEvent) => void; locale?: ComponentLocaleObject | undefined; - headerStyle?: React.CSSProperties | undefined; - bodyStyle?: React.CSSProperties | undefined; - afterClose?: () => void; beforeOpen?: () => void; beforeClose?: () => void; - cache?: boolean; shouldUpdatePosition?: boolean; - v2?: boolean; } diff --git a/components/field/__tests__/index-spec.tsx b/components/field/__tests__/index-spec.tsx index 403cc2813d..f2782db107 100644 --- a/components/field/__tests__/index-spec.tsx +++ b/components/field/__tests__/index-spec.tsx @@ -320,6 +320,15 @@ describe('field', () => { cy.then(() => { cy.wrap(field.getValue('input')).should('eq', 'test!'); }); + + cy.mount(); + cy.get('input').type('test'); + + cy.wrap(onChange).should('be.calledWith', 'input', 'test!'); + cy.wrap(getValueFromEvent).should('be.calledWith', 'test'); + cy.then(() => { + cy.wrap(field.getValue('input')).should('eq', 'test!'); + }); }); it('getValueFormatter & setValueFormatter', () => { const field = new Field( @@ -346,6 +355,12 @@ describe('field', () => { cy.then(() => { cy.wrap(field.getValue('input')).should('eq', 'test!'); }); + + cy.mount(); + cy.get('input').type('test'); + cy.then(() => { + cy.wrap(field.getValue('input')).should('eq', 'test!'); + }); }); it('rules', () => { diff --git a/components/field/__tests__/options-spec.tsx b/components/field/__tests__/options-spec.tsx index 16e68a5e77..5fe989f801 100644 --- a/components/field/__tests__/options-spec.tsx +++ b/components/field/__tests__/options-spec.tsx @@ -825,6 +825,14 @@ describe('options', () => { field.validateCallback('input'); cy.wrap(field.getError('input')).should('not.be.null'); }); + + cy.mount(); + cy.get('input').type('test'); + cy.then(() => { + cy.wrap(field.getError('input')).should('be.null'); + field.validateCallback('input'); + cy.wrap(field.getError('input')).should('not.be.null'); + }); }); }); }); diff --git a/components/mixin-ui-state/index.tsx b/components/mixin-ui-state/index.tsx index bb0d056a9d..397e364cf7 100644 --- a/components/mixin-ui-state/index.tsx +++ b/components/mixin-ui-state/index.tsx @@ -1,15 +1,15 @@ import React, { Component, - HTMLAttributes, - DetailedReactHTMLElement, - ReactHTMLElement, - ReactSVGElement, - DOMElement, - DOMAttributes, - FunctionComponentElement, - CElement, - ComponentState, - ReactElement, + type HTMLAttributes, + type DetailedReactHTMLElement, + type ReactHTMLElement, + type ReactSVGElement, + type DOMElement, + type DOMAttributes, + type FunctionComponentElement, + type CElement, + type ComponentState, + type ReactElement, } from 'react'; import classnames from 'classnames'; import { func } from '../util'; @@ -26,8 +26,8 @@ type ClonableElement

= | ReactElement

; export interface UIStateProps { - onFocus?: () => unknown; - onBlur?: () => unknown; + onFocus?: (...rest: unknown[]) => unknown; + onBlur?: (...rest: unknown[]) => unknown; } export interface UIStateState { diff --git a/components/notification/__tests__/index-spec.js b/components/notification/__tests__/index-spec.ts similarity index 69% rename from components/notification/__tests__/index-spec.js rename to components/notification/__tests__/index-spec.ts index 051ad94d7d..dc3c471d2e 100644 --- a/components/notification/__tests__/index-spec.js +++ b/components/notification/__tests__/index-spec.ts @@ -1,14 +1,7 @@ -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 Notification from '../index'; -Enzyme.configure({ adapter: new Adapter() }); - describe('notification', () => { - const delay = time => new Promise(resolve => setTimeout(resolve, time)); + const delay = (time: number | undefined) => new Promise(resolve => setTimeout(resolve, time)); afterEach(() => { Notification.destroy(); @@ -45,10 +38,7 @@ describe('notification', () => { content: '嘿嘿', }); - const dom = document.querySelector('.next-message'); - - assert(!!dom); - + cy.get('.next-message').should('exist'); Notification.close(key); }); }); diff --git a/components/notification/config.js b/components/notification/config.ts similarity index 100% rename from components/notification/config.js rename to components/notification/config.ts diff --git a/components/notification/index.d.ts b/components/notification/index.d.ts deleted file mode 100644 index a7c52c2ab4..0000000000 --- a/components/notification/index.d.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ReactNode, MouseEventHandler, CSSProperties } from 'react'; - -export interface NotificationConfig { - offset?: [number, number]; - maxCount?: number; - size?: 'large' | 'medium'; - duration?: number; - getContainer?: () => HTMLElement; - placement?: 'topRight' | 'topLeft' | 'bottomLeft' | 'bottomRight'; -} - -export interface NotificationOptions { - key?: string; - type?: 'success' | 'error' | 'warning' | 'notice' | 'help'; - title?: ReactNode; - content?: ReactNode; - icon?: string; - duration?: number; - onClick?: MouseEventHandler; - style?: CSSProperties; - className?: string; - onClose?: () => void; -} - -export default class Notification { - static config: (config: NotificationConfig) => NotificationConfig; - static open: (options: NotificationOptions) => string; - static close: (key: string) => void; - static destroy: () => void; - static success: (options: NotificationOptions) => string; - static error: (options: NotificationOptions) => string; - static warning: (options: NotificationOptions) => string; - static notice: (options: NotificationOptions) => string; - static help: (options: NotificationOptions) => string; -} diff --git a/components/notification/index.jsx b/components/notification/index.tsx similarity index 65% rename from components/notification/index.jsx rename to components/notification/index.tsx index 11e144462e..7141d9a195 100644 --- a/components/notification/index.jsx +++ b/components/notification/index.tsx @@ -7,7 +7,13 @@ import Message from '../message'; import uuid from '../util/guid'; import config from './config'; -const getAnimation = placement => { +import type { + NotificationConfig, + NotificationOptions, + Notification as INotification, +} from './types'; + +const getAnimation = (placement: string) => { switch (placement) { case 'tl': case 'bl': @@ -16,11 +22,33 @@ const getAnimation = placement => { case 'br': return 'slideInRight'; default: - return null; + return undefined; } }; -class Notification extends Component { +interface NotificationProps { + prefix?: string; +} + +interface NotificationState { + notifications: NotificationOptions[]; +} + +// let instance: Notification; +let instance: InstanceType | null; +let mounting = false; +let waitOpens: NotificationOptions[] = []; +function close(key: string) { + if (!instance) { + const index = waitOpens.findIndex(item => item.key === key); + waitOpens.splice(index, 1); + return; + } + + instance.close(key); +} + +class Notification extends Component { static propTypes = { prefix: PropTypes.string, }; @@ -28,8 +56,9 @@ class Notification extends Component { static defaultProps = { prefix: 'next-', }; + timers: number[]; - constructor(props) { + constructor(props: NotificationProps) { super(props); this.state = { notifications: [], @@ -44,7 +73,7 @@ class Notification extends Component { }); } - close = key => { + close = (key: string) => { const { notifications } = this.state; const index = notifications.findIndex(notification => notification.key === key); @@ -72,7 +101,7 @@ class Notification extends Component { } }; - open = ({ key, duration, ...others }) => { + open = ({ key, duration, ...others }: NotificationOptions) => { const notifications = [...this.state.notifications]; if (!key) { key = uuid('notification-'); @@ -88,9 +117,9 @@ class Notification extends Component { } else { let timer; - if (duration > 0) { - timer = setTimeout(() => { - this.close(key); + if (duration && duration > 0) { + timer = window.setTimeout(() => { + this.close(key!); }, duration); this.timers.push(timer); } @@ -104,7 +133,7 @@ class Notification extends Component { if (config.maxCount > 0 && config.maxCount < notifications.length) { while (notifications.length > config.maxCount) { const { key } = notifications[0]; - this.close(key); + this.close(key!); notifications.splice(0, 1); } } @@ -136,25 +165,27 @@ class Notification extends Component { }} singleMode={false} > - {notifications.map(({ key, type, title, content, icon, onClick, style, className }) => ( - close(key)} - > - {content} - - ))} + {notifications.map( + ({ key, type, title, content, icon, onClick, style, className }) => ( + close(key!)} + > + {content} + + ) + )}

); @@ -164,14 +195,12 @@ class Notification extends Component { const ConfigedNotification = ConfigProvider.config(Notification, { exportNames: ['open', 'close'], }); -let instance; -let mounting = false; -let waitOpens = []; -function open(options = {}) { +function open(options: NotificationOptions = {}) { if (!options.title && !options.content) return; - const duration = !options.duration && options.duration !== 0 ? config.duration : options.duration; + const duration = + !options.duration && options.duration !== 0 ? config.duration : options.duration; if (!instance) { if (!options.key) { @@ -193,6 +222,8 @@ function open(options = {}) { document.body.appendChild(div); } + // 类型提示使用 createRoot,考虑到兼容性,暂时不处理 + // eslint-disable-next-line react/no-deprecated ReactDOM.render( , div, () => { - waitOpens.forEach(item => instance.open(item)); + waitOpens.forEach(item => instance!.open(item)); waitOpens = []; mounting = false; } @@ -221,35 +252,30 @@ function open(options = {}) { return key; } -function close(key) { - if (!instance) { - const index = waitOpens.findIndex(item => item.key === key); - waitOpens.splice(index, 1); - return; - } - - instance.close(key); -} - function destroy() { if (!instance) return; - const mountNode = ReactDOM.findDOMNode(instance).parentNode; + const mountNode = ReactDOM.findDOMNode(instance)?.parentNode; if (mountNode) { - ReactDOM.unmountComponentAtNode(mountNode); - mountNode.parentNode.removeChild(mountNode); + // eslint-disable-next-line react/no-deprecated + ReactDOM.unmountComponentAtNode(mountNode as Element); + mountNode.parentNode?.removeChild(mountNode); } } -const levels = {}; +interface objectAny { + [key: string]: () => string | undefined; +} +const levels: objectAny = {}; ['success', 'error', 'warning', 'notice', 'help'].forEach(type => { levels[type] = (options = {}) => { return open({ ...options, - type, + type: type as NotificationOptions['type'], }); }; }); + export default { config(...args) { return Object.assign(config, ...args); @@ -258,4 +284,6 @@ export default { close, destroy, ...levels, -}; +} as INotification; + +export type { NotificationConfig, NotificationOptions }; diff --git a/components/notification/mobile/index.jsx b/components/notification/mobile/index.tsx similarity index 82% rename from components/notification/mobile/index.jsx rename to components/notification/mobile/index.tsx index 6470b69733..9894a4346e 100644 --- a/components/notification/mobile/index.jsx +++ b/components/notification/mobile/index.tsx @@ -1,3 +1,4 @@ +// @ts-expect-error meet 未导出 Notification import { Notification as MeetNotification } from '@alifd/meet-react'; import NextNotification from '../index'; diff --git a/components/notification/style.js b/components/notification/style.ts similarity index 100% rename from components/notification/style.js rename to components/notification/style.ts diff --git a/components/notification/types.ts b/components/notification/types.ts new file mode 100644 index 0000000000..d1ac2f220e --- /dev/null +++ b/components/notification/types.ts @@ -0,0 +1,124 @@ +import type { ReactNode, MouseEventHandler, CSSProperties } from 'react'; + +/** + * @api Notification.Config + * @order 1 + */ +export interface NotificationConfig { + /** + * 对齐之后的偏移 [x, y] + * @en Offset after align, [x, y] + * @defaultValue [30, 30] + */ + offset?: [number, number]; + /** + * 最多同时出现的个数, 默认不限制 + * @en Max visible count, default Infinity + * @defaultValue - + */ + maxCount?: number; + /** + * 使用 `Message` 组件的 + * @en Use `Message` size prop + * @defaultValue large + */ + size?: 'large' | 'medium'; + /** + * 默认自动关闭延时,单位毫秒 + * @en Time in seconds before Notification is closed. When set to 0 or null, it will never be closed automatically + * @defaultValue 4500 + */ + duration?: number; + /** + * 配置渲染节点的输出位置 + * @en Return the mount node for Notification + * @defaultValue `() => document.body` + */ + getContainer?: () => HTMLElement; + /** + * 弹出位置,可选 `tl` `tr` `bl` `br` + * @en Position of Notification, can be one of `tl` `tr` `bl` `br` + * @defaultValue topRight + */ + placement?: 'topRight' | 'topLeft' | 'bottomLeft' | 'bottomRight'; +} + +/** + * @api NotificationOptions + * @order 2 + */ +export interface NotificationOptions { + /** + * 当前通知唯一标志, 默认会自动生成 + * @en The unique identifier of the Notification + * @defaultValue - + */ + key?: string; + /** + * 通知类型,`Notification.open`可选参数,可选值:`success`\|`error`\|`warning`\|`notice`\|`help` + * @en Notification type, `Notification.open` optional parameter, optional value: `success`\|`error`\|`warning`\|`notice`\|`help` + * @defaultValue `success` + */ + type?: 'success' | 'error' | 'warning' | 'notice' | 'help'; + /** + * 通知提醒标题 + * @en The title of notification box + */ + title?: ReactNode; + /** + * 通知提醒内容 + * @en The content of notification box + */ + content?: ReactNode; + /** + * 自定义图标 + * @en Customized icon + */ + icon?: string; + /** + * 默认 4.5 秒后自动关闭,配置为 0 则不自动关闭(单位毫秒) + * @en Time in seconds before Notification is closed. When set to 0 or null, it will never be closed automatically + * @defaultValue 4500 + */ + duration?: number; + /** + * 自定义内联样式 [详见](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/e434515761b36830c3e58a970abf5186f005adac/types/react/index.d.ts#L794) + * @en Customized inline style + */ + style?: CSSProperties; + /** + * 自定义 CSS class + * @en Customized CSS class + */ + className?: string; + /** + * 点击默认关闭按钮时触发的回调函数 + * @en Specify a function that will be called when the close button is clicked + */ + onClose?: () => void; + /** + * 点击通知时触发的回调函数 + * @en Specify a function that will be called when the notification is clicked + */ + onClick?: MouseEventHandler; + /** + * @skip + */ + timer?: number; +} + +/** + * @api Notification + * @order 0 + */ +export interface Notification { + config: (config: NotificationConfig) => NotificationConfig; + open: (options: NotificationOptions) => string; + close: (key: string) => void; + destroy: () => void; + success: (options: NotificationOptions) => string; + error: (options: NotificationOptions) => string; + warning: (options: NotificationOptions) => string; + notice: (options: NotificationOptions) => string; + help: (options: NotificationOptions) => string; +} diff --git a/components/overlay/index.d.ts b/components/overlay/index.d.ts index 6ed2123a9f..3c3d415bd3 100644 --- a/components/overlay/index.d.ts +++ b/components/overlay/index.d.ts @@ -202,9 +202,7 @@ export interface PopupProps extends React.HTMLAttributes, CommonPro placement?: string; } -export class Popup extends React.Component { - static propTypes: object; -} +export class Popup extends React.Component {} export interface OverlayProps extends React.HTMLAttributes, CommonProps { /** * 弹层内容 diff --git a/components/search/__docs__/demo/base/index.tsx b/components/search/__docs__/demo/base/index.tsx index ca907faace..51551d85d3 100644 --- a/components/search/__docs__/demo/base/index.tsx +++ b/components/search/__docs__/demo/base/index.tsx @@ -8,13 +8,13 @@ function onSearch(v: string) { ReactDOM.render(
-

simple

, - , -

default

, - , -

custom text

, - , -

custom text widthout icon

, +

simple

+ +

default

+ +

custom text

+ +

custom text widthout icon

, /** * 键盘上下键切换菜单高亮选项的回调 */ - onToggleHighlightItem?: () => void; + onToggleHighlightItem?: (highlightKey?: unknown, type?: unknown) => void; /** * 是否开启虚拟滚动模式 diff --git a/components/step/__docs__/adaptor/index.jsx b/components/step/__docs__/adaptor/index.jsx deleted file mode 100644 index d6ae66e7a3..0000000000 --- a/components/step/__docs__/adaptor/index.jsx +++ /dev/null @@ -1,139 +0,0 @@ -import React from 'react'; -import { Types, parseData, NodeType, ContentType } from '@alifd/adaptor-helper'; -import { Step } from '@alifd/next'; - -const _propsValue = ({ shape, level, location }) => { - return { - shape: shape, - direction: level, - labelPlacement: location === 'right' ? 'hoz' : 'ver', - }; -}; - -export default { - name: 'Step', - shape: ['circle', 'arrow', 'dot'], - editor: (shape) => { - if (shape === 'arrow') { - return { - props: [{ - name: 'state', - type: Types.enum, - options: ['normal', 'disabled'], - default: 'normal' - }, { - name: 'width', - type: Types.number, - default: 500 - }], - data: { - active: true, - disabled: true, - default: 'Step 1\nStep 2\n*Step 3\nStep 4' - } - }; - } - - return { - props: [{ - name: 'level', - label: 'Orientation', - type: Types.enum, - options: [{ value: 'hoz', label: 'Horizontal' }, { value: 'ver', label: 'Vertical' }] - }, - ...( - shape === 'circle' ? [{ - name: 'state', - type: Types.enum, - options: ['normal', 'percent', 'disabled'], - default: 'normal' - }, { - name: 'location', - type: Types.enum, - options: ['down', 'right'], - default: 'down' - - }] : [{ - name: 'state', - label: 'Status', - type: Types.enum, - options: ['normal', 'disabled'], - default: 'normal' - }] - ), { - name: 'width', - type: Types.number, - default: 600 - }, { - name: 'height', - type: Types.number, - default: 300 - }], - data: { - active: true, - disabled: true, - default: 'Step 1\n\tOpen the door Put the elephant into the fridge\n*Step 2\n\tOpen the door Put the elephant into the fridge\nStep 3\n\tOpen the door Put the elephant into the fridge\nStep 4\n\tOpen the door Put the elephant into the fridge' - } - }; - }, - propsValue:_propsValue, - adaptor: ({ shape, level, state, location, width, height, data, style, ...others }) => { - const list = parseData(data, { parseContent: true }).filter(({ type }) => type === NodeType.node); - const dataSouce = []; - let current = 0; - list.forEach((item, index) => { - const { value = '' } = item.value.find(({ type }) => type === ContentType.icon) || {}; - dataSouce.push({ - key: index, - icon: value, - title: item.value.filter(({ type }) => type === ContentType.text).map(({ value }) => value).join(''), - content: item.children && item.children.length > 0 ? item.children[0].value.filter(({ type }) => type === ContentType.text).map(({ value }) => value).join('') : '', - disabled: state === 'disabled' || item.state === 'disabled', - }); - - if (item.state === 'active') { - current = index; - } - }); - - if (state === 'percent' && dataSouce[current]) { - dataSouce[current].percent = 60; - } - - return ( -
- - { - dataSouce.map(item => ) - } - -
- ); - }, - demoOptions: (demo) => { - if (demo.node.props.level === 'hoz') { - demo.node.props = { - ...demo.node.props, - width: 600, - height: 120 - }; - } else if (demo.node.props.level === 'ver') { - demo.node.props = { - ...demo.node.props, - width: 200, - height: 300 - }; - } - - return demo; - } -}; diff --git a/components/step/__docs__/adaptor/index.tsx b/components/step/__docs__/adaptor/index.tsx new file mode 100644 index 0000000000..0108c0b383 --- /dev/null +++ b/components/step/__docs__/adaptor/index.tsx @@ -0,0 +1,178 @@ +import React from 'react'; +import { Types, parseData, NodeType, ContentType } from '@alifd/adaptor-helper'; +import { Step } from '@alifd/next'; +import type { IContent } from '@alifd/adaptor-helper/types/parse-data'; +import type { ItemProps, StepShape } from '../../types'; + +const _propsValue = ({ shape, level, location }: any) => { + return { + shape: shape, + direction: level, + labelPlacement: location === 'right' ? 'hoz' : 'ver', + }; +}; + +export default { + name: 'Step', + shape: ['circle', 'arrow', 'dot'], + editor: (shape: StepShape) => { + if (shape === 'arrow') { + return { + props: [ + { + name: 'state', + type: Types.enum, + options: ['normal', 'disabled'], + default: 'normal', + }, + { + name: 'width', + type: Types.number, + default: 500, + }, + ], + data: { + active: true, + disabled: true, + default: 'Step 1\nStep 2\n*Step 3\nStep 4', + }, + }; + } + + return { + props: [ + { + name: 'level', + label: 'Orientation', + type: Types.enum, + options: [ + { value: 'hoz', label: 'Horizontal' }, + { value: 'ver', label: 'Vertical' }, + ], + }, + ...(shape === 'circle' + ? [ + { + name: 'state', + type: Types.enum, + options: ['normal', 'percent', 'disabled'], + default: 'normal', + }, + { + name: 'location', + type: Types.enum, + options: ['down', 'right'], + default: 'down', + }, + ] + : [ + { + name: 'state', + label: 'Status', + type: Types.enum, + options: ['normal', 'disabled'], + default: 'normal', + }, + ]), + { + name: 'width', + type: Types.number, + default: 600, + }, + { + name: 'height', + type: Types.number, + default: 300, + }, + ], + data: { + active: true, + disabled: true, + default: + 'Step 1\n\tOpen the door Put the elephant into the fridge\n*Step 2\n\tOpen the door Put the elephant into the fridge\nStep 3\n\tOpen the door Put the elephant into the fridge\nStep 4\n\tOpen the door Put the elephant into the fridge', + }, + }; + }, + propsValue: _propsValue, + adaptor: ({ shape, level, state, location, width, height, data, style, ...others }: any) => { + const list = parseData(data, { parseContent: true }).filter( + ({ type }) => type === NodeType.node + ); + const dataSouce: (ItemProps & { key: number })[] = []; + let current = 0; + list.forEach((item, index) => { + const { value = '' } = + (item.value as IContent[]).find(({ type }) => type === ContentType.icon) || {}; + dataSouce.push({ + key: index, + icon: value, + title: (item.value as IContent[]) + .filter(({ type }) => type === ContentType.text) + .map(({ value }) => value) + .join(''), + content: + item.children && item.children.length > 0 + ? (item.children[0].value as IContent[]) + .filter(({ type }) => type === ContentType.text) + .map(({ value }) => value) + .join('') + : '', + disabled: state === 'disabled' || item.state === 'disabled', + }); + + if (item.state === 'active') { + current = index; + } + }); + + if (state === 'percent' && dataSouce[current]) { + dataSouce[current].percent = 60; + } + + return ( +
+ + {dataSouce.map(item => ( + + ))} + +
+ ); + }, + demoOptions: (demo: any) => { + if (demo.node.props.level === 'hoz') { + demo.node.props = { + ...demo.node.props, + width: 600, + height: 120, + }; + } else if (demo.node.props.level === 'ver') { + demo.node.props = { + ...demo.node.props, + width: 200, + height: 300, + }; + } + + return demo; + }, +}; diff --git a/components/step/__docs__/demo/basic/index.tsx b/components/step/__docs__/demo/basic/index.tsx index c060f90c4d..86aa5d7b5e 100644 --- a/components/step/__docs__/demo/basic/index.tsx +++ b/components/step/__docs__/demo/basic/index.tsx @@ -8,7 +8,7 @@ const steps = [ ['Step 3', 'Close the refrigerator door'], ].map((item, index) => ( ; +interface IState { + currentStep: number; + stepType: StepShape; + stepAnimation: boolean; + labelPlacement: StepDirection; + itemRender?: StepProps['itemRender']; + content?: string; +} const StepItem = Step.Item, ButtonGroup = Button.Group; -const renders = { +const renders: Record = { 1: function itemRender1(index) { return (
@@ -27,8 +38,8 @@ const contents = [ 'Description3', ]; -class Component extends React.Component { - constructor(props) { +class Component extends React.Component { + constructor(props: unknown) { super(props); this.state = { @@ -54,23 +65,23 @@ class Component extends React.Component { currentStep: s < 0 ? 0 : s, }); } - onClick(currentStep) { + onClick(currentStep: number) { console.log(currentStep); this.setState({ currentStep: currentStep, }); } - onStepTypeChange(value) { + onStepTypeChange(value: StepShape) { this.setState({ stepType: value }); } - onStepAnimation(value) { + onStepAnimation(value: boolean) { this.setState({ stepAnimation: value }); } - onLabelPlacementChange(value) { + onLabelPlacementChange(value: StepDirection) { this.setState({ labelPlacement: value }); } - onItemRenderChange(value) { + onItemRenderChange(value: number) { this.setState({ itemRender: renders[value], content: contents[value] }); } render() { diff --git a/components/step/__docs__/demo/custom-step-item/index.tsx b/components/step/__docs__/demo/custom-step-item/index.tsx index 23a83bfe02..fdd42fd16b 100644 --- a/components/step/__docs__/demo/custom-step-item/index.tsx +++ b/components/step/__docs__/demo/custom-step-item/index.tsx @@ -4,7 +4,7 @@ import { Step, Icon } from '@alifd/next'; const steps = ['one', 'two', 'three', 'four']; -function itemRender(index) { +function itemRender(index: number) { return (
{index + 1} @@ -12,7 +12,7 @@ function itemRender(index) { ); } -function itemRender2(index, status) { +function itemRender2(index: number, status: string) { return (
{status === 'finish' ? : {index + 1}}{' '} diff --git a/components/step/__docs__/demo/direction/index.tsx b/components/step/__docs__/demo/direction/index.tsx index 9b07f233c2..c5426435ce 100644 --- a/components/step/__docs__/demo/direction/index.tsx +++ b/components/step/__docs__/demo/direction/index.tsx @@ -1,22 +1,18 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState } from 'react'; import ReactDOM from 'react-dom'; import { Button, Step, Radio } from '@alifd/next'; +import type { StepDirection } from '../../../types'; const Step1Content = () => { - const [conditions, setconditions] = useState([]); + const [conditions, setconditions] = useState([]); const createNewSelectItem = () => { - const newType = { - type: 'null', - fieldName: 'null', - }; - const newConditions = [...conditions, newType]; - setconditions(newConditions); + setconditions(pre => pre.concat(1)); }; return (
{conditions && conditions.length > 0 && - conditions.map((item, index) => ( + conditions.map((_item, index) => (
{
); }; -const CreateDemo = () => { - const [conditions, setconditions] = useState([]); - const [direction, setDirection] = useState('ver'); - const createNewSelectItem = () => { - const newType = { - type: 'null', - fieldName: 'null', - }; - const newConditions = [...conditions, newType]; - setconditions(newConditions); - }; - console.log('direction: ', direction); + +const App = () => { + const [direction, setDirection] = useState('ver'); return (
setDirection(value)} > - hoz - ver + hoz + ver @@ -63,4 +50,5 @@ const CreateDemo = () => {
); }; -ReactDOM.render(, mountNode); + +ReactDOM.render(, mountNode); diff --git a/components/step/__docs__/demo/read-only/index.tsx b/components/step/__docs__/demo/read-only/index.tsx index a55ba0424a..eeab47a8d6 100644 --- a/components/step/__docs__/demo/read-only/index.tsx +++ b/components/step/__docs__/demo/read-only/index.tsx @@ -1,11 +1,16 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Step, Button } from '@alifd/next'; +import { StepProps } from '@alifd/meet-react/lib/step'; const StepItem = Step.Item, ButtonGroup = Button.Group; -class Component extends React.Component { - constructor(props) { + +type stateType = { + currentStep: number; +}; +class Component extends React.Component { + constructor(props: StepProps) { super(props); this.state = { @@ -26,7 +31,7 @@ class Component extends React.Component { currentStep: s < 0 ? 0 : s, }); } - onClick(currentStep) { + onClick(currentStep: number) { console.log(currentStep); this.setState({ diff --git a/components/step/__docs__/demo/step-process-failed/index.tsx b/components/step/__docs__/demo/step-process-failed/index.tsx index 2fa546f543..b72f66baf6 100644 --- a/components/step/__docs__/demo/step-process-failed/index.tsx +++ b/components/step/__docs__/demo/step-process-failed/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { Step, Box, Icon } from '@alifd/next'; +import { Step, Icon } from '@alifd/next'; ReactDOM.render(
@@ -9,7 +9,7 @@ ReactDOM.render( In Progress} content={download image failed} - itemRender={(index, status) => { + itemRender={() => { return (

**optional**:
'circle', 'arrow', 'dot' | Enum | 'circle' | -| direction | dispaly direction

**optional**:
'horizontal', 'vertical' | Enum | 'horizontal' | -| labelPlacement | Content arrangement in horizontal layout

**optional**:
'horizontal', 'vertical' | Enum | 'vertical' | -| readOnly | enable read-only mode | Boolean | - | -| animation | enable animation | Boolean | true | -| itemRender | custom node render function

**Function signature**:
Function(index: Number, status: String) => Node | Function | - | -| stretch | stretch width of step item | Boolean | false | +| Param | Description | Type | Default Value | Required | +| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------- | -------- | +| current | Current step | number | 0 | | +| shape | Shape | StepShape | 'circle' | | +| direction | Direction | StepDirection | 'hoz' | | +| labelPlacement | Content arrangement in horizontal layout | StepDirection | 'ver' | | +| readOnly | Read only mode | boolean | - | | +| animation | Enable animation | boolean | true | | +| itemRender | Custom node render function

**signature**:
**params**:
_index_: Node index
_status_: Node status
_title_: Node title(only for `shape='circle'`)
_content_: Node content(only for `shape='circle'`)
**return**:
Render content of the node | (
index: number,
status: StepStatus,
title?: ReactNode,
content?: ReactNode
) => React.ReactNode | - | | +| stretch | Stretch the width | boolean | false | | ### Step.Item +| Param | Description | Type | Default Value | Required | +| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -------- | +| status | The status of a step, if not passed, is generated based on the current attribute of the outer Step | StepStatus | - | | +| title | Title | React.ReactNode | - | | +| icon | Icon | string | - | | +| content | Content for vertical content filling, invalid when shape is arrow | React.ReactNode | - | | +| itemRender | Custom node render function (it will overwirde Step's itemRender)

**signature**:
**params**:
_index_: Node index
_status_: Node status
_title_: Node title(only for `shape='circle'`)
_content_: Node content(only for `shape='circle'`)
**return**:
Render content of the node | (
index: number,
status: StepStatus,
title?: React.ReactNode,
content?: React.ReactNode
) => React.ReactNode | - | | +| percent | Percent | number | - | | +| disabled | Disabled | boolean | - | | +| onClick | Callback when clicking on the step

**signature**:
**params**:
_index_: Node index | (index: number) => unknown | - | | -| Param | Descripiton | Type | Default Value | -| -------------- | ------------------------------------------------------ | -------- | ------------ | -| status |The status of a step, if not passed, is generated based on the current attribute of the outer Step, with optional values `wait`, `process`, `finish`

**optional**:
'wait', 'process', 'finish' | Enum | - | -| title | title | ReactNode | - | -| percent | percent | Number | - | -| icon | icon | String | - | -| content | Content for vertical content filling | ReactNode | - | -| disabled | disable step node | Boolean | - | -| itemRender | custom node render function (it will overwirde Step's itemRender)
**Function signature**:
Function(index: Number, status: String) => Node | Function | - | -| onClick | the callback when click to step node

**Function signature**:
Function(index: Number) => void
**Parameters**:
_index_: {Number} node index | Function | () => { } | \ No newline at end of file +### StepDirection + +```typescript +export type StepDirection = 'hoz' | 'ver'; +``` + +### StepStatus + +```typescript +export type StepStatus = 'wait' | 'process' | 'finish'; +``` + +### StepShape + +```typescript +export type StepShape = 'circle' | 'arrow' | 'dot'; +``` diff --git a/components/step/__docs__/index.md b/components/step/__docs__/index.md index 58de373f6c..9388a6b5e9 100644 --- a/components/step/__docs__/index.md +++ b/components/step/__docs__/index.md @@ -17,35 +17,53 @@ ### Step -| 参数 | 说明 | 类型 | 默认值 | -| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------- | -| current | 当前步骤 | Number | 0 | -| shape | 类型

**可选值**:
'circle', 'arrow', 'dot' | Enum | 'circle' | -| direction | 展示方向

**可选值**:
'hoz', 'ver' | Enum | 'hoz' | -| labelPlacement | 横向布局时( direction 为 hoz )的内容排列

**可选值**:
'hoz', 'ver' | Enum | 'ver' | -| readOnly | 是否只读模式 | Boolean | - | -| animation | 是否开启动效 | Boolean | true | -| itemRender | StepItem 的自定义渲染

**签名**:
Function(index: Number, status: String) => Node
**参数**:
_index_: {Number} 节点索引
_status_: {String} 节点状态
**返回值**:
{Node} 节点的渲染结果
| Function | null | -| stretch | 宽度横向拉伸 | Boolean | false | +| 参数 | 说明 | 类型 | 默认值 | 是否必填 | +| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | -------- | -------- | +| current | 当前步骤 | number | 0 | | +| shape | 类型 | StepShape | 'circle' | | +| direction | 展示方向 | StepDirection | 'hoz' | | +| labelPlacement | 横向布局时的内容排列方式 | StepDirection | 'ver' | | +| readOnly | 是否只读模式 | boolean | - | | +| animation | 是否开启动效 | boolean | true | | +| itemRender | 自定义渲染节点

**签名**:
**参数**:
_index_: 节点索引
_status_: 节点状态
_title_: 节点标题,仅在 `shape='circle'` 时会传递
_content_: 节点内容,仅在 `shape='circle'` 时会传递
**返回值**:
节点的渲染结果 | (
index: number,
status: StepStatus,
title?: ReactNode,
content?: ReactNode
) => React.ReactNode | - | | +| stretch | 宽度是否横向拉伸 | boolean | false | | ### Step.Item -| 参数 | 说明 | 类型 | 默认值 | -| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -------- | -| status | 步骤的状态,如不传,会根据外层的 Step 的 current 属性生成,可选值为 `wait`, `process`, `finish`

**可选值**:
'wait', 'process', 'finish' | Enum | - | -| title | 标题 | ReactNode | - | -| icon | 图标 | String | - | -| content | 内容填充, shape为 arrow 时无效 | ReactNode | - | -| itemRender | StepItem 的自定义渲染, 会覆盖父节点设置的itemRender

**签名**:
Function(index: Number, status: String) => Node
**参数**:
_index_: {Number} 节点索引
_status_: {String} 节点状态
**返回值**:
{Node} 节点的渲染结果
| Function | - | -| percent | 百分比 | Number | - | -| disabled | 是否禁用 | Boolean | - | -| onClick | 点击步骤时的回调

**签名**:
Function(index: Number) => void
**参数**:
_index_: {Number} 节点索引 | Function | () => {} | +| 参数 | 说明 | 类型 | 默认值 | 是否必填 | +| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------ | -------- | +| status | 步骤的状态,如不传,会根据外层的 Step 的 current 属性生成 | StepStatus | - | | +| title | 标题 | React.ReactNode | - | | +| icon | 图标 | string | - | | +| content | 内容填充,shape 为 arrow 时无效 | React.ReactNode | - | | +| itemRender | StepItem 的自定义渲染,会覆盖父节点设置的 itemRender

**签名**:
**参数**:
_index_: 节点索引
_status_: 节点状态
_title_: 节点标题,仅在 `shape='circle'` 时会传递
_content_: 节点内容,仅在 `shape='circle'` 时会传递
**返回值**:
节点的渲染结果 | (
index: number,
status: StepStatus,
title?: React.ReactNode,
content?: React.ReactNode
) => React.ReactNode | - | | +| percent | 百分比 | number | - | | +| disabled | 是否禁用 | boolean | - | | +| onClick | 点击步骤时的回调

**签名**:
**参数**:
_index_: 节点索引 | (index: number) => unknown | - | | + +### StepDirection + +```typescript +export type StepDirection = 'hoz' | 'ver'; +``` + +### StepStatus + +```typescript +export type StepStatus = 'wait' | 'process' | 'finish'; +``` + +### StepShape + +```typescript +export type StepShape = 'circle' | 'arrow' | 'dot'; +``` ## FAQ ### 为什么设置 Step 的展示方向不生效? -`Step`组件有三种类型(shape) `shape?: 'arrow' | 'circle' | 'dot';`, 其中: +`Step`组件有三种类型(shape) `shape?: 'arrow' | 'circle' | 'dot';`, 其中: - `shape='arrow'` 只有一种模式; - `shape='circle'` 有两种方向; diff --git a/components/step/__docs__/theme/index.jsx b/components/step/__docs__/theme/index.jsx deleted file mode 100644 index c8ffca1cf7..0000000000 --- a/components/step/__docs__/theme/index.jsx +++ /dev/null @@ -1,327 +0,0 @@ -import React from 'react'; -import { Demo, DemoGroup, initDemo } from '../../../demo-helper'; -import Step from '../../index'; -import '../../style'; -import '../../../demo-helper/style'; - -const StepItem = Step.Item; -const parseBool = str => { - switch (str) { - case 'true': - return true; - case 'false': - return false; - default: - return true; - } -}; -const i18nMap = { - 'zh-cn': { - basic: '基本', - vertical: '垂直', - horizontal: '水平', - verticalGroup: '垂直', - horizontalGroup: '水平', - percent: '百分比', - circle: '圆型', - arrow: '箭头型', - dot: '点型', - disabled: '禁用', - stepOne: '步骤一', - stepTwo: '步骤二', - stepThree: '步骤三', - stepFour: '步骤四', - stepOneContent: '打开冰箱门', - stepTwoContent: '把大象塞进去', - stepThreeContent: '关上冰箱门' - }, - 'en-us': { - basic: 'Basic', - vertical: 'Portrait', - horizontal: 'Landscape', - verticalGroup: 'Vertical', - horizontalGroup: 'Horizontal', - percent: 'Percent', - circle: 'Circle', - arrow: 'Arrow', - dot: 'Dot', - disabled: 'Disabled', - stepOne: 'Step 1', - stepTwo: 'Step 2', - stepThree: 'Step 3', - stepFour: 'Step 4', - stepOneContent: 'Open the door,', - stepTwoContent: 'put the elephant into the fridge', - stepThreeContent: 'close the door' - } -}; - -class FunctionDemo extends React.Component { - constructor(props) { - super(props); - this.state = { - circleDdemoFunction: { - icon: { - label: '图标', - value: 'false', - enum: [{ value: 'true', label: '显示' }, { value: 'false', label: '隐藏' }] - }, - content: { - label: '内容', - value: 'true', - enum: [{ value: 'true', label: '显示' }, { value: 'false', label: '隐藏' }], - }, - contentPos: { - label: '内容位置', - value: 'ver', - enum: [{ value: 'ver', label: '节点下方' }, { value: 'hoz', label: '节点右侧' }], - } - }, - dotDemoFunction: { - icon: { - label: '图标', - value: 'true', - enum: [{ value: 'true', label: '显示' }, { value: 'false', label: '隐藏' }] - }, - content: { - label: '内容', - value: 'true', - enum: [{ value: 'true', label: '显示' }, { value: 'false', label: '隐藏' }], - } - } - }; - } - - onFunctionChange = (ret) => { - this.setState({ - demoFunction: ret, - }); - } - - itemRender = ({ flag, index, ...props }) => { - !flag.withIcon && delete props.icon; - !flag.withContent && delete props.content; - // !flag.withProgress && delete props.percent; - return ; - } - render() { - const { title, i18n, itemData, shape } = this.props; - let demoFunction, flag, result; - if (shape === 'circle') { - demoFunction = this.state.circleDdemoFunction; - flag = { - withIcon: parseBool(demoFunction.icon.value), - labelPlacement: demoFunction.contentPos.value, - withContent: parseBool(demoFunction.content.value), - }; - } else { - demoFunction = this.state.dotDemoFunction; - flag = { - withIcon: parseBool(demoFunction.icon.value), - // withProgress: typeof demoFunction[1].value === 'boolean' ? demoFunction[1].value : parseBool(demoFunction[1].value), - withContent: parseBool(demoFunction.content.value), - }; - } - if (shape === 'arrow') { - result = ( - - - - { - itemData.horizontal.map((v, index) => this.itemRender({ flag, index, ...v })) - } - - - - - { - itemData.disabled.map((v, index) => this.itemRender({ flag, index, ...v })) - } - - - - - ); - } else if (shape === 'dot') { - result = ( - - - - { - itemData.horizontal.map((v, index) => this.itemRender({ flag, index, ...v })) - } - - - - - { - itemData.disabled.map((v, index) => this.itemRender({ flag, index, ...v })) - } - - - - - - - { - itemData.horizontal.map((v, index) => this.itemRender({ flag, index, ...v })) - } - - - - - { - itemData.disabled.map((v, index) => this.itemRender({ flag, index, ...v })) - } - - - - ); - } else { - result = ( - - - - { - itemData.horizontal.map((v, index) => this.itemRender({ flag, index, ...v })) - } - - - - - { - itemData.percent.map((v, index) => this.itemRender({ flag, index, ...v })) - } - - - - - { - itemData.disabled.map((v, index) => this.itemRender({ flag, index, ...v })) - } - - - - - - - { - itemData.horizontal.map((v, index) => this.itemRender({ flag, index, ...v })) - } - - - - - { - itemData.percent.map((v, index) => this.itemRender({ flag, index, ...v })) - } - - - - - { - itemData.disabled.map((v, index) => this.itemRender({ flag, index, ...v })) - } - - - - ); - } - return result; - } -} - -/* eslint-disable */ -function render(lang) { - const i18n = i18nMap[lang]; - const horizontal = [{ - title: i18n.stepOne, - content: `${i18n.stepOneContent} ${i18n.stepTwoContent}` - }, { - title: i18n.stepTwo, - content: `${i18n.stepOneContent} ${i18n.stepTwoContent}`, - icon: 'atm' - }, { - title: i18n.stepThree, - content: `${i18n.stepOneContent} ${i18n.stepTwoContent}`, - }, { - title: i18n.stepFour, - content: `${i18n.stepOneContent} ${i18n.stepTwoContent}`, - } - ], - percent = [{ - title: i18n.stepOne, - content: `${i18n.stepOneContent} ${i18n.stepTwoContent}` - }, { - title: i18n.stepTwo, - content: `${i18n.stepOneContent} ${i18n.stepTwoContent}`, - icon: 'atm' - }, { - title: i18n.stepThree, - content: `${i18n.stepOneContent} ${i18n.stepTwoContent}`, - percent: 60 - }, { - title: i18n.stepFour, - content: `${i18n.stepOneContent} ${i18n.stepTwoContent}`, - } - ], - disabled = [{ - title: i18n.stepOne, - content: `${i18n.stepOneContent} ${i18n.stepTwoContent}`, - disabled: true - }, { - title: i18n.stepTwo, - content: `${i18n.stepOneContent} ${i18n.stepTwoContent}`, - icon: 'atm', - disabled: true - }, { - title: i18n.stepThree, - content: `${i18n.stepOneContent} ${i18n.stepTwoContent}`, - disabled: true - }, { - title: i18n.stepFour, - content: `${i18n.stepOneContent} ${i18n.stepTwoContent}`, - disabled: true - } - ], - arrowData = { - horizontal: [{ - title: i18n.stepOne - }, { - title: i18n.stepTwo - }, { - title: i18n.stepThree - }, { - title: i18n.stepFour - }], - disabled: [{ - title: i18n.stepOne, - disabled: true - }, { - title: i18n.stepTwo, - disabled: true - }, { - title: i18n.stepThree, - disabled: true - }, { - title: i18n.stepFour, - disabled: true - } - ] - } - return ReactDOM.render( -
- - - -
, document.getElementById('container')); -} - -window.renderDemo = function (lang) { - lang = lang || 'en-us' - render(lang) -}; - -renderDemo(); - -initDemo('step'); diff --git a/components/step/__docs__/theme/index.tsx b/components/step/__docs__/theme/index.tsx new file mode 100644 index 0000000000..2aae6611b7 --- /dev/null +++ b/components/step/__docs__/theme/index.tsx @@ -0,0 +1,418 @@ +import React, { type ReactNode } from 'react'; +import ReactDOM from 'react-dom'; +import { Demo, DemoGroup, initDemo, DemoFunctionDefineForObject } from '../../../demo-helper'; +import Step from '../../index'; +import type { StepDirection, ItemProps, StepShape } from '../../types'; +import '../../style'; +import '../../../demo-helper/style'; + +const StepItem = Step.Item; +const parseBool = (str: unknown) => { + switch (str) { + case 'true': + return true; + case 'false': + return false; + default: + return true; + } +}; +const i18nMap = { + 'zh-cn': { + basic: '基本', + vertical: '垂直', + horizontal: '水平', + verticalGroup: '垂直', + horizontalGroup: '水平', + percent: '百分比', + circle: '圆型', + arrow: '箭头型', + dot: '点型', + disabled: '禁用', + stepOne: '步骤一', + stepTwo: '步骤二', + stepThree: '步骤三', + stepFour: '步骤四', + stepOneContent: '打开冰箱门', + stepTwoContent: '把大象塞进去', + stepThreeContent: '关上冰箱门', + }, + 'en-us': { + basic: 'Basic', + vertical: 'Portrait', + horizontal: 'Landscape', + verticalGroup: 'Vertical', + horizontalGroup: 'Horizontal', + percent: 'Percent', + circle: 'Circle', + arrow: 'Arrow', + dot: 'Dot', + disabled: 'Disabled', + stepOne: 'Step 1', + stepTwo: 'Step 2', + stepThree: 'Step 3', + stepFour: 'Step 4', + stepOneContent: 'Open the door,', + stepTwoContent: 'put the elephant into the fridge', + stepThreeContent: 'close the door', + }, +}; + +interface FunctionDemoProps { + title: string; + i18n: Record; + itemData: Record; + shape: StepShape; +} + +type DemoFunctionMap = Record; + +interface FunctionDemoState { + circleDdemoFunction: DemoFunctionMap; + dotDemoFunction: DemoFunctionMap; + demoFunction?: DemoFunctionMap; +} + +interface Flag { + withIcon: boolean; + withContent: boolean; + labelPlacement?: StepDirection; +} + +class FunctionDemo extends React.Component { + constructor(props: FunctionDemoProps) { + super(props); + this.state = { + circleDdemoFunction: { + icon: { + label: '图标', + value: 'false', + enum: [ + { value: 'true', label: '显示' }, + { value: 'false', label: '隐藏' }, + ], + }, + content: { + label: '内容', + value: 'true', + enum: [ + { value: 'true', label: '显示' }, + { value: 'false', label: '隐藏' }, + ], + }, + contentPos: { + label: '内容位置', + value: 'ver', + enum: [ + { value: 'ver', label: '节点下方' }, + { value: 'hoz', label: '节点右侧' }, + ], + }, + }, + dotDemoFunction: { + icon: { + label: '图标', + value: 'true', + enum: [ + { value: 'true', label: '显示' }, + { value: 'false', label: '隐藏' }, + ], + }, + content: { + label: '内容', + value: 'true', + enum: [ + { value: 'true', label: '显示' }, + { value: 'false', label: '隐藏' }, + ], + }, + }, + }; + } + + onFunctionChange = (ret: DemoFunctionMap) => { + this.setState({ + demoFunction: ret, + }); + }; + + itemRender = ({ + flag, + index, + ...props + }: ItemProps & { + flag: Flag; + index: number; + }) => { + !flag.withIcon && delete props.icon; + !flag.withContent && delete props.content; + // !flag.withProgress && delete props.percent; + return ; + }; + render() { + const { title, i18n, itemData, shape } = this.props; + let demoFunction: DemoFunctionMap, flag: Flag, result: ReactNode; + if (shape === 'circle') { + demoFunction = this.state.circleDdemoFunction; + flag = { + withIcon: parseBool(demoFunction.icon.value), + labelPlacement: demoFunction.contentPos.value as StepDirection, + withContent: parseBool(demoFunction.content.value), + }; + } else { + demoFunction = this.state.dotDemoFunction; + flag = { + withIcon: parseBool(demoFunction.icon.value), + // withProgress: typeof demoFunction[1].value === 'boolean' ? demoFunction[1].value : parseBool(demoFunction[1].value), + withContent: parseBool(demoFunction.content.value), + }; + } + if (shape === 'arrow') { + result = ( + + + + + {itemData.horizontal.map((v, index) => + this.itemRender({ ...v, flag, index }) + )} + + + + + {itemData.disabled.map((v, index) => + this.itemRender({ ...v, flag, index }) + )} + + + + + ); + } else if (shape === 'dot') { + result = ( + + + + + {itemData.horizontal.map((v, index) => + this.itemRender({ ...v, flag, index }) + )} + + + + + {itemData.disabled.map((v, index) => + this.itemRender({ ...v, flag, index }) + )} + + + + + + + {itemData.horizontal.map((v, index) => + this.itemRender({ ...v, flag, index }) + )} + + + + + {itemData.disabled.map((v, index) => + this.itemRender({ ...v, flag, index }) + )} + + + + + ); + } else { + result = ( + + + + + {itemData.horizontal.map((v, index) => + this.itemRender({ ...v, flag, index }) + )} + + + + + {itemData.percent.map((v, index) => + this.itemRender({ ...v, flag, index }) + )} + + + + + {itemData.disabled.map((v, index) => + this.itemRender({ ...v, flag, index }) + )} + + + + + + + {itemData.horizontal.map((v, index) => + this.itemRender({ ...v, flag, index }) + )} + + + + + {itemData.percent.map((v, index) => + this.itemRender({ ...v, flag, index }) + )} + + + + + {itemData.disabled.map((v, index) => + this.itemRender({ ...v, flag, index }) + )} + + + + + ); + } + return result; + } +} + +function render(lang: 'zh-cn' | 'en-us') { + const i18n = i18nMap[lang]; + const horizontal = [ + { + title: i18n.stepOne, + content: `${i18n.stepOneContent} ${i18n.stepTwoContent}`, + }, + { + title: i18n.stepTwo, + content: `${i18n.stepOneContent} ${i18n.stepTwoContent}`, + icon: 'atm', + }, + { + title: i18n.stepThree, + content: `${i18n.stepOneContent} ${i18n.stepTwoContent}`, + }, + { + title: i18n.stepFour, + content: `${i18n.stepOneContent} ${i18n.stepTwoContent}`, + }, + ], + percent = [ + { + title: i18n.stepOne, + content: `${i18n.stepOneContent} ${i18n.stepTwoContent}`, + }, + { + title: i18n.stepTwo, + content: `${i18n.stepOneContent} ${i18n.stepTwoContent}`, + icon: 'atm', + }, + { + title: i18n.stepThree, + content: `${i18n.stepOneContent} ${i18n.stepTwoContent}`, + percent: 60, + }, + { + title: i18n.stepFour, + content: `${i18n.stepOneContent} ${i18n.stepTwoContent}`, + }, + ], + disabled = [ + { + title: i18n.stepOne, + content: `${i18n.stepOneContent} ${i18n.stepTwoContent}`, + disabled: true, + }, + { + title: i18n.stepTwo, + content: `${i18n.stepOneContent} ${i18n.stepTwoContent}`, + icon: 'atm', + disabled: true, + }, + { + title: i18n.stepThree, + content: `${i18n.stepOneContent} ${i18n.stepTwoContent}`, + disabled: true, + }, + { + title: i18n.stepFour, + content: `${i18n.stepOneContent} ${i18n.stepTwoContent}`, + disabled: true, + }, + ], + arrowData = { + horizontal: [ + { + title: i18n.stepOne, + }, + { + title: i18n.stepTwo, + }, + { + title: i18n.stepThree, + }, + { + title: i18n.stepFour, + }, + ], + disabled: [ + { + title: i18n.stepOne, + disabled: true, + }, + { + title: i18n.stepTwo, + disabled: true, + }, + { + title: i18n.stepThree, + disabled: true, + }, + { + title: i18n.stepFour, + disabled: true, + }, + ], + }; + // eslint-disable-next-line react/no-render-return-value + return ReactDOM.render( +
+ + + +
, + document.getElementById('container') + ); +} + +window.renderDemo = function (lang?: 'en-us' | 'zh-cn') { + lang = lang || 'en-us'; + render(lang); +}; + +renderDemo(); + +initDemo('step'); diff --git a/components/step/__tests__/a11y-spec.js b/components/step/__tests__/a11y-spec.tsx similarity index 82% rename from components/step/__tests__/a11y-spec.js rename to components/step/__tests__/a11y-spec.tsx index 2adaed7351..9e004e0d56 100644 --- a/components/step/__tests__/a11y-spec.js +++ b/components/step/__tests__/a11y-spec.tsx @@ -1,62 +1,44 @@ import React from 'react'; -import Enzyme from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; -import { unmount, testReact } from '../../util/__tests__/legacy/a11y/validate'; +import { testReact } from '../../util/__tests__/a11y/validate'; import Icon from '../../icon'; import Step from '../index'; import '../style'; -Enzyme.configure({ adapter: new Adapter() }); - const StepItem = Step.Item; -/* eslint-disable no-undef, react/jsx-filename-extension */ describe('Step A11y', () => { - let wrapper; - - afterEach(() => { - if (wrapper) { - wrapper.unmount(); - wrapper = null; - } - unmount(); - }); - it('should not have any violations', async () => { - wrapper = await testReact( + await testReact( ); - return wrapper; }); it('should not have any violations for element title', async () => { const title = 测试; - wrapper = await testReact( + await testReact( ); - return wrapper; }); it('should not have any violations for element content', async () => { const content = 测试; - wrapper = await testReact( + await testReact( ); - return wrapper; }); it('should not have any violations for different shapes', async () => { - wrapper = await testReact( + await testReact(
@@ -70,11 +52,10 @@ describe('Step A11y', () => {
); - return wrapper; }); it('should not have any violations for vertical direction', async () => { - wrapper = await testReact( + await testReact(
@@ -93,11 +74,10 @@ describe('Step A11y', () => {
); - return wrapper; }); it('should not have any violations for labelPlacement', async () => { - wrapper = await testReact( + await testReact(
@@ -111,11 +91,10 @@ describe('Step A11y', () => {
); - return wrapper; }); it('should not have any violations for icon', async () => { - wrapper = await testReact( + await testReact(
@@ -129,29 +108,27 @@ describe('Step A11y', () => {
); - return wrapper; }); it('should not have any violations for custom node', async () => { - function itemRender(index) { + function itemRender(index: number) { return (
{index + 1}
); } - wrapper = await testReact( + await testReact( ); - return wrapper; }); it('should not have any violations for progress', async () => { - wrapper = await testReact( + await testReact(
@@ -170,21 +147,19 @@ describe('Step A11y', () => {
); - return wrapper; }); it('should not have any violations for step.item with itemRender', async () => { const steps = ['one', 'two', 'three', 'four']; - const itemRender = (index, status) => { + const itemRender = (index: number, status: string) => { return status === 'finish' ? : index + 1; }; - wrapper = await testReact( + await testReact( {steps.map(item => ( ))} ); - return wrapper; }); }); diff --git a/components/step/__tests__/index-spec.js b/components/step/__tests__/index-spec.js deleted file mode 100644 index 6164135fa6..0000000000 --- a/components/step/__tests__/index-spec.js +++ /dev/null @@ -1,466 +0,0 @@ -import React, { useState } from 'react'; -import Enzyme, { mount } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; -import assert from 'power-assert'; -import Icon from '../../icon'; -import Button from '../../button/index'; -import Step from '../index'; -import { mountReact } from '../../util/__tests__/legacy/a11y/validate'; -import '../style'; - -Enzyme.configure({ adapter: new Adapter() }); - -const StepItem = Step.Item; -/* eslint-disable */ -describe('Step', () => { - describe('render', () => { - it('should support rtl', () => { - const wrapper = mount( - - - - - - ); - - assert( - wrapper - .find('.next-step-circle') - .at(0) - .instance() - .getAttribute('dir') === 'rtl' - ); - }); - it('should render with default props', () => { - const wrapper = mount( - - - - - - ); - - assert(wrapper.find('.next-step-circle').length === 1); - assert(wrapper.find('.next-step-horizontal').length === 1); - }); - - it('should render with element title', () => { - const title = 测试; - const wrapper = mount( - - - - - - ); - - assert( - wrapper - .find('.next-step-item-title') - .at(0) - .html() === '
测试
' - ); - }); - - it('should render with element content', () => { - const content = 测试; - const wrapper = mount( - - - - ); - - assert( - wrapper - .find('.next-step-item-content') - .at(0) - .html() === '
测试
' - ); - }); - - it('should render with diffrent types', () => { - const wrapper = mount( - - - - - - ); - const wrapper2 = mount( - - - - - - ); - assert(wrapper.find('.next-step-arrow').length === 1); - assert(wrapper2.find('.next-step-dot').length === 1); - }); - - it('should render with diffrent directions', () => { - const wrapper = mount( - - - - - - ); - const wrapper2 = mount( - - - - - - ); - const wrapper3 = mount( - - - - - - ); - - assert(wrapper.find('.next-step-vertical').length === 1); - assert(wrapper2.find('.next-step-vertical').length === 1); - assert(wrapper3.find('.next-step-vertical').length === 0); - }); - - it('should render with labelPlacement', () => { - const wrapper = mount( - - - - - - ); - const wrapper2 = mount( - - - - - - ); - assert(wrapper.find('.next-step-label-vertical').length === 1); - assert(wrapper2.find('.next-step-label-horizontal').length === 1); - }); - - it('should render when direction and labelPlacement are both hoz', () => { - const steps = ['知道自己不懂', '不知道自己懂', '知道自己懂了'].map((item, index) => ( - } /> - )); - const wrapper = mount( - - {steps} - - ); - assert(wrapper.find('.next-step-horizontal').length === 1); - assert(wrapper.find('.next-step-label-horizontal').length === 1); - }); - - it('should render with icon ', () => { - const wrapper = mount( - - - - - - ); - const wrapper2 = mount( - - - - - - ); - - assert(wrapper.find('.next-icon-atm').length === 1); - assert(wrapper2.find('.next-icon-atm').length === 1); - }); - - it('should render with custom node ', () => { - function itemRender(index, status) { - return ( -
- {index + 1} -
- ); - } - - const wrapper = mount( - - - - - - ); - - assert(wrapper.find('.custom-node').length === 3); - }); - - it('should render with progress ', () => { - const wrapper0 = mount( - - - - - - ); - const wrapper2 = mount( - - - - - - ); - const wrapper3 = mount( - - - - - - ); - assert(wrapper0.find('.next-progress-circle.next-step-item-progress').length === 1); - assert(wrapper2.find('.next-progress-circle.next-step-item-progress').length === 1); - assert(wrapper3.find('.next-progress-circle.next-step-item-progress').length === 0); - }); - - it('should render step.item with itemRender', () => { - const steps = ['one', 'two', 'three', 'four']; - const itemRender = (index, status) => { - return status === 'finish' ? : index + 1; - }; - const wrapper = mount( - - {steps.map(item => ( - - ))} - - ); - assert(wrapper.find('.next-icon-good').length === 2); - }); - it('should stretch work', () => { - const wrapper = mount( - - - - - - ); - - const item = wrapper.find('.next-step-item'); - assert( - item - .at(0) - .instance() - .style.width.startsWith('calc((100%') - ); - assert(item.at(2).instance().style.width === 'auto'); - }); - }); - - describe('action', () => { - it('should click step node with transitionEnd', () => { - const wrapper = mount( - - - - - - ); - wrapper - .find('.next-step-item-node') - .at(2) - .simulate('click'); - assert( - wrapper - .find('.next-step-item-node') - .at(2) - .instance().className === 'next-step-item-node clicked' - ); - - wrapper - .find('.next-step-item-node') - .at(2) - .simulate('transitionEnd'); - assert( - wrapper - .find('.next-step-item-node') - .at(2) - .instance().className === 'next-step-item-node' - ); - }); - - it('should change current step', () => { - const wrapper = mount( - - - - - - ); - assert(wrapper.props().current === 1); - - wrapper.setProps({ current: 2 }); - assert(wrapper.props().current === 2); - }); - it('should change labelPlacement', () => { - const wrapper = mount( - - - - - - ); - - assert(wrapper.find('.next-step-label-vertical').length === 1); - wrapper.setProps({ labelPlacement: 'hoz' }, () => { - assert(wrapper.find('.next-step-label-horizontal').length === 1); - // 横向模式下会调整 next-step-item-tail 的宽度值 - const $tail = wrapper.find('.next-step-item-tail'); - assert( - $tail.length === 3 && - $tail.at(0).instance().style.width !== '' && - $tail.at(2).instance().style.width === '' - ); - // 重新设置为垂直居中 应该去掉 next-step-item-tail 的宽度值 - wrapper.setProps({ labelPlacement: 'ver' }, () => { - assert($tail.length === 3 && $tail.at(0).instance().style.width === ''); - }); - }); - }); - - it('should trigger click event', () => { - let ret_1 = -1; - const wrapper = mount( - - (ret_1 = index)} /> - - - - ); - - wrapper - .find('.next-step-item-first') - .find('.next-step-item-node') - .simulate('click'); - assert(ret_1 === 0); - - let ret_2 = -1; - const wrapper2 = mount( - - (ret_2 = index)} /> - - - - ); - - wrapper2 - .find('.next-step-item-first') - .find('.next-step-item-node-placeholder') - .simulate('click'); - assert(ret_2 === 0); - }); - - it('should resize when content changed', async () => { - const StepContent = () => { - const [conditions, setconditions] = useState([]); - const createNewSelectItem = () => { - const newType = { - type: 'null', - fieldName: 'null', - }; - const newConditions = [...conditions, newType]; - setconditions(newConditions); - }; - return ( -
- {conditions && - conditions.length > 0 && - conditions.map((item, index) => ( -
- ))} - -
- ); - }; - const wrapper = await mountReact( - - } /> - - - - ); - - const originHeight = parseFloat( - wrapper - .find('.next-step') - .at(0) - .instance() - .style.height.slice(0, -2) - ); - wrapper.find('.next-btn').simulate('click'); - assert(document.querySelectorAll('[id^="step-content-"]').length === 1); - wrapper.find('.next-btn').simulate('click'); - - assert(document.querySelectorAll('[id^="step-content-"]').length === 2); - wrapper.setProps({ direction: 'ver' }); - wrapper.setProps({ direction: 'hoz' }); - const changedHeight = parseFloat( - wrapper - .find('.next-step') - .at(0) - .instance() - .style.height.slice(0, -2) - ); - assert(changedHeight > originHeight); - wrapper.setProps({ direction: 'ver' }); - wrapper.find('.next-btn').simulate('click'); - wrapper.setProps({ direction: 'hoz' }); - wrapper.setProps({ direction: 'ver' }); - }); - - // it('should trigger keyboard event', () => { - // const wrapper = mount( - // - // - // - // - // - // ); - - // wrapper - // .find('.next-step-item-first') - // .simulate('keydown', {keyCode: 40}); - - // assert(wrapper.find('.next-step-item-body').at(1).instance().getAttribute('tabindex')==='0'); - - // wrapper - // .find('.next-step-item-first') - // .simulate('keydown', {keyCode: 38}); - // assert(wrapper.find('.next-step-item-body').at(0).instance().getAttribute('tabindex')==='0'); - - // wrapper - // .find('.next-step-item-first') - // .simulate('keydown', {keyCode: 39}); - // assert(wrapper.find('.next-step-item-body').at(1).instance().getAttribute('tabindex')==='0'); - - // wrapper - // .find('.next-step-item-first') - // .simulate('keydown', {keyCode: 37}); - // assert(wrapper.find('.next-step-item-body').at(0).instance().getAttribute('tabindex')==='0'); - - // }); - }); -}); diff --git a/components/step/__tests__/index-spec.tsx b/components/step/__tests__/index-spec.tsx new file mode 100644 index 0000000000..c5f3ff6127 --- /dev/null +++ b/components/step/__tests__/index-spec.tsx @@ -0,0 +1,427 @@ +import React, { useState } from 'react'; +import Icon from '../../icon'; +import Button from '../../button/index'; +import Step from '../index'; +import '../style'; +import type { StepStatus } from '../types'; + +const StepItem = Step.Item; +describe('Step', () => { + describe('render', () => { + it('should support rtl', () => { + cy.mount( + + + + + + ); + cy.get('.next-step-circle').should('have.attr', 'dir', 'rtl'); + }); + it('should render with default props', () => { + cy.mount( + + + + + + ); + + cy.get('.next-step-circle').should('have.length', 1); + cy.get('.next-step-horizontal').should('have.length', 1); + }); + + it('should render with element title', () => { + const title = 测试; + cy.mount( + + + + + + ); + cy.get('.next-step-item-title').should('have.html', '测试'); + }); + + it('should render with element content', () => { + const content = 测试; + cy.mount( + + + + ); + cy.get('.next-step-item-content').should('have.html', '测试'); + }); + + it('should render with diffrent types', () => { + cy.mount( + + + + + + ); + cy.get('.next-step-arrow').should('have.length', 1); + cy.mount( + + + + + + ); + + cy.get('.next-step-dot').should('have.length', 1); + }); + + it('should render with diffrent directions', () => { + cy.mount( + + + + + + ); + cy.get('.next-step-vertical').should('have.length', 1); + cy.mount( + + + + + + ); + cy.get('.next-step-vertical').should('have.length', 1); + cy.mount( + + + + + + ); + cy.get('.next-step-vertical').should('have.length', 0); + }); + + it('should render with labelPlacement', () => { + cy.mount( + + + + + + ); + cy.get('.next-step-label-vertical').should('have.length', 1); + cy.mount( + + + + + + ); + cy.get('.next-step-label-horizontal').should('have.length', 1); + }); + + it('should render when direction and labelPlacement are both hoz', () => { + const steps = ['知道自己不懂', '不知道自己懂', '知道自己懂了'].map((item, index) => ( + } /> + )); + cy.mount( + + {steps} + + ); + cy.get('.next-step-horizontal').should('have.length', 1); + cy.get('.next-step-label-horizontal').should('have.length', 1); + }); + + it('should render with icon ', () => { + cy.mount( + + + + + + ); + cy.get('.next-icon-atm').should('have.length', 1); + cy.mount( + + + + + + ); + cy.get('.next-icon-atm').should('have.length', 1); + }); + + it('should render with custom node ', () => { + function itemRender(index: number) { + return ( +
+ {index + 1} +
+ ); + } + + cy.mount( + + + + + + ); + cy.get('.custom-node').should('have.length', 3); + }); + + it('should render with progress ', () => { + cy.mount( + + + + + + ); + cy.get('.next-progress-circle.next-step-item-progress').should('have.length', 1); + cy.mount( + + + + + + ); + cy.get('.next-progress-circle.next-step-item-progress').should('have.length', 1); + cy.mount( + + + + + + ); + cy.get('.next-progress-circle.next-step-item-progress').should('have.length', 0); + }); + + it('should render step.item with itemRender', () => { + const steps = ['one', 'two', 'three', 'four']; + const itemRender = (index: number, status: StepStatus) => { + return status === 'finish' ? : index + 1; + }; + cy.mount( + + {steps.map(item => ( + + ))} + + ); + cy.get('.next-icon-good').should('have.length', 2); + }); + + it('should stretch work', () => { + cy.mount( + + + + + + ); + cy.get('.next-step-item') + .eq(0) + .should('have.attr', 'style', 'width: calc(50% - 50px);'); + cy.get('.next-step-item').eq(2).should('have.attr', 'style', 'width: auto;'); + }); + }); + + describe('action', () => { + it('should click step node with transitionEnd', () => { + cy.mount( + + + + + + ); + cy.get('.next-step-item-node').eq(2).click(); + cy.get('.next-step-item-node') + .eq(2) + .should('have.attr', 'class', 'next-step-item-node clicked'); + + cy.get('.next-step-item-node').eq(2).trigger('transitionEnd'); + cy.get('.next-step-item-node') + .eq(2) + .should('have.attr', 'class', 'next-step-item-node'); + }); + + it('should change current step', () => { + cy.mount( + + + + + + ); + cy.get('.next-step-item') + .eq(0) + .should( + 'have.attr', + 'class', + 'next-step-item next-step-item-finish next-step-item-first' + ); + cy.get('.next-step-item') + .eq(1) + .should('have.attr', 'class', 'next-step-item next-step-item-process'); + + cy.mount( + + + + + + ); + + cy.get('.next-step-item') + .eq(2) + .should( + 'have.attr', + 'class', + 'next-step-item next-step-item-process next-step-item-last' + ); + }); + it('should change labelPlacement', () => { + cy.mount( + + + + + + ); + + cy.get('.next-step-label-vertical').should('have.length', 1); + cy.get('.next-step-item') + .eq(0) + .then(dom => { + const containerRect = dom + .find('.next-step-item-container') + .get(0) + .getBoundingClientRect(); + const bodyRect = dom + .find('.next-step-item-body') + .get(0) + .getBoundingClientRect(); + cy.wrap(bodyRect.top >= containerRect.top + containerRect.height).should( + 'be.true' + ); + }); + cy.mount( + + + + + + ); + + cy.get('.next-step-label-horizontal').should('have.length', 1); + cy.get('.next-step-item') + .eq(0) + .then(dom => { + const containerRect = dom + .find('.next-step-item-container') + .get(0) + .getBoundingClientRect(); + const bodyRect = dom + .find('.next-step-item-body') + .get(0) + .getBoundingClientRect(); + cy.wrap(bodyRect.top >= containerRect.top).should('be.true'); + cy.wrap(bodyRect.top < containerRect.top + containerRect.height).should( + 'be.true' + ); + }); + }); + + it('should trigger click event', () => { + const click = cy.spy(); + cy.mount( + + { + click(index); + }} + /> + + + + ); + cy.get('.next-step-item-first .next-step-item-node').trigger('click'); + cy.then(() => { + cy.wrap(click.callCount).should('eq', 1); + cy.wrap(click.firstCall).should('be.calledWith', 0); + }); + + cy.mount( + + { + click(index); + }} + /> + + + + ); + cy.get('.next-step-item-first .next-step-item-node-placeholder').trigger('click'); + cy.then(() => { + cy.wrap(click.callCount).should('eq', 2); + cy.wrap(click.secondCall).should('be.calledWith', 0); + }); + }); + + it('should resize when content changed', () => { + const StepContent = () => { + const [conditions, setConditions] = useState([]); + const createNewSelectItem = () => { + setConditions(pre => pre.concat(1)); + }; + return ( +
+ {conditions.map((_, index) => ( +
+ ))} + +
+ ); + }; + + cy.mount( + + } /> + + + + ); + + let originHeight: number | undefined; + cy.get('.next-step').then($step => { + originHeight = $step.outerHeight(); + cy.get('#add-content-btn').click(); + cy.get('[id^="step-content-"]').should('have.length', 1); + cy.get('#add-content-btn').click(); + cy.get('[id^="step-content-"]').should('have.length', 2); + }); + + cy.get('.next-step').then($step => { + const changedHeight = $step.outerHeight(); + cy.wrap(changedHeight).should('be.greaterThan', originHeight); + }); + }); + }); +}); diff --git a/components/step/index.d.ts b/components/step/index.d.ts deleted file mode 100644 index 23e7f98742..0000000000 --- a/components/step/index.d.ts +++ /dev/null @@ -1,106 +0,0 @@ -/// - -import React from 'react'; -import { CommonProps } from '../util'; - -interface HTMLAttributesWeak extends React.HTMLAttributes { - title?: any; - onClick?: any; -} - -export interface ItemProps extends Omit, CommonProps { - /** - * 步骤的状态,如不传,会根据外层的 Step 的 current 属性生成,可选值为 `wait`, `process`, `finish` - */ - status?: 'wait' | 'process' | 'finish'; - - /** - * 标题 - */ - title?: React.ReactNode; - - /** - * 图标 - */ - icon?: string; - - /** - * 内容,用于垂直状态下的内容填充 - */ - content?: React.ReactNode; - - /** - * StepItem 的自定义渲染, 会覆盖父节点设置的itemRender - */ - itemRender?: (index: number, status: string) => React.ReactNode; - - /** - * 百分比 - */ - percent?: number; - - /** - * 是否禁用 - */ - disabled?: boolean; - - /** - * 点击步骤时的回调 - */ - onClick?: (index: number) => void; - - /** - * 自定义样式 - */ - className?: string; -} - -export class Item extends React.Component {} -export interface StepProps extends React.HTMLAttributes, CommonProps { - /** - * 当前步骤 - */ - current?: number; - - /** - * 展示方向 - */ - direction?: 'hoz' | 'ver'; - /** - * 宽度是否横向拉伸 - */ - stretch?: boolean; - /** - * 横向布局时的内容排列 - */ - labelPlacement?: 'hoz' | 'ver'; - - /** - * 类型 - */ - shape?: 'circle' | 'arrow' | 'dot'; - - /** - * 是否只读模式 - */ - readOnly?: boolean; - - /** - * 是否开启动效 - */ - animation?: boolean; - - /** - * 自定义样式名 - */ - className?: string; - - /** - * StepItem 的自定义渲染 - */ - itemRender?: (index: number, status: string) => React.ReactNode; -} - -export default class Step extends React.Component { - static Item: typeof Item; -} diff --git a/components/step/index.jsx b/components/step/index.jsx deleted file mode 100644 index a6e35cb6f8..0000000000 --- a/components/step/index.jsx +++ /dev/null @@ -1,21 +0,0 @@ -import ConfigProvider from '../config-provider'; -import Step from './view/step'; -import StepItem from './view/step-item'; - -Step.Item = StepItem; - -export default ConfigProvider.config(Step, { - transform: /* istanbul ignore next */ (props, deprecated) => { - if ('type' in props) { - deprecated('type', 'shape', 'Step'); - - let { type, direction, labelPlacement, ...others } = props; - direction = direction === 'vertical' ? 'ver' : direction === 'horizontal' ? 'hoz' : direction; - labelPlacement = - labelPlacement === 'vertical' ? 'ver' : labelPlacement === 'horizontal' ? 'hoz' : labelPlacement; - props = { shape: type, direction, labelPlacement, ...others }; - } - - return props; - }, -}); diff --git a/components/step/index.tsx b/components/step/index.tsx new file mode 100644 index 0000000000..74d49b46b7 --- /dev/null +++ b/components/step/index.tsx @@ -0,0 +1,34 @@ +import ConfigProvider from '../config-provider'; +import Step from './view/step'; +import Item from './view/step-item'; +import type { StepProps, ItemProps, DeprecatedStepProps } from './types'; +import { assignSubComponent } from '../util/component'; + +export type { StepProps, ItemProps }; + +const WithStepItem = assignSubComponent(Step, { Item }); + +export default ConfigProvider.config(WithStepItem, { + transform: (props, deprecated) => { + if ('type' in props) { + deprecated('type', 'shape', 'Step'); + const { type, direction, labelPlacement, ...others } = props as DeprecatedStepProps; + const resolvedDirection = + direction === 'vertical' ? 'ver' : direction === 'horizontal' ? 'hoz' : direction; + const resolvedLabelPlacement = + labelPlacement === 'vertical' + ? 'ver' + : labelPlacement === 'horizontal' + ? 'hoz' + : labelPlacement; + props = { + shape: type, + direction: resolvedDirection, + labelPlacement: resolvedLabelPlacement, + ...others, + }; + } + + return props; + }, +}); diff --git a/components/step/mobile/index.jsx b/components/step/mobile/index.tsx similarity index 100% rename from components/step/mobile/index.jsx rename to components/step/mobile/index.tsx diff --git a/components/step/style.js b/components/step/style.ts similarity index 100% rename from components/step/style.js rename to components/step/style.ts diff --git a/components/step/types.ts b/components/step/types.ts new file mode 100644 index 0000000000..e798e8a5a7 --- /dev/null +++ b/components/step/types.ts @@ -0,0 +1,201 @@ +import React, { type ReactNode } from 'react'; +import { CommonProps } from '../util'; + +type HTMLAttributesWeak = Omit< + React.HTMLAttributes, + 'content' | 'onClick' | 'title' | 'direction' +>; + +/** + * @api + */ +export type StepDirection = 'hoz' | 'ver'; +/** + * @api + */ +export type StepStatus = 'wait' | 'process' | 'finish'; +/** + * @api + */ +export type StepShape = 'circle' | 'arrow' | 'dot'; + +/** + * Step.Item 继承自 Step 的私有的属性 + */ +export interface ItemPrivateProps { + index?: number; + shape?: StepShape; + direction?: StepDirection; + onResize?: () => void; + stretch?: boolean; + labelPlacement?: StepDirection; + readOnly?: boolean; + parentWidth?: number; + parentHeight?: number; + parentLeft?: number; + parentTop?: number; + parentRight?: number; + parentBottom?: number; + animation?: boolean; + current?: number; + total?: number; +} + +/** + * @api Step.Item + */ +export interface ItemProps + extends HTMLAttributesWeak, + CommonProps, + ItemPrivateProps { + /** + * 步骤的状态,如不传,会根据外层的 Step 的 current 属性生成 + * @en The status of a step, if not passed, is generated based on the current attribute of the outer Step + */ + status?: StepStatus; + + /** + * 标题 + * @en Title + */ + title?: React.ReactNode; + + /** + * 图标 + * @en Icon + */ + icon?: string; + + /** + * 内容填充,shape 为 arrow 时无效 + * @en Content for vertical content filling, invalid when shape is arrow + */ + content?: React.ReactNode; + + /** + * StepItem 的自定义渲染,会覆盖父节点设置的 itemRender + * @en Custom node render function (it will overwirde Step's itemRender) + * @param index - 节点索引 - node index + * @param status - 节点状态 - node status + * @param title - 节点标题,仅在 `shape='circle'` 时会传递 - node title(only for `shape='circle'`) + * @param content - 节点内容,仅在 `shape='circle'` 时会传递 - node content(only for `shape='circle'`) + * @returns 节点的渲染结果 - render content of the node + */ + itemRender?: ( + index: number, + status: StepStatus, + title?: React.ReactNode, + content?: React.ReactNode + ) => React.ReactNode; + + /** + * 百分比 + * @en Percent + */ + percent?: number; + + /** + * 是否禁用 + * @en disabled + */ + disabled?: boolean; + + /** + * 点击步骤时的回调 + * @en Callback when clicking on the step + * @param index - 节点索引 - node index + */ + onClick?: (index: number) => unknown; +} + +/** + * @api Step + */ +export interface StepProps + extends Omit, 'type' | 'direction'>, + CommonProps { + /** + * 当前步骤 + * @en Current step + * @defaultValue 0 + */ + current?: number; + + /** + * 类型 + * @en Shape + * @defaultValue 'circle' + */ + shape?: StepShape; + + /** + * 展示方向 + * @en Direction + * @defaultValue 'hoz' + */ + direction?: StepDirection; + + /** + * 横向布局时的内容排列方式 + * @en Content arrangement in horizontal layout + * @defaultValue 'ver' + */ + labelPlacement?: StepDirection; + + /** + * 是否只读模式 + * @en Read only mode + */ + readOnly?: boolean; + + /** + * 是否开启动效 + * @en Enable animation + * @defaultValue true + */ + animation?: boolean; + + /** + * 自定义渲染节点 + * @en Custom node render function + * @param index - 节点索引 - node index + * @param status - 节点状态 - node status + * @param title - 节点标题,仅在 `shape='circle'` 时会传递 - node title(only for `shape='circle'`) + * @param content - 节点内容,仅在 `shape='circle'` 时会传递 - node content(only for `shape='circle'`) + * @returns 节点的渲染结果 - render content of the node + */ + itemRender?: ( + index: number, + status: StepStatus, + title?: ReactNode, + content?: ReactNode + ) => React.ReactNode; + + /** + * 宽度是否横向拉伸 + * @en Stretch the width + * @defaultValue false + */ + stretch?: boolean; +} + +export interface StepState { + parentWidth: string | number; + parentHeight: string | number; + currentfocus: number; +} + +export interface DeprecatedStepProps extends Omit { + /** + * @deprecated Use shape instead + */ + type?: 'circle' | 'arrow' | 'dot'; + /** + * @deprecated Available enums: 'hoz' | 'ver' + */ + direction?: 'hoz' | 'ver' | 'horizontal' | 'vertical'; + /** + * @deprecated Available enums: 'hoz' | 'ver' + */ + labelPlacement?: 'hoz' | 'ver' | 'horizontal' | 'vertical'; +} diff --git a/components/step/view/step-item.jsx b/components/step/view/step-item.tsx similarity index 68% rename from components/step/view/step-item.jsx rename to components/step/view/step-item.tsx index c0feece6d0..ba795048a3 100644 --- a/components/step/view/step-item.jsx +++ b/components/step/view/step-item.tsx @@ -1,5 +1,5 @@ import ReactDOM from 'react-dom'; -import React, { Component } from 'react'; +import React, { Component, type ReactNode } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import ResizeObserver from 'resize-observer-polyfill'; @@ -7,60 +7,33 @@ import Icon from '../../icon'; import Progress from '../../progress'; import ConfigProvider from '../../config-provider'; import { support, events, dom, obj } from '../../util'; +import type { ItemProps, StepStatus } from '../types'; + +const getWidth = (el: HTMLElement) => dom.getStyle(el, 'width') as number; +const getHeight = (el: HTMLElement) => dom.getStyle(el, 'height') as number; /** Step.Item */ -class StepItem extends Component { +class StepItem extends Component { static propTypes = { ...ConfigProvider.propTypes, prefix: PropTypes.string, rtl: PropTypes.bool, - /** - * 步骤的状态,如不传,会根据外层的 Step 的 current 属性生成,可选值为 `wait`, `process`, `finish` - */ status: PropTypes.oneOf(['wait', 'process', 'finish']), - /** - * 标题 - */ title: PropTypes.node, direction: PropTypes.oneOf(['hoz', 'ver']), labelPlacement: PropTypes.oneOf(['hoz', 'ver']), shape: PropTypes.oneOf(['circle', 'arrow', 'dot']), - /** - * 图标 - */ icon: PropTypes.string, - /** - * 内容填充, shape为 arrow 时无效 - */ content: PropTypes.node, - /** - * StepItem 的自定义渲染, 会覆盖父节点设置的itemRender - * @param {Number} index 节点索引 - * @param {String} status 节点状态 - * @returns {Node} 节点的渲染结果 - */ itemRender: PropTypes.func, - /** - * 百分比 - */ percent: PropTypes.number, index: PropTypes.number, total: PropTypes.number, - animation: PropTypes.bool, // 是否开启动效,由父级传入 - /** - * 是否禁用 - */ + animation: PropTypes.bool, disabled: PropTypes.bool, parentWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), parentHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - /** - * 点击步骤时的回调 - * @param {Number} index 节点索引 - */ onClick: PropTypes.func, - /** - * 自定义样式 - */ className: PropTypes.string, readOnly: PropTypes.bool, onResize: PropTypes.func, @@ -75,7 +48,16 @@ class StepItem extends Component { stretch: false, }; - constructor(props) { + ro: ResizeObserver; + container: HTMLDivElement; + stepNode: HTMLDivElement; + step: HTMLLIElement; + body: HTMLDivElement; + title: HTMLDivElement; + tail: HTMLDivElement; + eventHandler: { off: () => void } | undefined; + + constructor(props: ItemProps) { super(props); this.removeClickedCls = this.removeClickedCls.bind(this); this._refHandlerCreator = this._refHandlerCreator.bind(this); @@ -85,7 +67,7 @@ class StepItem extends Component { if (!this.body || shape === 'arrow') { return; } - if (direction === 'vertical' || direction === 'ver') { + if (direction === 'ver') { this.resize(); } else { onResize && onResize(); @@ -95,23 +77,19 @@ class StepItem extends Component { componentDidMount() { const { shape, direction, labelPlacement, index, total, stretch } = this.props; - this.body && this.ro.observe(ReactDOM.findDOMNode(this.body)); + this.body && this.ro.observe(ReactDOM.findDOMNode(this.body) as HTMLDivElement); if (shape === 'arrow') { return; } - if (direction === 'vertical' || direction === 'ver') { + if (direction === 'ver') { this.resize(); - this.forceUpdate(); // 解决Step嵌套的情况下,嵌套节点宽度为0的问题 - this.eventHandler = events.on(window, 'resize', this.resize); // 调整垂直Step - } else if ( - (direction === 'horizontal' || direction === 'hoz') && - (labelPlacement === 'horizontal' || labelPlacement === 'hoz') && - index !== total - 1 - ) { - // 调整横向Content + this.forceUpdate(); // 解决 Step 嵌套的情况下,嵌套节点宽度为 0 的问题 + this.eventHandler = events.on(window, 'resize', this.resize); // 调整垂直 Step + } else if (direction === 'hoz' && labelPlacement === 'hoz' && index !== total! - 1) { + // 调整横向 Content this.adjustTail(); } - if (stretch && (direction === 'horizontal' || direction === 'hoz')) { + if (stretch && direction === 'hoz') { this.adjustItemWidth(); } } @@ -129,20 +107,16 @@ class StepItem extends Component { }); }; - if (direction === 'vertical' || direction === 'ver') { + if (direction === 'ver') { this.resize(); - } else if (direction === 'horizontal' || direction === 'hoz') { + } else if (direction === 'hoz') { const pos = rtl ? 'right' : 'left'; dom.setStyle(this.body, { width: '', [pos]: '', }); - if ( - shape === 'circle' && - (labelPlacement === 'horizontal' || labelPlacement === 'hoz') && - index !== total - 1 - ) { - // 调整横向Content + if (shape === 'circle' && labelPlacement === 'hoz' && index !== total! - 1) { + // 调整横向 Content this.adjustTail(); } else { resetTailStyle(); @@ -150,7 +124,7 @@ class StepItem extends Component { if (stretch) { this.adjustItemWidth(); } - } else if (index !== total - 1) { + } else if (index !== total! - 1) { resetTailStyle(); } } @@ -162,10 +136,11 @@ class StepItem extends Component { adjustItemWidth() { const { index, total, labelPlacement } = this.props; const lastNodeWidth = - labelPlacement === 'horizontal' || labelPlacement === 'hoz' + labelPlacement === 'hoz' ? this.container.offsetWidth + this.body.offsetWidth : this.title.offsetWidth; - const width = total - 1 !== index ? `calc((100% - ${lastNodeWidth}px)/${total - 1})` : 'auto'; + const width = + total! - 1 !== index ? `calc((100% - ${lastNodeWidth}px)/${total! - 1})` : 'auto'; dom.setStyle(this.step, { width, }); @@ -175,53 +150,65 @@ class StepItem extends Component { const width = this.container.offsetWidth + this.title.offsetWidth; dom.setStyle(this.tail, { width: `calc(100% - ${width}px)`, - top: `${dom.getStyle(this.container, 'height') / 2}px`, + top: `${getHeight(this.container) / 2}px`, }); } resize() { const { direction } = this.props; - if (direction === 'vertical' || direction === 'ver') { - const stepWidth = dom.getStyle(this.step, 'width'); - const stepHozWhitespace = dom.getNodeHozWhitespace(this.step.parentNode); + if (direction === 'ver') { + const stepWidth = getWidth(this.step); + const stepHozWhitespace = dom.getNodeHozWhitespace(this.step.parentNode as HTMLElement); const stepBodyHozWhitespace = dom.getNodeHozWhitespace(this.body); const { rtl } = this.props; - rtl ? (this.body.style.right = `${stepWidth}px`) : (this.body.style.left = `${stepWidth}px`); + rtl + ? (this.body.style.right = `${stepWidth}px`) + : (this.body.style.left = `${stepWidth}px`); dom.setStyle(this.body, { width: - dom.getStyle(this.step.parentNode.parentNode, 'width') - + getWidth(this.step.parentNode!.parentNode as HTMLElement) - stepWidth - stepHozWhitespace - stepBodyHozWhitespace, }); - dom.setStyle( - this.tail, - 'height', - dom.getStyle(this.body, 'height') - dom.getStyle(this.container, 'height') - ); + dom.setStyle(this.tail, 'height', getHeight(this.body) - getHeight(this.container)); } } _getNode() { const { prefix, index, status, icon, shape, percent, itemRender } = this.props; - let nodeElement = icon; + let nodeElement: ReactNode = icon; if (shape === 'dot') { - nodeElement = icon ? :
; + nodeElement = icon ? ( + + ) : ( +
+ ); } else if (shape === 'circle' && percent) { - nodeElement = ; + nodeElement = ( + + ); } else if (shape === 'circle' && !!itemRender && typeof itemRender === 'function') { nodeElement = null; // 如果是需要自定义节点,则不处理,返回空 } else { nodeElement = (
- {icon ? : this._itemRender(index, status)} + {icon ? : this._itemRender(index!, status!)}
); } return nodeElement; } - getNode(args) { + getNode(args: { + stepCls: string; + overlayCls: { width: string } | null; + others: Omit; + }) { const { prefix, itemRender, index, status, title, content, shape } = this.props; const { others, stepCls, overlayCls } = args; const nodeElement = this._getNode(); @@ -253,7 +240,7 @@ class StepItem extends Component { ref={this._refHandlerCreator('container')} >
- {itemRender(index, status, title, content)} + {itemRender!(index!, status!, title, content)}
); @@ -264,7 +251,12 @@ class StepItem extends Component { } return ( -
  • +
  • {finalNodeElement}
    -
    +
    {title}
    {content}
    + {/* @ts-expect-error overlayCls 可能是 null,跟 style 类型不匹配 */}
    @@ -288,15 +284,15 @@ class StepItem extends Component { getStyle() { const { parentWidth, parentHeight, direction, total, index, shape } = this.props; - let width = 'auto'; + let width: string | number = 'auto'; if (Number(parentWidth) && Number(parentHeight)) { if (!support.flex && shape === 'arrow') { - width = Math.floor(parentWidth / total - parentHeight / 2 - parentHeight / 8); + width = Math.floor(parentWidth! / total! - parentHeight! / 2 - parentHeight! / 8); } } - if (shape !== 'arrow' && (direction === 'horizontal' || direction === 'hoz')) { - width = total - 1 !== index ? `${Math.floor(100 / total)}%` : 'auto'; + if (shape !== 'arrow' && direction === 'hoz') { + width = total! - 1 !== index ? `${Math.floor(100 / total!)}%` : 'auto'; } return { width: width, @@ -314,7 +310,7 @@ class StepItem extends Component { ? dom.removeClass(this.stepNode, 'clicked') : dom.addClass(this.stepNode, 'clicked'); } - this.props.onClick(index); + this.props.onClick!(index!); }; removeClickedCls() { @@ -325,7 +321,7 @@ class StepItem extends Component { } // 节点的渲染方法 - _itemRender(index, status) { + _itemRender(index: number, status: StepStatus) { const { itemRender } = this.props; if (itemRender) { return itemRender(index, status); @@ -333,38 +329,17 @@ class StepItem extends Component { return status === 'finish' ? : index + 1; } - _refHandlerCreator(refName) { - const self = this; - return function(ref) { - self[refName] = ref; + _refHandlerCreator( + refName: Name + ) { + return (ref: this[Name]) => { + this[refName] = ref; }; } render() { - // eslint-disable-next-line - const { - prefix, - locale, - className, - status, - title, - icon, - index, - total, - shape, - content, - direction, - disabled, - onClick, - readOnly, - animation, - parentHeight, - itemRender, - parentWidth, - labelPlacement, - rtl, - onResize, - } = this.props; + const { prefix, className, status, title, index, total, shape, disabled, readOnly } = + this.props; const others = obj.pickOthers(StepItem.propTypes, this.props); @@ -372,10 +347,10 @@ class StepItem extends Component { [`${prefix}step-item`]: true, [`${prefix}step-item-${status}`]: status, [`${prefix}step-item-first`]: index === 0, - [`${prefix}step-item-last`]: index === total - 1, + [`${prefix}step-item-last`]: index === total! - 1, [`${prefix}step-item-disabled`]: disabled, [`${prefix}step-item-read-only`]: readOnly, - [className]: className, + [className!]: className, }); const overlayCls = status === 'finish' ? { width: '100%' } : null; diff --git a/components/step/view/step.jsx b/components/step/view/step.tsx similarity index 52% rename from components/step/view/step.jsx rename to components/step/view/step.tsx index a9c35214da..a18acdcf35 100644 --- a/components/step/view/step.jsx +++ b/components/step/view/step.tsx @@ -1,58 +1,29 @@ import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import classNames from 'classnames'; -import React, { Component, Children } from 'react'; +import React, { Component, Children, type ReactNode, type ReactElement } from 'react'; import { polyfill } from 'react-lifecycles-compat'; import ConfigProvider from '../../config-provider'; import { support, events, dom, obj } from '../../util'; +import type { StepProps, StepState } from '../types'; -const getHeight = el => dom.getStyle(el, 'height'); -const setHeight = (el, height) => dom.setStyle(el, 'height', height); +const getHeight = (el: HTMLElement) => dom.getStyle(el, 'height') as number; +const setHeight = (el: HTMLElement, height: number | string) => dom.setStyle(el, 'height', height); /** Step */ -class Step extends Component { +class Step extends Component { static propTypes = { ...ConfigProvider.propTypes, prefix: PropTypes.string, rtl: PropTypes.bool, - /** - * 当前步骤 - */ current: PropTypes.number, - /** - * 展示方向 - */ direction: PropTypes.oneOf(['hoz', 'ver']), - /** - * 横向布局时( direction 为 hoz )的内容排列 - */ labelPlacement: PropTypes.oneOf(['hoz', 'ver']), - /** - * 类型 - */ shape: PropTypes.oneOf(['circle', 'arrow', 'dot']), - /** - * 是否只读模式 - */ readOnly: PropTypes.bool, - /** - * 是否开启动效 - */ animation: PropTypes.bool, - /** - * 自定义样式名 - */ className: PropTypes.string, - /** - * StepItem 的自定义渲染 - * @param {Number} index 节点索引 - * @param {String} status 节点状态 - * @returns {Node} 节点的渲染结果 - */ itemRender: PropTypes.func, - /** - * 宽度横向拉伸 - */ stretch: PropTypes.bool, }; @@ -71,15 +42,18 @@ class Step extends Component { prefix: PropTypes.string, }; - static getDerivedStateFromProps(newProps) { + static getDerivedStateFromProps(newProps: StepProps) { if ('current' in newProps) { return { current: newProps.current, }; } + return null; } - constructor(props, context) { + step: HTMLOListElement | null = null; + + constructor(props: StepProps, context?: unknown) { super(props, context); this.state = { parentWidth: 'auto', @@ -90,7 +64,6 @@ class Step extends Component { } componentDidMount() { - /* istanbul ignore if */ if (!support.flex) { this.resize(); events.on(window, 'resize', this.resize); @@ -103,7 +76,6 @@ class Step extends Component { } componentWillUnmount() { - /* istanbul ignore if */ if (!support.flex) { events.off(window, 'resize', this.resize); } @@ -111,20 +83,22 @@ class Step extends Component { adjustHeight() { const { shape, direction, prefix, labelPlacement } = this.props; - const step = ReactDOM.findDOMNode(this.step); - if ( - shape !== 'arrow' && - (direction === 'horizontal' || direction === 'hoz') && - (labelPlacement === 'vertical' || labelPlacement === 'ver') - ) { - const height = Array.prototype.slice - .call(step.getElementsByClassName(`${prefix}step-item`)) - .reduce((ret, re) => { - const itemHeight = - getHeight(re.getElementsByClassName(`${prefix}step-item-container`)[0]) + - getHeight(re.getElementsByClassName(`${prefix}step-item-body`)[0]); - return Math.max(itemHeight, ret); - }, 0); + const step = ReactDOM.findDOMNode(this.step) as HTMLOListElement; + if (shape !== 'arrow' && direction === 'hoz' && labelPlacement === 'ver') { + const height = ( + Array.prototype.slice.call( + step.getElementsByClassName(`${prefix}step-item`) + ) as HTMLElement[] + ).reduce((ret, re) => { + const itemHeight = + getHeight( + re.getElementsByClassName(`${prefix}step-item-container`)[0] as HTMLElement + ) + + getHeight( + re.getElementsByClassName(`${prefix}step-item-body`)[0] as HTMLElement + ); + return Math.max(itemHeight, ret); + }, 0); setHeight(step, height); } else { setHeight(step, ''); @@ -140,42 +114,8 @@ class Step extends Component { } } - // set dir key for aria handle - // handleKeyDown = e => { - // const { shape, children } = this.props; - // const { length: max } = children; - // let { currentfocus } = this.state; - // const initPosition = currentfocus; - // switch (e.keyCode) { - // case KEYCODE.RIGHT: - // case KEYCODE.DOWN: - // currentfocus++; - // break; - // case KEYCODE.LEFT: - // case KEYCODE.UP: - // currentfocus--; - // break; - // default: - // break; - // } - // currentfocus = - // currentfocus >= max ? 0 : currentfocus < 0 ? max - 1 : currentfocus; - // this.setState({ currentfocus }, () => { - // const child = this.step.children[currentfocus]; - // if (!child) return; - // const focusItem = - // shape === 'arrow' - // ? child - // : child.querySelector('.next-step-item-body'); - // focusItem && focusItem.focus(); - // }); - // if (initPosition !== currentfocus) { - // e.preventDefault(); - // } - // }; - - _getValidChildren(children) { - const result = []; + _getValidChildren(children: ReactNode) { + const result: ReactElement[] = []; React.Children.forEach(children, child => { if (React.isValidElement(child)) { result.push(child); @@ -184,27 +124,36 @@ class Step extends Component { return result; } - _stepRefHandler = ref => { + _stepRefHandler = (ref: HTMLOListElement | null) => { this.step = ref; }; render() { - // eslint-disable-next-line - const { className, current, labelPlacement, shape, readOnly, animation, itemRender, rtl, stretch } = this.props; + const { + className, + current, + labelPlacement, + shape, + readOnly, + animation, + itemRender, + rtl, + stretch, + } = this.props; const others = obj.pickOthers(Step.propTypes, this.props); - let { prefix, direction, children } = this.props; + let { prefix, direction } = this.props; prefix = this.context.prefix || prefix; const { parentWidth, parentHeight } = this.state; - // type不同对应的direction不同 + // type 不同对应的 direction 不同 direction = shape === 'arrow' ? 'hoz' : direction; - // children去除null - children = this._getValidChildren(children); + // children 去除 null + const children = this._getValidChildren(this.props.children); // 修改子节点属性 const cloneChildren = Children.map(children, (child, index) => { - const status = index < current ? 'finish' : index === current ? 'process' : 'wait'; + const status = index < current! ? 'finish' : index === current ? 'process' : 'wait'; return React.cloneElement(child, { prefix, @@ -220,7 +169,6 @@ class Step extends Component { readOnly, animation, tabIndex: 0, - // tabIndex: this.state.currentfocus === index ? '0' : '-1', 'aria-current': status === 'process' ? 'step' : null, itemRender: child.props.itemRender ? child.props.itemRender : itemRender, // 优先使用Item的itemRender onResize: () => { @@ -230,14 +178,14 @@ class Step extends Component { }); }); - const _direction = direction === 'ver' || direction === 'vertical' ? 'vertical' : 'horizontal'; - const _labelPlacement = labelPlacement === 'ver' || labelPlacement === 'vertical' ? 'vertical' : 'horizontal'; + const _direction = direction === 'ver' ? 'vertical' : 'horizontal'; + const _labelPlacement = labelPlacement === 'ver' ? 'vertical' : 'horizontal'; const stepCls = classNames({ [`${prefix}step`]: true, [`${prefix}step-${shape}`]: shape, [`${prefix}step-${_direction}`]: _direction, [`${prefix}step-label-${_labelPlacement}`]: _labelPlacement, - [className]: className, + [className!]: className, }); if (rtl) { diff --git a/package-lock.json b/package-lock.json index 0a721227d8..7f8b31c741 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64699,4 +64699,4 @@ "dev": true } } -} \ No newline at end of file +}