diff --git a/.eslintrc b/.eslintrc index 498b366d60..f87c8017b9 100644 --- a/.eslintrc +++ b/.eslintrc @@ -4,6 +4,9 @@ "node": true }, "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "./tsconfig.json" + }, "extends": ["@alifd/eslint-config-next", "plugin:@typescript-eslint/recommended", "prettier"], "plugins": ["@typescript-eslint", "eslint-plugin-tsdoc"], "settings": { @@ -30,10 +33,13 @@ "max-statements": "off", "max-len": "off", "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": "warn", + "@typescript-eslint/no-unused-vars": ["warn", {"ignoreRestSiblings": true}], "import/prefer-default-export": "off", "@typescript-eslint/no-explicit-any": ["error", { "ignoreRestArgs": true }], "@typescript-eslint/ban-ts-comment": "error", + "import/export": "off", + "@typescript-eslint/consistent-type-exports": "warn", + "@typescript-eslint/consistent-type-imports": "warn", "no-use-before-define": "off", "@typescript-eslint/no-use-before-define": "error", "react/no-deprecated": "error", diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 622979ab3e..333380c898 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -23,5 +23,7 @@ jobs: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 30 start-date: '2023-10-12' + exempt-pr-labels: Technical Upgrade + exempt-issue-labels: Technical Upgrade,PR welcome,Easy to solve stale-issue-message: '你好,该 issue 已 30 天没有活动,因此被标记为 stale,如果之后的 7 天仍然没有活动,该 issue 将被自动关闭' stale-pr-message: '你好,该 pr 已 30 天没有活动,因此被标记为 stale,如果之后的 7 天仍然没有活动,该 pr 将被自动关闭' diff --git a/CHANGELOG.md b/CHANGELOG.md index b60c218632..608e22e7e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,63 @@ # Change Log +## [1.27.7](https://github.com/alibaba-fusion/next/compare/1.27.6...1.27.7) (2024-03-08) + + +### Bug Fixes + +* **Collapse:** hotfix panel className missing ([8430d71](https://github.com/alibaba-fusion/next/commit/8430d71ab58a13024b17a20298d2e7cef50ce9ad)) + + +## [1.27.6](https://github.com/alibaba-fusion/next/compare/1.27.5...1.27.6) (2024-03-07) + + +### Features + +* **DatePicker:** improve focus logic, close [#3998](https://github.com/alibaba-fusion/next/issues/3998) ([#4769](https://github.com/alibaba-fusion/next/issues/4769)) ([1cdd236](https://github.com/alibaba-fusion/next/commit/1cdd236486305ff8940498d24f7f396d46a40746)) +* **TreeSelect:** support useDetailValue, close [#3531](https://github.com/alibaba-fusion/next/issues/3531) ([#4771](https://github.com/alibaba-fusion/next/issues/4771)) ([d19ebdd](https://github.com/alibaba-fusion/next/commit/d19ebdd78c699f8e525a8694a6599f18ff0beec2)) + + +### Bug Fixes + +* **Shell:** phone shell should hidden when collapsed, close [#3886](https://github.com/alibaba-fusion/next/issues/3886) ([#4766](https://github.com/alibaba-fusion/next/issues/4766)) ([94d3030](https://github.com/alibaba-fusion/next/commit/94d3030682a64de37f5a62b6766ac46b3b209695)) +* **Table:** fix merging cell width in locked columns, close [#4716](https://github.com/alibaba-fusion/next/issues/4716) ([#4752](https://github.com/alibaba-fusion/next/issues/4752)) ([9bda719](https://github.com/alibaba-fusion/next/commit/9bda719c7dae5e47d7f3ade3ef05cd00ca5a11b1)) +* **Upload:** should hide trigger when limit is reached for Upload.Dragger, close [#3951](https://github.com/alibaba-fusion/next/issues/3951) ([#4761](https://github.com/alibaba-fusion/next/issues/4761)) ([f2d5303](https://github.com/alibaba-fusion/next/commit/f2d5303214984891cd8b638c43f1005d21364f7d)) + + +### Documentation + +* **Calendar2:** remove legacy api, close [#3100](https://github.com/alibaba-fusion/next/issues/3100) ([8a6536f](https://github.com/alibaba-fusion/next/commit/8a6536fdb4b0fe83756ed2b1e1e8f40953401da4)) +* **Field:** improve document description of parseName, close [#3453](https://github.com/alibaba-fusion/next/issues/3453) ([004fa0e](https://github.com/alibaba-fusion/next/commit/004fa0e9e3ad085209859e5eb78b76caaf48ad3e)) + + +### Code Refactoring + +* **Collapse:** convert to TypeScript, impove docs and tests ([#4713](https://github.com/alibaba-fusion/next/pull/4713)) +* **Field:** convert to TypeScript, impove docs and tests ([#4710](https://github.com/alibaba-fusion/next/pull/4710)) +* **Timeline:** convert to TypeScript, impove docs and tests ([#4715](https://github.com/alibaba-fusion/next/pull/4715)) + + +## [1.27.5](https://github.com/alibaba-fusion/next/compare/1.27.4...1.27.5) (2024-02-22) + +### Bug Fixes + +* **ConfigProvider:** improve config types, close [#4751](https://github.com/alibaba-fusion/next/issues/4751) ([b442d93](https://github.com/alibaba-fusion/next/commit/b442d9310bf503203ba4cc36ac6fb5766f030289)) +* **TimePicker2:** should has focus style when visible, close [#4657](https://github.com/alibaba-fusion/next/issues/4657) ([#4738](https://github.com/alibaba-fusion/next/issues/4738)) ([228b621](https://github.com/alibaba-fusion/next/commit/228b621023fb8e63d79b5783393954b8a6e12db5)) +* **Overlay:** solve problems caused by numerical floating, close [#4740](https://github.com/alibaba-fusion/next/issues/4740) ([8f29094](https://github.com/alibaba-fusion/next/commit/8f290948b08d6fda23121f6c75178b738b2b84c2)) +* rollback [#4746](https://github.com/alibaba-fusion/next/issues/4746) and fix textarea clear spec ([e486542](https://github.com/alibaba-fusion/next/commit/e486542d786f63ce189adc8c1908f782a2082a03)) + + +### Code Refactoring + +* **Cascader:** convert to TypeScript, impove docs and tests ([#4730](https://github.com/alibaba-fusion/next/pull/4730)) +* **Grid:** convert to TypeScript, impove docs and tests ([#4703](https://github.com/alibaba-fusion/next/pull/4703)) +* **List:** convert to TypeScript, impove docs and tests ([#4702](https://github.com/alibaba-fusion/next/pull/4702)) +* **Validate:** convert to TypeScript, improve tests ([910c957](https://github.com/alibaba-fusion/next/commit/910c957fc9623f642c5400f680af497bc1e5c4c6)) + + +## [1.27.5-beta.1](https://github.com/alibaba-fusion/next/compare/1.27.5-beta.0...1.27.5-beta.1) (2024-02-22) + + ## [1.27.5-beta.0](https://github.com/alibaba-fusion/next/compare/1.27.4...1.27.5-beta.0) (2024-02-22) diff --git a/LATESTLOG.md b/LATESTLOG.md index f736cf3c8f..660140b5a6 100644 --- a/LATESTLOG.md +++ b/LATESTLOG.md @@ -1,4 +1,9 @@ # Latest Log -## [1.27.5-beta.0](https://github.com/alibaba-fusion/next/compare/1.27.4...1.27.5-beta.0) (2024-02-22) +## [1.27.7](https://github.com/alibaba-fusion/next/compare/1.27.6...1.27.7) (2024-03-08) + + +### Bug Fixes + +* **Collapse:** hotfix panel className missing ([8430d71](https://github.com/alibaba-fusion/next/commit/8430d71ab58a13024b17a20298d2e7cef50ce9ad)) diff --git a/components/animate/types.ts b/components/animate/types.ts index 7c2d6619ce..3ee20e378b 100644 --- a/components/animate/types.ts +++ b/components/animate/types.ts @@ -110,7 +110,17 @@ export interface AnimateProps extends React.HTMLAttributes, CommonP /** * @api Animate.Expand */ -export interface ExpandProps { +export interface ExpandProps + extends Omit< + AnimateProps, + | 'animation' + | 'beforeEnter' + | 'onEnter' + | 'afterEnter' + | 'beforeLeave' + | 'onLeave' + | 'afterLeave' + > { /** * 动画 className * @en The animation className diff --git a/components/calendar2/__docs__/index.en-us.md b/components/calendar2/__docs__/index.en-us.md index f43df8bf99..3df393a51c 100644 --- a/components/calendar2/__docs__/index.en-us.md +++ b/components/calendar2/__docs__/index.en-us.md @@ -19,7 +19,7 @@ Calendar could be used to display dates, such as schedules, timetables, price ca Calendar use dayjs as a core part to manipulate and display time values. For real usage, it could be used with the latest `dayjs` package. Setting dayjs's locale by: -````js +```js import { DatePicker2, ConfigProvider } from '@alifd/next'; import 'dayjs/locale/en'; import en from '@alifd/next/lib/locale/en-us'; @@ -27,29 +27,28 @@ import en from '@alifd/next/lib/locale/en-us'; function App() { return ( - + ); } ReactDOM.render(, mountNode); -```` +``` ## API ### Calendar -| Param | Description | Type | Default Value | -| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------------- | -| defaultValue | Default value of calendar | custom | - | -| shape | Shape of calendar

**option**:
'card', 'fullscreen', 'panel' | Enum | 'fullscreen' | -| value | Value of calendar | custom | - | -| mode | Mode of panel

**option**:
'date', 'month', 'year' | Enum | 'date' | -| showOtherMonth | Show dates of other month in current date | Boolean | true | -| defaultVisibleMonth | Default visible month of panel

**signature**:
Function() => void | Function | - | -| onSelect | Callback when select a date

**signature**:
Function(value: Object) => void
**parameter**:
_value_: {Object} date object | Function | func.noop | -| onModeChange | Callback when change mode

**签名**:
Function(mode: string) => void
**参数**:
_mode_: {string} mode type: date month year | Function | func.noop | -| dateCellRender | Render function for date cell

**signature**:
Function(value: Object) => ReactNode
**parameter**:
_value_: {Object} date object
**return**:
{ReactNode} null
| Function | (value) => value.date() | -| monthCellRender | Render function for month cell

**signature**:
Function(calendarDate: Object) => ReactNode
**parameter**:
_calendarDate_: {Object} current date object
**return**:
{ReactNode} null
| Function | - | -| yearRange | Year Range,[START_YEAR, END_YEAR] \(only shape in ‘card’, 'fullscreen') | Array<Number> | - | -| disabledDate | Function to disable dates

**signature**:
Function(calendarDate: Object) => Boolean
**parameter**:
_calendarDate_: {Object} current date object
_view_: {Enum} current view type: 'year', 'month', 'date'
**return**:
{Boolean} null
| Function | - | - +| Param | Description | Type | Default Value | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------- | ----------------------- | +| defaultValue | Default value of calendar | custom | - | +| shape | Shape of calendar

**option**:
'card', 'fullscreen', 'panel' | Enum | 'fullscreen' | +| value | Value of calendar | custom | - | +| mode | Mode of panel

**option**:
'date', 'month', 'year' | Enum | 'date' | +| showOtherMonth | Show dates of other month in current date | Boolean | true | +| defaultVisibleMonth | Default visible month of panel

**signature**:
Function() => void | Function | - | +| onSelect | Callback when select a date

**signature**:
Function(value: Object) => void
**parameter**:
_value_: {Object} date object | Function | func.noop | +| onPanelChange | Callback when the pane changed

**签名**:
Function(value: Object, mode: string) => void
**参数**:
_value_: {Object} date object
_mode_: {string} mode type: date month | Function | func.noop | +| dateCellRender | Render function for date cell

**signature**:
Function(value: Object) => ReactNode
**parameter**:
_value_: {Object} date object
**return**:
{ReactNode} null
| Function | (value) => value.date() | +| monthCellRender | Render function for month cell

**signature**:
Function(calendarDate: Object) => ReactNode
**parameter**:
_calendarDate_: {Object} current date object
**return**:
{ReactNode} null
| Function | - | +| yearRange | Year Range,[START_YEAR, END_YEAR] \(only shape in ‘card’, 'fullscreen') | Array<Number> | - | +| disabledDate | Function to disable dates

**signature**:
Function(calendarDate: Object) => Boolean
**parameter**:
_calendarDate_: {Object} current date object
_view_: {Enum} current view type: 'year', 'month', 'date'
**return**:
{Boolean} null
| Function | - | diff --git a/components/calendar2/__docs__/index.md b/components/calendar2/__docs__/index.md index 14546b19c3..03880ccd38 100644 --- a/components/calendar2/__docs__/index.md +++ b/components/calendar2/__docs__/index.md @@ -10,6 +10,7 @@ 按照日历形式展示数据的容器。 ### 何时使用 + 1.22版本增加当前组件 日历组件是一个偏向于展示与受控的基础组件,可用于日程、课表、价格日历、农历展示等。 @@ -18,7 +19,7 @@ 由于 `Calendar` 组件内部使用 `dayjs` 对象来设置日期(请使用最新版 dayjs),部分 `Locale` 读取自 [日期库`dayjs`的国际化](https://dayjs.gitee.io/docs/zh-CN/i18n/i18n)。 -````js +```js import { DatePicker2, ConfigProvider } from '@alifd/next'; import 'dayjs/locale/en'; import en from '@alifd/next/lib/locale/en-us'; @@ -26,29 +27,28 @@ import en from '@alifd/next/lib/locale/en-us'; function App() { return ( - + ); } ReactDOM.render(, mountNode); -```` +``` ## API ### Calendar -| 参数 | 说明 | 类型 | 默认值 | -| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | --------------------- | -| defaultValue | 默认选中的日期(dayjs 对象) | custom | - | -| shape | 展现形态

**可选值**:
'card', 'fullscreen', 'panel' | Enum | 'fullscreen' | -| value | 选中的日期值 (dayjs 对象) | custom | - | -| mode | 面板模式 | Enum | - | -| showOtherMonth | 是否展示非本月的日期 | Boolean | true | -| defaultVisibleMonth | 默认展示的月份

**签名**:
Function() => void | Function | - | -| onSelect | 选择日期单元格时的回调

**签名**:
Function(value: Object) => void
**参数**:
_value_: {Object} 对应的日期值 (dayjs 对象) | Function | func.noop | -| onModeChange | 面板模式变化时的回调

**签名**:
Function(mode: String) => void
**参数**:
_mode_: {String} 对应面板模式 date month year | Function | func.noop | -| onVisibleMonthChange | 展现的月份变化时的回调

**签名**:
Function(value: Object, reason: String) => void
**参数**:
_value_: {Object} 显示的月份 (dayjs 对象)
_reason_: {String} 触发月份改变原因 | Function | func.noop | -| dateCellRender | 自定义日期渲染函数

