From 62835100a882873f47da1831afdadae6093ad8ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AA=86=E6=9E=97?= Date: Tue, 27 Feb 2024 13:58:59 +0800 Subject: [PATCH] refactor(Drawer): Document optimization & Upgrade testing tools and optimize use cases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(Overlay): solve problems caused by numerical floating, close #4740 chore(*): Release-1.27.5-beta.1 fix(*): rollback #4746 and fix textarea clear spec chore(*): Release-1.27.5 fix(Drawer): not to change package-lock.json Update stale.yml chore(BuildTool): 支持指定主题包调试`run start --theme xxx` chore(DatePicker): fix theme demo margin, close #3627 chore(Shell): fix spell error "palceholder -> placeholder", close #3564 docs(Field): improve document description of parseName, close #3453 chore(Pagination): improve document of pageSizeSelector, fix pageJump runtime error, close #3306 docs(Calendar2): remove legacy api, close #3100 fix(Table): fix merging cell width in locked columns, close #4716 (#4752) * fix(Table): 修复合并单元格的锁列滑动问题 * fix(Table): should support for merging cells in locked columns, close #4716 * fix(Table): 修复测试用例 chore(*): update lock fix(Shell): phone shell should hidden when collapsed, close #3886 (#4766) Co-authored-by: lancely refactor(Timeline): rename timeline file to ts refactor(Timeline): ts & doc improvement refactor(Timeline): test improve refactor(Timeline): fix cr comments fix(Upload): should hide trigger when limit is reached for Upload.Dragger, close #3951 (#4761) Co-authored-by: lancely feat(DatePicker): improve focus logic, close #3998 (#4769) * feat(DatePicker): interactive optimization of date selection box close #3998 * feat(DatePicker): improve focus logic --------- Co-authored-by: WB01081293 Co-authored-by: 珵之 refactor(Field): rename to ts refactor(Field): convert to TypeScript, impove docs and tests refactor(Field): convert to TypeScript, impove docs and tests chore(Field): improve for codereview chore(Field): fix dependency version feat(TreeSelect): support useDetailValue, close #3531 (#4771) * feat(TreeSelect): support useDetailValue, close #3531 * chore(TreeSelect): improve useDetailValue tc and demos by codereview * test(TreeSelect): add control mode spec for useDetailValue refactor(Collapse): rename to ts refactor(Collapse): convert to TypeScript, impove docs and tests refactor(Collapse): imporve docs and types chore(Collapse): improve by codereview chore(Collapse): remove useless propTypes comments chore(*): Release-1.27.6 fix(Collapse): hotfix panel className missing chore(*): Release-1.27.7 chore(Divider): adjust ts types chore(Divider): adjust ts & docs & test --- .../drawer/__docs__/demo/basic/index.tsx | 3 +- .../drawer/__docs__/demo/placement/index.tsx | 7 +- .../drawer/__docs__/demo/quick/index.tsx | 4 +- .../drawer/__docs__/demo/select/index.tsx | 17 +- .../drawer/__docs__/demo/size/index.tsx | 4 +- components/drawer/__docs__/index.en-us.md | 80 +++-- components/drawer/__docs__/index.md | 76 +++-- components/drawer/__docs__/theme/index.tsx | 35 +- components/drawer/__tests__/a11y-spec.tsx | 24 +- components/drawer/__tests__/index-spec.tsx | 175 ++++------ components/drawer/__tests__/index-v2-spec.tsx | 181 ++++------- components/drawer/drawer-v2.tsx | 54 ++-- components/drawer/drawer.tsx | 112 ++----- components/drawer/index.tsx | 15 +- components/drawer/inner.tsx | 31 +- components/drawer/show.tsx | 61 +++- components/drawer/types.ts | 301 +++++++++++++++--- components/field/__tests__/index-spec.tsx | 15 + components/field/__tests__/options-spec.tsx | 8 + components/select/index.d.ts | 7 +- 20 files changed, 711 insertions(+), 499 deletions(-) diff --git a/components/drawer/__docs__/demo/basic/index.tsx b/components/drawer/__docs__/demo/basic/index.tsx index c5fc5f0708..3b721e6c21 100644 --- a/components/drawer/__docs__/demo/basic/index.tsx +++ b/components/drawer/__docs__/demo/basic/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Button, Drawer } from '@alifd/next'; +import type { DrawerProps } from '@alifd/next/lib/drawer'; class Demo extends React.Component { state = { @@ -13,7 +14,7 @@ class Demo extends React.Component { }); }; - onClose = (reason, e) => { + onClose: DrawerProps['onClose'] = (reason, e) => { console.log('onClose: ', reason, e); this.setState({ visible: false, diff --git a/components/drawer/__docs__/demo/placement/index.tsx b/components/drawer/__docs__/demo/placement/index.tsx index 6f4ae95d7d..aece9b3df6 100644 --- a/components/drawer/__docs__/demo/placement/index.tsx +++ b/components/drawer/__docs__/demo/placement/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Radio, Button, Drawer } from '@alifd/next'; +import type { RadioProps } from '@alifd/next/lib/radio'; class Demo extends React.Component { state = { @@ -14,13 +15,13 @@ class Demo extends React.Component { }); }; - onClose = reason => { + onClose = () => { this.setState({ visible: false, }); }; - onPlacementChange = dir => { + onPlacementChange: RadioProps['onChange'] = dir => { this.setState({ placement: dir, }); @@ -44,7 +45,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..2b77ab2012 100644 --- a/components/drawer/__docs__/demo/quick/index.tsx +++ b/components/drawer/__docs__/demo/quick/index.tsx @@ -1,9 +1,11 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Button, Drawer } from '@alifd/next'; +import type { QuickShowRet } from '@alifd/next/lib/drawer'; -let instance = null; +let instance: QuickShowRet | null = null; const show = () => { + instance && instance.hide(); instance = Drawer.show({ title: 'quick', hasMask: false, diff --git a/components/drawer/__docs__/demo/select/index.tsx b/components/drawer/__docs__/demo/select/index.tsx index 9bade387c5..23d3b72397 100644 --- a/components/drawer/__docs__/demo/select/index.tsx +++ b/components/drawer/__docs__/demo/select/index.tsx @@ -1,17 +1,26 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Radio, Drawer, Select } from '@alifd/next'; +import type { SelectProps } from '@alifd/next/lib/select'; +import type { RadioProps } from '@alifd/next/lib/radio'; + +interface onToggleHighlightItemProps { + deep: number; + value: string; + label: string; +} const Option = Select.Option; -const onChange = function (value) { +const onChange: SelectProps['onChange'] = value => { console.log(value); }; -const onBlur = function (e) { + +const onBlur: SelectProps['onBlur'] = e => { console.log(/onblur/, e); }; -const onToggleHighlightItem = function (item, type) { +const onToggleHighlightItem = (item: onToggleHighlightItemProps, type: 'up' | 'down') => { console.log(item, type); }; @@ -20,7 +29,7 @@ class Demo extends React.Component { placement: 'right', }; - onPlacementChange = dir => { + onPlacementChange: RadioProps['onChange'] = dir => { this.setState({ placement: dir, }); 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__/index.en-us.md b/components/drawer/__docs__/index.en-us.md index f1e6b39ca2..b990dd89b4 100644 --- a/components/drawer/__docs__/index.en-us.md +++ b/components/drawer/__docs__/index.en-us.md @@ -19,47 +19,73 @@ version 1.25 add api `v2` to support open new version Dialog, feature as list: feature: -- use css (not js) to compute position, will easier +- use css (not js) to compute position, will easier - support `width/height` to fix width/height of drawer, or you can set `auto` to follow content width/height ## API ### Drawer -> Inherited Overlay.Popup's API unless otherwise specified - -| Param | Descripiton | Type | Default Value | -| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | --------------------------------------------------------------------------------- | -| trigger | trigger the overlay to show or hide elements | ReactElement | - | -| triggerType | trigger the overlay to show or hide operations, either 'click', 'hover', 'focus', or an array of them, such as ['hover', 'focus'] | String/Array | 'hover' | -| visible | whether the overlay is visiible currently | Boolean | - | -| animation | configure animation, support the {in: 'enter-class', out: 'leave-class' } object parameters, if set to false, do not play the animation. Refer to `Animate` component documentation for available animations. | Object/Boolean | { in: 'expandInDown', out: 'expandOutUp' } | -| hasMask | whether to show the mask | Boolean | false | -| closeable | [deprecated]controls how the dialog is closed. The value can be either a String or Boolean, where the string consists of the following values:
**close** clicking the close button can close the dialog
**mask** clicking the mask can close the dialog
**esc** pressing the esc key can close the dialog
such as 'close' or 'close,esc,mask'
If set to true, all of the above close methods take effect
If set to false, all of the above close methods will fail | String/Boolean | 'esc,close' | -| closeMode | [recommand]controls how the dialog is closed. The value can be either a String or Array:
**close** clicking the close button can close the dialog
**mask** clicking the mask can close the dialog
**esc** pressing the esc key can close the dialog
for example 'close' or ['close','esc','mask'] | Array<Enum>/Enum | - | -| onVisibleChange | callback function triggered when the ovlery is visible or hidden

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

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

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

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

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

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

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

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

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

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