**签名**:
Function(value: Object) => ReactNode
**参数**:
_value_: {Object} 日期值(dayjs对象)
**返回值**:
{ReactNode} null
| Function | value => value.date() | -| monthCellRender | 自定义月份渲染函数

**签名**:
Function(calendarDate: Object) => ReactNode
**参数**:
_calendarDate_: {Object} 对应 Calendar 返回的自定义日期对象
**返回值**:
{ReactNode} null
| Function | - | -| yearRange | 年份范围,[START_YEAR, END_YEAR] \(只在shape 为 ‘card’, 'fullscreen' 下生效) | Array<Number> | - | -| disabledDate | 不可选择的日期

**签名**:
Function(calendarDate: Object, view: String) => Boolean
**参数**:
_calendarDate_: {Object} 对应 Calendar 返回的自定义日期对象
_view_: {String} 当前视图类型,year: 年, month: 月, date: 日
**返回值**:
{Boolean} null
| Function | - | +| 参数 | 说明 | 类型 | 默认值 | +| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | --------------------- | +| defaultValue | 默认选中的日期(dayjs 对象) | custom | - | +| shape | 展现形态

**可选值**:
'card', 'fullscreen', 'panel' | Enum | 'fullscreen' | +| value | 选中的日期值 (dayjs 对象) | custom | - | +| mode | 面板模式 | Enum | - | +| showOtherMonth | 是否展示非本月的日期 | Boolean | true | +| defaultVisibleMonth | 默认展示的月份

**签名**:
Function() => void | Function | - | +| onSelect | 选择日期单元格时的回调

**签名**:
Function(value: Object) => void
**参数**:
_value_: {Object} 对应的日期值 (dayjs 对象) | Function | func.noop | +| onPanelChange | 面板变化时的回调

**签名**:
Function(value: Object, mode: String) => void
**参数**:
_value_: {Object} 显示的日期 (dayjs 对象)
_mode_: {String} 对应面板模式 date month | Function | func.noop | +| dateCellRender | 自定义日期渲染函数

**签名**:
Function(value: Object) => ReactNode
**参数**:
_value_: {Object} 日期值(dayjs对象)
**返回值**:
{ReactNode} null
| Function | value => value.date() | +| monthCellRender | 自定义月份渲染函数

**签名**:
Function(calendarDate: Object) => ReactNode
**参数**:
_calendarDate_: {Object} 对应 Calendar 返回的自定义日期对象
**返回值**:
{ReactNode} null
| Function | - | +| yearRange | 年份范围,[START_YEAR, END_YEAR] \(只在shape 为 ‘card’, 'fullscreen' 下生效) | Array<Number> | - | +| disabledDate | 不可选择的日期

**签名**:
Function(calendarDate: Object, view: String) => Boolean
**参数**:
_calendarDate_: {Object} 对应 Calendar 返回的自定义日期对象
_view_: {String} 当前视图类型,year: 年, month: 月, date: 日
**返回值**:
{Boolean} null
| Function | - | diff --git a/components/collapse/__docs__/adaptor/index.jsx b/components/collapse/__docs__/adaptor/index.jsx deleted file mode 100644 index e08d5473db..0000000000 --- a/components/collapse/__docs__/adaptor/index.jsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; -import { Collapse } from '@alifd/next'; -import { Types, parseData, NodeType } from '@alifd/adaptor-helper'; - -export default { - name: 'Collapse', - editor: () => ({ - props: [{ - name: 'state', - label: 'Status', - type: Types.enum, - options: ['normal', 'disabled'], - default: 'normal' - }, { - name: 'width', - type: Types.number, - default: 400 - }], - data: { - active: true, - disable: true, - default: '*Panel Header 1\n\tPeople always make mistakes, frustrated, nerve-racking, but cannot remain stagnant.\nPanel Header 2\n\tPeople always make mistakes, frustrated, nerve-racking, but cannot remain stagnant.\nPanel Header 3\n\tPeople always make mistakes, frustrated, nerve-racking, but cannot remain stagnant.\n' - } - }), - adaptor: ({ state, width, data, style = {}, ...others }) => { - const list = parseData(data).filter(node => NodeType.node === node.type); - let expandedKeys = []; - const children = list.map(({ state, value, children }, index) => { - if (state === 'active') { - expandedKeys.push(`panel_${index}`); - } - - return ( - - {children && children.length > 0 ? children[0].value : ''} - - ); - }); - return ( - - { - children - } - - ); - } -}; diff --git a/components/collapse/__docs__/adaptor/index.tsx b/components/collapse/__docs__/adaptor/index.tsx new file mode 100644 index 0000000000..d9389d6c26 --- /dev/null +++ b/components/collapse/__docs__/adaptor/index.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { Collapse } from '@alifd/next'; +import { Types, parseData, NodeType } from '@alifd/adaptor-helper'; + +interface AdaptorProps { + state: string; + width: number; + data: string; + style: React.CSSProperties; +} + +export default { + name: 'Collapse', + editor: () => ({ + props: [ + { + name: 'state', + label: 'Status', + type: Types.enum, + options: ['normal', 'disabled'], + default: 'normal', + }, + { + name: 'width', + type: Types.number, + default: 400, + }, + ], + data: { + active: true, + disable: true, + default: + '*Panel Header 1\n\tPeople always make mistakes, frustrated, nerve-racking, but cannot remain stagnant.\nPanel Header 2\n\tPeople always make mistakes, frustrated, nerve-racking, but cannot remain stagnant.\nPanel Header 3\n\tPeople always make mistakes, frustrated, nerve-racking, but cannot remain stagnant.\n', + }, + }), + adaptor: ({ state, width, data, style = {}, ...others }: AdaptorProps) => { + const list = parseData(data).filter(node => NodeType.node === node.type); + const expandedKeys = [] as string[]; + const children = list.map(({ state, value, children }, index) => { + if (state === 'active') { + expandedKeys.push(`panel_${index}`); + } + + return ( + + {children && children.length > 0 ? children[0].value : ''} + + ); + }); + return ( + + {children} + + ); + }, +}; diff --git a/components/collapse/__docs__/demo/basic/index.tsx b/components/collapse/__docs__/demo/basic/index.tsx index 4a14ca6280..849bd70715 100644 --- a/components/collapse/__docs__/demo/basic/index.tsx +++ b/components/collapse/__docs__/demo/basic/index.tsx @@ -1,9 +1,8 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { Collapse, Radio } from '@alifd/next'; +import { Collapse } from '@alifd/next'; const Panel = Collapse.Panel; -const RadioGroup = Radio.Group; ReactDOM.render( diff --git a/components/collapse/__docs__/demo/event/index.tsx b/components/collapse/__docs__/demo/event/index.tsx index 7788aec2ff..d952406973 100644 --- a/components/collapse/__docs__/demo/event/index.tsx +++ b/components/collapse/__docs__/demo/event/index.tsx @@ -5,25 +5,22 @@ import { Collapse } from '@alifd/next'; const Panel = Collapse.Panel; class Demo extends React.Component { - constructor(props, context) { - super(props, context); - this.state = { - expandedKeys: [], - }; - } + state = { + expandedKeys: [], + }; - onExpand(expandedKeys) { + onExpand = (expandedKeys: string[]) => { this.setState({ expandedKeys, }); - } + }; - onClick(key) { + onClick = (key: any) => { console.log('clicked', key); - } + }; render() { return ( - + Promotions are marketing campaigns ran by Marketplace. Participate to sale your products during that promotion and make a profit diff --git a/components/collapse/__docs__/index.en-us.md b/components/collapse/__docs__/index.en-us.md index a586d1abdd..9684beefc5 100644 --- a/components/collapse/__docs__/index.en-us.md +++ b/components/collapse/__docs__/index.en-us.md @@ -12,29 +12,52 @@ ### When to use When some earas may toggle between collapse state and expand state. + ## API ### Collapse -| Param | Description | Type | Default Value | -| ------------------- | -------------------------------------------------- | -------- | --------- | -| dataSource | data model | Array | - | -| defaultExpandedKeys | default expand panel keys | Array | - | -| expandedKeys | expand panel keys | Array | - | -| onExpand | callback when panel state changes

**signature**:
Function() => void | Function | func.noop | -| disabled | disable all panel | Boolean | - | -| accordion | accordion mode, you can only open at most one panel | Boolean | false | +| Param | Description | Type | Default Value | Required | +| ------------------- | ------------------------------------------------ | -------------------------------------------- | ------------- | -------- | +| dataSource | Use data model to build | Array | - | | +| defaultExpandedKeys | Default expanded keys | KeyType[] | - | | +| expandedKeys | Controlled expanded keys | KeyType[] | - | | +| onExpand | Callback when the expanded state changes | (expandedKeys: KeyType \| KeyType[]) => void | - | | +| disabled | All disabled | boolean | - | | +| accordion | Accordion mode, only one can be opened at a time | boolean | false | | ### Collapse.Panel -| Param | Description | Type | Default Value | -| -------- | -------- | --------- | --- | -| disabled | disable this panel | Boolean | - | -| title | panel title | ReactNode | - | +| Param | Description | Type | Default Value | Required | +| ---------- | ------------------------------- | ------------------------------------------------------------------------------------------------ | ------------- | -------- | +| disabled | Whether to disable user actions | boolean | - | | +| title | Title | React.ReactNode | - | | +| isExpanded | Whether to expand | boolean | false | | +| onClick | Click callback function | \| ((e: React.MouseEvent \| React.KeyboardEvent) => void)
\| null | - | | + +### KeyType + +```typescript +export type KeyType = string | number; +``` + +### DataItem + +```typescript +export type DataItem = { + id?: string; + title?: React.ReactNode; + content?: React.ReactNode; + disabled?: boolean; + key?: KeyType; + onClick?: (key: KeyType) => void; + [propName: string]: unknown; +}; +``` ## ARIA and KeyBoard -| KeyBoard | Descripiton | -| :---------- | :------------------------------ | -| Tab | navigate to the next collapse panel | -| Space | toggle expanded | \ No newline at end of file +| KeyBoard | Descripiton | +| :------- | :---------------------------------- | +| Tab | navigate to the next collapse panel | +| Space | toggle expanded | diff --git a/components/collapse/__docs__/index.md b/components/collapse/__docs__/index.md index 6b4101809a..14bc0ecbbb 100644 --- a/components/collapse/__docs__/index.md +++ b/components/collapse/__docs__/index.md @@ -19,25 +19,47 @@ ### Collapse -| 参数 | 说明 | 类型 | 默认值 | -| ------------------- | ----------------------------------------------------- | -------- | --------- | -| dataSource | 使用数据模型构建 | Array | - | -| defaultExpandedKeys | 默认展开keys | Array | - | -| expandedKeys | 受控展开keys | Array | - | -| onExpand | 展开状态发升变化时候的回调

**签名**:
Function() => void | Function | func.noop | -| disabled | 所有禁用 | Boolean | - | -| accordion | 手风琴模式,一次只能打开一个 | Boolean | false | +| 参数 | 说明 | 类型 | 默认值 | 是否必填 | +| ------------------- | ---------------------------- | -------------------------------------------- | ------ | -------- | +| dataSource | 使用数据模型构建 | Array | - | | +| defaultExpandedKeys | 默认展开 keys | KeyType[] | - | | +| expandedKeys | 受控展开 keys | KeyType[] | - | | +| onExpand | 展开状态发升变化时候的回调 | (expandedKeys: KeyType \| KeyType[]) => void | - | | +| disabled | 所有禁用 | boolean | - | | +| accordion | 手风琴模式,一次只能打开一个 | boolean | false | | ### Collapse.Panel -| 参数 | 说明 | 类型 | 默认值 | -| -------- | -------- | --------- | --- | -| disabled | 是否禁止用户操作 | Boolean | - | -| title | 标题 | ReactNode | - | +| 参数 | 说明 | 类型 | 默认值 | 是否必填 | +| ---------- | ---------------- | ------------------------------------------------------------------------------------------------ | ------ | -------- | +| disabled | 是否禁止用户操作 | boolean | - | | +| title | 标题 | React.ReactNode | - | | +| isExpanded | 是否展开 | boolean | false | | +| onClick | 点击回调函数 | \| ((e: React.MouseEvent \| React.KeyboardEvent) => void)
\| null | - | | + +### KeyType + +```typescript +export type KeyType = string | number; +``` + +### DataItem + +```typescript +export type DataItem = { + id?: string; + title?: React.ReactNode; + content?: React.ReactNode; + disabled?: boolean; + key?: KeyType; + onClick?: (key: KeyType) => void; + [propName: string]: unknown; +}; +``` ## 无障碍键盘操作指南 -| 按键 | 说明 | -| :---- | :------------------- | -| Tab | 切换到下一个collapse panel | -| Space | 切换collapse的折叠状态 | +| 按键 | 说明 | +| :---- | :-------------------------- | +| Tab | 切换到下一个 collapse panel | +| Space | 切换 collapse 的折叠状态 | diff --git a/components/collapse/__docs__/theme/index.jsx b/components/collapse/__docs__/theme/index.jsx deleted file mode 100644 index ffbbf893f7..0000000000 --- a/components/collapse/__docs__/theme/index.jsx +++ /dev/null @@ -1,79 +0,0 @@ -import ReactDOM from 'react-dom'; -import '../../../demo-helper/style'; -import { Demo, DemoGroup, initDemo } from '../../../demo-helper'; -import '../../style'; -import Collapse from '../../index'; - -// import component - -const Panel = Collapse.Panel; - -const i18nMap = { - 'zh-cn': { - title: '这是区块的标题', - content: '人总是要犯错误、受挫折、伤脑筋的,不过决不能停滞不前;应该完成的任务,即使为它牺牲生命,也要完成。社会之河的圣水就是因为被一股永不停滞的激流推动向前才得以保持洁净。这意味着河岸偶尔也会被冲垮,短时间造成损失,可是如果怕河堤溃决,便设法永远堵死这股激流,那只会招致停滞和死亡。' - }, - 'en-us': { - title: 'Panel Header ', - content: 'People always make mistakes, frustrated, nerve-racking, but cannot remain stagnant; should finish the task, even if it\'s life, but also to complete. Society of holy water because the river is a never-ending stream of pushing forward was able to keep clean. This means that sometimes river was washed away, causing short-term losses, but if the fear of embankments break, they managed to always blocked this torrent, it will only lead to stagnation and death.' - } -}; - -function render(i18n) { - const title = i18n.title; - const content = i18n.content; - return ReactDOM.render(( -
-

手风琴 Collapse

- - - - {/**/} - - {title}1
{title}1
{title}1
}> - {content} -
- - {content} - {/* --------- this is for config platform ----------- */} -
-
-
- {/* --------- this is for config platform ----------- */} - - - {content} - - - {content} - - - - - - - {content} - - - {content} - - - {content} - - - {content} - - - - -
- ), document.getElementById('container')); -} - -window.renderDemo = function (lang) { - render(i18nMap[lang]); -}; - -window.renderDemo('en-us'); - -initDemo('collapse'); diff --git a/components/collapse/__docs__/theme/index.tsx b/components/collapse/__docs__/theme/index.tsx new file mode 100644 index 0000000000..2d61a46775 --- /dev/null +++ b/components/collapse/__docs__/theme/index.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import '../../../demo-helper/style'; +import { Demo, DemoGroup, initDemo } from '../../../demo-helper'; +import '../../style'; +import Collapse from '../../index'; + +interface i18nContent { + title: string; + content: string; +} + +const Panel = Collapse.Panel; + +const i18nMap = { + 'zh-cn': { + title: '这是区块的标题', + content: + '人总是要犯错误、受挫折、伤脑筋的,不过决不能停滞不前;应该完成的任务,即使为它牺牲生命,也要完成。社会之河的圣水就是因为被一股永不停滞的激流推动向前才得以保持洁净。这意味着河岸偶尔也会被冲垮,短时间造成损失,可是如果怕河堤溃决,便设法永远堵死这股激流,那只会招致停滞和死亡。', + }, + 'en-us': { + title: 'Panel Header ', + content: + "People always make mistakes, frustrated, nerve-racking, but cannot remain stagnant; should finish the task, even if it's life, but also to complete. Society of holy water because the river is a never-ending stream of pushing forward was able to keep clean. This means that sometimes river was washed away, causing short-term losses, but if the fear of embankments break, they managed to always blocked this torrent, it will only lead to stagnation and death.", + }, +} as { [key: string]: i18nContent }; + +function render(i18n: i18nContent) { + const title = i18n.title; + const content = i18n.content; + ReactDOM.render( +
+

手风琴 Collapse

+ + + + {/**/} + + + {title}1
+ {title}1
+ {title}1 +
+ } + > + {content} +
+ + {content} + {/* --------- this is for config platform ----------- */} +
+ {/* @ts-expect-error div has no type */} +
+
+ {/* --------- this is for config platform ----------- */} + + {content} + {content} + + + + + {content} + {content} + {content} + {content} + + + +
, + document.getElementById('container') + ); +} + +(window as any).renderDemo = function (lang: string) { + render(i18nMap[lang]); +}; + +window.renderDemo('en-us'); + +initDemo('collapse'); diff --git a/components/collapse/__tests__/a11y-spec.js b/components/collapse/__tests__/a11y-spec.tsx similarity index 78% rename from components/collapse/__tests__/a11y-spec.js rename to components/collapse/__tests__/a11y-spec.tsx index ee0d18d3a3..de56cadcba 100644 --- a/components/collapse/__tests__/a11y-spec.js +++ b/components/collapse/__tests__/a11y-spec.tsx @@ -1,35 +1,19 @@ import React from 'react'; -import Enzyme from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; import Collapse from '../index'; import '../style'; -import { unmount, testReact } from '../../util/__tests__/legacy/a11y/validate'; - -Enzyme.configure({ adapter: new Adapter() }); +import { testReact } from '../../util/__tests__/a11y/validate'; const Panel = Collapse.Panel; -/* eslint-disable no-undef, react/jsx-filename-extension */ describe('Collapse A11y', () => { - let wrapper; - - afterEach(() => { - if (wrapper) { - wrapper.unmount(); - wrapper = null; - } - unmount(); - }); - it('should not have any violations for children rendered component', async () => { - wrapper = await testReact( + await testReact( Pannel Content Pannel Content
others
); - return wrapper; }); it('should not have any violations for data rendered component', async () => { @@ -45,9 +29,8 @@ describe('Collapse A11y', () => { 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.', }, ]; - wrapper = await testReact(, { + await testReact(, { incomplete: true, }); - return wrapper; }); }); diff --git a/components/collapse/__tests__/index-spec.js b/components/collapse/__tests__/index-spec.tsx similarity index 68% rename from components/collapse/__tests__/index-spec.js rename to components/collapse/__tests__/index-spec.tsx index 71454c88bc..808879f655 100644 --- a/components/collapse/__tests__/index-spec.js +++ b/components/collapse/__tests__/index-spec.tsx @@ -1,44 +1,35 @@ -import React from 'react'; -import Enzyme, { mount } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; -import assert from 'power-assert'; -import sinon from 'sinon'; +import React, { type ReactElement } from 'react'; import Collapse from '../index'; - -Enzyme.configure({ adapter: new Adapter() }); +import '../style'; const Panel = Collapse.Panel; -/* global describe, it */ -/* eslint-disable react/jsx-filename-extension */ - describe('Collapse', () => { describe('render', () => { it('[normal] Should render null', () => { - const wrapper = mount(); - assert(wrapper.find(Collapse).length === 1); + cy.mount(); + cy.get('.next-collapse').should('have.length', 1); }); it('[normal] Should render from children', () => { - const wrapper = mount( + cy.mount( Pannel Content Pannel Content
others
); - assert(wrapper.find(Collapse).length === 1); - assert(wrapper.find(Panel).length === 2); + cy.get('.next-collapse').should('have.length', 1); + cy.get('.next-collapse-panel').should('have.length', 2); }); it('hidden panel should be hidden', () => { - const wrapper = mount( + cy.mount( Pannel Content Pannel Content ); - const el = wrapper.find('.next-collapse-panel-hidden'); - assert(el.length === 2); + cy.get('.next-collapse-panel-hidden').should('have.length', 2); }); it('Should render from dataSource', () => { @@ -54,27 +45,27 @@ describe('Collapse', () => { 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.', }, ]; - const wrapper = mount(); - assert(wrapper.find(Panel).length === 2); + cy.mount(); + cy.get('.next-collapse-panel').should('have.length', 2); }); it('should default expand keys passed in `defaultExpandedKeys`', () => { - const wrapper = mount( + cy.mount( Pannel Content Pannel Content
others
); - assert(wrapper.find(Collapse).length === 1); - assert(wrapper.find(Panel).length === 2); + cy.get('.next-collapse').should('have.length', 1); + cy.get('.next-collapse-panel').should('have.length', 2); }); }); describe('defaultExpandedKeys', () => { describe('default mode', () => { it('should expand panel with string key', () => { - const wrapper = mount( + cy.mount( Pannel Content Pannel Content @@ -84,12 +75,13 @@ describe('Collapse', () => {
others
); - const el = wrapper.find('.next-collapse-panel').at(2); - assert(el.hasClass('next-collapse-panel-expanded')); + cy.get('.next-collapse-panel') + .eq(2) + .should('have.class', 'next-collapse-panel-expanded'); }); it('should expand panel with number key', () => { - const wrapper = mount( + cy.mount( Pannel Content Pannel Content @@ -99,12 +91,13 @@ describe('Collapse', () => {
others
); - const el = wrapper.find('.next-collapse-panel').at(2); - assert(el.hasClass('next-collapse-panel-expanded')); + cy.get('.next-collapse-panel') + .eq(2) + .should('have.class', 'next-collapse-panel-expanded'); }); it('should close default expanded string keys', () => { - const wrapper = mount( + cy.mount( Pannel Content Pannel Content @@ -114,15 +107,13 @@ describe('Collapse', () => {
others
); - wrapper - .find('.next-collapse-panel-title') - .at(2) - .simulate('click'); - assert(wrapper.find('.next-collapse-panel-expanded').length === 0); + cy.get('.next-collapse-panel').eq(2).as('secondPanel'); + cy.get('@secondPanel').find('.next-collapse-panel-title').click(); + cy.get('@secondPanel').should('not.have.class', 'next-collapse-panel-expanded'); }); it('should close default expanded number keys', () => { - const wrapper = mount( + cy.mount( Pannel Content Pannel Content @@ -132,11 +123,9 @@ describe('Collapse', () => {
others
); - wrapper - .find('.next-collapse-panel-title') - .at(2) - .simulate('click'); - assert(wrapper.find('.next-collapse-panel-expanded').length === 0); + cy.get('.next-collapse-panel').eq(2).as('secondPanel'); + cy.get('@secondPanel').find('.next-collapse-panel-title').click(); + cy.get('@secondPanel').should('not.have.class', 'next-collapse-panel-expanded'); }); it('should open default expanded datasource using number keys', () => { @@ -154,9 +143,10 @@ describe('Collapse', () => { 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.', }, ]; - const wrapper = mount(); - const el = wrapper.find('.next-collapse-panel').at(1); - assert(el.hasClass('next-collapse-panel-expanded')); + cy.mount(); + cy.get('.next-collapse-panel') + .eq(1) + .should('have.class', 'next-collapse-panel-expanded'); }); it('should close default expanded datasource using number keys on click', () => { @@ -174,18 +164,15 @@ describe('Collapse', () => { 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.', }, ]; - const wrapper = mount(); - wrapper - .find('.next-collapse-panel-title') - .at(1) - .simulate('click'); - assert(wrapper.find('.next-collapse-panel-expanded').length === 0); + cy.mount(); + cy.get('.next-collapse-panel-title').eq(1).click(); + cy.get('.next-collapse-panel-expanded').should('have.length', 0); }); }); describe('accordian mode', () => { it('should expand panel with string key', () => { - const wrapper = mount( + cy.mount( Pannel Content Pannel Content @@ -195,12 +182,13 @@ describe('Collapse', () => {
others
); - const el = wrapper.find('.next-collapse-panel').at(2); - assert(el.hasClass('next-collapse-panel-expanded')); + cy.get('.next-collapse-panel') + .eq(2) + .should('have.class', 'next-collapse-panel-expanded'); }); it('should expand panel with number key', () => { - const wrapper = mount( + cy.mount( Pannel Content Pannel Content @@ -210,12 +198,13 @@ describe('Collapse', () => {
others
); - const el = wrapper.find('.next-collapse-panel').at(2); - assert(el.hasClass('next-collapse-panel-expanded')); + cy.get('.next-collapse-panel') + .eq(2) + .should('have.class', 'next-collapse-panel-expanded'); }); it('should close default expanded string keys', () => { - const wrapper = mount( + cy.mount( Pannel Content Pannel Content @@ -225,15 +214,13 @@ describe('Collapse', () => {
others
); - wrapper - .find('.next-collapse-panel-title') - .at(2) - .simulate('click'); - assert(wrapper.find('.next-collapse-panel-expanded').length === 0); + cy.get('.next-collapse-panel').eq(2).as('secondPanel'); + cy.get('@secondPanel').find('.next-collapse-panel-title').click(); + cy.get('@secondPanel').should('not.have.class', 'next-collapse-panel-expanded'); }); it('should close default expanded number keys', () => { - const wrapper = mount( + cy.mount( Pannel Content Pannel Content @@ -243,11 +230,9 @@ describe('Collapse', () => {
others
); - wrapper - .find('.next-collapse-panel-title') - .at(2) - .simulate('click'); - assert(wrapper.find('.next-collapse-panel-expanded').length === 0); + cy.get('.next-collapse-panel').eq(2).as('secondPanel'); + cy.get('@secondPanel').find('.next-collapse-panel-title').click(); + cy.get('@secondPanel').should('not.have.class', 'next-collapse-panel-expanded'); }); it('should open default expanded datasource using number keys', () => { @@ -265,9 +250,11 @@ describe('Collapse', () => { 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.', }, ]; - const wrapper = mount(); - const el = wrapper.find('.next-collapse-panel').at(1); - assert(el.hasClass('next-collapse-panel-expanded')); + cy.mount(); + + cy.get('.next-collapse-panel') + .eq(1) + .should('have.class', 'next-collapse-panel-expanded'); }); it('should close default expanded datasource using number keys on click', () => { @@ -285,12 +272,9 @@ describe('Collapse', () => { 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.', }, ]; - const wrapper = mount(); - wrapper - .find('.next-collapse-panel-title') - .at(1) - .simulate('click'); - assert(wrapper.find('.next-collapse-panel-expanded').length === 0); + cy.mount(); + cy.get('.next-collapse-panel-title').eq(1).click(); + cy.get('.next-collapse-panel-expanded').should('have.length', 0); }); }); }); @@ -304,23 +288,13 @@ describe('Collapse', () => { Pannel Content3 ); - const wrapper = mount(collapse); - - wrapper - .find('.next-collapse-panel-title') - .first() - .simulate('click'); - assert(wrapper.find('.next-collapse-panel-expanded').length === 1); - wrapper - .find('.next-collapse-panel-title') - .at(1) - .simulate('click'); - assert(wrapper.find('.next-collapse-panel-expanded').length === 2); - wrapper - .find('.next-collapse-panel-title') - .at(1) - .simulate('click'); - assert(wrapper.find('.next-collapse-panel-expanded').length === 1); + cy.mount(collapse); + cy.get('.next-collapse-panel-title').first().click(); + cy.get('.next-collapse-panel-expanded').should('have.length', 1); + cy.get('.next-collapse-panel-title').eq(1).click(); + cy.get('.next-collapse-panel-expanded').should('have.length', 2); + cy.get('.next-collapse-panel-title').eq(1).click(); + cy.get('.next-collapse-panel-expanded').should('have.length', 1); }); it('Should expanded by space key', () => { @@ -331,23 +305,14 @@ describe('Collapse', () => { Pannel Content3 ); - const wrapper = mount(collapse); - - wrapper - .find('.next-collapse-panel-title') - .first() - .simulate('keyDown', { keyCode: 32 }); - assert(wrapper.find('.next-collapse-panel-expanded').length === 1); - wrapper - .find('.next-collapse-panel-title') - .at(1) - .simulate('keyDown', { keyCode: 32 }); - assert(wrapper.find('.next-collapse-panel-expanded').length === 2); - wrapper - .find('.next-collapse-panel-title') - .at(1) - .simulate('keyDown', { keyCode: 32 }); - assert(wrapper.find('.next-collapse-panel-expanded').length === 1); + cy.mount(collapse); + + cy.get('.next-collapse-panel-title').first().trigger('keydown', { keyCode: 32 }); + cy.get('.next-collapse-panel-expanded').should('have.length', 1); + cy.get('.next-collapse-panel-title').eq(1).trigger('keydown', { keyCode: 32 }); + cy.get('.next-collapse-panel-expanded').should('have.length', 2); + cy.get('.next-collapse-panel-title').eq(1).trigger('keydown', { keyCode: 32 }); + cy.get('.next-collapse-panel-expanded').should('have.length', 1); }); it('should support accordion', () => { const collapse = ( @@ -357,22 +322,13 @@ describe('Collapse', () => { Pannel Content3 ); - const wrapper = mount(collapse); - wrapper - .find('.next-collapse-panel-title') - .first() - .simulate('click'); - assert(wrapper.find('.next-collapse-panel-expanded').length === 1); - wrapper - .find('.next-collapse-panel-title') - .at(1) - .simulate('click'); - assert(wrapper.find('.next-collapse-panel-expanded').length === 1); - wrapper - .find('.next-collapse-panel-title') - .at(2) - .simulate('click'); - assert(wrapper.find('.next-collapse-panel-expanded').length === 1); + cy.mount(collapse); + cy.get('.next-collapse-panel-title').first().click(); + cy.get('.next-collapse-panel-expanded').should('have.length', 1); + cy.get('.next-collapse-panel-title').eq(1).click(); + cy.get('.next-collapse-panel-expanded').should('have.length', 1); + cy.get('.next-collapse-panel-title').eq(2).click(); + cy.get('.next-collapse-panel-expanded').should('have.length', 1); }); it('disabled', () => { @@ -381,30 +337,22 @@ describe('Collapse', () => { Pannel Content ); - const wrapper = mount(collapse); - wrapper - .find('.next-collapse-panel-title') - .first() - .simulate('click'); //模拟点击 - assert(wrapper.find('.next-collapse-panel-expanded').length === 0); + cy.mount(collapse); + cy.get('.next-collapse-panel-title').first().click(); + cy.get('.next-collapse-panel-expanded').should('have.length', 0); }); it('[onExpand] Call when the trigger', () => { - const onExpand = sinon.spy(); - + const onExpand = cy.spy(); const collapse = ( Pannel Content1 Pannel Content2 ); - const wrapper = mount(collapse); - wrapper - .find('.next-collapse-panel-title') - .first() - .simulate('click'); //模拟点击 - - assert(onExpand.calledOnce); + cy.mount(collapse); + cy.get('.next-collapse-panel-title').first().click(); + cy.wrap(onExpand).should('be.calledOnce'); }); it('under Control', () => { @@ -414,31 +362,29 @@ describe('Collapse', () => { Pannel Content2
); - const wrapper = mount(collapse); - wrapper - .find('.next-collapse-panel-title') - .first() - .simulate('click'); //模拟点击 - - assert(wrapper.find('.next-collapse-panel-expanded').length === 1); + cy.mount(collapse); + cy.get('.next-collapse-panel-title').first().click(); + cy.get('.next-collapse-panel-expanded').should('have.length', 1); }); }); describe('react api', () => { - it('calls componentWillReceiveProps', done => { + it('calls componentWillReceiveProps', () => { const collapse = ( Pannel Content ); - const wrapper = mount(collapse); + cy.mount(collapse).then(({ component, rerender }) => { + return rerender( + React.cloneElement(component as ReactElement, { expandedKeys: ['0'] }) + ); + }); - wrapper.setProps({ expandedKeys: ['0'] }); - assert(wrapper.find('.next-collapse-panel-expanded').length === 1); - done(); + cy.get('.next-collapse-panel-expanded').should('have.length', 1); }); }); describe('panel', () => { - it('id should be auto add', done => { + it('id should be auto add', () => { const collapse = ( @@ -446,26 +392,22 @@ describe('Collapse', () => { ); - const wrapper = mount(collapse); - assert(wrapper.find('#test-id-1-heading').length === 1); - assert(wrapper.find('#test-id-1-region').length === 1); - done(); + cy.mount(collapse); + cy.get('#test-id-1-heading').should('have.length', 1); + cy.get('#test-id-1-region').should('have.length', 1); }); - it('all id should be auto add', done => { + it('all id should be auto add', () => { const collapse = ( Pannel Content Pannel Content ); - const wrapper = mount(collapse); - - assert(wrapper.find('.next-collapse#test-id-2').length === 1); - const panels = wrapper.find('.next-collapse-panel'); - assert(panels.length === 2); - assert(panels.at(0).getDOMNode().id); - assert(panels.at(1).getDOMNode().id); - done(); + cy.mount(collapse); + cy.get('.next-collapse#test-id-2').should('have.length', 1); + cy.get('.next-collapse-panel').should('have.length', 2); + cy.get('.next-collapse-panel').eq(0).should('have.attr', 'id'); + cy.get('.next-collapse-panel').eq(1).should('have.attr', 'id'); }); }); }); diff --git a/components/collapse/collapse.jsx b/components/collapse/collapse.tsx similarity index 69% rename from components/collapse/collapse.jsx rename to components/collapse/collapse.tsx index 241216de60..23f918f440 100644 --- a/components/collapse/collapse.jsx +++ b/components/collapse/collapse.tsx @@ -1,49 +1,28 @@ -import React from 'react'; +import React, { type Key, type ReactElement } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { polyfill } from 'react-lifecycles-compat'; import ConfigProvider from '../config-provider'; import { func, obj } from '../util'; import Panel from './panel'; +import type { CollapseProps, DataItem, KeyType } from './types'; /** Collapse */ -class Collapse extends React.Component { +class Collapse extends React.Component< + CollapseProps, + { + expandedKeys: KeyType[]; + } +> { static propTypes = { - /** - * 样式前缀 - */ prefix: PropTypes.string, - /** - * 组件接受行内样式 - */ style: PropTypes.object, - /** - * 使用数据模型构建 - */ dataSource: PropTypes.array, - /** - * 默认展开keys - */ defaultExpandedKeys: PropTypes.array, - /** - * 受控展开keys - */ expandedKeys: PropTypes.array, - /** - * 展开状态发升变化时候的回调 - */ onExpand: PropTypes.func, - /** - * 所有禁用 - */ disabled: PropTypes.bool, - /** - * 扩展class - */ className: PropTypes.string, - /** - * 手风琴模式,一次只能打开一个 - */ accordion: PropTypes.bool, children: PropTypes.node, id: PropTypes.string, @@ -60,10 +39,10 @@ class Collapse extends React.Component { prefix: PropTypes.string, }; - constructor(props) { + constructor(props: CollapseProps) { super(props); - let expandedKeys; + let expandedKeys: KeyType[] | undefined; if ('expandedKeys' in props) { expandedKeys = props.expandedKeys; } else { @@ -75,7 +54,7 @@ class Collapse extends React.Component { }; } - static getDerivedStateFromProps(props) { + static getDerivedStateFromProps(props: CollapseProps) { if ('expandedKeys' in props) { return { expandedKeys: typeof props.expandedKeys === 'undefined' ? [] : props.expandedKeys, @@ -84,7 +63,7 @@ class Collapse extends React.Component { return null; } - onItemClick(key) { + onItemClick(key: KeyType) { let expandedKeys = this.state.expandedKeys; if (this.props.accordion) { expandedKeys = String(expandedKeys[0]) === String(key) ? [] : [key]; @@ -102,19 +81,19 @@ class Collapse extends React.Component { this.setExpandedKey(expandedKeys); } - genratePanelId(itemId, index) { + genratePanelId(itemId: string | undefined, index: number) { const { id: collapseId } = this.props; let id; if (itemId) { - // 优先用 item自带的id + // 优先用 item 自带的 id id = itemId; } else if (collapseId) { - // 其次用 collapseId 和 index 生成id + // 其次用 collapseId 和 index 生成 id id = `${collapseId}-panel-${index}`; } return id; } - getProps(item, index, key) { + getProps(item: DataItem, index: number, key: KeyType) { const expandedKeys = this.state.expandedKeys; const { title } = item; let disabled = this.props.disabled; @@ -129,7 +108,12 @@ class Collapse extends React.Component { isExpanded = String(expandedKeys[0]) === String(key); } else { isExpanded = expandedKeys.some(expandedKey => { - if (expandedKey === null || expandedKey === undefined || key === null || key === undefined) { + if ( + expandedKey === null || + expandedKey === undefined || + key === null || + key === undefined + ) { return false; } @@ -152,7 +136,7 @@ class Collapse extends React.Component { : () => { this.onItemClick(key); if ('onClick' in item) { - item.onClick(key); + item.onClick?.(key); } }, }; @@ -161,13 +145,14 @@ class Collapse extends React.Component { getItemsByDataSource() { const { props } = this; const { dataSource } = props; - // 是否有dataSource.item传入过key - const hasKeys = dataSource.some(item => 'key' in item); + // 是否有 dataSource.item 传入过 key + const hasKeys = dataSource!.some(item => 'key' in item); - return dataSource.map((item, index) => { - // 传入过key就用item.key 没传入则统一使用index为key + return dataSource!.map((item, index) => { + // 传入过 key 就用 item.key 没传入则统一使用 index 为 key const key = hasKeys ? item.key : `${index}`; return ( + // @ts-expect-error FIXME 这里要确保 key 一定存在才能正常运行,hasKeys 的判断方式需要改进 {item.content} @@ -176,14 +161,22 @@ class Collapse extends React.Component { } getItemsByChildren() { - // 是否有child传入过key - const allKeys = React.Children.map(this.props.children, child => child && child.key); + // 是否有 child 传入过 key + const allKeys = React.Children.map( + this.props.children, + (child: ReactElement) => child && child.key + ); const hasKeys = Boolean(allKeys && allKeys.length); - return React.Children.map(this.props.children, (child, index) => { - if (child && typeof child.type === 'function' && child.type.isNextPanel) { - // 传入过key就用child.key 没传入则统一使用index为key + return React.Children.map(this.props.children, (child: ReactElement, index) => { + if ( + child && + typeof child.type === 'function' && + (child.type as typeof Panel).isNextPanel + ) { + // 传入过 key 就用 child.key 没传入则统一使用 index 为 key const key = hasKeys ? child.key : `${index}`; + // @ts-expect-error FIXME 这里要确保 key 一定存在才能正常运行,hasKeys 的判断方式需要改进 return React.cloneElement(child, this.getProps(child.props, index, key)); } else { return child; @@ -191,11 +184,11 @@ class Collapse extends React.Component { }); } - setExpandedKey(expandedKeys) { + setExpandedKey(expandedKeys: KeyType[]) { if (!('expandedKeys' in this.props)) { this.setState({ expandedKeys }); } - this.props.onExpand(this.props.accordion ? expandedKeys[0] : expandedKeys); + this.props.onExpand?.(this.props.accordion ? expandedKeys[0] : expandedKeys); } render() { @@ -203,7 +196,7 @@ class Collapse extends React.Component { const collapseClassName = classNames({ [`${prefix}collapse`]: true, [`${prefix}collapse-disabled`]: disabled, - [className]: Boolean(className), + [className!]: className, }); const others = obj.pickOthers(Collapse.propTypes, this.props); diff --git a/components/collapse/index.d.ts b/components/collapse/index.d.ts deleted file mode 100644 index dc4ccbcdf1..0000000000 --- a/components/collapse/index.d.ts +++ /dev/null @@ -1,96 +0,0 @@ -/// - -import React from 'react'; -import { CommonProps } from '../util'; - -interface HTMLAttributesWeak extends React.HTMLAttributes { - title?: any; -} - -export interface PanelProps extends HTMLAttributesWeak, CommonProps { - /** - * 样式类名的品牌前缀 - */ - prefix?: string; - - /** - * 子组件接受行内样式 - */ - style?: React.CSSProperties; - - /** - * 是否禁止用户操作 - */ - disabled?: boolean; - - /** - * 标题 - */ - title?: React.ReactNode; - - /** - * 扩展class - */ - className?: string; -} - -export class Panel extends React.Component {} - -type data = { - title?: React.ReactNode; - content?: React.ReactNode; - disabled?: boolean; - key?: string; - [propName: string]: any; -}; - -export interface CollapseProps extends React.HTMLAttributes, CommonProps { - /** - * 样式前缀 - */ - prefix?: string; - - /** - * 组件接受行内样式 - */ - style?: React.CSSProperties; - - /** - * 使用数据模型构建 - */ - dataSource?: Array; - - /** - * 默认展开keys - */ - defaultExpandedKeys?: Array; - - /** - * 受控展开keys - */ - expandedKeys?: Array; - - /** - * 展开状态发升变化时候的回调 - */ - onExpand?: (expandedKeys: Array) => void; - - /** - * 所有禁用 - */ - disabled?: boolean; - - /** - * 扩展class - */ - className?: string; - - /** - * 手风琴模式,一次只能打开一个 - */ - accordion?: boolean; -} - -export default class Collapse extends React.Component { - static Panel: typeof Panel; -} diff --git a/components/collapse/index.jsx b/components/collapse/index.jsx deleted file mode 100644 index 3d9094b5ed..0000000000 --- a/components/collapse/index.jsx +++ /dev/null @@ -1,6 +0,0 @@ -import Collapse from './collapse'; -import Panel from './panel'; - -Collapse.Panel = Panel; - -export default Collapse; diff --git a/components/collapse/index.tsx b/components/collapse/index.tsx new file mode 100644 index 0000000000..dadcb8653b --- /dev/null +++ b/components/collapse/index.tsx @@ -0,0 +1,9 @@ +import { assignSubComponent } from '../util/component'; +import Collapse from './collapse'; +import Panel from './panel'; +import type { CollapseProps, PanelProps } from './types'; + +const CollapseWithPanel = assignSubComponent(Collapse, { Panel }); + +export default CollapseWithPanel; +export type { CollapseProps, PanelProps }; diff --git a/components/collapse/mobile/index.jsx b/components/collapse/mobile/index.tsx similarity index 67% rename from components/collapse/mobile/index.jsx rename to components/collapse/mobile/index.tsx index 340c246860..26d3ea3be1 100644 --- a/components/collapse/mobile/index.jsx +++ b/components/collapse/mobile/index.tsx @@ -1,3 +1,4 @@ +// @ts-expect-error FIXME: Module '"@alifd/meet-react"' has no exported member 'Collapse'. import { Collapse as MeetCollapse } from '@alifd/meet-react'; import NextCollapse from '../index'; diff --git a/components/collapse/panel.jsx b/components/collapse/panel.tsx similarity index 79% rename from components/collapse/panel.jsx rename to components/collapse/panel.tsx index 44d98a6451..b7dbbe619a 100644 --- a/components/collapse/panel.jsx +++ b/components/collapse/panel.tsx @@ -1,12 +1,13 @@ -import React from 'react'; +import React, { type KeyboardEvent } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import ConfigProvider from '../config-provider'; import Icon from '../icon'; import { func, KEYCODE } from '../util'; +import { type PanelProps } from './types'; /** Collapse.Panel */ -class Panel extends React.Component { +class Panel extends React.Component { static propTypes = { /** * 样式类名的品牌前缀 @@ -27,7 +28,7 @@ class Panel extends React.Component { */ title: PropTypes.node, /** - * 扩展class + * 扩展 class */ className: PropTypes.string, onClick: PropTypes.func, @@ -40,9 +41,9 @@ class Panel extends React.Component { onClick: func.noop, }; - static isNextPanel = true; // + static isNextPanel = true; - onKeyDown = e => { + onKeyDown = (e: KeyboardEvent) => { const { keyCode } = e; if (keyCode === KEYCODE.SPACE) { const { onClick } = this.props; @@ -51,14 +52,25 @@ class Panel extends React.Component { } }; render() { - const { title, children, className, isExpanded, disabled, style, prefix, onClick, id, ...others } = this.props; + const { + title, + children, + className, + isExpanded, + disabled, + style, + prefix, + onClick, + id, + ...others + } = this.props; const cls = classNames({ [`${prefix}collapse-panel`]: true, [`${prefix}collapse-panel-hidden`]: !isExpanded, [`${prefix}collapse-panel-expanded`]: isExpanded, [`${prefix}collapse-panel-disabled`]: disabled, - [className]: className, + [className!]: className, }); const iconCls = classNames({ @@ -66,7 +78,7 @@ class Panel extends React.Component { [`${prefix}collapse-panel-icon-expanded`]: isExpanded, }); - // 为了无障碍 需要添加两个id + // 为了无障碍 需要添加两个 id const headingId = id ? `${id}-heading` : undefined; const regionId = id ? `${id}-region` : undefined; return ( @@ -74,9 +86,9 @@ class Panel extends React.Component {
, 'title' | 'onClick'>; + +/** + * @api Collapse.Panel + */ +export interface PanelProps extends HTMLAttributesWeak, CommonProps { + /** + * 是否禁止用户操作 + * @en Whether to disable user actions + */ + disabled?: boolean; + + /** + * 标题 + * @en Title + */ + title?: React.ReactNode; + /** + * 是否展开 + * @en Whether to expand + * @defaultValue false + */ + + isExpanded?: boolean; + + /** + * 点击回调函数 + * @en Click callback function + */ + onClick?: + | ((e: React.MouseEvent | React.KeyboardEvent) => void) + | null; +} + +/** + * @api + */ +export type KeyType = string | number; + +/** + * @api + */ +export type DataItem = { + id?: string; + title?: React.ReactNode; + content?: React.ReactNode; + disabled?: boolean; + key?: KeyType; + onClick?: (key: KeyType) => void; + [propName: string]: unknown; +}; + +/** + * @api Collapse + */ +export interface CollapseProps extends React.HTMLAttributes, CommonProps { + /** + * 使用数据模型构建 + * @en Use data model to build + */ + dataSource?: Array; + + /** + * 默认展开 keys + * @en Default expanded keys + */ + defaultExpandedKeys?: KeyType[]; + + /** + * 受控展开 keys + * @en Controlled expanded keys + */ + expandedKeys?: KeyType[]; + + /** + * 展开状态发升变化时候的回调 + * @en Callback when the expanded state changes + */ + onExpand?: (expandedKeys: KeyType | KeyType[]) => void; + + /** + * 所有禁用 + * @en All disabled + */ + disabled?: boolean; + + /** + * 手风琴模式,一次只能打开一个 + * @en Accordion mode, only one can be opened at a time + * @defaultValue false + */ + accordion?: boolean; +} diff --git a/components/date-picker/__docs__/theme/index.jsx b/components/date-picker/__docs__/theme/index.jsx index 3fbd638ccb..4f067e69cd 100644 --- a/components/date-picker/__docs__/theme/index.jsx +++ b/components/date-picker/__docs__/theme/index.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import ReactDOM from 'react-dom'; import moment from 'moment'; import { Demo, DemoGroup, initDemo } from '../../../demo-helper'; import DatePicker from '../../index'; @@ -80,11 +81,11 @@ function renderDatePicker(locale, demoFunction, onFunctionChange, otherProps) { - + - + @@ -104,11 +105,11 @@ function renderRangePicker(locale, demoFunction, onFunctionChange, otherProps) { - + - + diff --git a/components/date-picker/__tests__/index-spec.js b/components/date-picker/__tests__/index-spec.js index 2e3275feea..9eceb4dd7e 100644 --- a/components/date-picker/__tests__/index-spec.js +++ b/components/date-picker/__tests__/index-spec.js @@ -1,14 +1,17 @@ import React, { useState } from 'react'; +import ReactTestUtils from 'react-dom/test-utils'; import Enzyme, { mount } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import assert from 'power-assert'; import moment from 'moment'; import DatePicker from '../index'; import { KEYCODE } from '../../util'; +import '../style'; Enzyme.configure({ adapter: new Adapter() }); const { RangePicker, MonthPicker, YearPicker, WeekPicker } = DatePicker; +const delay = duration => new Promise(r => setTimeout(r, duration)); const startValue = moment('2017-11-20', 'YYYY-MM-DD', true); const endValue = moment('2017-12-15', 'YYYY-MM-DD', true); const defaultTimeValue = moment('09:00:00', 'HH:mm:ss', true); @@ -1652,4 +1655,91 @@ describe('RangePicker', () => { ); }); }); + + describe('issues', () => { + it('should render replacing the Focus frame close #3998', async function() { + this.timeout(99999999); + const div = document.createElement('div'); + document.body.appendChild(div); + const wrapper = mount(, {attachTo: div}); + + const clickPanelInput = async (index) => { + const paneInputs = div.querySelectorAll('.next-range-picker-panel-header input'); + assert(paneInputs.length === 2); + paneInputs[index].click(); + await delay(200); + } + const assertPanelInputValue = (index, value) => { + const paneInputs = div.querySelectorAll('.next-range-picker-panel-header input'); + assert(paneInputs.length === 2); + assert(paneInputs[index].value === value); + } + const assertPanelInputFocus = (index) => { + // FIXME 框架限制,focus 状态无法改变,技术升级后再实现 + // const paneInputs = div.querySelectorAll('.next-range-picker-panel-header input'); + // assert(paneInputs.length === 2); + // assert(document.activeElement && document.activeElement === paneInputs[index]); + } + const assertPanelInputHasFocusClass = (index) => { + // FIXME 框架限制,focus 状态无法改变,技术升级后再实现 + // const paneInputs = div.querySelectorAll('.next-range-picker-panel-header .next-input'); + // assert(paneInputs.length === 2); + // assert(paneInputs[index].classList.contains('next-focus')); + } + const clickDate = async (value) => { + // ReactTestUtils.Simulate.click(div.querySelector(`td[title="${value}"] .next-calendar-date`)); + div.querySelector(`td[title="${value}"] .next-calendar-date`).click(); + await delay(100); + } + + const triggerInputs = div.querySelectorAll('.next-range-picker-trigger .next-input'); + assert(triggerInputs.length === 2); + triggerInputs[0].click(); + + await delay(500); + assertPanelInputFocus(0); + assertPanelInputHasFocusClass(0); + await clickDate('2024-03-01'); + assertPanelInputValue(0, '2024-03-01'); + assertPanelInputHasFocusClass(1); + await clickDate('2024-03-08'); + assertPanelInputValue(1, '2024-03-08'); + assertPanelInputHasFocusClass(1); + + await clickPanelInput(0); + assertPanelInputFocus(0); + assertPanelInputHasFocusClass(0); + await clickDate('2024-03-01'); + assertPanelInputValue(0, '2024-03-01'); + assertPanelInputHasFocusClass(1); + clickDate('2024-03-01'); + assertPanelInputHasFocusClass(1); + assertPanelInputValue(1, '2024-03-01'); + + clickPanelInput(1); + assertPanelInputFocus(1); + assertPanelInputHasFocusClass(1); + clickDate('2024-03-05'); + assertPanelInputValue(1, '2024-03-05'); + assertPanelInputHasFocusClass(0); + clickDate('2024-03-05'); + assertPanelInputHasFocusClass(0); + assertPanelInputValue(0, '2024-03-05'); + + document.body.click(); + await delay(300); + triggerInputs[1].click(); + await delay(500); + assertPanelInputFocus(1); + assertPanelInputHasFocusClass(1); + await clickDate('2024-03-08'); + assertPanelInputValue(1, '2024-03-08'); + assertPanelInputHasFocusClass(0); + await clickDate('2024-03-08'); + assertPanelInputValue(0, '2024-03-08'); + assertPanelInputHasFocusClass(0); + + wrapper.unmount(); + }); + }); }); diff --git a/components/date-picker/range-picker.jsx b/components/date-picker/range-picker.jsx index 2c6a8f20eb..b2ec3198a1 100644 --- a/components/date-picker/range-picker.jsx +++ b/components/date-picker/range-picker.jsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React, { Component, createRef } from 'react'; import PropTypes from 'prop-types'; import { polyfill } from 'react-lifecycles-compat'; import classnames from 'classnames'; @@ -243,6 +243,10 @@ class RangePicker extends Component { onVisibleChange: func.noop, }; + startDateInputRef = createRef(); + endDateInputRef = createRef(); + autoSwitchDateInput = false; + constructor(props, context) { super(props, context); const { format, timeFormat, dateTimeFormat } = getDateTimeFormat(props.format, props.showTime, props.type); @@ -320,7 +324,7 @@ class RangePicker extends Component { switch (active || prevActiveDateInput) { case 'startValue': { - if (!prevEndValue || value.valueOf() <= prevEndValue.valueOf()) { + if (!prevEndValue || this.autoSwitchDateInput) { newState.activeDateInput = 'endValue'; } @@ -357,7 +361,7 @@ class RangePicker extends Component { } case 'endValue': - if (!prevStartValue) { + if (!prevStartValue || this.autoSwitchDateInput) { newState.activeDateInput = 'startValue'; } @@ -397,6 +401,11 @@ class RangePicker extends Component { const newStartValue = 'startValue' in newState ? newState.startValue : prevStartValue; const newEndValue = 'endValue' in newState ? newState.endValue : prevEndValue; + // 每当 input 发生了自动切换,则关闭自动切换 + if (newState.activeDateInput !== prevActiveDateInput) { + this.autoSwitchDateInput = false; + } + // 受控状态选择不更新值 if ('value' in this.props) { delete newState.startValue; @@ -689,6 +698,28 @@ class RangePicker extends Component { return disabledTime; }; + enableAutoSwitchDateInput = () => { + this.autoSwitchDateInput = true; + } + + afterOpen = () => { + // autoFocus 逻辑手动处理 + switch(this.state.activeDateInput) { + case 'startValue': { + if (this.startDateInputRef.current) { + this.startDateInputRef.current.getInstance().focus(); + } + break; + } + case 'endValue': { + if (this.endDateInputRef.current) { + this.endDateInputRef.current.getInstance().focus(); + } + break; + } + } + } + renderPreview([startValue, endValue], others) { const { prefix, className, renderPreview } = this.props; const { dateTimeFormat } = this.state; @@ -826,6 +857,8 @@ class RangePicker extends Component { value={startDateInputValue} onFocus={() => this.onFocusDateInput('startValue')} className={startDateInputCls} + ref={this.startDateInputRef} + onClick={func.makeChain(this.enableAutoSwitchDateInput, sharedInputProps.onClick)} /> ); @@ -837,6 +870,8 @@ class RangePicker extends Component { value={endDateInputValue} onFocus={() => this.onFocusDateInput('endValue')} className={endDateInputCls} + ref={this.endDateInputRef} + onClick={func.makeChain(this.enableAutoSwitchDateInput, sharedInputProps.onClick)} /> ); @@ -1084,13 +1119,14 @@ class RangePicker extends Component { return (
({ - props: [{ - name: 'orientation', - type: Types.enum, - options: ['center', 'left', 'right'], - default: 'center' - }, - { - name: 'direction', - type: Types.enum, - options: ['hoz', 'ver'], - default: 'hoz' - }, - { - name: 'dashed', - type: Types.bool, - default: false, - }, - { - name: 'text', - type: Types.string, - default: '', - }], - }), - adaptor: ({ orientation, direction, dashed, text, ...others }) => { - return ; - }, - content: () => ({ - }) -}; diff --git a/components/divider/__docs__/adaptor/index.tsx b/components/divider/__docs__/adaptor/index.tsx new file mode 100644 index 0000000000..b88be22a88 --- /dev/null +++ b/components/divider/__docs__/adaptor/index.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { Divider } from '@alifd/next'; +import { Types } from '@alifd/adaptor-helper'; + +export default { + name: 'Divider', + editor: () => ({ + props: [ + { + name: 'orientation', + type: Types.enum, + options: ['center', 'left', 'right'], + default: 'center', + }, + { + name: 'direction', + type: Types.enum, + options: ['hoz', 'ver'], + default: 'hoz', + }, + { + name: 'dashed', + type: Types.bool, + default: false, + }, + { + name: 'text', + type: Types.string, + default: '', + }, + ], + }), + adaptor: ({ orientation, direction, dashed, text, ...others }: any) => { + return ( + + ); + }, + content: () => ({}), +}; diff --git a/components/divider/__docs__/theme/index.jsx b/components/divider/__docs__/theme/index.jsx deleted file mode 100644 index c870651f47..0000000000 --- a/components/divider/__docs__/theme/index.jsx +++ /dev/null @@ -1,135 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import '../../../demo-helper/style'; -import { Demo, DemoGroup, initDemo } from '../../../demo-helper'; -import ConfigProvider from '../../../config-provider'; -import zhCN from '../../../locale/zh-cn'; -import enUS from '../../../locale/en-us'; -import '../../style'; -import Divider from '../../index'; - -const i18nMap = { - 'zh-cn': { - 'divider': '分割线', - normal: '正常', - dashed: '虚线', - text: '标题', - textLeft: '左标题', - textRight: '右标题', - ver: '垂直模式', - }, - 'en-us': { - 'divider': 'Divider', - normal: 'Normal', - dashed: 'Dashed', - text: 'Text', - textLeft: 'Text Left', - textRight: 'Text Right', - ver: 'Vertical', - }, -}; - -class RenderDivider extends React.Component { - constructor(props) { - super(props); - this.state = { - demoFunction: { - } - }; - } - - onFunctionChange = (demoFunction) => { - this.setState({ demoFunction }); - } - - render() { - const { i18nMap } = this.props; - const { demoFunction } = this.state; - - return ( - - -
-

- Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. -

- -

- Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. -

-
-
- -
-

- Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. -

- -

- Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. -

-
-
- -
-

- Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. -

- {i18nMap.text} -

- Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. -

-
-
- -
-

- Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. -

- {i18nMap.textLeft} -

- Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. -

-
-
- -
-

- Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. -

- {i18nMap.textRight} -

- Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. -

-
-
- -
- Add - - Edit - - Delete -
-
-
-
); - } - -} - -function render(i18nMap, lang) { - ReactDOM.render( -
- -
-
, document.getElementById('container')); -} - -window.renderDemo = function(lang = 'en-us') { - render(i18nMap[lang], lang); -}; - -renderDemo(); - -initDemo('divider'); diff --git a/components/divider/__docs__/theme/index.tsx b/components/divider/__docs__/theme/index.tsx new file mode 100644 index 0000000000..f18d14da0d --- /dev/null +++ b/components/divider/__docs__/theme/index.tsx @@ -0,0 +1,178 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import '../../../demo-helper/style'; +import { Demo, DemoFunctionDefineForObject, DemoGroup, initDemo } from '../../../demo-helper'; +import ConfigProvider from '../../../config-provider'; +import zhCN from '../../../locale/zh-cn'; +import enUS from '../../../locale/en-us'; +import '../../style'; +import Divider from '../../index'; + +const i18nMap = { + 'zh-cn': { + divider: '分割线', + normal: '正常', + dashed: '虚线', + text: '标题', + textLeft: '左标题', + textRight: '右标题', + ver: '垂直模式', + }, + 'en-us': { + divider: 'Divider', + normal: 'Normal', + dashed: 'Dashed', + text: 'Text', + textLeft: 'Text Left', + textRight: 'Text Right', + ver: 'Vertical', + }, +}; + +type I18nMap = Record; +interface RenderDividerProps { + i18nMap: I18nMap; +} +interface DemoFunction { + [index: string]: DemoFunctionDefineForObject; +} +interface RenderDividerState { + demoFunction: DemoFunction; +} +class RenderDivider extends React.Component { + state = { + demoFunction: {}, + }; + onFunctionChange = (demoFunction: DemoFunction) => { + this.setState({ demoFunction }); + }; + + render() { + const { i18nMap } = this.props; + const { demoFunction } = this.state; + + return ( + + + +
+

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim + ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut + aliquip ex ea commodo consequat. +

+ +

+ Duis aute irure dolor in reprehenderit in voluptate velit esse + cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat + cupidatat non proident, sunt in culpa qui officia deserunt mollit + anim id est laborum. +

+
+
+ +
+

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim + ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut + aliquip ex ea commodo consequat. +

+ +

+ Duis aute irure dolor in reprehenderit in voluptate velit esse + cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat + cupidatat non proident, sunt in culpa qui officia deserunt mollit + anim id est laborum. +

+
+
+ +
+

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim + ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut + aliquip ex ea commodo consequat. +

+ {i18nMap.text} +

+ Duis aute irure dolor in reprehenderit in voluptate velit esse + cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat + cupidatat non proident, sunt in culpa qui officia deserunt mollit + anim id est laborum. +

+
+
+ +
+

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim + ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut + aliquip ex ea commodo consequat. +

+ {i18nMap.textLeft} +

+ Duis aute irure dolor in reprehenderit in voluptate velit esse + cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat + cupidatat non proident, sunt in culpa qui officia deserunt mollit + anim id est laborum. +

+
+
+ +
+

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim + ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut + aliquip ex ea commodo consequat. +

+ {i18nMap.textRight} +

+ Duis aute irure dolor in reprehenderit in voluptate velit esse + cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat + cupidatat non proident, sunt in culpa qui officia deserunt mollit + anim id est laborum. +

+
+
+ +
+ Add + + Edit + + Delete +
+
+
+
+ ); + } +} + +function render(i18nMap: I18nMap, lang: string) { + ReactDOM.render( + +
+ +
+
, + document.getElementById('container') + ); +} + +window.renderDemo = function (lang = 'en-us') { + render(i18nMap[lang], lang); +}; + +renderDemo(); + +initDemo('divider'); diff --git a/components/divider/__tests__/a11y-spec.js b/components/divider/__tests__/a11y-spec.js deleted file mode 100644 index 0a17f73752..0000000000 --- a/components/divider/__tests__/a11y-spec.js +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; -import Enzyme from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; -import Divider from '../index'; -import '../style'; -import { unmount, testReact } from '../../util/__tests__/legacy/a11y/validate'; - -Enzyme.configure({ adapter: new Adapter() }); - -/* eslint-disable no-undef, react/jsx-filename-extension */ -describe('Divider A11y', () => { - let wrapper; - - afterEach(() => { - if (wrapper) { - wrapper.unmount(); - wrapper = null; - } - unmount(); - }); - - it('should render', async () => { - wrapper = await testReact(); - return wrapper; - }); - - it('should render dashed', async () => { - wrapper = await testReact(); - return wrapper; - }); - - it('should render ver', async () => { - wrapper = await testReact(); - return wrapper; - }); - - it('should render orientation', async () => { - wrapper = await testReact(Left Text); - return wrapper; - }); -}); diff --git a/components/divider/__tests__/a11y-spec.tsx b/components/divider/__tests__/a11y-spec.tsx new file mode 100644 index 0000000000..ec842a015e --- /dev/null +++ b/components/divider/__tests__/a11y-spec.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import Divider from '../index'; +import '../style'; +import { testReact } from '../../util/__tests__/a11y/validate'; + +describe('Divider A11y', () => { + it('should render', async () => { + await testReact(); + }); + + it('should render dashed', async () => { + await testReact(); + }); + + it('should render ver', async () => { + await testReact(); + }); + + it('should render orientation', async () => { + await testReact(Left Text); + }); +}); diff --git a/components/divider/__tests__/index-spec.js b/components/divider/__tests__/index-spec.js deleted file mode 100644 index 6c167c3a2a..0000000000 --- a/components/divider/__tests__/index-spec.js +++ /dev/null @@ -1,69 +0,0 @@ -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 Divider from '../index'; -import '../style'; - -Enzyme.configure({ adapter: new Adapter() }); - -const render = element => { - let inc; - const container = document.createElement('div'); - document.body.appendChild(container); - ReactDOM.render(element, container, function() { - inc = this; - }); - return { - setProps: props => { - const clonedElement = React.cloneElement(element, props); - ReactDOM.render(clonedElement, container); - }, - unmount: () => { - ReactDOM.unmountComponentAtNode(container); - document.body.removeChild(container); - }, - instance: () => { - return inc; - }, - find: selector => { - return container.querySelectorAll(selector); - }, - }; -}; - -describe('Divider', () => { - let wrapper; - - beforeEach(() => { - const overlay = document.querySelectorAll('.next-overlay-wrapper'); - overlay.forEach(dom => { - document.body.removeChild(dom); - }); - }); - - afterEach(() => { - if (wrapper) { - wrapper.unmount(); - wrapper = null; - } - }); - - it('should render', () => { - wrapper = render(); - assert(document.querySelector('.next-divider')); - }); - - it('should render dashed', () => { - wrapper = render(); - assert(document.querySelector('.next-divider-dashed')); - }); - - it('should render text right', () => { - wrapper = render(Right Text); - - assert(document.querySelector('.next-divider-with-text-right')); - assert(document.querySelector('.next-divider-inner-text').innerHTML === 'Right Text'); - }); -}); diff --git a/components/divider/__tests__/index-spec.tsx b/components/divider/__tests__/index-spec.tsx new file mode 100644 index 0000000000..2546e07f5d --- /dev/null +++ b/components/divider/__tests__/index-spec.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import Divider from '../index'; +import '../style'; + +describe('Divider', () => { + it('should render', () => { + cy.mount(); + cy.get('.next-divider'); + }); + + it('should render dashed', () => { + cy.mount(); + cy.get('.next-divider-dashed'); + }); + + it('should render text right', () => { + cy.mount(Right Text); + cy.get('.next-divider-with-text-right'); + cy.get('.next-divider-inner-text').contains('Right Text'); + }); +}); diff --git a/components/divider/index.jsx b/components/divider/index.tsx similarity index 87% rename from components/divider/index.jsx rename to components/divider/index.tsx index d22f301c36..a3ea3a3fd5 100644 --- a/components/divider/index.jsx +++ b/components/divider/index.tsx @@ -2,28 +2,20 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { polyfill } from 'react-lifecycles-compat'; -import ConfigProvider from '../config-provider'; import { obj } from '../util'; +import type { DividerProps } from './types'; +import ConfigProvider from '../config-provider'; /** * Divider */ -class Divider extends Component { +class Divider extends Component { static propTypes = { prefix: PropTypes.string, children: PropTypes.any, className: PropTypes.string, - /** - * 是否为虚线 - */ dashed: PropTypes.bool, - /** - * 线是水平还是垂直类型 - */ direction: PropTypes.oneOf(['hoz', 'ver']), - /** - * 分割线标题的位置 - */ orientation: PropTypes.oneOf(['left', 'right', 'center']), }; @@ -56,4 +48,5 @@ class Divider extends Component { } } +export type { DividerProps }; export default ConfigProvider.config(polyfill(Divider)); diff --git a/components/divider/mobile/index.jsx b/components/divider/mobile/index.jsx deleted file mode 100644 index d7d55bbed5..0000000000 --- a/components/divider/mobile/index.jsx +++ /dev/null @@ -1,6 +0,0 @@ -import { Divider as MeetDivider } from '@alifd/meet-react'; -import NextDivider from '../index'; - -const Divider = MeetDivider ? MeetDivider : NextDivider; - -export default Divider; diff --git a/components/divider/mobile/index.tsx b/components/divider/mobile/index.tsx new file mode 100644 index 0000000000..2b1c71a25e --- /dev/null +++ b/components/divider/mobile/index.tsx @@ -0,0 +1,3 @@ +import { Divider } from '@alifd/meet-react'; + +export default Divider; diff --git a/components/divider/style.js b/components/divider/style.ts similarity index 100% rename from components/divider/style.js rename to components/divider/style.ts diff --git a/components/divider/index.d.ts b/components/divider/types.ts similarity index 62% rename from components/divider/index.d.ts rename to components/divider/types.ts index f02f61856b..4fee0a5385 100644 --- a/components/divider/index.d.ts +++ b/components/divider/types.ts @@ -1,21 +1,25 @@ -/// - import React from 'react'; import { CommonProps } from '../util'; - +/** + * @api Divider + */ export interface DividerProps extends React.HTMLAttributes, CommonProps { /** * 是否为虚线 + * @en Is it a dashed line + * @defaultValue false */ dashed?: boolean; /** * 线是水平还是垂直类型 + * @en Is the line of horizontal or vertical type + * @defaultValue 'hoz' */ direction?: 'hoz' | 'ver'; /** * 分割线标题的位置 + * @en The position of the divider title + * @defaultValue 'center' */ orientation?: 'left' | 'right' | 'center'; } - -export default class Divider extends React.Component {} diff --git a/components/drawer/__docs__/demo/basic/index.tsx b/components/drawer/__docs__/demo/basic/index.tsx index c5fc5f0708..e9f21ec266 100644 --- a/components/drawer/__docs__/demo/basic/index.tsx +++ b/components/drawer/__docs__/demo/basic/index.tsx @@ -13,7 +13,7 @@ class Demo extends React.Component { }); }; - onClose = (reason, e) => { + onClose = (reason: string, e: React.MouseEvent | KeyboardEvent) => { 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 6f4ae95d7d..6b4f31c553 100644 --- a/components/drawer/__docs__/demo/placement/index.tsx +++ b/components/drawer/__docs__/demo/placement/index.tsx @@ -14,13 +14,12 @@ class Demo extends React.Component { }); }; - onClose = reason => { + onClose = (reason: string, e: React.MouseEvent | KeyboardEvent) => { this.setState({ visible: false, }); }; - - onPlacementChange = dir => { + onPlacementChange = (dir: string | number | boolean, e: any) => { this.setState({ placement: dir, }); @@ -44,7 +43,7 @@ class Demo extends React.Component { v2 title="标题" visible={this.state.visible} - placement={this.state.placement} + placement={this.state.placement as 'right' | 'bottom' | 'left' | 'top'} onClose={this.onClose} > Start your business here by searching a popular product diff --git a/components/drawer/__docs__/demo/quick/index.tsx b/components/drawer/__docs__/demo/quick/index.tsx index f3ef96764b..6b4e4ae478 100644 --- a/components/drawer/__docs__/demo/quick/index.tsx +++ b/components/drawer/__docs__/demo/quick/index.tsx @@ -1,8 +1,9 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Button, Drawer } from '@alifd/next'; +import { QuickShowRet } from '../../../types'; -let instance = null; +let instance: QuickShowRet | null = null; const show = () => { instance = Drawer.show({ title: 'quick', diff --git a/components/drawer/__docs__/demo/select/index.tsx b/components/drawer/__docs__/demo/select/index.tsx index 9bade387c5..64784d45df 100644 --- a/components/drawer/__docs__/demo/select/index.tsx +++ b/components/drawer/__docs__/demo/select/index.tsx @@ -4,14 +4,14 @@ import { Radio, Drawer, Select } from '@alifd/next'; const Option = Select.Option; -const onChange = function (value) { +const onChange = function (value: any) { console.log(value); }; -const onBlur = function (e) { +const onBlur = function (e: any) { console.log(/onblur/, e); }; -const onToggleHighlightItem = function (item, type) { +const onToggleHighlightItem = function (item: any, type: any) { console.log(item, type); }; @@ -20,7 +20,7 @@ class Demo extends React.Component { placement: 'right', }; - onPlacementChange = dir => { + onPlacementChange = (dir: any) => { this.setState({ placement: dir, }); @@ -48,7 +48,7 @@ class Demo extends React.Component { autoWidth={false} onChange={onChange} onBlur={onBlur} - onToggleHighlightItem={onToggleHighlightItem} + onToggleHighlightItem={onToggleHighlightItem as any} defaultValue="jack" aria-label="name is" hasClear diff --git a/components/drawer/__docs__/demo/size/index.tsx b/components/drawer/__docs__/demo/size/index.tsx index 9b5dbc01a8..f2abe64036 100644 --- a/components/drawer/__docs__/demo/size/index.tsx +++ b/components/drawer/__docs__/demo/size/index.tsx @@ -9,7 +9,7 @@ const Demo = () => { return (
- @@ -23,7 +23,7 @@ const Demo = () => { > Start your business here by searching a popular product - diff --git a/components/drawer/__docs__/theme/index.tsx b/components/drawer/__docs__/theme/index.tsx index 133bb283aa..26ba948ac3 100644 --- a/components/drawer/__docs__/theme/index.tsx +++ b/components/drawer/__docs__/theme/index.tsx @@ -7,6 +7,15 @@ import Drawer from '../../index'; import zhCN from '../../../locale/zh-cn'; import enUS from '../../../locale/en-us'; +import { InnerProps } from '../../types'; + +interface FunctionProps { + lang: string; + i18n: { + title: string; + content: string; + }; +} const i18nMaps = { 'en-us': { @@ -20,7 +29,7 @@ const i18nMaps = { }, }; -class FunctionDemo extends Component { +class FunctionDemo extends Component { state = { demoFunction: { hasTitle: { @@ -75,13 +84,13 @@ class FunctionDemo extends Component { }, }, }; - onFunctionChange = demoFunction => { + onFunctionChange = (demoFunction: any) => { this.setState({ demoFunction, }); }; - renderMask(hasMask, content) { + renderMask(hasMask: boolean, content: object | null | undefined) { return hasMask ? (
{ - const i18n = i18nMaps[lang]; + const i18n = i18nMaps[lang as keyof typeof i18nMaps]; ReactDOM.render(, document.getElementById('container')); }; diff --git a/components/drawer/__tests__/a11y-spec.tsx b/components/drawer/__tests__/a11y-spec.tsx index 326198fd3b..a326191352 100644 --- a/components/drawer/__tests__/a11y-spec.tsx +++ b/components/drawer/__tests__/a11y-spec.tsx @@ -1,32 +1,12 @@ import React from 'react'; -import Enzyme, { mount } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; -import assert from 'power-assert'; import Drawer from '../index'; +import { testReact } from '../../util/__tests__/a11y/validate'; import '../style'; -import { test, unmount } from '../../util/__tests__/legacy/a11y/validate'; -import { roleType, isHeading, isButton } from '../../util/__tests__/legacy/a11y/checks'; - -/* eslint-disable react/jsx-filename-extension */ -/* global describe it afterEach */ - -Enzyme.configure({ adapter: new Adapter() }); describe('Drawer A11y', () => { describe('Basic', () => { - let wrapper; - - afterEach(() => { - if (wrapper && wrapper.unmount) { - wrapper.unmount(); - wrapper = null; - } - unmount(); - }); - it('should not have any violations', async () => { - wrapper = await mount(); - return test('.next-overlay-wrapper'); + await testReact(); }); }); }); diff --git a/components/drawer/__tests__/index-spec.tsx b/components/drawer/__tests__/index-spec.tsx index dd1d35819b..1817801cc5 100644 --- a/components/drawer/__tests__/index-spec.tsx +++ b/components/drawer/__tests__/index-spec.tsx @@ -1,122 +1,62 @@ import React from 'react'; -import ReactDOM from 'react-dom'; -import assert from 'power-assert'; -import Enzyme, { shallow } from 'enzyme'; -import ReactTestUtils from 'react-dom/test-utils'; -import Adapter from 'enzyme-adapter-react-16'; -import { dom } from '../../util'; import Drawer from '../index'; import ConfigProvider from '../../config-provider'; import '../style'; -Enzyme.configure({ adapter: new Adapter() }); - -/* eslint-disable react/jsx-filename-extension */ -/* global describe it afterEach */ -/* global describe it beforeEach */ -const { hasClass, getStyle } = dom; - -const render = element => { - let inc; - const container = document.createElement('div'); - document.body.appendChild(container); - ReactDOM.render(element, container, function () { - inc = this; - }); - return { - setProps: props => { - const clonedElement = React.cloneElement(element, props); - ReactDOM.render(clonedElement, container); - }, - unmount: () => { - ReactDOM.unmountComponentAtNode(container); - document.body.removeChild(container); - }, - instance: () => { - return inc; - }, - find: selector => { - return container.querySelectorAll(selector); - }, - }; -}; - -class DrawerDemo extends React.Component { - state = { - visible: false, - }; - - onOpen = () => { - this.setState({ - visible: true, - }); - }; - - onClose = () => { - this.setState({ - visible: false, - }); - }; - - render() { - return ( -
- - - 开启您的贸易生活从 Alibaba.com 开始 - -
- ); - } -} - describe('Drawer', () => { - let wrapper; - - beforeEach(() => { - const overlay = document.querySelectorAll('.next-overlay-wrapper'); - overlay.forEach(dom => { - document.body.removeChild(dom); - }); - }); - - afterEach(() => { - if (wrapper) { - wrapper.unmount(); - wrapper = null; - } - }); - it('should show and hide', () => { - wrapper = render(); - const btn = document.getElementById('open-drawer'); - ReactTestUtils.Simulate.click(btn); - - assert(document.querySelector('.next-drawer')); - const closeLink = document.querySelector('.next-drawer-close'); - ReactTestUtils.Simulate.click(closeLink); - - assert(!document.querySelector('.next-drawer')); + class DrawerDemo extends React.Component { + state = { + visible: false, + }; + + onOpen = () => { + this.setState({ + visible: true, + }); + }; + + onClose = () => { + this.setState({ + visible: false, + }); + }; + + render() { + return ( +
+ + + 开启您的贸易生活从 Alibaba.com 开始 + +
+ ); + } + } + cy.mount(); + cy.get('button#open-drawer').click(); + cy.get('.next-drawer').should('be.visible'); + cy.get('.next-drawer-close').click(); + cy.get('.next-drawer').should('not.exist'); }); it('should support placement', () => { - ['top', 'left', 'bottom', 'right'].forEach(dir => { - wrapper && wrapper.unmount(); - - wrapper = render(); - assert(hasClass(document.querySelector('.next-drawer'), `next-drawer-${dir}`)); + ['top', 'left', 'bottom', 'right'].forEach((dir: 'top' | 'left' | 'bottom' | 'right') => { + cy.mount(); + cy.get('.next-drawer').should('exist'); + cy.get(`.next-drawer-${dir}`).should('exist'); }); }); it('should work when set ', () => { - wrapper = render( + cy.mount(
@@ -125,30 +65,34 @@ describe('Drawer', () => {
); - - const overlay = document.querySelector('#dialog-popupcontainer > .next-overlay-wrapper'); - assert(overlay); + cy.get('#dialog-popupcontainer').within(() => { + cy.get('.next-overlay-wrapper').should('exist'); + }); }); it('should hide close link if set closeable to false', () => { - wrapper = render(); - assert(!document.querySelector('.next-drawer-close')); + cy.mount(); + cy.get('.next-drawer-close').should('not.exist'); + }); + + it('should hide close link if set closeMode to []', () => { + cy.mount(); + cy.get('.next-drawer-close').should('not.exist'); }); it('should support headerStyle/bodyStyle', () => { - wrapper = render( + cy.mount( body ); - - assert(getStyle(document.querySelector('.next-drawer-header'), 'background'), 'blue'); - assert(getStyle(document.querySelector('.next-drawer-body'), 'background'), 'red'); + cy.get('.next-drawer-header').should('have.css', 'background-color', 'rgb(0, 0, 255)'); + cy.get('.next-drawer-body').should('have.css', 'background-color', 'rgb(255, 0, 0)'); }); }); diff --git a/components/drawer/__tests__/index-v2-spec.tsx b/components/drawer/__tests__/index-v2-spec.tsx index e31df7abe1..d1af4efd99 100644 --- a/components/drawer/__tests__/index-v2-spec.tsx +++ b/components/drawer/__tests__/index-v2-spec.tsx @@ -1,125 +1,63 @@ import React from 'react'; -import ReactDOM from 'react-dom'; -import assert from 'power-assert'; -import Enzyme, { shallow } from 'enzyme'; -import ReactTestUtils from 'react-dom/test-utils'; -import Adapter from 'enzyme-adapter-react-16'; -import { dom } from '../../util'; import Drawer from '../index'; import ConfigProvider from '../../config-provider'; import '../style'; -Enzyme.configure({ adapter: new Adapter() }); - -/* eslint-disable react/jsx-filename-extension */ -/* global describe it afterEach */ -/* global describe it beforeEach */ -const { hasClass, getStyle } = dom; - -const render = element => { - let inc; - const container = document.createElement('div'); - document.body.appendChild(container); - ReactDOM.render(element, container, function () { - inc = this; - }); - return { - setProps: props => { - const clonedElement = React.cloneElement(element, props); - ReactDOM.render(clonedElement, container); - }, - unmount: () => { - ReactDOM.unmountComponentAtNode(container); - document.body.removeChild(container); - }, - instance: () => { - return inc; - }, - find: selector => { - return container.querySelectorAll(selector); - }, - }; -}; - -class DrawerDemo extends React.Component { - state = { - visible: false, - }; - - onOpen = () => { - this.setState({ - visible: true, - }); - }; - - onClose = () => { - this.setState({ - visible: false, - }); - }; - - render() { - return ( -
- - - 开启您的贸易生活从 Alibaba.com 开始 - -
- ); - } -} - describe('Drawer v2', () => { - let wrapper; - const delay = time => new Promise(resolve => setTimeout(resolve, time)); - - beforeEach(() => { - const overlay = document.querySelectorAll('.next-overlay-wrapper'); - overlay.forEach(dom => { - document.body.removeChild(dom); - }); - }); - - afterEach(() => { - if (wrapper) { - wrapper.unmount(); - wrapper = null; + it('should show and hide', () => { + class DrawerDemo extends React.Component { + state = { + visible: false, + }; + + onOpen = () => { + this.setState({ + visible: true, + }); + }; + + onClose = () => { + this.setState({ + visible: false, + }); + }; + + render() { + return ( +
+ + + 开启您的贸易生活从 Alibaba.com 开始 + +
+ ); + } } - }); - - it('should show and hide', async () => { - wrapper = render(); - const btn = document.getElementById('open-drawer'); - ReactTestUtils.Simulate.click(btn); - await delay(20); - assert(document.querySelector('.next-drawer')); - const closeLink = document.querySelector('.next-drawer-close'); - ReactTestUtils.Simulate.click(closeLink); - await delay(20); - - assert(!document.querySelector('.next-drawer')); + cy.mount(); + cy.get('button#open-drawer').click(); + cy.get('.next-drawer').should('be.visible'); + cy.get('.next-drawer-close').click(); + cy.get('.next-drawer').should('not.exist'); }); it('should support placement', () => { - ['top', 'left', 'bottom', 'right'].forEach(dir => { - wrapper && wrapper.unmount(); - - wrapper = render(); - assert(hasClass(document.querySelector('.next-drawer-wrapper'), `next-drawer-${dir}`)); + ['top', 'left', 'bottom', 'right'].forEach((dir: 'top' | 'left' | 'bottom' | 'right') => { + cy.mount(); + cy.get('.next-drawer').should('exist'); + cy.get(`.next-drawer-${dir}`).should('exist'); }); }); - it('should work when set ', async () => { - wrapper = render( + it('should work when set ', () => { + cy.mount(
@@ -128,27 +66,26 @@ describe('Drawer v2', () => {
); - - await delay(20); - assert(document.querySelector('#dialog-popupcontainer > .next-overlay-wrapper')); + cy.get('#dialog-popupcontainer').within(() => { + cy.get('.next-overlay-wrapper').should('exist'); + }); }); it('should support headerStyle/bodyStyle', () => { - wrapper = render( + cy.mount( body ); - - assert(getStyle(document.querySelector('.next-drawer-header'), 'background'), 'blue'); - assert(getStyle(document.querySelector('.next-drawer-body'), 'background'), 'red'); + cy.get('.next-drawer-header').should('have.css', 'background-color', 'rgb(0, 0, 255)'); + cy.get('.next-drawer-body').should('have.css', 'background-color', 'rgb(255, 0, 0)'); }); it('quick-calling should should support set prefix for dialog', () => { @@ -159,8 +96,9 @@ describe('Drawer v2', () => { content: , }); - assert(hasClass(document.querySelector('.test-drawer'), 'test-closeable')); - assert(document.querySelector('.drawer-quick-content')); + cy.get('.test-drawer').should('exist'); + cy.get('.test-closeable').should('exist'); + cy.get('.drawer-quick-content').should('exist'); hide(); }); diff --git a/components/drawer/drawer-v2.tsx b/components/drawer/drawer-v2.tsx index 12b4867a51..baf3016b48 100644 --- a/components/drawer/drawer-v2.tsx +++ b/components/drawer/drawer-v2.tsx @@ -9,11 +9,16 @@ 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'; const { OverlayContext } = Overlay; const noop = func.noop; -const getAnimation = placement => { +interface CustomDrawerElement extends HTMLDivElement { + bodyNode?: HTMLElement; +} + +const getAnimation = (placement: string) => { let animation; switch (placement) { case 'top': @@ -46,7 +51,7 @@ const getAnimation = placement => { return animation; }; -const Drawer = props => { +const Drawer = (props: DrawerProps) => { if (!useState || !useRef || !useEffect) { log.warning('need react version > 16.8.0'); return null; @@ -87,18 +92,18 @@ const Drawer = props => { ? () => popupContainer : popupContainer; const [container, setContainer] = useState(getContainer()); - const drawerRef = useRef(null); - const wrapperRef = useRef(null); - const lastFocus = useRef(null); - const locker = useRef(null); + const drawerRef = useRef(null); + const wrapperRef = useRef(null); + const lastFocus = useRef(null); + const locker = useRef | null>(null); const [uuid] = useState(guid()); const { setVisibleOverlayToParent, ...otherContext } = useContext(OverlayContext); const childIDMap = useRef(new Map()); const isAnimationEnd = useRef(false); // 动效是否结束, 因为时机非常快用 state 太慢 - const [, forceUpdate] = useState(); + const [, forceUpdate] = useState(); // 动效结束,强制重新渲染 - const markAnimationEnd = state => { + const markAnimationEnd = (state: boolean) => { isAnimationEnd.current = state; forceUpdate({}); }; @@ -132,7 +137,7 @@ const Drawer = props => { // 打开遮罩后 document.body 滚动处理 useEffect(() => { if (visible && hasMask) { - const style = { + const style: { paddingRight?: string; overflow: string } = { overflow: 'hidden', }; @@ -140,7 +145,8 @@ const Drawer = props => { const scrollWidth = dom.scrollbar().width; if (scrollWidth) { style.paddingRight = `${ - dom.getStyle(document.body, 'paddingRight') + dom.scrollbar().width + dom.getStyle(document.body, 'paddingRight').toString() + + dom.scrollbar().width }px`; } } @@ -148,12 +154,15 @@ const Drawer = props => { } }, [visible && hasMask]); - const handleClose = (targetType, e) => { + const handleClose = ( + targetType: string, + e: React.MouseEvent | KeyboardEvent + ) => { setVisibleOverlayToParent(uuid, null); typeof onClose === 'function' && onClose(targetType, e); }; - const keydownEvent = e => { + const keydownEvent = (e: KeyboardEvent) => { if (e.keyCode === 27 && canCloseByEsc && !childIDMap.current.size) { handleClose('esc', e); } @@ -187,7 +196,7 @@ const Drawer = props => { const handleExited = () => { if (!isAnimationEnd.current) { markAnimationEnd(true); - dom.setStyle(wrapperRef.current, 'display', 'none'); + dom.setStyle(wrapperRef.current as HTMLDivElement, 'display', 'none'); scrollLocker.unlock(document.body, locker.current); if (autoFocus && lastFocus.current) { @@ -217,7 +226,7 @@ const Drawer = props => { return null; } - const handleMaskClick = e => { + const handleMaskClick = (e: React.MouseEvent | KeyboardEvent) => { if (!canCloseByMask) { return; } @@ -227,14 +236,15 @@ const Drawer = props => { const handleEnter = () => { markAnimationEnd(false); - dom.setStyle(wrapperRef.current, 'display', ''); + dom.setStyle(wrapperRef.current as HTMLDivElement, 'display', ''); }; const handleEntered = () => { if (autoFocus && drawerRef.current && drawerRef.current.bodyNode) { const focusableNodes = focus.getFocusNodeList(drawerRef.current.bodyNode); if (focusableNodes.length > 0 && focusableNodes[0]) { - lastFocus.current = document.activeElement; - focusableNodes[0].focus(); + lastFocus.current = document.activeElement as HTMLElement; + const firstFocusableNode = focusableNodes[0] as HTMLElement; + firstFocusableNode.focus(); } } setVisibleOverlayToParent(uuid, drawerRef.current); @@ -248,14 +258,14 @@ const Drawer = props => { [`${prefix}overlay-inner`]: true, [`${prefix}drawer-wrapper`]: true, [`${prefix}drawer-${placement}`]: true, - [className]: !!className, + [className as string]: !!className, }); const drawerCls = classNames({ [`${prefix}drawer-v2`]: true, - [className]: !!className, + [className as string]: !!className, }); - const newAnimation = + const newAnimation: { in: string; out: string } | boolean | null = animation === null || animation === false ? null : animation @@ -268,7 +278,7 @@ const Drawer = props => { exit: 250, }; - const getVisibleOverlayFromChild = (id, node) => { + const getVisibleOverlayFromChild = (id: string, node: HTMLElement) => { if (node) { childIDMap.current.set(id, node); } else { @@ -310,7 +320,13 @@ const Drawer = props => {
+ | undefined + } timeout={timeout} onEnter={handleEnter} onEntered={handleEntered} diff --git a/components/drawer/drawer.tsx b/components/drawer/drawer.tsx index f6bccf0a92..070372b455 100644 --- a/components/drawer/drawer.tsx +++ b/components/drawer/drawer.tsx @@ -1,19 +1,26 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import Overlay from '../overlay'; +import Overlay, { PopupProps } from '../overlay'; import Inner from './inner'; import zhCN from '../locale/zh-cn'; import { obj } from '../util'; +import { DrawerProps } from './types'; const noop = () => {}; const { Popup } = Overlay; const { pickOthers } = obj; +interface CloseConfig { + canCloseByEsc?: boolean; + canCloseByCloseClick?: boolean; + canCloseByMask?: boolean; +} + /** * Drawer - * @description 继承 Overlay.Popup 的 API,除非特别说明 + * 继承 Overlay.Popup 的 API,除非特别说明 * */ -export default class Drawer extends Component { +export default class Drawer extends Component { static displayName = 'Drawer'; static propTypes = { @@ -60,8 +67,8 @@ export default class Drawer extends Component { ]), /** * 对话框关闭时触发的回调函数 - * @param {String} trigger 关闭触发行为的描述字符串 - * @param {Object} event 关闭时事件对象 + * @param trigger - 关闭触发行为的描述字符串 + * @param event - 关闭时事件对象 */ onClose: PropTypes.func, /** @@ -95,8 +102,10 @@ export default class Drawer extends Component { // 受控模式下(没有 trigger 的时候),只会在关闭时触发,相当于onClose onVisibleChange: PropTypes.func, /** - * 显示隐藏时动画的播放方式,支持 { in: 'enter-class', out: 'leave-class' } 的对象参数,如果设置为 false,则不播放动画。 请参考 Animate 组件的文档获取可用的动画名 - * @default { in: 'expandInDown', out: 'expandOutUp' } + * 显示隐藏时动画的播放方式,支持 `{@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, @@ -123,7 +132,9 @@ export default class Drawer extends Component { locale: zhCN.Drawer, }; - getAlign = placement => { + private overlay: Overlay | null = null; + + getAlign = (placement: string) => { let align; switch (placement) { case 'top': @@ -144,7 +155,7 @@ export default class Drawer extends Component { return align; }; - getAnimation = placement => { + getAnimation = (placement: string) => { if ('animation' in this.props) { return this.props.animation; } @@ -181,12 +192,12 @@ export default class Drawer extends Component { return animation; }; - getOverlayRef = ref => { + getOverlayRef = (ref: Overlay | null) => { this.overlay = ref; }; - mapcloseableToConfig = closeable => { - return ['esc', 'close', 'mask'].reduce((ret, option) => { + mapcloseableToConfig = (closeable: boolean | string): CloseConfig => { + return ['esc', 'close', 'mask'].reduce((ret: CloseConfig, option) => { const key = option.charAt(0).toUpperCase() + option.substr(1); const value = typeof closeable === 'boolean' @@ -194,16 +205,16 @@ export default class Drawer extends Component { : closeable.split(',').indexOf(option) > -1; if (option === 'esc' || option === 'mask') { - ret[`canCloseBy${key}`] = value; + ret[`canCloseBy${key}` as keyof CloseConfig] = value; } else { - ret[`canCloseBy${key}Click`] = value; + ret[`canCloseBy${key}Click` as keyof CloseConfig] = value; } return ret; }, {}); }; - handleVisibleChange = (visible, reason, e) => { + handleVisibleChange = (visible: boolean, reason: string, e: React.MouseEvent) => { const { onClose, onVisibleChange } = this.props; if (visible === false) { @@ -213,7 +224,17 @@ export default class Drawer extends Component { onVisibleChange && onVisibleChange(visible, reason, e); }; - renderInner(closeable) { + renderInner( + closeable: + | boolean + | 'esc' + | 'close' + | 'mask' + | 'close,mask' + | 'close,esc' + | 'mask,esc' + | undefined + ) { const { prefix, className, @@ -226,7 +247,8 @@ export default class Drawer extends Component { placement, rtl, } = this.props; - const others = pickOthers(Object.keys(Drawer.propTypes), this.props); + type OthersProps = Omit; + const others: OthersProps = pickOthers(Object.keys(Drawer.propTypes), this.props); return ( ) => void + ).bind(this, 'closeClick')} {...others} > {children} @@ -265,6 +289,8 @@ export default class Drawer extends Component { closeMode, rtl, popupContainer, + content, + title, ...others } = this.props; @@ -281,7 +307,9 @@ export default class Drawer extends Component { : closeMode : closeable; - const { canCloseByCloseClick, ...closeConfig } = this.mapcloseableToConfig(newCloseable); + const { canCloseByCloseClick, ...closeConfig } = this.mapcloseableToConfig( + newCloseable as boolean | string + ); const newPopupProps = { prefix, @@ -289,9 +317,9 @@ export default class Drawer extends Component { trigger, triggerType, onVisibleChange: this.handleVisibleChange, - animation: this.getAnimation(placement), + animation: this.getAnimation(placement as string), hasMask, - align: this.getAlign(placement), + align: this.getAlign(placement as string), ...closeConfig, canCloseByOutSideClick: false, disableScroll: true, @@ -306,7 +334,12 @@ 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 41e65d37a4..6f880287f3 100644 --- a/components/drawer/index.tsx +++ b/components/drawer/index.tsx @@ -7,8 +7,15 @@ import Drawer2 from './drawer-v2'; import Inner from './inner'; import { show, withContext } from './show'; +import { DrawerProps, QuickShowRet } from './types'; + +class Drawer extends React.Component { + static Inner: typeof Inner; + static show: (config?: DrawerProps) => QuickShowRet; + static withContext:

( + WrappedComponent: React.ComponentType

+ ) => React.ComponentType

; -class Drawer extends React.Component { render() { const { v2, ...others } = this.props; if (v2) { diff --git a/components/drawer/inner.tsx b/components/drawer/inner.tsx index 72aa526da6..c3ad1f3d99 100644 --- a/components/drawer/inner.tsx +++ b/components/drawer/inner.tsx @@ -4,11 +4,12 @@ import cx from 'classnames'; import Icon from '../icon'; import zhCN from '../locale/zh-cn'; import { obj } from '../util'; +import { InnerProps } from './types'; const noop = () => {}; const { pickOthers } = obj; -export default class Inner extends Component { +export default class Inner extends Component { static propTypes = { prefix: PropTypes.string, className: PropTypes.string, @@ -44,9 +45,13 @@ export default class Inner extends Component { [`${prefix}drawer-header`]: true, [`${prefix}drawer-no-title`]: !title, }); + const ariaProps: object = { + role: 'heading', + 'aria-level': 1, + }; return ( -

+
{title} {closeLink}
@@ -67,14 +72,17 @@ export default class Inner extends Component { renderCloseLink() { const { prefix, closeable, onClose, locale } = this.props; + const ariaProps: object = { + role: 'button', + 'aria-label': locale ? locale.close : 'close-icon', + }; if (closeable) { return ( @@ -92,10 +100,10 @@ export default class Inner extends Component { [`${prefix}drawer`]: true, [`${prefix}drawer-${placement}`]: !v2, [`${prefix}closeable`]: closeable, - [className]: !!className, + [className as string]: !!className, }); - const ariaProps = { + const ariaProps: object = { role, 'aria-modal': 'true', }; diff --git a/components/drawer/show.tsx b/components/drawer/show.tsx index 4707017b3d..f22b8fbc9a 100644 --- a/components/drawer/show.tsx +++ b/components/drawer/show.tsx @@ -1,11 +1,28 @@ import React, { Component } from 'react'; import ReactDOM from 'react-dom'; -import PropTypes from 'prop-types'; -import cx from 'classnames'; import ConfigProvider from '../config-provider'; import Drawer from './drawer-v2'; +import { DrawerProps } from './types'; -class Modal extends React.Component { +interface ModalProps extends DrawerProps { + content?: React.ReactNode; +} + +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 { state = { visible: true, loading: false, @@ -30,26 +47,32 @@ class Modal extends React.Component { const ConfigModal = ConfigProvider.config(Modal, { componentName: 'Drawer' }); /** - * 创建对话框 - * @exportName show - * @param {Object} config 配置项 - * @returns {Object} 包含有 hide 方法,可用来关闭对话框 + * 创建对话框。 + * + * @remarks + * 该函数导出的名字是 `show`。 + * + * @param config - 配置项。 + * @returns 返回一个对象,该对象包含有 `hide` 方法,可用来关闭对话框。 */ -export const show = (config = {}) => { - const container = document.createElement('div'); +export const show = (config: ShowConfig = {}): ShowReturn => { + const container: HTMLDivElement = document.createElement('div'); + const unmount = () => { if (config.afterClose) { config.afterClose(); } + // eslint-disable-next-line ReactDOM.unmountComponentAtNode(container); - container.parentNode.removeChild(container); + container.parentNode?.removeChild(container); }; document.body.appendChild(container); let newContext = config.contextConfig; if (!newContext) newContext = ConfigProvider.getContext(); - let instance, myRef; + let instance: ModalInstance | null = null, + myRef: Modal | null = null; const handleClose = () => { const inc = instance && instance.getInstance(); @@ -58,7 +81,7 @@ export const show = (config = {}) => { config.onClose(); } }; - + // eslint-disable-next-line ReactDOM.render( { onClose={handleClose} afterClose={unmount} ref={ref => { - myRef = ref; + myRef = ref as unknown as Modal; }} /> , container, function () { - instance = myRef; + instance = myRef as unknown as ModalInstance; } ); return { @@ -80,8 +103,10 @@ export const show = (config = {}) => { }; }; -export const withContext = WrappedComponent => { - const HOC = props => { +export const withContext =

( + WrappedComponent: React.ComponentType

+): React.ComponentType

=> { + const HOC: React.FC

= (props: P) => { return ( {contextConfig => ( diff --git a/components/drawer/types.ts b/components/drawer/types.ts index aa241b3d5f..333f3f45dc 100644 --- a/components/drawer/types.ts +++ b/components/drawer/types.ts @@ -1,16 +1,14 @@ -/// - -import React from 'react'; +import React, { ReactNode } from 'react'; import { PopupProps } from '../overlay'; import { CloseMode } from '../dialog'; import { CommonProps } from '../util'; +import { ComponentLocaleObject } from '../locale/types'; -interface HTMLAttributesWeak extends PopupProps { - title?: any; - onClose?: any; -} +interface HTMLAttributesWeak extends PopupProps {} -export interface DrawerProps extends Omit, CommonProps { +export interface DrawerProps + extends Omit, + CommonProps { /** * [废弃]同closeMode, 控制对话框关闭的方式,值可以为字符串或者布尔值,其中字符串是由以下值组成: * **mask** 表示点击遮罩区域可以关闭对话框 @@ -18,7 +16,8 @@ export interface DrawerProps extends Omit, Common * 如 'mask' 或 'esc,mask' * 如果设置为 true,则以上关闭方式全部生效 * 如果设置为 false,则以上关闭方式全部失效 - * @deprecated + * + * @deprecated 由于设计变更,该属性已被弃用。请使用 `closeMode` 属性来控制对话框关闭的方式。 */ closeable?: 'close' | 'mask' | 'esc' | boolean | 'close,mask' | 'close,esc' | 'mask,esc'; /** @@ -43,9 +42,11 @@ export interface DrawerProps extends Omit, Common bodyStyle?: React.CSSProperties; headerStyle?: React.CSSProperties; /** - * 显示隐藏时动画的播放方式 - * @property {String} in 进场动画 - * @property {String} out 出场动画 + * 显示隐藏时动画的播放方式。`animation` 对象包含两个属性: `in` 和 `out`。 + * - `in`: 进场动画 + * - `out`: 出场动画 + * + * @param animation - 指定进场和出场动画的对象。 */ animation?: { in: string; out: string } | boolean; visible?: boolean; @@ -61,14 +62,14 @@ export interface DrawerProps extends Omit, Common height?: number | string; /** * [v2 废弃] 受控模式下(没有 trigger 的时候),只会在关闭时触发,相当于onClose - * @deprecated + * @deprecated 该属性在v2版本已被废弃,不再推荐使用。请改用 `onClose` 事件处理器来处理关闭事件。 */ - onVisibleChange?: (visible: boolean, reason: string) => void; + onVisibleChange?: (visible: boolean, reason: string, e?: React.MouseEvent) => void; /** * [v2] 弹窗关闭后的回调 */ afterClose?: () => void; - onClose?: (reason: string, e: React.MouseEvent) => void; + onClose?: (reason: string, e: React.MouseEvent | KeyboardEvent) => void; /** * 位于页面的位置 */ @@ -82,12 +83,32 @@ export interface DrawerProps extends Omit, Common * 内容 */ content?: React.ReactNode; + /** + * 渲染组件的容器,如果是函数需要返回 ref,如果是字符串则是该 DOM 的 id,也可以直接传入 DOM 节点 + */ + popupContainer?: string | HTMLElement | null; } export interface QuickShowRet { hide: () => void; } -export default class Drawer extends React.Component { - static show(config: DrawerProps): QuickShowRet; +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/__docs__/demo/custom/index.tsx b/components/field/__docs__/demo/custom/index.tsx index b9f433ab4f..05ed7453b2 100644 --- a/components/field/__docs__/demo/custom/index.tsx +++ b/components/field/__docs__/demo/custom/index.tsx @@ -2,38 +2,23 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Button, Field } from '@alifd/next'; -class Custom extends React.Component { - constructor(props) { - super(props); - - this.state = { - value: typeof props.value === 'undefined' ? [] : props.value, - }; - } - - // update value - componentWillReceiveProps(nextProps) { - if ('value' in nextProps) { - this.setState({ - value: typeof nextProps.value === 'undefined' ? [] : nextProps.value, - }); - } - } +interface CustomProps { + value?: string[]; + onChange: (value: string[]) => void; +} +class Custom extends React.Component { onAdd = () => { - const value = this.state.value.concat([]); - value.push('new'); - - this.setState({ - value, - }); - this.props.onChange(value); + const { value = [] } = this.props; + const newValue = value.concat('new'); + this.props.onChange(newValue); }; render() { + const { value = [] } = this.props; return (

- {this.state.value.map((v, i) => { + {value.map((v, i) => { return ; })}