**签名**:
**参数**:
_animation_: 指定进场和出场动画的对象。 | { in: string; out: string } \| false | \{ in: 'expandInDown', out: 'expandOutUp' \} | | - | +| visible | 是否显示 | boolean | - | | - | +| width | 宽度,仅在 placement是 left right 的时候生效 | number \| string | - | | - | +| height | 高度,仅在 placement是 top bottom 的时候生效 | number \| string | - | | - | +| afterClose | [v2] 弹窗关闭后的回调 | () => void | - | | - | +| onClose | 对话框关闭时触发的回调函数 | (reason: string, e: React.MouseEvent \| KeyboardEvent) => void | `() => {}` | | - | +| placement | 位于页面的位置 | 'top' \| 'right' \| 'bottom' \| 'left' | 'right' | | - | +| v2 | 开启v2 | boolean | false | | - | +| content | 内容 | React.ReactNode | - | | - | +| popupContainer | 渲染组件的容器 | string \| HTMLElement \| null | - | | - | +| hasMask | 是否显示遮罩 | boolean | true | | - | @@ -54,10 +76,10 @@ 以下只列举 config 可以传入的常用属性,Drawer 组件其他属性也可以传入 -| 属性 | 说明 | 类型 | 默认值 | -| :------ | :-- | :-------- | :-- | -| title | 标题 | ReactNode | '' | -| content | 内容 | ReactNode | '' | +| 属性 | 说明 | 类型 | 默认值 | +| :------ | :--- | :-------- | :----- | +| title | 标题 | ReactNode | '' | +| content | 内容 | ReactNode | '' | ### Drawer.withContext @@ -70,8 +92,8 @@ ## 无障碍键盘操作指南 -| 键盘 | 说明 | -| :-------- | :--------------------------------------- | -| esc | 按下ESC键将会关闭dialog而不触发任何的动作 | +| 键盘 | 说明 | +| :-------- | :------------------------------------------------------------------------ | +| esc | 按下ESC键将会关闭dialog而不触发任何的动作 | | tab | 正向聚焦到任何可以被聚焦的元素, 在Dialog显示的时候,焦点始终保持在框体内 | | shift+tab | 反向聚焦到任何可以被聚焦的元素,在Dialog显示的时候,焦点始终保持在框体内 | diff --git a/components/drawer/__docs__/theme/index.tsx b/components/drawer/__docs__/theme/index.tsx index 133bb283aa..c5769e31d2 100644 --- a/components/drawer/__docs__/theme/index.tsx +++ b/components/drawer/__docs__/theme/index.tsx @@ -2,12 +2,26 @@ import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import '../../../demo-helper/style'; import '../../style'; -import { Demo, DemoGroup, initDemo } from '../../../demo-helper'; +import { + Demo, + DemoGroup, + initDemo, + type DemoFunctionDefineForArray, + type DemoFunctionDefineForObject, +} from '../../../demo-helper'; import Drawer from '../../index'; import zhCN from '../../../locale/zh-cn'; import enUS from '../../../locale/en-us'; +interface FunctionProps { + lang: string; + i18n: { + title: string; + content: string; + }; +} + const i18nMaps = { 'en-us': { title: 'Title Here', @@ -20,7 +34,7 @@ const i18nMaps = { }, }; -class FunctionDemo extends Component { +class FunctionDemo extends Component { state = { demoFunction: { hasTitle: { @@ -75,13 +89,15 @@ class FunctionDemo extends Component { }, }, }; - onFunctionChange = demoFunction => { + onFunctionChange = ( + demoFunction: Record | DemoFunctionDefineForArray[] + ) => { 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..1d1d93d903 100644 --- a/components/drawer/__tests__/index-spec.tsx +++ b/components/drawer/__tests__/index-spec.tsx @@ -1,122 +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 开始 - -
- ); - } -} +import type { DrawerProps } from '../types'; 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<{ animation: DrawerProps['animation'] }> { + 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 +66,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..d3205f255a 100644 --- a/components/drawer/__tests__/index-v2-spec.tsx +++ b/components/drawer/__tests__/index-v2-spec.tsx @@ -1,125 +1,64 @@ 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 开始 - -
- ); - } -} +import type { DrawerProps } from '../types'; 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<{ animation: DrawerProps['animation'] }> { + 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 +67,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,9 +97,12 @@ 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(); + cy.then(() => { + hide(); + }); }); }); diff --git a/components/drawer/drawer-v2.tsx b/components/drawer/drawer-v2.tsx index 12b4867a51..b9dcbd35e0 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 type { DrawerV2Props } 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: DrawerV2Props) => { 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!, '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!, '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,16 +258,16 @@ const Drawer = props => { [`${prefix}overlay-inner`]: true, [`${prefix}drawer-wrapper`]: true, [`${prefix}drawer-${placement}`]: true, - [className]: !!className, + [className!]: !!className, }); const drawerCls = classNames({ [`${prefix}drawer-v2`]: true, - [className]: !!className, + [className!]: !!className, }); - const newAnimation = + const newAnimation: DrawerV2Props['animation'] = animation === null || animation === false - ? null + ? undefined : animation ? animation : getAnimation(placement); @@ -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 { diff --git a/components/drawer/drawer.tsx b/components/drawer/drawer.tsx index f6bccf0a92..62353a86ae 100644 --- a/components/drawer/drawer.tsx +++ b/components/drawer/drawer.tsx @@ -1,114 +1,56 @@ import PropTypes from 'prop-types'; -import React, { Component } from 'react'; +import React, { Component, type ComponentType } from 'react'; import Overlay from '../overlay'; import Inner from './inner'; import zhCN from '../locale/zh-cn'; import { obj } from '../util'; +import type { DrawerProps, InnerProps } from './types'; -const noop = () => {}; +const noop: InnerProps['onClose'] = () => {}; 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 = { - ...(Popup.propTypes || {}), + ...((Popup as ComponentType).propTypes || {}), prefix: PropTypes.string, pure: PropTypes.bool, rtl: PropTypes.bool, - // 不建议使用trigger trigger: PropTypes.element, triggerType: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), - /** - * 宽度,仅在 placement是 left right 的时候生效 - */ width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - /** - * 高度,仅在 placement是 top bottom 的时候生效 - */ height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - /** - * [废弃]同closeMode, 控制对话框关闭的方式,值可以为字符串或者布尔值,其中字符串是由以下值组成: - * **close** 表示点击关闭按钮可以关闭对话框 - * **mask** 表示点击遮罩区域可以关闭对话框 - * **esc** 表示按下 esc 键可以关闭对话框 - * 如 'close' 或 'close,esc,mask' - * 如果设置为 true,则以上关闭方式全部生效 - * 如果设置为 false,则以上关闭方式全部失效 - */ closeable: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), - /** - * 隐藏时是否保留子节点,不销毁 - */ cache: PropTypes.bool, - /** - * [推荐]控制对话框关闭的方式,值可以为字符串或者数组,其中字符串、数组均为以下值的枚举: - * **close** 表示点击关闭按钮可以关闭对话框 - * **mask** 表示点击遮罩区域可以关闭对话框 - * **esc** 表示按下 esc 键可以关闭对话框 - * 如 'close' 或 ['close','esc','mask'], [] - * @version 1.21 - */ closeMode: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.oneOf(['close', 'mask', 'esc'])), PropTypes.oneOf(['close', 'mask', 'esc']), ]), - /** - * 对话框关闭时触发的回调函数 - * @param {String} trigger 关闭触发行为的描述字符串 - * @param {Object} event 关闭时事件对象 - */ onClose: PropTypes.func, - /** - * [v2废弃]对话框打开后的回调函数 - */ afterOpen: PropTypes.func, - /** - * 位于页面的位置 - */ placement: PropTypes.oneOf(['top', 'right', 'bottom', 'left']), - /** - * 标题 - */ title: PropTypes.node, - /** - * header上的样式 - */ headerStyle: PropTypes.object, - /** - * body上的样式 - */ bodyStyle: PropTypes.object, - /** - * 是否显示 - */ visible: PropTypes.bool, - /** - * 是否显示遮罩 - */ hasMask: PropTypes.bool, - // 受控模式下(没有 trigger 的时候),只会在关闭时触发,相当于onClose onVisibleChange: PropTypes.func, - /** - * 显示隐藏时动画的播放方式,支持 { in: 'enter-class', out: 'leave-class' } 的对象参数,如果设置为 false,则不播放动画。 请参考 Animate 组件的文档获取可用的动画名 - * @default { in: 'expandInDown', out: 'expandOutUp' } - */ animation: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]), locale: PropTypes.object, - // for ConfigProvider popupContainer: PropTypes.any, - /** - * 开启 v2 - */ v2: PropTypes.bool, - /** - * [v2] 弹窗关闭后的回调 - */ afterClose: PropTypes.func, }; @@ -123,7 +65,9 @@ export default class Drawer extends Component { locale: zhCN.Drawer, }; - getAlign = placement => { + private overlay: Overlay | null = null; + + getAlign = (placement: string | undefined) => { let align; switch (placement) { case 'top': @@ -144,7 +88,7 @@ export default class Drawer extends Component { return align; }; - getAnimation = placement => { + getAnimation = (placement: string | undefined) => { if ('animation' in this.props) { return this.props.animation; } @@ -181,12 +125,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 +138,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 +157,7 @@ export default class Drawer extends Component { onVisibleChange && onVisibleChange(visible, reason, e); }; - renderInner(closeable) { + renderInner(closeable: InnerProps['closeable']) { const { prefix, className, @@ -226,7 +170,7 @@ export default class Drawer extends Component { placement, rtl, } = this.props; - const others = pickOthers(Object.keys(Drawer.propTypes), this.props); + const others = pickOthers(Drawer.propTypes, this.props); return ( {children} @@ -265,6 +209,8 @@ export default class Drawer extends Component { closeMode, rtl, popupContainer, + content, + title, ...others } = this.props; @@ -281,7 +227,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, diff --git a/components/drawer/index.tsx b/components/drawer/index.tsx index 41e65d37a4..6c782ee926 100644 --- a/components/drawer/index.tsx +++ b/components/drawer/index.tsx @@ -7,8 +7,21 @@ import Drawer2 from './drawer-v2'; import Inner from './inner'; import { show, withContext } from './show'; +import type { DrawerV2Props, DrawerV1Props } from './types'; + +export interface QuickShowRet { + hide: () => void; +} + +export type DrawerProps = DrawerV2Props | DrawerV1Props; + +class Drawer extends React.Component { + static Inner: typeof Inner; + 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..67f837b91d 100644 --- a/components/drawer/inner.tsx +++ b/components/drawer/inner.tsx @@ -4,11 +4,19 @@ import cx from 'classnames'; import Icon from '../icon'; import zhCN from '../locale/zh-cn'; import { obj } from '../util'; +import type { InnerProps } from './types'; const noop = () => {}; const { pickOthers } = obj; -export default class Inner extends Component { +interface ariaRoleProps { + role?: string; + 'aria-modal'?: boolean | 'true' | 'false'; + 'aria-level'?: number; + 'aria-label'?: string; +} + +export default class Inner extends Component { static propTypes = { prefix: PropTypes.string, className: PropTypes.string, @@ -44,9 +52,13 @@ export default class Inner extends Component { [`${prefix}drawer-header`]: true, [`${prefix}drawer-no-title`]: !title, }); + const ariaProps: ariaRoleProps = { + role: 'heading', + 'aria-level': 1, + }; return ( -

+
{title} {closeLink}
@@ -67,15 +79,14 @@ export default class Inner extends Component { renderCloseLink() { const { prefix, closeable, onClose, locale } = this.props; + const ariaProps: ariaRoleProps = { + role: 'button', + 'aria-label': locale?.close as string, + }; if (closeable) { return ( - + ); @@ -92,10 +103,10 @@ export default class Inner extends Component { [`${prefix}drawer`]: true, [`${prefix}drawer-${placement}`]: !v2, [`${prefix}closeable`]: closeable, - [className]: !!className, + [className!]: !!className, }); - const ariaProps = { + const ariaProps: ariaRoleProps = { role, 'aria-modal': 'true', }; diff --git a/components/drawer/show.tsx b/components/drawer/show.tsx index 4707017b3d..276eba5d3e 100644 --- a/components/drawer/show.tsx +++ b/components/drawer/show.tsx @@ -1,11 +1,17 @@ -import React, { Component } from 'react'; +import React, { type JSXElementConstructor } 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 type { DrawerProps } from './types'; +import type { AnyProps } from '../config-provider/config'; +import type { ConsumerState } from '../config-provider/consumer'; -class Modal extends React.Component { +interface ModalState { + visible?: boolean; + loading?: boolean; +} + +class Modal extends React.Component { state = { visible: true, loading: false, @@ -29,27 +35,39 @@ class Modal extends React.Component { const ConfigModal = ConfigProvider.config(Modal, { componentName: 'Drawer' }); +export type Config = DrawerProps & { + afterClose?: () => void; + onClose?: () => void; + contextConfig?: ConsumerState; +}; + /** - * 创建对话框 - * @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: Config = {}) => { + const container: HTMLDivElement = document.createElement('div'); + const unmount = () => { if (config.afterClose) { config.afterClose(); } + // eslint-disable-next-line react/no-deprecated 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: InstanceType | null, + myRef: InstanceType | null; const handleClose = () => { const inc = instance && instance.getInstance(); @@ -59,6 +77,7 @@ export const show = (config = {}) => { } }; + // eslint-disable-next-line react/no-deprecated ReactDOM.render( { }; }; -export const withContext = WrappedComponent => { - const HOC = props => { +export interface ContextDialog { + show: (config?: Config) => { hide: () => void }; +} + +export interface WithContextDrawerProps { + contextDialog: ContextDialog; +} + +export const withContext =

( + WrappedComponent: JSXElementConstructor

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