diff --git a/docs/components/LanguageButton/LanguageButton.tsx b/docs/components/LanguageButton/LanguageButton.tsx index 7c855c4020..19841859a0 100644 --- a/docs/components/LanguageButton/LanguageButton.tsx +++ b/docs/components/LanguageButton/LanguageButton.tsx @@ -11,7 +11,7 @@ interface ButtonProps { [key: string]: any; } -function LanguageButton(props: ButtonProps) { +const LanguageButton = React.forwardRef((props: ButtonProps, ref) => { const router = useRouter(); const { language, onChangeLanguage } = React.useContext(AppContext); const { className, ...rest } = props; @@ -27,12 +27,13 @@ function LanguageButton(props: ButtonProps) { router.push(pathname, `${languageToPath(nextLanguage)}${pathname}`); }, - [language] + [onChangeLanguage, router, isZH] ); return ( ); -} +}); + +LanguageButton.displayName = 'LanguageButton'; export default LanguageButton; diff --git a/docs/pages/components/affix/zh-CN/index.md b/docs/pages/components/affix/zh-CN/index.md index be2322b13a..60d9d37e53 100644 --- a/docs/pages/components/affix/zh-CN/index.md +++ b/docs/pages/components/affix/zh-CN/index.md @@ -20,6 +20,8 @@ 容器在可视范围内才固定元素,当滚动页面容器不在可视范围则取消固定元素。 + + ## Props ### `` diff --git a/docs/pages/components/drawer/en-US/backdrop.md b/docs/pages/components/drawer/en-US/backdrop.md deleted file mode 100644 index e989929457..0000000000 --- a/docs/pages/components/drawer/en-US/backdrop.md +++ /dev/null @@ -1,71 +0,0 @@ -### Backdrop - - - -```js -class Demo extends React.Component { - constructor(props) { - super(props); - this.state = { - backdrop: false, - show: false - }; - this.close = this.close.bind(this); - this.toggleDrawer = this.toggleDrawer.bind(this); - } - close() { - this.setState({ - show: false - }); - } - toggleDrawer() { - this.setState({ show: true }); - } - render() { - const { backdrop, show } = this.state; - - return ( -
- Backdrop: - { - this.setState({ backdrop: value }); - }} - > - true - false - static - -
- - - - - - - Drawer Title - - - - - - - - - -
- ); - } -} - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/drawer/en-US/basic.md b/docs/pages/components/drawer/en-US/basic.md deleted file mode 100644 index 783ba6b292..0000000000 --- a/docs/pages/components/drawer/en-US/basic.md +++ /dev/null @@ -1,53 +0,0 @@ -### Default - - - -```js -class Demo extends React.Component { - constructor(props) { - super(props); - this.state = { - show: false - }; - this.close = this.close.bind(this); - this.toggleDrawer = this.toggleDrawer.bind(this); - } - close() { - this.setState({ - show: false - }); - } - toggleDrawer() { - this.setState({ show: true }); - } - render() { - return ( -
- - - - - - Drawer Title - - - - - - - - - -
- ); - } -} - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/drawer/en-US/full.md b/docs/pages/components/drawer/en-US/full.md deleted file mode 100644 index ebc3bbad9d..0000000000 --- a/docs/pages/components/drawer/en-US/full.md +++ /dev/null @@ -1,70 +0,0 @@ -### Fullpage - - - -```js -class Demo extends React.Component { - constructor(props) { - super(props); - this.state = { - show: false - }; - this.close = this.close.bind(this); - this.toggleDrawer = this.toggleDrawer.bind(this); - } - close() { - this.setState({ - show: false - }); - } - toggleDrawer(placement) { - this.setState({ - placement, - show: true - }); - } - render() { - const { placement, show } = this.state; - - return ( -
- - } onClick={() => this.toggleDrawer('left')}> - Left - - } onClick={() => this.toggleDrawer('right')}> - Right - - } onClick={() => this.toggleDrawer('top')}> - Top - - } onClick={() => this.toggleDrawer('bottom')}> - Bottom - - - - - - Drawer Title - - - - - - - - - -
- ); - } -} - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/drawer/en-US/index.md b/docs/pages/components/drawer/en-US/index.md index 44b522d77a..c6742b4a86 100644 --- a/docs/pages/components/drawer/en-US/index.md +++ b/docs/pages/components/drawer/en-US/index.md @@ -1,38 +1,56 @@ # Drawer -A floating layer that slides out from the edge of the page can be used instead of Modal to put more content. +A panel that slides out from the edge of the page can replace Modal to present more content. -## Usage +## Import -```js -import { Drawer } from 'rsuite'; -``` + ## Examples - +### Default + + + +### Backdrop + + + +### Placement + + + +### Size + + + +### Fullpage + + ## Props + + ### `` -| Property | Type `(Default)` | Description | -| ----------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| autoFocus | boolean `(true)` | When set to true, the Drawer is opened and is automatically focused on its own, accessible to screen readers | -| backdrop | unions: boolean, 'static' | When set to true, the Drawer will display the background when it is opened. Clicking on the background will close the Drawer. If you do not want to close the Drawer, set it to 'static'. | -| backdropClassName | string | Add an optional extra class name to .modal-backdrop It could end up looking like class="modal-backdrop foo-modal-backdrop in". | -| classPrefix | string `('drawer')` | The prefix of the component CSS class | -| enforceFocus | boolean `(true)` | When set to true, Drawer will prevent the focus from leaving when opened, making it easier for the secondary screen reader to access | -| full | boolean | Full screen | -| keyboard | boolean | close Drawer when press `esc` | -| onEnter | () => void | Callback fired before the Drawer transitions in | -| onEntered | () => void | Callback fired after the Drawer finishes transitioning in | -| onEntering | () => void | Callback fired as the Drawer begins to transition in | -| onExit | () => void | Callback fired right before the Drawer transitions out | -| onExited | () => void | Callback fired after the Drawer finishes transitioning out | -| onExiting | () => void | Callback fired as the Drawer begins to transition out | -| onHide | () => void | Callback fired when Drawer hide | -| onShow | () => void | Callback fired when Drawer display | -| placement | enum: 'top','right','bottom', 'left' `(right)` | The placement of Drawer | -| show \* | boolean | Show Drawer | -| size | enum: 'lg', 'md', 'sm', 'xs' | Set Drawer size | +| Property | Type `(Default)` | Description | +| ----------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| autoFocus | boolean `(true)` | When set to true, the Drawer is opened and is automatically focused on its own, accessible to screen readers | +| backdrop | unions: boolean, 'static' | When set to true, the Drawer will display the background when it is opened. Clicking on the background will close the Drawer. If you do not want to close the Drawer, set it to 'static'. | +| backdropClassName | string | Add an optional extra class name to .modal-backdrop It could end up looking like class="modal-backdrop foo-modal-backdrop in". | +| classPrefix | string `('drawer')` | The prefix of the component CSS class | +| enforceFocus | boolean `(true)` | When set to true, Drawer will prevent the focus from leaving when opened, making it easier for the secondary screen reader to access | +| full | boolean | Full screen | +| keyboard | boolean | close Drawer when press `esc` | +| onEnter | () => void | Callback fired before the Drawer transitions in | +| onEntered | () => void | Callback fired after the Drawer finishes transitioning in | +| onEntering | () => void | Callback fired as the Drawer begins to transition in | +| onExit | () => void | Callback fired right before the Drawer transitions out | +| onExited | () => void | Callback fired after the Drawer finishes transitioning out | +| onExiting | () => void | Callback fired as the Drawer begins to transition out | +| onClose | () => void | Callback fired when Drawer hide | +| onOpen | () => void | Callback fired when Drawer display | +| placement | Placement `(right)` | The placement of Drawer | +| open \* | boolean | Open Drawer | +| size | enum: 'lg', 'md', 'sm', 'xs' | Set Drawer size | diff --git a/docs/pages/components/drawer/en-US/placement.md b/docs/pages/components/drawer/en-US/placement.md deleted file mode 100644 index b5753b3efb..0000000000 --- a/docs/pages/components/drawer/en-US/placement.md +++ /dev/null @@ -1,68 +0,0 @@ -### Placement - - - -```js -class Demo extends React.Component { - constructor(props) { - super(props); - this.state = { - show: false - }; - this.close = this.close.bind(this); - this.toggleDrawer = this.toggleDrawer.bind(this); - } - close() { - this.setState({ - show: false - }); - } - toggleDrawer(placement) { - this.setState({ - placement, - show: true - }); - } - render() { - return ( -
- - } onClick={() => this.toggleDrawer('left')}> - Left - - } onClick={() => this.toggleDrawer('right')}> - Right - - } onClick={() => this.toggleDrawer('top')}> - Top - - } onClick={() => this.toggleDrawer('bottom')}> - Bottom - - - - - - Drawer Title - - - - - - - - - -
- ); - } -} - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/drawer/en-US/size.md b/docs/pages/components/drawer/en-US/size.md deleted file mode 100644 index 81dc38587e..0000000000 --- a/docs/pages/components/drawer/en-US/size.md +++ /dev/null @@ -1,83 +0,0 @@ -### Size - - - -```js -class Demo extends React.Component { - constructor(props) { - super(props); - this.state = { - size: 'xs', - show: false - }; - this.close = this.close.bind(this); - this.toggleDrawer = this.toggleDrawer.bind(this); - this.handleChangeSize = this.handleChangeSize.bind(this); - } - close() { - this.setState({ - show: false - }); - } - toggleDrawer(placement) { - this.setState({ - placement, - show: true - }); - } - handleChangeSize(size) { - this.setState({ size }); - } - render() { - const { size, placement, show } = this.state; - - return ( -
- - - Large - Medium - Small - Xsmall - - - - } onClick={() => this.toggleDrawer('left')}> - Left - - } onClick={() => this.toggleDrawer('right')}> - Right - - } onClick={() => this.toggleDrawer('top')}> - Top - - } onClick={() => this.toggleDrawer('bottom')}> - Bottom - - - - - - Drawer Title - - - - - - - - - -
- ); - } -} - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/drawer/fragments/backdrop.md b/docs/pages/components/drawer/fragments/backdrop.md new file mode 100644 index 0000000000..05792e5387 --- /dev/null +++ b/docs/pages/components/drawer/fragments/backdrop.md @@ -0,0 +1,43 @@ + + +```js +const App = () => { + const [backdrop, setBackdrop] = React.useState('static'); + const [open, setOpen] = React.useState(false); + return ( +
+ Backdrop: + + static + true + false + +
+ + + + + setOpen(false)}> + + Drawer Title + + + + + + + + + +
+ ); +}; + +ReactDOM.render(); +``` + + diff --git a/docs/pages/components/drawer/fragments/basic.md b/docs/pages/components/drawer/fragments/basic.md new file mode 100644 index 0000000000..ee50ad0af1 --- /dev/null +++ b/docs/pages/components/drawer/fragments/basic.md @@ -0,0 +1,33 @@ + + +```js +const App = () => { + const [open, setOpen] = React.useState(false); + return ( +
+ + + + setOpen(false)}> + + Drawer Title + + + + + + + + + +
+ ); +}; +ReactDOM.render(); +``` + + diff --git a/docs/pages/components/drawer/fragments/full.md b/docs/pages/components/drawer/fragments/full.md new file mode 100644 index 0000000000..98198c82f7 --- /dev/null +++ b/docs/pages/components/drawer/fragments/full.md @@ -0,0 +1,52 @@ + + +```js +const App = () => { + const [open, setOpen] = React.useState(false); + const [placement, setPlacement] = React.useState(); + + const handleOpen = key => { + setOpen(true); + setPlacement(key); + }; + return ( +
+ + } onClick={() => handleOpen('left')}> + Left + + } onClick={() => handleOpen('right')}> + Right + + } onClick={() => handleOpen('top')}> + Top + + } onClick={() => handleOpen('bottom')}> + Bottom + + + + setOpen(false)}> + + Drawer Title + + + + + + + + + +
+ ); +}; + +ReactDOM.render(); +``` + + diff --git a/docs/pages/components/drawer/fragments/import.md b/docs/pages/components/drawer/fragments/import.md new file mode 100644 index 0000000000..7781ea88b2 --- /dev/null +++ b/docs/pages/components/drawer/fragments/import.md @@ -0,0 +1,6 @@ +```js +import { Drawer } from 'rsuite'; + +// or +import Drawer from 'rsuite/lib/Drawer'; +``` diff --git a/docs/pages/components/drawer/fragments/placement.md b/docs/pages/components/drawer/fragments/placement.md new file mode 100644 index 0000000000..507a229894 --- /dev/null +++ b/docs/pages/components/drawer/fragments/placement.md @@ -0,0 +1,52 @@ + + +```js +const App = () => { + const [open, setOpen] = React.useState(false); + const [placement, setPlacement] = React.useState(); + + const handleOpen = key => { + setOpen(true); + setPlacement(key); + }; + return ( +
+ + } onClick={() => handleOpen('left')}> + Left + + } onClick={() => handleOpen('right')}> + Right + + } onClick={() => handleOpen('top')}> + Top + + } onClick={() => handleOpen('bottom')}> + Bottom + + + + setOpen(false)}> + + Drawer Title + + + + + + + + + +
+ ); +}; + +ReactDOM.render(); +``` + + diff --git a/docs/pages/components/drawer/fragments/size.md b/docs/pages/components/drawer/fragments/size.md new file mode 100644 index 0000000000..81b515a3d3 --- /dev/null +++ b/docs/pages/components/drawer/fragments/size.md @@ -0,0 +1,61 @@ + + +```js +const App = () => { + const [size, setSize] = React.useState('xs'); + const [open, setOpen] = React.useState(false); + const [placement, setPlacement] = React.useState(); + + const handleOpen = key => { + setOpen(true); + setPlacement(key); + }; + return ( +
+ + + Large + Medium + Small + Xsmall + + + + } onClick={() => handleOpen('left')}> + Left + + } onClick={() => handleOpen('right')}> + Right + + } onClick={() => handleOpen('top')}> + Top + + } onClick={() => handleOpen('bottom')}> + Bottom + + + + setOpen(false)}> + + Drawer Title + + + + + + + + + +
+ ); +}; + +ReactDOM.render(); +``` + + diff --git a/docs/pages/components/drawer/index.tsx b/docs/pages/components/drawer/index.tsx index 2821a79ca7..42a18dd807 100644 --- a/docs/pages/components/drawer/index.tsx +++ b/docs/pages/components/drawer/index.tsx @@ -5,7 +5,6 @@ import DefaultPage from '@/components/Page'; export default function Page() { return ( ); diff --git a/docs/pages/components/drawer/zh-CN/backdrop.md b/docs/pages/components/drawer/zh-CN/backdrop.md deleted file mode 100644 index 7c639ac614..0000000000 --- a/docs/pages/components/drawer/zh-CN/backdrop.md +++ /dev/null @@ -1,71 +0,0 @@ -### 背景板 - - - -```js -class Demo extends React.Component { - constructor(props) { - super(props); - this.state = { - backdrop: false, - show: false - }; - this.close = this.close.bind(this); - this.toggleDrawer = this.toggleDrawer.bind(this); - } - close() { - this.setState({ - show: false - }); - } - toggleDrawer() { - this.setState({ show: true }); - } - render() { - const { backdrop, show } = this.state; - - return ( -
- Backdrop: - { - this.setState({ backdrop: value }); - }} - > - true - false - static - -
- - - - - - - Drawer Title - - - - - - - - - -
- ); - } -} - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/drawer/zh-CN/basic.md b/docs/pages/components/drawer/zh-CN/basic.md deleted file mode 100644 index cb6b0ab47d..0000000000 --- a/docs/pages/components/drawer/zh-CN/basic.md +++ /dev/null @@ -1,53 +0,0 @@ -### 默认 - - - -```js -class Demo extends React.Component { - constructor(props) { - super(props); - this.state = { - show: false - }; - this.close = this.close.bind(this); - this.toggleDrawer = this.toggleDrawer.bind(this); - } - close() { - this.setState({ - show: false - }); - } - toggleDrawer() { - this.setState({ show: true }); - } - render() { - return ( -
- - - - - - Drawer Title - - - - - - - - - -
- ); - } -} - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/drawer/zh-CN/full.md b/docs/pages/components/drawer/zh-CN/full.md deleted file mode 100644 index daaa625988..0000000000 --- a/docs/pages/components/drawer/zh-CN/full.md +++ /dev/null @@ -1,70 +0,0 @@ -### 全屏 - - - -```js -class Demo extends React.Component { - constructor(props) { - super(props); - this.state = { - show: false - }; - this.close = this.close.bind(this); - this.toggleDrawer = this.toggleDrawer.bind(this); - } - close() { - this.setState({ - show: false - }); - } - toggleDrawer(placement) { - this.setState({ - placement, - show: true - }); - } - render() { - const { placement, show } = this.state; - - return ( -
- - } onClick={() => this.toggleDrawer('left')}> - Left - - } onClick={() => this.toggleDrawer('right')}> - Right - - } onClick={() => this.toggleDrawer('top')}> - Top - - } onClick={() => this.toggleDrawer('bottom')}> - Bottom - - - - - - Drawer Title - - - - - - - - - -
- ); - } -} - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/drawer/zh-CN/index.md b/docs/pages/components/drawer/zh-CN/index.md index ab76476ab0..f2bf116778 100644 --- a/docs/pages/components/drawer/zh-CN/index.md +++ b/docs/pages/components/drawer/zh-CN/index.md @@ -1,38 +1,56 @@ # Drawer 抽屉 -一个从页面边缘滑动出来的浮层,可以替代 Modal 放更多内容。 +一个从页面边缘滑动出来的面板,可以替代 Modal 呈现更多内容。 ## 获取组件 -```js -import { Drawer } from 'rsuite'; -``` + ## 演示 - +### 默认 + + + +### 背景板 + + + +### 显示位置 + + + +### 尺寸 + + + +### 全屏 + + ## Props + + ### `` -| 属性名称 | 类型 `(默认值)` | 描述 | -| ----------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------- | -| autoFocus | boolean `(true)` | 当设置为 true, Drawer 被打开是自动焦点移到其自身,辅助屏幕阅读器容易访问 | -| backdrop | unions: boolean, 'static' | 当设置为 true,Drawer 打开时会显示背景,点击背景会关闭 Drawer,如果不想关闭 Drawer,可以设置为 'static' | -| backdropClassName | string | 为背景设置一个自定义 className | -| classPrefix | string `('drawer')` | 组件 CSS 类的前缀 | -| enforceFocus | boolean `(true)` | 当设置为 true, Drawer 将防止焦点在打开时离开,辅助屏幕阅读器容易访问 | -| full | boolean | 撑满全屏 | -| keyboard | boolean | 按下 esc 键时关闭 Drawer | -| onEnter | () => void | 显示前动画过渡的回调函数 | -| onEntered | () => void | 显示后动画过渡的回调函数 | -| onEntering | () => void | 显示中动画过渡的回调函数 | -| onExit | () => void | 退出前动画过渡的回调函数 | -| onExited | () => void | 退出后动画过渡的回调函数 | -| onExiting | () => void | 退出中动画过渡的回调函数 | -| onHide | () => void | 隐藏时的回调函数 | -| onShow | () => void | 显示时的回调函数 | -| placement | enum: 'top','right','bottom', 'left' `(right)` | 设置 Drawer 显示的位置 | -| show \* | boolean | 显示 Drawer | -| size | enum: 'lg', 'md', 'sm', 'xs' | 设置 Drawer 尺寸 | +| 属性名称 | 类型 `(默认值)` | 描述 | +| ----------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------- | +| autoFocus | boolean `(true)` | 当设置为 true, Drawer 被打开是自动焦点移到其自身,辅助屏幕阅读器容易访问 | +| backdrop | unions: boolean, 'static' | 当设置为 true,Drawer 打开时会显示背景,点击背景会关闭 Drawer,如果不想关闭 Drawer,可以设置为 'static' | +| backdropClassName | string | 为背景设置一个自定义 className | +| classPrefix | string `('drawer')` | 组件 CSS 类的前缀 | +| enforceFocus | boolean `(true)` | 当设置为 true, Drawer 将防止焦点在打开时离开,辅助屏幕阅读器容易访问 | +| full | boolean | 撑满全屏 | +| keyboard | boolean | 按下 esc 键时关闭 Drawer | +| onEnter | () => void | 显示前动画过渡的回调函数 | +| onEntered | () => void | 显示后动画过渡的回调函数 | +| onEntering | () => void | 显示中动画过渡的回调函数 | +| onExit | () => void | 退出前动画过渡的回调函数 | +| onExited | () => void | 退出后动画过渡的回调函数 | +| onExiting | () => void | 退出中动画过渡的回调函数 | +| onClose | () => void | 隐藏时的回调函数 | +| onOpen | () => void | 显示时的回调函数 | +| placement | Placement `(right)` | 设置 Drawer 显示的位置 | +| open \* | boolean | 显示 Drawer | +| size | enum: 'lg'|'md'|'sm'|'xs' | 设置 Drawer 尺寸 | diff --git a/docs/pages/components/drawer/zh-CN/placement.md b/docs/pages/components/drawer/zh-CN/placement.md deleted file mode 100644 index 7284673622..0000000000 --- a/docs/pages/components/drawer/zh-CN/placement.md +++ /dev/null @@ -1,68 +0,0 @@ -### 显示位置 - - - -```js -class Demo extends React.Component { - constructor(props) { - super(props); - this.state = { - show: false - }; - this.close = this.close.bind(this); - this.toggleDrawer = this.toggleDrawer.bind(this); - } - close() { - this.setState({ - show: false - }); - } - toggleDrawer(placement) { - this.setState({ - placement, - show: true - }); - } - render() { - return ( -
- - } onClick={() => this.toggleDrawer('left')}> - Left - - } onClick={() => this.toggleDrawer('right')}> - Right - - } onClick={() => this.toggleDrawer('top')}> - Top - - } onClick={() => this.toggleDrawer('bottom')}> - Bottom - - - - - - Drawer Title - - - - - - - - - -
- ); - } -} - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/drawer/zh-CN/size.md b/docs/pages/components/drawer/zh-CN/size.md deleted file mode 100644 index a48497ae95..0000000000 --- a/docs/pages/components/drawer/zh-CN/size.md +++ /dev/null @@ -1,83 +0,0 @@ -### 抽屉尺寸 - - - -```js -class Demo extends React.Component { - constructor(props) { - super(props); - this.state = { - size: 'xs', - show: false - }; - this.close = this.close.bind(this); - this.toggleDrawer = this.toggleDrawer.bind(this); - this.handleChangeSize = this.handleChangeSize.bind(this); - } - close() { - this.setState({ - show: false - }); - } - toggleDrawer(placement) { - this.setState({ - placement, - show: true - }); - } - handleChangeSize(size) { - this.setState({ size }); - } - render() { - const { size, placement, show } = this.state; - - return ( -
- - - Large - Medium - Small - Xsmall - - - - } onClick={() => this.toggleDrawer('left')}> - Left - - } onClick={() => this.toggleDrawer('right')}> - Right - - } onClick={() => this.toggleDrawer('top')}> - Top - - } onClick={() => this.toggleDrawer('bottom')}> - Bottom - - - - - - Drawer Title - - - - - - - - - -
- ); - } -} - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/dropdown/en-US/active.md b/docs/pages/components/dropdown/en-US/active.md deleted file mode 100644 index f1e8a1ccec..0000000000 --- a/docs/pages/components/dropdown/en-US/active.md +++ /dev/null @@ -1,30 +0,0 @@ -### Option Active State - - - -```js -const instance = ( - - - Active Item - Item B - Item C - Item D - - - - Item A - Item B - Item C - Item D - - Item E-1 - Active Item - - - -); -ReactDOM.render(instance); -``` - - diff --git a/docs/pages/components/dropdown/en-US/basic.md b/docs/pages/components/dropdown/en-US/basic.md deleted file mode 100644 index b4c3773031..0000000000 --- a/docs/pages/components/dropdown/en-US/basic.md +++ /dev/null @@ -1,20 +0,0 @@ -### Default - - - -```js -const instance = ( - - New File - New File with Current Profile - Download As... - Export PDF - Export HTML - Settings - About - -); -ReactDOM.render(instance); -``` - - diff --git a/docs/pages/components/dropdown/en-US/disabled.md b/docs/pages/components/dropdown/en-US/disabled.md deleted file mode 100644 index 946638d634..0000000000 --- a/docs/pages/components/dropdown/en-US/disabled.md +++ /dev/null @@ -1,25 +0,0 @@ -### Disabled State - -You can disable the entire component or disable individual options by configuring the `disabled` property. - - - -```js -const instance = ( - - - Item A - Item B - Item C - - - Disabled Item A - Disabled Item B - Item C - - -); -ReactDOM.render(instance); -``` - - diff --git a/docs/pages/components/dropdown/en-US/icons.md b/docs/pages/components/dropdown/en-US/icons.md deleted file mode 100644 index 8e93ec9400..0000000000 --- a/docs/pages/components/dropdown/en-US/icons.md +++ /dev/null @@ -1,20 +0,0 @@ -### Dropdown with Icon - - - -```js -const instance = ( - }> - }>New File - }>New File with Current Profile - }>Download As... - }>Export PDF - }>Export HTML - }>Settings - }>About - -); -ReactDOM.render(instance); -``` - - diff --git a/docs/pages/components/dropdown/en-US/index.md b/docs/pages/components/dropdown/en-US/index.md index 3d1fa6c97e..a8e3e40e48 100644 --- a/docs/pages/components/dropdown/en-US/index.md +++ b/docs/pages/components/dropdown/en-US/index.md @@ -6,59 +6,133 @@ A drop-down menu is a navigation that uses selectpicker if you want to select a - `` Drop-down menu options. - `` A submenu is created in the Drop-down menu. -## Usage +## Import -```js -import { Dropdown } from 'rsuite'; -``` + ## Examples - +### Default + + + +### Trigger + +Set the trigger event with the `trigger` attribute, support the event: + +- `click` (default) +- `hover` +- `contextMenu` + +> Support multiple events: `Array` + + + +### Option Active State + + + +### Disabled State + +You can disable the entire component or disable individual options by configuring the `disabled` property. + + + +### With Button + +The default value of the `toggleAs` property of`Dropdown` is `Button`. You can set the button-related props (eg. size, appearance) and display it in the style of a button. + + + +### No caret variation + + + +### Dropdown with Icon + + + +### Divider and Panel + +- `divider` Sets the divider options. +- `panel` Set up a panel. + + + +### Placement + + + +### Submenu + + + +### Custom + + + +### Used with Buttons + + + +### Menu items + + + +### Used with Popover + + + +### Used with `next/link` + + ## Props + + + ### `` -| Property | Type`(default)` | Description | -| --------------- | --------------------------------------------------------- | --------------------------------------------------------------------------------------- | -| activeKey | any | The option to activate the state, corresponding to the `eventkey` in the Dropdown.item. | -| classPrefix | string `('dropdown')` | The prefix of the component CSS class | -| disabled | boolean | Whether or not component is disabled | -| icon | React.Element<typeof Icon> | Set the icon | -| menuStyle | React.CSSProperties | The style of the menu. | -| onClose | () => void | The callback function that the menu closes | -| onOpen | () => void | Menu Pop-up callback function | -| onSelect | (eventKey: any, event: SyntheticEvent<any>) => void | Selected callback function | -| onToggle | (open?: boolean) => void | Callback function for menu state switching. | -| open | boolean | Controlled open state | -| placement | enum: [Placement8](#types) `('bottomStart')` | The placement of Menu | -| renderTitle | (children?: React.Node) => React.Node | Custom title | -| title | React.Node | Menu defaults to display content. | -| toggleClassName | string | A css class to apply to the Toggle DOM node | -| toggleAs | React.ElementType `(Button)` | You can use a custom element for this component | -| trigger | union: [Trigger](#types) `('click')` | Triggering events | +| Property | Type`(default)` | Description | +| --------------- | ----------------------------------- | --------------------------------------------------------------------------------------- | +| activeKey | string | The option to activate the state, corresponding to the `eventkey` in the Dropdown.item. | +| classPrefix | string `('dropdown')` | The prefix of the component CSS class | +| disabled | boolean | Whether or not component is disabled | +| icon | Element<typeof Icon> | Set the icon | +| menuStyle | CSSProperties | The style of the menu. | +| onClose | () => void | The callback function that the menu closes | +| onOpen | () => void | Menu Pop-up callback function | +| onSelect | (eventKey: string, event) => void | Selected callback function | +| onToggle | (open?: boolean) => void | Callback function for menu state switching. | +| open | boolean | Controlled open state | +| placement | Placement `('bottomStart')` | The placement of Menu | +| renderTitle | (children?: ReactNode) => ReactNode | Custom title | +| title | ReactNode | Menu defaults to display content. | +| toggleClassName | string | A css class to apply to the Toggle DOM node | +| toggleAs | ElementType `(Button)` | You can use a custom element for this component | +| trigger | Trigger `('click')` | Triggering events | ### `` -| Property | Type`(default)` | Description | -| ----------- | --------------------------------------------------------- | ---------------------------------------------------- | -| active | boolean | Active the current option | -| children \* | React.Node | The content of the component | -| classPrefix | string `('dropdown-item')` | The prefix of the component CSS class | -| as | React.ElementType`('a')` | You can use a custom element type for this component | -| disabled | boolean | Disable the current option | -| divider | boolean | Whether to display the divider | -| eventKey | any | The value of the current option | -| icon | React.Element<typeof Icon> | Set the icon | -| onSelect | (eventKey: any, event: SyntheticEvent<any>) => void | Select the callback function for the current option | -| panel | boolean | Displays a custom panel | -| renderItem | (item:React.Node) => React.Node | Custom rendering item | +| Property | Type`(default)` | Description | +| ----------- | --------------------------------- | ---------------------------------------------------- | +| active | boolean | Active the current option | +| children \* | ReactNode | The content of the component | +| classPrefix | string `('dropdown-item')` | The prefix of the component CSS class | +| as | ElementType`('a')` | You can use a custom element type for this component | +| disabled | boolean | Disable the current option | +| divider | boolean | Whether to display the divider | +| eventKey | string | The value of the current option | +| icon | Element<typeof Icon> | Set the icon | +| onSelect | (eventKey: string, event) => void | Select the callback function for the current option | +| panel | boolean | Displays a custom panel | +| renderItem | (item:ReactNode) => ReactNode | Custom rendering item | ### `` -| Property | Type`(default)` | Description | -| -------- | -------------------------------- | ----------------------------------------------------------- | -| icon | React.Element<typeof Icon> | Set the icon | -| pullLeft | boolean | The submenu expands from the left and defaults to the right | -| title | string | Define the title as a submenu | +| Property | Type`(default)` | Description | +| -------- | -------------------------- | ----------------------------------------------------------- | +| icon | Element<typeof Icon> | Set the icon | +| pullLeft | boolean | The submenu expands from the left and defaults to the right | +| title | string | Define the title as a submenu | diff --git a/docs/pages/components/dropdown/en-US/menu-items.md b/docs/pages/components/dropdown/en-US/menu-items.md deleted file mode 100644 index e4d2cc742b..0000000000 --- a/docs/pages/components/dropdown/en-US/menu-items.md +++ /dev/null @@ -1,25 +0,0 @@ -### Menu items - - - -```js -const instance = ( - - New File - New File with Current Profile - Download As... - Export PDF - Export HTML - Settings - About - -); -ReactDOM.render(instance); -``` - - diff --git a/docs/pages/components/dropdown/en-US/size.md b/docs/pages/components/dropdown/en-US/size.md deleted file mode 100644 index 05430dd8f1..0000000000 --- a/docs/pages/components/dropdown/en-US/size.md +++ /dev/null @@ -1,30 +0,0 @@ -### With Button - -The default value of the `toggleAs` property of`Dropdown` is `Button`. You can set the button-related props (eg. size, appearance) and display it in the style of a button. - - - -```js -const SizeDropdown = props => ( - - New File - New File with Current Profile - Download As... - Export PDF - Export HTML - Settings - About - -); -const instance = ( - - - - - - -); -ReactDOM.render(instance); -``` - - diff --git a/docs/pages/components/dropdown/en-US/trigger.md b/docs/pages/components/dropdown/en-US/trigger.md deleted file mode 100644 index 9a244b0295..0000000000 --- a/docs/pages/components/dropdown/en-US/trigger.md +++ /dev/null @@ -1,37 +0,0 @@ -### Trigger - -Set the trigger event with the `trigger` attribute, support the event: - -- `click` (default) -- `hover` -- `contextMenu` - -> Support multiple events: `Array` - - - -```js -const CustomDropdown = ({ ...props }) => ( - - New File - New File with Current Profile - Download As... - Export PDF - Export HTML - Settings - About - -); - -const instance = ( - - - - - - -); -ReactDOM.render(instance); -``` - - diff --git a/docs/pages/components/dropdown/zh-CN/active.md b/docs/pages/components/dropdown/fragments/active.md similarity index 97% rename from docs/pages/components/dropdown/zh-CN/active.md rename to docs/pages/components/dropdown/fragments/active.md index 800c1f0012..52451e1ce3 100644 --- a/docs/pages/components/dropdown/zh-CN/active.md +++ b/docs/pages/components/dropdown/fragments/active.md @@ -1,5 +1,3 @@ -### 选项激活状态 - ```js diff --git a/docs/pages/components/dropdown/zh-CN/basic.md b/docs/pages/components/dropdown/fragments/basic.md similarity index 97% rename from docs/pages/components/dropdown/zh-CN/basic.md rename to docs/pages/components/dropdown/fragments/basic.md index 4d9ba2df77..9efc0c6480 100644 --- a/docs/pages/components/dropdown/zh-CN/basic.md +++ b/docs/pages/components/dropdown/fragments/basic.md @@ -1,5 +1,3 @@ -### 默认 - ```js diff --git a/docs/pages/components/dropdown/en-US/buttons.md b/docs/pages/components/dropdown/fragments/buttons.md similarity index 98% rename from docs/pages/components/dropdown/en-US/buttons.md rename to docs/pages/components/dropdown/fragments/buttons.md index 12978792ac..320a5577fb 100644 --- a/docs/pages/components/dropdown/en-US/buttons.md +++ b/docs/pages/components/dropdown/fragments/buttons.md @@ -1,5 +1,3 @@ -### Used with Buttons - ```js diff --git a/docs/pages/components/dropdown/en-US/custom.md b/docs/pages/components/dropdown/fragments/custom.md similarity index 98% rename from docs/pages/components/dropdown/en-US/custom.md rename to docs/pages/components/dropdown/fragments/custom.md index 8e0bbce8b2..8909e3e304 100644 --- a/docs/pages/components/dropdown/en-US/custom.md +++ b/docs/pages/components/dropdown/fragments/custom.md @@ -1,5 +1,3 @@ -### Custom - ```js diff --git a/docs/pages/components/dropdown/zh-CN/disabled.md b/docs/pages/components/dropdown/fragments/disabled.md similarity index 83% rename from docs/pages/components/dropdown/zh-CN/disabled.md rename to docs/pages/components/dropdown/fragments/disabled.md index f44f0c19ea..2566be9c0d 100644 --- a/docs/pages/components/dropdown/zh-CN/disabled.md +++ b/docs/pages/components/dropdown/fragments/disabled.md @@ -1,7 +1,3 @@ -### 禁用状态 - -可以禁用整个组件,也可以禁用单个选项,只需配置 `disabled` 属性。 - ```js diff --git a/docs/pages/components/dropdown/en-US/divider.md b/docs/pages/components/dropdown/fragments/divider.md similarity index 87% rename from docs/pages/components/dropdown/en-US/divider.md rename to docs/pages/components/dropdown/fragments/divider.md index d28e931911..6e457f627c 100644 --- a/docs/pages/components/dropdown/en-US/divider.md +++ b/docs/pages/components/dropdown/fragments/divider.md @@ -1,8 +1,3 @@ -### Divider and Panel - -- `divider` Sets the divider options. -- `panel` Set up a panel. - ```js diff --git a/docs/pages/components/dropdown/zh-CN/icons.md b/docs/pages/components/dropdown/fragments/icons.md similarity index 97% rename from docs/pages/components/dropdown/zh-CN/icons.md rename to docs/pages/components/dropdown/fragments/icons.md index 1d6b0969e4..3afa1e13c9 100644 --- a/docs/pages/components/dropdown/zh-CN/icons.md +++ b/docs/pages/components/dropdown/fragments/icons.md @@ -1,5 +1,3 @@ -### 带图标的 - ```js diff --git a/docs/pages/components/dropdown/fragments/import.md b/docs/pages/components/dropdown/fragments/import.md new file mode 100644 index 0000000000..c74eb669c4 --- /dev/null +++ b/docs/pages/components/dropdown/fragments/import.md @@ -0,0 +1,6 @@ +```js +import { Dropdown } from 'rsuite'; + +// or +import Dropdown from 'rsuite/lib/Dropdown'; +``` diff --git a/docs/pages/components/dropdown/zh-CN/menu-items.md b/docs/pages/components/dropdown/fragments/menu-items.md similarity index 97% rename from docs/pages/components/dropdown/zh-CN/menu-items.md rename to docs/pages/components/dropdown/fragments/menu-items.md index 833ae1c821..ecf842496b 100644 --- a/docs/pages/components/dropdown/zh-CN/menu-items.md +++ b/docs/pages/components/dropdown/fragments/menu-items.md @@ -1,5 +1,3 @@ -### 菜单项 - ```js diff --git a/docs/pages/components/dropdown/en-US/no-caret.md b/docs/pages/components/dropdown/fragments/no-caret.md similarity index 95% rename from docs/pages/components/dropdown/en-US/no-caret.md rename to docs/pages/components/dropdown/fragments/no-caret.md index 0fc22fb3cb..063eb7215f 100644 --- a/docs/pages/components/dropdown/en-US/no-caret.md +++ b/docs/pages/components/dropdown/fragments/no-caret.md @@ -1,5 +1,3 @@ -### No caret variation - ```js diff --git a/docs/pages/components/dropdown/en-US/placement.md b/docs/pages/components/dropdown/fragments/placement.md similarity index 99% rename from docs/pages/components/dropdown/en-US/placement.md rename to docs/pages/components/dropdown/fragments/placement.md index 73f1f46c33..e9e94788f7 100644 --- a/docs/pages/components/dropdown/en-US/placement.md +++ b/docs/pages/components/dropdown/fragments/placement.md @@ -1,5 +1,3 @@ -### Placement - ```js diff --git a/docs/pages/components/dropdown/en-US/submenu.md b/docs/pages/components/dropdown/fragments/submenu.md similarity index 98% rename from docs/pages/components/dropdown/en-US/submenu.md rename to docs/pages/components/dropdown/fragments/submenu.md index 77a2ea977c..98bfd8642f 100644 --- a/docs/pages/components/dropdown/en-US/submenu.md +++ b/docs/pages/components/dropdown/fragments/submenu.md @@ -1,5 +1,3 @@ -### Submenu - ```js diff --git a/docs/pages/components/dropdown/zh-CN/size.md b/docs/pages/components/dropdown/fragments/toggle-as.md similarity index 81% rename from docs/pages/components/dropdown/zh-CN/size.md rename to docs/pages/components/dropdown/fragments/toggle-as.md index 74834398ea..471e7d0339 100644 --- a/docs/pages/components/dropdown/zh-CN/size.md +++ b/docs/pages/components/dropdown/fragments/toggle-as.md @@ -1,7 +1,3 @@ -### 与按钮组合 - -`Dropdown` 的 `toggleAs` 属性默认值为是 `Button`, 可以设置按钮相关的属性(例如: size, appearance), 以按钮的样式展示。 - ```js diff --git a/docs/pages/components/dropdown/zh-CN/trigger.md b/docs/pages/components/dropdown/fragments/trigger.md similarity index 80% rename from docs/pages/components/dropdown/zh-CN/trigger.md rename to docs/pages/components/dropdown/fragments/trigger.md index 44392b1b01..c34e3fb8f1 100644 --- a/docs/pages/components/dropdown/zh-CN/trigger.md +++ b/docs/pages/components/dropdown/fragments/trigger.md @@ -1,13 +1,3 @@ -### 触发事件 - -通过 `trigger` 属性设置触发事件,支持事件: - -- `click` (默认值) -- `hover` -- `contextMenu` - -> 同时支持多个事件 `Array` - ```js diff --git a/docs/pages/components/dropdown/en-US/with-popover.md b/docs/pages/components/dropdown/fragments/with-popover.md similarity index 82% rename from docs/pages/components/dropdown/en-US/with-popover.md rename to docs/pages/components/dropdown/fragments/with-popover.md index 30a2c84cf1..6d8fdddbee 100644 --- a/docs/pages/components/dropdown/en-US/with-popover.md +++ b/docs/pages/components/dropdown/fragments/with-popover.md @@ -1,10 +1,8 @@ -### Used with Popover - ```js -const MenuPopover = ({ onSelect, ...rest }) => ( - +const MenuPopover = React.forwardRef(({ onSelect, ...rest }, ref) => ( + New File @@ -17,19 +15,19 @@ const MenuPopover = ({ onSelect, ...rest }) => ( About -); +)); const WithPopover = () => { - const triggerRef = React.createRef(); + const ref = React.useRef(); function handleSelectMenu(eventKey, event) { console.log(eventKey); - triggerRef.current.hide(); + ref.current.close(); } return ( } > diff --git a/docs/pages/components/dropdown/en-US/with-router.md b/docs/pages/components/dropdown/fragments/with-router.md similarity index 95% rename from docs/pages/components/dropdown/en-US/with-router.md rename to docs/pages/components/dropdown/fragments/with-router.md index ea073aae4c..2921cba383 100644 --- a/docs/pages/components/dropdown/en-US/with-router.md +++ b/docs/pages/components/dropdown/fragments/with-router.md @@ -1,5 +1,3 @@ -### Used with `next/link` - ```js diff --git a/docs/pages/components/dropdown/index.tsx b/docs/pages/components/dropdown/index.tsx index 5caa690bff..55af3c0640 100644 --- a/docs/pages/components/dropdown/index.tsx +++ b/docs/pages/components/dropdown/index.tsx @@ -15,23 +15,6 @@ import DefaultPage from '@/components/Page'; export default function Page() { return ( - -```js -const instance = ( - - - Save as... - Save & New - - - - - { - return } />; - }} - > - }>Save as... - }>Save & New - - - - { - return ( - } placement="left"> - {' '} - New - - ); - }} - > - }>New User - }>New Group - } title="More"> - }>New User - }>New Group - - - -); -ReactDOM.render(instance); -``` - - diff --git a/docs/pages/components/dropdown/zh-CN/custom.md b/docs/pages/components/dropdown/zh-CN/custom.md deleted file mode 100644 index 79379e4d97..0000000000 --- a/docs/pages/components/dropdown/zh-CN/custom.md +++ /dev/null @@ -1,50 +0,0 @@ -### 自定义 - - - -```js -const instance = ( - - - - Edit - - - View - - - Delete - - - - { - return ; - }} - > - - New User - - - New Group - - - { - return } circle />; - }} - > - - New User - - - New Group - - - -); -ReactDOM.render(instance); -``` - - diff --git a/docs/pages/components/dropdown/zh-CN/divider.md b/docs/pages/components/dropdown/zh-CN/divider.md deleted file mode 100644 index 82680d109f..0000000000 --- a/docs/pages/components/dropdown/zh-CN/divider.md +++ /dev/null @@ -1,28 +0,0 @@ -### 分割线与面板 - -- `divider` 设置分割选项。 -- `panel` 设置一个面板。 - - - -```js -const instance = ( - - -

Signed in as

- foobar -
- - Your profile - Your stars - Your Gists - - Help - Settings - Sign out -
-); -ReactDOM.render(instance); -``` - - diff --git a/docs/pages/components/dropdown/zh-CN/index.md b/docs/pages/components/dropdown/zh-CN/index.md index b3f1f7d154..c521df07ab 100644 --- a/docs/pages/components/dropdown/zh-CN/index.md +++ b/docs/pages/components/dropdown/zh-CN/index.md @@ -8,57 +8,131 @@ ## 获取组件 -```js -import { Dropdown } from 'rsuite'; -``` + ## 演示 - +### 默认 + + + +### 触发事件 + +通过 `trigger` 属性设置触发事件,支持事件: + +- `click` (默认值) +- `hover` +- `contextMenu` + +> 同时支持多个事件 `Array` + + + +### 选项激活状态 + + + +### 禁用状态 + +可以禁用整个组件,也可以禁用单个选项,只需配置 `disabled` 属性。 + + + +### 与按钮组合 + +`Dropdown` 的 `toggleAs` 属性默认值为是 `Button`, 可以设置按钮相关的属性(例如: size, appearance), 以按钮的样式展示。 + + + +### 没有插入号 + + + +### 带图标的 + + + +### 分割线与面板 + +- `divider` 设置分割选项。 +- `panel` 设置一个面板。 + + + +### 菜单位置 + + + +### 多级菜单 + + + +### 自定义 + + + +### 与按钮组合使用 + + + +### 菜单项 + + + +### 与 Popover 组合使用 + + + +### 与 `next/link` 组合 + + ## Props + + + ### `` -| 属性名称 | 类型 `(默认值)` | 描述 | -| --------------- | --------------------------------------------------------- | ------------------------------------------------ | -| activeKey | any | 激活状态的选项,对应 Dropdown.Item 中的 eventKey | -| classPrefix | string `('dropdown')` | 组件 CSS 类的前缀 | -| disabled | boolean | 禁用组件 | -| icon | React.Element<typeof Icon> | 设置图标 | -| menuStyle | React.CSSProperties | 菜单样式 | -| onClose | () => void | 菜单关闭的回调函数 | -| onOpen | () => void | 菜单弹出的回调函数 | -| onSelect | (eventKey: any, event: SyntheticEvent<any>) => void | 选择后的回调函数 | -| onToggle | (open?: boolean) => void | 菜单状态切换的回调函数 | -| open | boolean | 受控的打开状态 | -| placement | enum: [Placement8](#types)`('bottomStart')` | 菜单显示位置 | -| renderTitle | (children?: React.Node) => React.Node | 自定义 title | -| title | React.Node | 菜单默认显示内容 | -| toggleClassName | string | 设置 Toggle 的 className | -| toggleAs | React.ElementType `(Button)` | 为组件自定义元素类型 | -| trigger | union: [Trigger](#types) `('click')` | 触发事件 | +| 属性名称 | 类型 `(默认值)` | 描述 | +| --------------- | ----------------------------------- | ------------------------------------------------ | +| activeKey | string | 激活状态的选项,对应 Dropdown.Item 中的 eventKey | +| classPrefix | string `('dropdown')` | 组件 CSS 类的前缀 | +| disabled | boolean | 禁用组件 | +| icon | Element<typeof Icon> | 设置图标 | +| menuStyle | CSSProperties | 菜单样式 | +| onClose | () => void | 菜单关闭的回调函数 | +| onOpen | () => void | 菜单弹出的回调函数 | +| onSelect | (eventKey: string, event) => void | 选择后的回调函数 | +| onToggle | (open?: boolean) => void | 菜单状态切换的回调函数 | +| open | boolean | 受控的打开状态 | +| placement | Placement `('bottomStart')` | 菜单显示位置 | +| renderTitle | (children?: ReactNode) => ReactNode | 自定义 title | +| title | ReactNode | 菜单默认显示内容 | +| toggleClassName | string | 设置 Toggle 的 className | +| toggleAs | ElementType `(Button)` | 为组件自定义元素类型 | +| trigger | Trigger `('click')` | 触发事件 | ### `` -| 属性名称 | 类型 `(默认值)` | 描述 | -| ----------- | --------------------------------------------------------- | ---------------------- | -| active | boolean | 选中当前选项 | -| children \* | React.Node | 组件内容 | -| classPrefix | string `('dropdown-item')` | -| as | React.ElementType `('a')` | 为组件自定义元素类型 | -| disabled | boolean | 禁用当前选项 | -| divider | boolean | 显示为分割线 | -| eventKey | any | 当前选项的值 | -| icon | React.Element<typeof Icon> | 设置图标 | -| onSelect | (eventKey: any, event: SyntheticEvent<any>) => void | 选中当前选项的回调函数 | -| panel | boolean | 显示一个自定义的面板 | -| renderItem | (item:React.Node) => React.Node | 自定义渲染选项 | +| 属性名称 | 类型 `(默认值)` | 描述 | +| ----------- | ------------------------------ | ---------------------- | +| active | boolean | 选中当前选项 | +| children \* | ReactNode | 组件内容 | +| classPrefix | string `('dropdown-item')` | +| as | ElementType `('a')` | 为组件自定义元素类型 | +| disabled | boolean | 禁用当前选项 | +| divider | boolean | 显示为分割线 | +| eventKey | string | 当前选项的值 | +| icon | Element<typeof Icon> | 设置图标 | +| onSelect | (eventKey: any, event) => void | 选中当前选项的回调函数 | +| panel | boolean | 显示一个自定义的面板 | +| renderItem | (item:ReactNode) => ReactNode | 自定义渲染选项 | ### `` -| 属性名称 | 类型 | 描述 | -| -------- | -------------------------------- | -------------------------------- | -| icon | React.Element<typeof Icon> | 设置图标 | -| pullLeft | boolean | 子菜单从左侧展开,默认为右侧展开 | -| title | string | 作为子菜单定义标题 | +| 属性名称 | 类型 | 描述 | +| -------- | -------------------------- | -------------------------------- | +| icon | Element<typeof Icon> | 设置图标 | +| pullLeft | boolean | 子菜单从左侧展开,默认为右侧展开 | +| title | string | 作为子菜单定义标题 | diff --git a/docs/pages/components/dropdown/zh-CN/no-caret.md b/docs/pages/components/dropdown/zh-CN/no-caret.md deleted file mode 100644 index 82e26b271b..0000000000 --- a/docs/pages/components/dropdown/zh-CN/no-caret.md +++ /dev/null @@ -1,20 +0,0 @@ -### 没有插入号 - - - -```js -const instance = ( - - New File - New File with Current Profile - Download As... - Export PDF - Export HTML - Settings - About - -); -ReactDOM.render(instance); -``` - - diff --git a/docs/pages/components/dropdown/zh-CN/placement.md b/docs/pages/components/dropdown/zh-CN/placement.md deleted file mode 100644 index 2acdd8ca94..0000000000 --- a/docs/pages/components/dropdown/zh-CN/placement.md +++ /dev/null @@ -1,81 +0,0 @@ -### 菜单位置 - - - -```js -const items = [ - New File, - New File with Current Profile, - Download As..., - Export PDF, - Export HTML, - Settings, - About -]; - -const instance = ( - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - {items} - - - - {items} - -
- - {items} - - - - {items} - -
- - {items} - - - - {items} - -
- - {items} - - - - {items} - -
-); -ReactDOM.render(instance); -``` - - diff --git a/docs/pages/components/dropdown/zh-CN/submenu.md b/docs/pages/components/dropdown/zh-CN/submenu.md deleted file mode 100644 index d08624c499..0000000000 --- a/docs/pages/components/dropdown/zh-CN/submenu.md +++ /dev/null @@ -1,37 +0,0 @@ -### 多级菜单 - - - -```js -const instance = ( - - - Item 1 - - - Item 2-1-1 - Item 2-1-2 - Item 2-1-3 - - Item 2-2 - Item 2-3 - - - - Item 3-1-1 - Item 3-1-2 - Item 3-1-3 - - Item 3-2 - Item 3-3 - - Item 4 - Item 5 - Item 6 - - -); -ReactDOM.render(instance); -``` - - diff --git a/docs/pages/components/dropdown/zh-CN/with-popover.md b/docs/pages/components/dropdown/zh-CN/with-popover.md deleted file mode 100644 index 1ad26924aa..0000000000 --- a/docs/pages/components/dropdown/zh-CN/with-popover.md +++ /dev/null @@ -1,43 +0,0 @@ -### 与 Popover 组合使用 - - - -```js -const MenuPopover = ({ onSelect, ...rest }) => ( - - - - New File - New File with Current Profile - - Download As... - Export PDF - Export HTML - Settings - About - - -); - -const WithPopover = () => { - const triggerRef = React.createRef(); - function handleSelectMenu(eventKey, event) { - console.log(eventKey); - triggerRef.current.hide(); - } - return ( - } - > - - - ); -}; - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/dropdown/zh-CN/with-router.md b/docs/pages/components/dropdown/zh-CN/with-router.md deleted file mode 100644 index 415b0c72f3..0000000000 --- a/docs/pages/components/dropdown/zh-CN/with-router.md +++ /dev/null @@ -1,27 +0,0 @@ -### 与 `next/link` 组合 - - - -```js -const MyLink = React.forwardRef((props, ref) => { - const { href, as, ...rest } = props; - return ( - - - - ); -}); - -const NavLink = props => ; - -const instance = ( - - Guide - Components - Tools - -); -ReactDOM.render(instance); -``` - - diff --git a/docs/pages/components/form/en-US/modal-layout.md b/docs/pages/components/form/en-US/modal-layout.md index 3035e34faa..e854114c15 100644 --- a/docs/pages/components/form/en-US/modal-layout.md +++ b/docs/pages/components/form/en-US/modal-layout.md @@ -33,7 +33,7 @@ class ModalDemo extends React.Component { render() { return (
- + New User diff --git a/docs/pages/components/form/zh-CN/modal-layout.md b/docs/pages/components/form/zh-CN/modal-layout.md index 6fc592c504..3299a9cf31 100644 --- a/docs/pages/components/form/zh-CN/modal-layout.md +++ b/docs/pages/components/form/zh-CN/modal-layout.md @@ -33,7 +33,7 @@ class ModalDemo extends React.Component { render() { return (
- + New User diff --git a/docs/pages/components/input-picker/en-US/async.md b/docs/pages/components/input-picker/en-US/async.md deleted file mode 100644 index dce74859ec..0000000000 --- a/docs/pages/components/input-picker/en-US/async.md +++ /dev/null @@ -1,71 +0,0 @@ -### Async - - - -```js -/** - * import data from - * https://github.com/rsuite/rsuite/blob/master/docs/public/data/users-role.json - */ - -class AsynExample extends React.Component { - constructor(props) { - super(props); - this.state = { - loading: false, - items: [] - }; - this.handleSearch = this.handleSearch.bind(this); - this.getUsers('react'); - } - - getUsers(word) { - fetch(`https://api.github.com/search/users?q=${word}`) - .then(response => response.json()) - .then(data => { - console.log(data); - this.setState({ - loading: false, - items: data.items - }); - }) - .catch(e => console.log('Oops, error', e)); - } - - handleSearch(word) { - if (!word) { - return; - } - this.setState({ - loading: true - }); - this.getUsers(word); - } - render() { - const { items, loading } = this.state; - return ( - { - if (loading) { - return ( -

- Loading... -

- ); - } - return menu; - }} - /> - ); - } -} - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/input-picker/en-US/basic.md b/docs/pages/components/input-picker/en-US/basic.md deleted file mode 100644 index 09401bc5fc..0000000000 --- a/docs/pages/components/input-picker/en-US/basic.md +++ /dev/null @@ -1,15 +0,0 @@ -### Default - - - -```js -/** - * import data from - * https://github.com/rsuite/rsuite/blob/master/docs/public/data/users-role.json - */ - -const instance = ; -ReactDOM.render(instance); -``` - - diff --git a/docs/pages/components/input-picker/en-US/controlled.md b/docs/pages/components/input-picker/en-US/controlled.md deleted file mode 100644 index ecf05e9ad8..0000000000 --- a/docs/pages/components/input-picker/en-US/controlled.md +++ /dev/null @@ -1,40 +0,0 @@ -### Controlled - - - -```js -/** - * import data from - * https://github.com/rsuite/rsuite/blob/master/docs/public/data/users-role.json - */ - -class Demo extends React.Component { - constructor() { - super(); - this.state = { - value: null - }; - this.handleChange = this.handleChange.bind(this); - } - - handleChange(value) { - this.setState({ - value - }); - } - render() { - return ( - - ); - } -} - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/input-picker/en-US/index.md b/docs/pages/components/input-picker/en-US/index.md index e2d2f03ecf..c666240778 100644 --- a/docs/pages/components/input-picker/en-US/index.md +++ b/docs/pages/components/input-picker/en-US/index.md @@ -1,68 +1,101 @@ # InputPicker -Single item selector with text box input +Single item selector with text box input. -- `` +## Import -## Usage - -```js -import { InputPicker } from 'rsuite'; -``` + ## Examples - +### Default + + + +### Size + + + +### Block + + + +### Group + + + +### Creatable + + + +### Custom + + + +### Disabled + + + +### Async + + + +### Controlled + + ## Props + + + ### `` -| Property | Type `(Default)` | Description | -| ------------------ | ------------------------------------------------------------------------------------- | ----------------------------------------------------------- | -| block | boolean | Blocking an entire row | -| classPrefix | string `('picker')` | The prefix of the component CSS class | -| cleanable | boolean `(true)` | Whether the option can be emptied. | -| container | HTMLElement or (() => HTMLElement) | Sets the rendering container | -| creatable | boolean `(true)` | Settings can create new options | -| data \* | Array<[DataItemType](#types)> | Selectable data | -| defaultValue | string | Default value | -| disabled | boolean | Whether or not component is disabled | -| disabledItemValues | string[] | Disable optional | -| groupBy | string | Set grouping criteria 'key' in 'data' | -| labelKey | string `('label')` | Set options to display the 'key' in 'data' | -| listProps | [listprops] | List-related properties in `react-virtualized` | -| maxHeight | number `(320)` | Set the max height of the Dropdown | -| menuClassName | string | A css class to apply to the Menu DOM node. | -| menuStyle | Object | A style to apply to the Menu DOM node. | -| onChange | (value:string, event) => void | callback function when value changes | -| onClean | (event:SyntheticEvent) => void | Callback fired when value clean | -| onClose | () => void | Close callback functions | -| onEnter | () => void | Callback fired before the overlay transitions in | -| onEntered | () => void | Callback fired after the overlay finishes transitioning in | -| onEntering | () => void | Callback fired as the overlay begins to transition in | -| onExit | () => void | Callback fired right before the overlay transitions out | -| onExited | () => void | Callback fired after the overlay finishes transitioning out | -| onExiting | () => void | Callback fired as the overlay begins to transition out | -| onGroupTitleClick | (event) => void | Click the callback function for the group header | -| onOpen | () => void | Open callback function | -| onSearch | (searchKeyword:string, event) => void | callback function for Search | -| onSelect | (value:string, item: [DataItemType](#types) , event) => void | option is clicked after the selected callback function | -| placeholder | React.Node `('Select')` | Setting placeholders | -| placement | enum: [Placement](#types)`('bottomStart')` | The placement of component | -| preventOverflow | boolean | Prevent floating element overflow | -| renderExtraFooter | () => React.Node | custom render extra footer | -| renderMenu | (menu:React.Node) => React.Node | Customizing the Rendering Menu list | -| renderMenuGroup | (groupTitle:React.Node, item:[DataItemType](#types)) => React.Node | Custom Render Options Group | -| renderMenuItem | (label:React.Node, item:[DataItemType](#types)) => React.Node | Custom Render Options | -| renderValue | (value:string, item: [DataItemType](#types),selectedElement:React.Node) => React.Node | Custom Render selected options | -| searchBy | (keyword: string, label: React.ReactNode, item: ItemDataType) => boolean | Custom search rules | -| searchable | boolean `(true)` | Whether you can search for options. | -| size | enum: 'lg', 'md', 'sm', 'xs' `('md')` | A picker can have different sizes | -| sort | (isGroup: boolean) => (a: any, b: any) => number | Sort options | -| toggleAs | React.ElementType `('a')` | You can use a custom element for this component | -| value | string | Value (Controlled) | -| valueKey | string `('value')` | Set option value 'key' in 'data' | -| virtualized | boolean `(true)` | Whether using Virtualized List | +| Property | Type `(Default)` | Description | +| ------------------ | ------------------------------------------------------------------------- | ----------------------------------------------------------- | +| block | boolean | Blocking an entire row | +| classPrefix | string `('picker')` | The prefix of the component CSS class | +| cleanable | boolean `(true)` | Whether the option can be emptied. | +| container | HTMLElement | (() => HTMLElement) | Sets the rendering container | +| creatable | boolean `(true)` | Settings can create new options | +| data \* | DataItemType[] | Selectable data | +| defaultValue | string | Default value | +| disabled | boolean | Whether or not component is disabled | +| disabledItemValues | string[] | Disable optional | +| groupBy | string | Set grouping criteria 'key' in 'data' | +| labelKey | string `('label')` | Set options to display the 'key' in 'data' | +| listProps | [listprops] | List-related properties in `react-virtualized` | +| maxHeight | number `(320)` | Set the max height of the Dropdown | +| menuClassName | string | A css class to apply to the Menu DOM node. | +| menuStyle | CSSProperties | A style to apply to the Menu DOM node. | +| onChange | (value:string, event) => void | callback function when value changes | +| onClean | (event:SyntheticEvent) => void | Callback fired when value clean | +| onClose | () => void | Close callback functions | +| onEnter | () => void | Callback fired before the overlay transitions in | +| onEntered | () => void | Callback fired after the overlay finishes transitioning in | +| onEntering | () => void | Callback fired as the overlay begins to transition in | +| onExit | () => void | Callback fired right before the overlay transitions out | +| onExited | () => void | Callback fired after the overlay finishes transitioning out | +| onExiting | () => void | Callback fired as the overlay begins to transition out | +| onGroupTitleClick | (event) => void | Click the callback function for the group header | +| onOpen | () => void | Open callback function | +| onSearch | (searchKeyword:string, event) => void | callback function for Search | +| onSelect | (value:string, item: DataItemType , event) => void | option is clicked after the selected callback function | +| placeholder | ReactNode `('Select')` | Setting placeholders | +| placement | Placement `('bottomStart')` | The placement of component | +| preventOverflow | boolean | Prevent floating element overflow | +| renderExtraFooter | () => ReactNode | custom render extra footer | +| renderMenu | (menu:ReactNode) => ReactNode | Customizing the Rendering Menu list | +| renderMenuGroup | (groupTitle:ReactNode, item:DataItemType) => ReactNode | Custom Render Options Group | +| renderMenuItem | (label:ReactNode, item:DataItemType) => ReactNode | Custom Render Options | +| renderValue | (value:string, item: DataItemType,selectedElement:ReactNode) => ReactNode | Custom Render selected options | +| searchBy | (keyword: string, label: ReactNode, item: ItemDataType) => boolean | Custom search rules | +| searchable | boolean `(true)` | Whether you can search for options. | +| size | enum: 'lg'|'md'|'sm'|'xs' `('md')` | A picker can have different sizes | +| sort | (isGroup: boolean) => (a: any, b: any) => number | Sort options | +| toggleAs | ElementType `('a')` | You can use a custom element for this component | +| value | string | Value (Controlled) | +| valueKey | string `('value')` | Set option value 'key' in 'data' | +| virtualized | boolean `(true)` | Whether using Virtualized List | [listprops]: https://github.com/bvaughn/react-virtualized/blob/master/docs/List.md#prop-types diff --git a/docs/pages/components/input-picker/fragments/async.md b/docs/pages/components/input-picker/fragments/async.md new file mode 100644 index 0000000000..7ee0d7b829 --- /dev/null +++ b/docs/pages/components/input-picker/fragments/async.md @@ -0,0 +1,47 @@ + + +```js +const useUsers = (defaultUsers = []) => { + const [users, setUsers] = React.useState(defaultUsers); + const [loading, setLoading] = React.useState(false); + const featUsers = word => { + setLoading(true); + fetch(`https://api.github.com/search/users?q=${word}`) + .then(response => response.json()) + .then(data => { + setUsers(data.items); + setLoading(false); + }) + .catch(e => console.log('Oops, error', e)); + }; + + return [users, loading, featUsers]; +}; + +const App = () => { + const [users, loading, featUsers] = useUsers(); + return ( + { + if (loading) { + return ( +

+ Loading... +

+ ); + } + return menu; + }} + /> + ); +}; + +ReactDOM.render(); +``` + + diff --git a/docs/pages/components/input-picker/zh-CN/basic.md b/docs/pages/components/input-picker/fragments/basic.md similarity index 95% rename from docs/pages/components/input-picker/zh-CN/basic.md rename to docs/pages/components/input-picker/fragments/basic.md index b7c93ea5a3..9cc9260378 100644 --- a/docs/pages/components/input-picker/zh-CN/basic.md +++ b/docs/pages/components/input-picker/fragments/basic.md @@ -1,5 +1,3 @@ -### 默认 - ```js diff --git a/docs/pages/components/input-picker/en-US/block.md b/docs/pages/components/input-picker/fragments/block.md similarity index 95% rename from docs/pages/components/input-picker/en-US/block.md rename to docs/pages/components/input-picker/fragments/block.md index d1cc12136d..f24711afed 100644 --- a/docs/pages/components/input-picker/en-US/block.md +++ b/docs/pages/components/input-picker/fragments/block.md @@ -1,5 +1,3 @@ -### Block - ```js diff --git a/docs/pages/components/input-picker/fragments/controlled.md b/docs/pages/components/input-picker/fragments/controlled.md new file mode 100644 index 0000000000..9af6517903 --- /dev/null +++ b/docs/pages/components/input-picker/fragments/controlled.md @@ -0,0 +1,17 @@ + + +```js +/** + * import data from + * https://github.com/rsuite/rsuite/blob/master/docs/public/data/users-role.json + */ + +const App = () => { + const [value, setValue] = React.useState(null); + return ; +}; + +ReactDOM.render(); +``` + + diff --git a/docs/pages/components/input-picker/en-US/creatable.md b/docs/pages/components/input-picker/fragments/creatable.md similarity index 96% rename from docs/pages/components/input-picker/en-US/creatable.md rename to docs/pages/components/input-picker/fragments/creatable.md index 973d849744..64333011d0 100644 --- a/docs/pages/components/input-picker/en-US/creatable.md +++ b/docs/pages/components/input-picker/fragments/creatable.md @@ -1,5 +1,3 @@ -### Creatable - ```js diff --git a/docs/pages/components/input-picker/en-US/custom.md b/docs/pages/components/input-picker/fragments/custom.md similarity index 98% rename from docs/pages/components/input-picker/en-US/custom.md rename to docs/pages/components/input-picker/fragments/custom.md index 0f0015aa53..e80230fe8a 100644 --- a/docs/pages/components/input-picker/en-US/custom.md +++ b/docs/pages/components/input-picker/fragments/custom.md @@ -1,5 +1,3 @@ -### Custom - ```js diff --git a/docs/pages/components/input-picker/en-US/disabled.md b/docs/pages/components/input-picker/fragments/disabled.md similarity index 97% rename from docs/pages/components/input-picker/en-US/disabled.md rename to docs/pages/components/input-picker/fragments/disabled.md index dc7eab06a2..0b83087286 100644 --- a/docs/pages/components/input-picker/en-US/disabled.md +++ b/docs/pages/components/input-picker/fragments/disabled.md @@ -1,5 +1,3 @@ -### Disabled - ```js diff --git a/docs/pages/components/input-picker/en-US/group.md b/docs/pages/components/input-picker/fragments/group.md similarity index 98% rename from docs/pages/components/input-picker/en-US/group.md rename to docs/pages/components/input-picker/fragments/group.md index 96208aa518..a38543e23b 100644 --- a/docs/pages/components/input-picker/en-US/group.md +++ b/docs/pages/components/input-picker/fragments/group.md @@ -1,5 +1,3 @@ -### Group - ```js diff --git a/docs/pages/components/input-picker/fragments/import.md b/docs/pages/components/input-picker/fragments/import.md new file mode 100644 index 0000000000..41751f1912 --- /dev/null +++ b/docs/pages/components/input-picker/fragments/import.md @@ -0,0 +1,6 @@ +```js +import { InputPicker } from 'rsuite'; + +// or +import InputPicker from 'rsuite/lib/InputPicker'; +``` diff --git a/docs/pages/components/input-picker/en-US/size.md b/docs/pages/components/input-picker/fragments/size.md similarity index 98% rename from docs/pages/components/input-picker/en-US/size.md rename to docs/pages/components/input-picker/fragments/size.md index 86ebcf2ae5..053713eeb3 100644 --- a/docs/pages/components/input-picker/en-US/size.md +++ b/docs/pages/components/input-picker/fragments/size.md @@ -1,5 +1,3 @@ -### Size - ```js diff --git a/docs/pages/components/input-picker/index.tsx b/docs/pages/components/input-picker/index.tsx index d1253b0d0f..c5c797fe04 100644 --- a/docs/pages/components/input-picker/index.tsx +++ b/docs/pages/components/input-picker/index.tsx @@ -6,20 +6,5 @@ import useFetchData from '@/utils/useFetchData'; export default function Page() { const { response: data } = useFetchData('users-role'); - return ( - - ); + return ; } diff --git a/docs/pages/components/input-picker/zh-CN/async.md b/docs/pages/components/input-picker/zh-CN/async.md deleted file mode 100644 index d6e9d0c88b..0000000000 --- a/docs/pages/components/input-picker/zh-CN/async.md +++ /dev/null @@ -1,71 +0,0 @@ -### 异步 - - - -```js -/** - * import data from - * https://github.com/rsuite/rsuite/blob/master/docs/public/data/users-role.json - */ - -class AsynExample extends React.Component { - constructor(props) { - super(props); - this.state = { - loading: false, - items: [] - }; - this.handleSearch = this.handleSearch.bind(this); - this.getUsers('react'); - } - - getUsers(word) { - fetch(`https://api.github.com/search/users?q=${word}`) - .then(response => response.json()) - .then(data => { - console.log(data); - this.setState({ - loading: false, - items: data.items - }); - }) - .catch(e => console.log('Oops, error', e)); - } - - handleSearch(word) { - if (!word) { - return; - } - this.setState({ - loading: true - }); - this.getUsers(word); - } - render() { - const { items, loading } = this.state; - return ( - { - if (loading) { - return ( -

- Loading... -

- ); - } - return menu; - }} - /> - ); - } -} - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/input-picker/zh-CN/block.md b/docs/pages/components/input-picker/zh-CN/block.md deleted file mode 100644 index 04a73d5395..0000000000 --- a/docs/pages/components/input-picker/zh-CN/block.md +++ /dev/null @@ -1,15 +0,0 @@ -### 撑满 - - - -```js -/** - * import data from - * https://github.com/rsuite/rsuite/blob/master/docs/public/data/users-role.json - */ - -const instance = ; -ReactDOM.render(instance); -``` - - diff --git a/docs/pages/components/input-picker/zh-CN/controlled.md b/docs/pages/components/input-picker/zh-CN/controlled.md deleted file mode 100644 index ff27827bc3..0000000000 --- a/docs/pages/components/input-picker/zh-CN/controlled.md +++ /dev/null @@ -1,40 +0,0 @@ -### 受控 - - - -```js -/** - * import data from - * https://github.com/rsuite/rsuite/blob/master/docs/public/data/users-role.json - */ - -class Demo extends React.Component { - constructor() { - super(); - this.state = { - value: null - }; - this.handleChange = this.handleChange.bind(this); - } - - handleChange(value) { - this.setState({ - value - }); - } - render() { - return ( - - ); - } -} - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/input-picker/zh-CN/creatable.md b/docs/pages/components/input-picker/zh-CN/creatable.md deleted file mode 100644 index 2864a01066..0000000000 --- a/docs/pages/components/input-picker/zh-CN/creatable.md +++ /dev/null @@ -1,21 +0,0 @@ -### 可新建 - - - -```js -/** - * import data from - * https://github.com/rsuite/rsuite/blob/master/docs/public/data/users-role.json - */ - -const instance = ( -
- -
- -
-); -ReactDOM.render(instance); -``` - - diff --git a/docs/pages/components/input-picker/zh-CN/custom.md b/docs/pages/components/input-picker/zh-CN/custom.md deleted file mode 100644 index 370939b916..0000000000 --- a/docs/pages/components/input-picker/zh-CN/custom.md +++ /dev/null @@ -1,46 +0,0 @@ -### 自定义选项 - - - -```js -/** - * import data from - * https://github.com/rsuite/rsuite/blob/master/docs/public/data/users-role.json - */ - -const instance = ( - { - return ( -
- {label} -
- ); - }} - renderMenuGroup={(label, item) => { - return ( -
- {label} - ({item.children.length}) -
- ); - }} - renderValue={(value, item, selectedElement) => { - return ( -
- - User : - {' '} - {value} -
- ); - }} - /> -); -ReactDOM.render(instance); -``` - - diff --git a/docs/pages/components/input-picker/zh-CN/disabled.md b/docs/pages/components/input-picker/zh-CN/disabled.md deleted file mode 100644 index 475d4f1e47..0000000000 --- a/docs/pages/components/input-picker/zh-CN/disabled.md +++ /dev/null @@ -1,27 +0,0 @@ -### 禁用 - - - -```js -/** - * import data from - * https://github.com/rsuite/rsuite/blob/master/docs/public/data/users-role.json - */ - -const instance = ( -
- -
-

禁用选项

- -
-); -ReactDOM.render(instance); -``` - - diff --git a/docs/pages/components/input-picker/zh-CN/group.md b/docs/pages/components/input-picker/zh-CN/group.md deleted file mode 100644 index 00c965d6ea..0000000000 --- a/docs/pages/components/input-picker/zh-CN/group.md +++ /dev/null @@ -1,50 +0,0 @@ -### 分组 - - - -```js -/** - * import data from - * https://github.com/rsuite/rsuite/blob/master/docs/public/data/users-role.json - */ - -const instance = ( -
- -
-

排序:

- { - if (isGroup) { - return (a, b) => { - return compare(a.groupTitle, b.groupTitle); - }; - } - - return (a, b) => { - return compare(a.value, b.value); - }; - }} - style={{ width: 224 }} - /> -
-); - -function compare(a, b) { - let nameA = a.toUpperCase(); - let nameB = b.toUpperCase(); - - if (nameA < nameB) { - return -1; - } - if (nameA > nameB) { - return 1; - } - return 0; -} -ReactDOM.render(instance); -``` - - diff --git a/docs/pages/components/input-picker/zh-CN/index.md b/docs/pages/components/input-picker/zh-CN/index.md index 900c41e2dd..0219464a3d 100644 --- a/docs/pages/components/input-picker/zh-CN/index.md +++ b/docs/pages/components/input-picker/zh-CN/index.md @@ -2,67 +2,100 @@ 带文本框输入的单项选择器 -- `` - ## 获取组件 -```js -import { InputPicker } from 'rsuite'; -``` + ## 演示 - +### 默认 + + + +### 尺寸 + + + +### 撑满 + + + +### 分组 + + + +### 可新建 + + + +### 自定义选项 + + + +### 禁用 + + + +### 异步 + + + +### 受控 + + ## Props + + + ### `` -| 属性名称 | 类型`(默认值)` | 描述 | -| ------------------ | ------------------------------------------------------------------------------------- | -------------------------------------- | -| block | boolean | 堵塞整行 | -| classPrefix | string `('picker')` | 组件 CSS 类的前缀 | -| cleanable | boolean `(true)` | 可以清除 | -| container | HTMLElement or (() => HTMLElement) | 设置渲染的容器 | -| creatable | boolean `(true)` | 设置可以新建选项 | -| data \* | Array<[DataItemType](#types)> | 组件数据 | -| defaultValue | string | 设置默认值 `非受控` | -| disabled | boolean | 禁用组件 | -| disabledItemValues | string[] | 禁用选项 | -| groupBy | string | 设置分组条件在 `data` 中的 `key` | -| labelKey | string `('label')` | 设置选项显示内容在 `data` 中的 `key` | -| listProps | [listprops] | `react-virtualized` 中 List 的相关属性 | -| maxHeight | number `(320)` | 设置 Dropdown 的最大高度 | -| menuClassName | string | 应用于菜单 DOM 节点的 css class | -| menuStyle | React.CSSProperties | 应用于菜单 DOM 节点的 style | -| onChange | (value:string, event) => void | `value` 发生改变时的回调函数 | -| onClean | (event:SyntheticEvent) => void | 值清理时触发回调 | -| onClose | () => void | 关闭回调函数 | -| onEnter | () => void | 显示前动画过渡的回调函数 | -| onEntered | () => void | 显示后动画过渡的回调函数 | -| onEntering | () => void | 显示中动画过渡的回调函数 | -| onExit | () => void | 退出前动画过渡的回调函数 | -| onExited | () => void | 退出后动画过渡的回调函数 | -| onExiting | () => void | 退出中动画过渡的回调函数 | -| onGroupTitleClick | (event) => void | 点击分组标题的回调函数 | -| onOpen | () => void | 打开回调函数 | -| onSearch | (searchKeyword:string, event) => void | 搜索的回调函数 | -| onSelect | (value:string, item: DataItemType , event) => void | 选项被点击选择后的回调函数 | -| placeholder | React.Node `('Select')` | 占位符 | -| placement | enum: [Placement](#types)`('bottomStart')` | 位置 | -| preventOverflow | boolean | 防止浮动元素溢出 | -| renderExtraFooter | () => React.Node | 自定义页脚内容 | -| renderMenu | (menu:React.Node) => React.Node | 自定义渲染菜单列表 | -| renderMenuGroup | (groupTitle:React.Node, item:[DataItemType](#types)) => React.Node | 自定义渲染选项组 | -| renderMenuItem | (label:React.Node, item:[DataItemType](#types)) => React.Node | 自定义渲染选项 | -| renderValue | (value:string, item: [DataItemType](#types),selectedElement:React.Node) => React.Node | 自定义渲染被选中的选项 | -| searchBy | (keyword: string, label: React.ReactNode, item: ItemDataType) => boolean | 自定义搜索规则 | -| searchable | boolean `(true)` | 可以搜索 | -| size | enum: 'lg', 'md', 'sm', 'xs' `('md')` | 设置组件尺寸 | -| sort | (isGroup: boolean) => (a: any, b: any) => number | 对选项排序 | -| toggleAs | React.ElementType `('a')` | 为组件自定义元素类型 | -| value | string | 设置值 `受控`, | -| valueKey | string `('value')` | 设置选项值在 `data` 中的 `key` | -| virtualized | boolean `(true)` | 是否开启虚拟列表 | +| 属性名称 | 类型`(默认值)` | 描述 | +| ------------------ | ------------------------------------------------------------------------- | -------------------------------------- | +| block | boolean | 堵塞整行 | +| classPrefix | string `('picker')` | 组件 CSS 类的前缀 | +| cleanable | boolean `(true)` | 可以清除 | +| container | HTMLElement | (() => HTMLElement) | 设置渲染的容器 | +| creatable | boolean `(true)` | 设置可以新建选项 | +| data \* | DataItemType[] | 组件数据 | +| defaultValue | string | 设置默认值 `非受控` | +| disabled | boolean | 禁用组件 | +| disabledItemValues | string[] | 禁用选项 | +| groupBy | string | 设置分组条件在 `data` 中的 `key` | +| labelKey | string `('label')` | 设置选项显示内容在 `data` 中的 `key` | +| listProps | [listprops] | `react-virtualized` 中 List 的相关属性 | +| maxHeight | number `(320)` | 设置 Dropdown 的最大高度 | +| menuClassName | string | 应用于菜单 DOM 节点的 css class | +| menuStyle | CSSProperties | 应用于菜单 DOM 节点的 style | +| onChange | (value:string, event) => void | `value` 发生改变时的回调函数 | +| onClean | (event:SyntheticEvent) => void | 值清理时触发回调 | +| onClose | () => void | 关闭回调函数 | +| onEnter | () => void | 显示前动画过渡的回调函数 | +| onEntered | () => void | 显示后动画过渡的回调函数 | +| onEntering | () => void | 显示中动画过渡的回调函数 | +| onExit | () => void | 退出前动画过渡的回调函数 | +| onExited | () => void | 退出后动画过渡的回调函数 | +| onExiting | () => void | 退出中动画过渡的回调函数 | +| onGroupTitleClick | (event) => void | 点击分组标题的回调函数 | +| onOpen | () => void | 打开回调函数 | +| onSearch | (searchKeyword:string, event) => void | 搜索的回调函数 | +| onSelect | (value:string, item: DataItemType , event) => void | 选项被点击选择后的回调函数 | +| placeholder | ReactNode `('Select')` | 占位符 | +| placement | Placement `('bottomStart')` | 位置 | +| preventOverflow | boolean | 防止浮动元素溢出 | +| renderExtraFooter | () => ReactNode | 自定义页脚内容 | +| renderMenu | (menu:ReactNode) => ReactNode | 自定义渲染菜单列表 | +| renderMenuGroup | (groupTitle:ReactNode, item:DataItemType) => ReactNode | 自定义渲染选项组 | +| renderMenuItem | (label:ReactNode, item:DataItemType) => ReactNode | 自定义渲染选项 | +| renderValue | (value:string, item: DataItemType,selectedElement:ReactNode) => ReactNode | 自定义渲染被选中的选项 | +| searchBy | (keyword: string, label: ReactNode, item: ItemDataType) => boolean | 自定义搜索规则 | +| searchable | boolean `(true)` | 可以搜索 | +| size | enum: 'lg'|'md'|'sm'|'xs' `('md')` | 设置组件尺寸 | +| sort | (isGroup: boolean) => (a: any, b: any) => number | 对选项排序 | +| toggleAs | ElementType `('a')` | 为组件自定义元素类型 | +| value | string | 设置值 `受控`, | +| valueKey | string `('value')` | 设置选项值在 `data` 中的 `key` | +| virtualized | boolean `(true)` | 是否开启虚拟列表 | [listprops]: https://github.com/bvaughn/react-virtualized/blob/master/docs/List.md#prop-types diff --git a/docs/pages/components/input-picker/zh-CN/size.md b/docs/pages/components/input-picker/zh-CN/size.md deleted file mode 100644 index a73bc59594..0000000000 --- a/docs/pages/components/input-picker/zh-CN/size.md +++ /dev/null @@ -1,23 +0,0 @@ -### 尺寸 - - - -```js -/** - * import data from - * https://github.com/rsuite/rsuite/blob/master/docs/public/data/users-role.json - */ - -const styles = { width: 224, display: 'block', marginBottom: 10 }; -const instance = ( -
- - - - -
-); -ReactDOM.render(instance); -``` - - diff --git a/docs/pages/components/modal/en-US/backdrop.md b/docs/pages/components/modal/en-US/backdrop.md deleted file mode 100644 index 7628daadcf..0000000000 --- a/docs/pages/components/modal/en-US/backdrop.md +++ /dev/null @@ -1,71 +0,0 @@ -### Backdrop - -When set to true, the Modal will display the background when it is opened. Clicking on the background will close the Modal. If you do not want to close the Modal, set it to 'static'. - - - -```js -class Demo extends React.Component { - constructor(props) { - super(props); - this.state = { - backdrop: false, - show: false - }; - this.close = this.close.bind(this); - this.open = this.open.bind(this); - } - close() { - this.setState({ show: false }); - } - open() { - this.setState({ show: true }); - } - render() { - const { backdrop, show } = this.state; - return ( -
- Backdrop: - - { - this.setState({ backdrop: value }); - }} - > - true - false - static - -
- - - - - - - Modal Title - - - - - - - - - -
- ); - } -} - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/modal/en-US/basic.md b/docs/pages/components/modal/en-US/basic.md deleted file mode 100644 index cb068c77db..0000000000 --- a/docs/pages/components/modal/en-US/basic.md +++ /dev/null @@ -1,52 +0,0 @@ -### Default - - - -```js -class BasicDemo extends React.Component { - constructor(props) { - super(props); - this.state = { - show: false - }; - this.close = this.close.bind(this); - this.open = this.open.bind(this); - } - close() { - this.setState({ show: false }); - } - open() { - this.setState({ show: true }); - } - render() { - return ( -
- - - - - - - Modal Title - - - - - - - - - -
- ); - } -} - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/modal/en-US/confirm.md b/docs/pages/components/modal/en-US/confirm.md deleted file mode 100644 index 8ef0768d6e..0000000000 --- a/docs/pages/components/modal/en-US/confirm.md +++ /dev/null @@ -1,58 +0,0 @@ -### Confirm - - - -```js -class Confirm extends React.Component { - constructor(props) { - super(props); - this.state = { - show: false - }; - this.close = this.close.bind(this); - this.open = this.open.bind(this); - } - close() { - this.setState({ show: false }); - } - open() { - this.setState({ show: true }); - } - render() { - return ( -
- - - - - - - - {' '} - Once a project is disabled, there will be no update on project report, and project - members can access history data only. Are you sure you want to proceed? - - - - - - -
- ); - } -} - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/modal/en-US/dynamic.md b/docs/pages/components/modal/en-US/dynamic.md deleted file mode 100644 index 7f03817c8f..0000000000 --- a/docs/pages/components/modal/en-US/dynamic.md +++ /dev/null @@ -1,70 +0,0 @@ -### Dynamic - - - -```js -class Demo extends React.Component { - constructor(props) { - super(props); - this.state = { - show: false, - overflow: true, - rows: 0 - }; - this.close = this.close.bind(this); - this.open = this.open.bind(this); - this.resetRows = this.resetRows.bind(this); - } - close() { - this.setState({ show: false }); - } - resetRows() { - this.setState({ rows: 0 }); - } - open(event) { - this.setState({ show: true }); - setTimeout(() => { - this.setState({ - rows: 80 - }); - }, 2000); - } - render() { - const { overflow, show } = this.state; - return ( -
- - - - - - - Modal Title - - - {this.state.rows ? ( - - ) : ( -
- -
- )} -
- - - - -
-
- ); - } -} - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/modal/en-US/full.md b/docs/pages/components/modal/en-US/full.md deleted file mode 100644 index 6484fc0fac..0000000000 --- a/docs/pages/components/modal/en-US/full.md +++ /dev/null @@ -1,53 +0,0 @@ -### Full - - - -```js -class Demo extends React.Component { - constructor(props) { - super(props); - this.state = { - show: false - }; - this.close = this.close.bind(this); - this.open = this.open.bind(this); - } - close() { - this.setState({ show: false }); - } - open(size) { - this.setState({ show: true }); - } - render() { - return ( -
- - - - - - Modal Title - - - - - - - - - -
- ); - } -} - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/modal/en-US/index.md b/docs/pages/components/modal/en-US/index.md index 56cbc39710..4ae69910df 100644 --- a/docs/pages/components/modal/en-US/index.md +++ b/docs/pages/components/modal/en-US/index.md @@ -9,65 +9,95 @@ Modal box containing the following components: - `` - `` -## Usage +## Import -```js -import { Modal } from 'rsuite'; -``` + ## Examples - +### Default + + + +### Backdrop + +When set to true, the Modal will display the background when it is opened. Clicking on the background will close the Modal. If you do not want to close the Modal, set it to 'static'. + + + +### Size + + + +### Full + + + +### Overflow + + + +### Dynamic + + + +### Confirm + + ## Props ### `` -| Property | Type `(Default)` | Description | -| ----------------- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| autoFocus | boolean `(true)` | When set to true, the Modal is opened and is automatically focused on its own, accessible to screen readers | -| backdrop | unions: boolean, 'static' | When set to true, the Modal will display the background when it is opened. Clicking on the background will close the Modal. If you do not want to close the Modal, set it to 'static'. | -| backdropClassName | string | Add an optional extra class name to .modal-backdrop It could end up looking like class="modal-backdrop foo-modal-backdrop in". | -| classPrefix | string `('modal')` | The prefix of the component CSS class | -| dialogClassName | string | CSS class applied to Dialog DOM nodes. | -| dialogAs | React.ElementType `(ModalDialog)` | You can use a custom element type for Dialog | -| enforceFocus | boolean `(true)` | When set to true, Modal will prevent the focus from leaving when opened, making it easier for the secondary screen reader to access | -| full | boolean | Full screen | -| keyboard | boolean `(true)` | Close Modal when `esc` key is pressed. | -| onEnter | () => void | Callback fired before the Modal transitions in | -| onEntered | () => void | Callback fired after the Modal finishes transitioning in | -| onEntering | () => void | Callback fired as the Modal begins to transition in | -| onExit | () => void | Callback fired right before the Modal transitions out | -| onExited | () => void | Callback fired after the Modal finishes transitioning out | -| onExiting | () => void | Callback fired as the Modal begins to transition out | -| onHide | () => void | Callback fired when Modal hide | -| onShow | () => void | Callback fired when Modal display | -| overflow | boolean `(true)` | Automatically sets the height when the body content is too long. | -| show \* | boolean | Show Modal | -| size | enum: 'lg', 'md', 'sm', 'xs' | Set Modal size | +| Property | Type `(Default)` | Description | +| ----------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| autoFocus | boolean `(true)` | When set to true, the Modal is opened and is automatically focused on its own, accessible to screen readers | +| backdrop | unions: boolean, 'static' | When set to true, the Modal will display the background when it is opened. Clicking on the background will close the Modal. If you do not want to close the Modal, set it to 'static'. | +| backdropClassName | string | Add an optional extra class name to .modal-backdrop It could end up looking like class="modal-backdrop foo-modal-backdrop in". | +| classPrefix | string `('modal')` | The prefix of the component CSS class | +| dialogAs | ElementType `(ModalDialog)` | You can use a custom element type for Dialog | +| dialogClassName | string | CSS class applied to Dialog DOM nodes. | +| enforceFocus | boolean `(true)` | When set to true, Modal will prevent the focus from leaving when opened, making it easier for the secondary screen reader to access | +| full | boolean | Full screen | +| keyboard | boolean `(true)` | Close Modal when `esc` key is pressed. | +| onClose | () => void | Callback fired when Modal hide | +| onEnter | () => void | Callback fired before the Modal transitions in | +| onEntered | () => void | Callback fired after the Modal finishes transitioning in | +| onEntering | () => void | Callback fired as the Modal begins to transition in | +| onExit | () => void | Callback fired right before the Modal transitions out | +| onExited | () => void | Callback fired after the Modal finishes transitioning out | +| onExiting | () => void | Callback fired as the Modal begins to transition out | +| onOpen | () => void | Callback fired when Modal display | +| open \* | boolean | Show Modal | +| overflow | boolean `(true)` | Automatically sets the height when the body content is too long. | +| size | enum: 'lg'|'md'|'sm'|'xs' `('md')` | Set Modal size | ### `` -| Property | Type `(Default)` | Description | -| ----------- | ------------------------------------------ | ------------------------------------- | -| classPrefix | string `('modal-header')` | The prefix of the component CSS class | -| closeButton | boolean `(true)` | Display close button | -| onHide | (event: SyntheticEvent<any>) => void | Called when Modal is hidden | +| Property | Type `(Default)` | Description | +| ----------- | ------------------------------------------ | ----------------------------------------------- | +| as | ElementType `('div')` | You can use a custom element for this component | +| classPrefix | string `('modal-header')` | The prefix of the component CSS class | +| closeButton | boolean `(true)` | Display close button | +| onClose | (event: SyntheticEvent<any>) => void | Called when Modal is hidden | ### `` -| Property | Type `(Default)` | Description | -| ----------- | ------------------------ | ------------------------------------- | -| classPrefix | string `('modal-title')` | The prefix of the component CSS class | +| Property | Type `(Default)` | Description | +| ----------- | ------------------------ | ----------------------------------------------- | +| as | ElementType `('h4')` | You can use a custom element for this component | +| classPrefix | string `('modal-title')` | The prefix of the component CSS class | ### `` -| Property | Type `(Default)` | Description | -| ----------- | ------------------------- | ------------------------------------- | -| classPrefix | string `('modal-footer')` | The prefix of the component CSS class | +| Property | Type `(Default)` | Description | +| ----------- | ------------------------- | ----------------------------------------------- | +| as | ElementType `('div')` | You can use a custom element for this component | +| classPrefix | string `('modal-footer')` | The prefix of the component CSS class | ### `` -| Property | Type `(Default)` | Description | -| ----------- | ----------------------- | ------------------------------------- | -| classPrefix | string `('modal-body')` | The prefix of the component CSS class | +| Property | Type `(Default)` | Description | +| ----------- | ----------------------- | ----------------------------------------------- | +| as | ElementType `('div')` | You can use a custom element for this component | +| classPrefix | string `('modal-body')` | The prefix of the component CSS class | diff --git a/docs/pages/components/modal/en-US/overflow.md b/docs/pages/components/modal/en-US/overflow.md deleted file mode 100644 index cf8141c796..0000000000 --- a/docs/pages/components/modal/en-US/overflow.md +++ /dev/null @@ -1,62 +0,0 @@ -### Overflow - - - -```js -class Demo extends React.Component { - constructor(props) { - super(props); - this.state = { - show: false, - overflow: true - }; - this.close = this.close.bind(this); - this.open = this.open.bind(this); - } - close() { - this.setState({ show: false }); - } - open(event) { - this.setState({ show: true }); - } - render() { - const { overflow, show } = this.state; - return ( -
- Overflow - { - this.setState({ overflow: checked }); - }} - /> -
- - - - - - - Modal Title - - - - - - - - - -
- ); - } -} - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/modal/en-US/size.md b/docs/pages/components/modal/en-US/size.md deleted file mode 100644 index b2cc876bdc..0000000000 --- a/docs/pages/components/modal/en-US/size.md +++ /dev/null @@ -1,67 +0,0 @@ -### Size - - - -```js -class Demo extends React.Component { - constructor(props) { - super(props); - this.state = { - show: false - }; - this.close = this.close.bind(this); - this.open = this.open.bind(this); - } - close() { - this.setState({ - show: false - }); - } - open(size) { - this.setState({ - size, - show: true - }); - } - render() { - return ( -
- - - - - - - - - Modal Title - - - - - - - - - -
- ); - } -} - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/modal/fragments/backdrop.md b/docs/pages/components/modal/fragments/backdrop.md new file mode 100644 index 0000000000..2613b3d618 --- /dev/null +++ b/docs/pages/components/modal/fragments/backdrop.md @@ -0,0 +1,47 @@ + + +```js +const App = () => { + const [open, setOpen] = React.useState(false); + const [backdrop, setBackdrop] = React.useState('static'); + const handleOpen = () => setOpen(true); + const handleClose = () => setOpen(false); + + return ( +
+ Backdrop: + + setBackdrop(value)}> + static + true + false + +
+ + + + + + + Modal Title + + + + + + + + + + +
+ ); +}; +ReactDOM.render(); +``` + + diff --git a/docs/pages/components/modal/fragments/basic.md b/docs/pages/components/modal/fragments/basic.md new file mode 100644 index 0000000000..76ae56c05b --- /dev/null +++ b/docs/pages/components/modal/fragments/basic.md @@ -0,0 +1,37 @@ + + +```js +const App = () => { + const [open, setOpen] = React.useState(false); + const handleOpen = () => setOpen(true); + const handleClose = () => setOpen(false); + return ( +
+ + + + + + + Modal Title + + + + + + + + + +
+ ); +}; + +ReactDOM.render(); +``` + + diff --git a/docs/pages/components/modal/fragments/confirm.md b/docs/pages/components/modal/fragments/confirm.md new file mode 100644 index 0000000000..2288d0701a --- /dev/null +++ b/docs/pages/components/modal/fragments/confirm.md @@ -0,0 +1,43 @@ + + +```js +const App = () => { + const [open, setOpen] = React.useState(false); + const handleOpen = () => setOpen(true); + const handleClose = () => setOpen(false); + + return ( +
+ + + + + + + + Once a project is disabled, there will be no update on project report, and project members + can access history data only. Are you sure you want to proceed ? + + + + + + +
+ ); +}; + +ReactDOM.render(); +``` + + diff --git a/docs/pages/components/modal/fragments/dynamic.md b/docs/pages/components/modal/fragments/dynamic.md new file mode 100644 index 0000000000..f018cf7a2b --- /dev/null +++ b/docs/pages/components/modal/fragments/dynamic.md @@ -0,0 +1,56 @@ + + +```js +const App = () => { + const [open, setOpen] = React.useState(false); + const [rows, setRows] = React.useState(0); + const handleOpen = () => setOpen(true); + const handleClose = () => setOpen(false); + + const handleEntered = () => { + setTimeout(() => setRows(80), 2000); + }; + + return ( +
+ + + + + { + setRows(0); + }} + > + + Modal Title + + + {rows ? ( + + ) : ( +
+ +
+ )} +
+ + + + +
+
+ ); +}; + +ReactDOM.render(); +``` + + diff --git a/docs/pages/components/modal/fragments/full.md b/docs/pages/components/modal/fragments/full.md new file mode 100644 index 0000000000..545e0c3574 --- /dev/null +++ b/docs/pages/components/modal/fragments/full.md @@ -0,0 +1,38 @@ + + +```js +const App = () => { + const [open, setOpen] = React.useState(false); + const handleOpen = () => setOpen(true); + const handleClose = () => setOpen(false); + return ( +
+ + + + + + Modal Title + + + + + + + + + +
+ ); +}; + +ReactDOM.render(); +``` + + diff --git a/docs/pages/components/modal/fragments/import.md b/docs/pages/components/modal/fragments/import.md new file mode 100644 index 0000000000..e10f25200c --- /dev/null +++ b/docs/pages/components/modal/fragments/import.md @@ -0,0 +1,6 @@ +```js +import { Modal } from 'rsuite'; + +// or +import Modal from 'rsuite/lib/Modal'; +``` diff --git a/docs/pages/components/modal/fragments/overflow.md b/docs/pages/components/modal/fragments/overflow.md new file mode 100644 index 0000000000..d349714b32 --- /dev/null +++ b/docs/pages/components/modal/fragments/overflow.md @@ -0,0 +1,42 @@ + + +```js +const App = () => { + const [open, setOpen] = React.useState(false); + const [overflow, setOverflow] = React.useState(true); + const handleOpen = () => setOpen(true); + const handleClose = () => setOpen(false); + + return ( +
+ Overflow + setOverflow(checked)} /> +
+ + + + + + + Modal Title + + + + + + + + + +
+ ); +}; + +ReactDOM.render(); +``` + + diff --git a/docs/pages/components/modal/fragments/size.md b/docs/pages/components/modal/fragments/size.md new file mode 100644 index 0000000000..c7f8ca7a10 --- /dev/null +++ b/docs/pages/components/modal/fragments/size.md @@ -0,0 +1,51 @@ + + +```js +const App = () => { + const [open, setOpen] = React.useState(false); + const [size, setSize] = React.useState(); + const handleOpen = value => { + setSize(value); + setOpen(true); + }; + const handleClose = () => setOpen(false); + + return ( +
+ + + + + + + + + Modal Title + + + + + + + + + +
+ ); +}; +ReactDOM.render(); +``` + + diff --git a/docs/pages/components/modal/index.tsx b/docs/pages/components/modal/index.tsx index 40eb9c6b72..9aac861f30 100644 --- a/docs/pages/components/modal/index.tsx +++ b/docs/pages/components/modal/index.tsx @@ -5,7 +5,6 @@ import DefaultPage from '@/components/Page'; export default function Page() { return ( - -```js -class Demo extends React.Component { - constructor(props) { - super(props); - this.state = { - backdrop: false, - show: false - }; - this.close = this.close.bind(this); - this.open = this.open.bind(this); - } - close() { - this.setState({ show: false }); - } - open() { - this.setState({ show: true }); - } - render() { - const { backdrop, show } = this.state; - return ( -
- Backdrop: - - { - this.setState({ backdrop: value }); - }} - > - true - false - static - -
- - - - - - - Modal Title - - - - - - - - - -
- ); - } -} - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/modal/zh-CN/basic.md b/docs/pages/components/modal/zh-CN/basic.md deleted file mode 100644 index d2fc2d9320..0000000000 --- a/docs/pages/components/modal/zh-CN/basic.md +++ /dev/null @@ -1,52 +0,0 @@ -### 默认 - - - -```js -class BasicDemo extends React.Component { - constructor(props) { - super(props); - this.state = { - show: false - }; - this.close = this.close.bind(this); - this.open = this.open.bind(this); - } - close() { - this.setState({ show: false }); - } - open() { - this.setState({ show: true }); - } - render() { - return ( -
- - - - - - - Modal Title - - - - - - - - - -
- ); - } -} - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/modal/zh-CN/confirm.md b/docs/pages/components/modal/zh-CN/confirm.md deleted file mode 100644 index 8ef0768d6e..0000000000 --- a/docs/pages/components/modal/zh-CN/confirm.md +++ /dev/null @@ -1,58 +0,0 @@ -### Confirm - - - -```js -class Confirm extends React.Component { - constructor(props) { - super(props); - this.state = { - show: false - }; - this.close = this.close.bind(this); - this.open = this.open.bind(this); - } - close() { - this.setState({ show: false }); - } - open() { - this.setState({ show: true }); - } - render() { - return ( -
- - - - - - - - {' '} - Once a project is disabled, there will be no update on project report, and project - members can access history data only. Are you sure you want to proceed? - - - - - - -
- ); - } -} - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/modal/zh-CN/dynamic.md b/docs/pages/components/modal/zh-CN/dynamic.md deleted file mode 100644 index b2425d5c04..0000000000 --- a/docs/pages/components/modal/zh-CN/dynamic.md +++ /dev/null @@ -1,70 +0,0 @@ -### 动态的 - - - -```js -class Demo extends React.Component { - constructor(props) { - super(props); - this.state = { - show: false, - overflow: true, - rows: 0 - }; - this.close = this.close.bind(this); - this.open = this.open.bind(this); - this.resetRows = this.resetRows.bind(this); - } - close() { - this.setState({ show: false }); - } - resetRows() { - this.setState({ rows: 0 }); - } - open(event) { - this.setState({ show: true }); - setTimeout(() => { - this.setState({ - rows: 80 - }); - }, 2000); - } - render() { - const { overflow, show } = this.state; - return ( -
- - - - - - - Modal Title - - - {this.state.rows ? ( - - ) : ( -
- -
- )} -
- - - - -
-
- ); - } -} - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/modal/zh-CN/full.md b/docs/pages/components/modal/zh-CN/full.md deleted file mode 100644 index 8083fdc86e..0000000000 --- a/docs/pages/components/modal/zh-CN/full.md +++ /dev/null @@ -1,53 +0,0 @@ -### 全屏 - - - -```js -class Demo extends React.Component { - constructor(props) { - super(props); - this.state = { - show: false - }; - this.close = this.close.bind(this); - this.open = this.open.bind(this); - } - close() { - this.setState({ show: false }); - } - open(size) { - this.setState({ show: true }); - } - render() { - return ( -
- - - - - - Modal Title - - - - - - - - - -
- ); - } -} - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/modal/zh-CN/index.md b/docs/pages/components/modal/zh-CN/index.md index d2d6c0d2aa..08e98b2307 100644 --- a/docs/pages/components/modal/zh-CN/index.md +++ b/docs/pages/components/modal/zh-CN/index.md @@ -11,40 +11,66 @@ ## 获取组件 -```js -import { Modal } from 'rsuite'; -``` + ## 演示 - +### 默认 + + + +### 背景板 + +当设置为 true,Modal 打开时会显示背景,点击背景会关闭 Modal,如果不想关闭 Modal,可以设置为 'static' + + + +### 尺寸 + + + +### 全屏 + + + +### 溢出 + + + +### 动态 + + + +### Confirm + + ## Props ### `` -| 属性名称 | 类型 `(默认值)` | 描述 | -| ----------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------- | -| autoFocus | boolean `(true)` | 当设置为 true, Modal 被打开是自动焦点移到其自身,辅助屏幕阅读器容易访问 | -| backdrop | unions: boolean, 'static' | 当设置为 true,Modal 打开时会显示背景,点击背景会关闭 Modal,如果不想关闭 Modal,可以设置为 'static' | -| backdropClassName | string | 应用于 backdrop DOM 节点的 css class | -| classPrefix | string `('modal')` | 组件 CSS 类的前缀 | -| dialogClassName | string | 应用于 Dialog DOM 节点的 css class | -| dialogAs | React.ElementType `(ModalDialog)` | 以对 Dialog 使用自定义元素类型 | -| enforceFocus | boolean `(true)` | 当设置为 true, Modal 将防止焦点在打开时离开,辅助屏幕阅读器容易访问 | -| full | boolean | 撑满全屏 | -| keyboard | boolean `(true)` | 按下 esc 键时关闭 Modal | -| onEnter | () => void | 显示前动画过渡的回调函数 | -| onEntered | () => void | 显示后动画过渡的回调函数 | -| onEntering | () => void | 显示中动画过渡的回调函数 | -| onExit | () => void | 退出前动画过渡的回调函数 | -| onExited | () => void | 退出后动画过渡的回调函数 | -| onExiting | () => void | 退出中动画过渡的回调函数 | -| onHide | () => void | 隐藏时的回调函数 | -| onShow | () => void | 显示时的回调函数 | -| overflow | boolean `(true)` | body 内容过长时自动设置高度 | -| show \* | boolean | 显示 Modal | -| size | enum: 'lg', 'md', 'sm', 'xs' `(sm)` | Modal 尺寸 | +| 属性名称 | 类型 `(默认值)` | 描述 | +| ----------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| autoFocus | boolean `(true)` | 当设置为 true, Modal 被打开是自动焦点移到其自身,辅助屏幕阅读器容易访问 | +| backdrop | unions: boolean, 'static' | 当设置为 true,Modal 打开时会显示背景,点击背景会关闭 Modal,如果不想关闭 Modal,可以设置为 'static' | +| backdropClassName | string | 应用于 backdrop DOM 节点的 css class | +| classPrefix | string `('modal')` | 组件 CSS 类的前缀 | +| dialogAs | ElementType `(ModalDialog)` | 以对 Dialog 使用自定义元素类型 | +| dialogClassName | string | 应用于 Dialog DOM 节点的 css class | +| enforceFocus | boolean `(true)` | 当设置为 true, Modal 将防止焦点在打开时离开,辅助屏幕阅读器容易访问 | +| full | boolean | 撑满全屏 | +| keyboard | boolean `(true)` | 按下 esc 键时关闭 Modal | +| onClose | () => void | 隐藏时的回调函数 | +| onEnter | () => void | 显示前动画过渡的回调函数 | +| onEntered | () => void | 显示后动画过渡的回调函数 | +| onEntering | () => void | 显示中动画过渡的回调函数 | +| onExit | () => void | 退出前动画过渡的回调函数 | +| onExited | () => void | 退出后动画过渡的回调函数 | +| onExiting | () => void | 退出中动画过渡的回调函数 | +| onOpen | () => void | 显示时的回调函数 | +| open \* | boolean | 显示 Modal | +| overflow | boolean `(true)` | body 内容过长时自动设置高度 | +| size | enum: 'lg'|'md'|'sm'|'xs' `('md')` | Modal 尺寸 | ### `` @@ -52,7 +78,7 @@ import { Modal } from 'rsuite'; | ----------- | ------------------------------------------ | --------------------------- | | classPrefix | string `('modal-header')` | 组件 CSS 类的前缀 | | closeButton | boolean `(true)` | 当设置为 true, 显示关闭按钮 | -| onHide | (event: SyntheticEvent<any>) => void | 点击关闭按钮的回调函数 | +| onClose | (event: SyntheticEvent<any>) => void | 点击关闭按钮的回调函数 | ### `` diff --git a/docs/pages/components/modal/zh-CN/overflow.md b/docs/pages/components/modal/zh-CN/overflow.md deleted file mode 100644 index b0ba55d2bc..0000000000 --- a/docs/pages/components/modal/zh-CN/overflow.md +++ /dev/null @@ -1,62 +0,0 @@ -### 滚动条 - - - -```js -class Demo extends React.Component { - constructor(props) { - super(props); - this.state = { - show: false, - overflow: true - }; - this.close = this.close.bind(this); - this.open = this.open.bind(this); - } - close() { - this.setState({ show: false }); - } - open(event) { - this.setState({ show: true }); - } - render() { - const { overflow, show } = this.state; - return ( -
- Overflow - { - this.setState({ overflow: checked }); - }} - /> -
- - - - - - - Modal Title - - - - - - - - - -
- ); - } -} - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/modal/zh-CN/size.md b/docs/pages/components/modal/zh-CN/size.md deleted file mode 100644 index 336f3a62ce..0000000000 --- a/docs/pages/components/modal/zh-CN/size.md +++ /dev/null @@ -1,67 +0,0 @@ -### 尺寸 - - - -```js -class Demo extends React.Component { - constructor(props) { - super(props); - this.state = { - show: false - }; - this.close = this.close.bind(this); - this.open = this.open.bind(this); - } - close() { - this.setState({ - show: false - }); - } - open(size) { - this.setState({ - size, - show: true - }); - } - render() { - return ( -
- - - - - - - - - Modal Title - - - - - - - - - -
- ); - } -} - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/popover/en-US/basic.md b/docs/pages/components/popover/en-US/basic.md deleted file mode 100644 index 4ba8e0cc36..0000000000 --- a/docs/pages/components/popover/en-US/basic.md +++ /dev/null @@ -1,17 +0,0 @@ -### Default - - - -```js -const instance = ( -
- -

This is a defalut Popover

-

Content

-
-
-); -ReactDOM.render(instance); -``` - - diff --git a/docs/pages/components/popover/en-US/index.md b/docs/pages/components/popover/en-US/index.md index 3cea642eec..e39a859c92 100644 --- a/docs/pages/components/popover/en-US/index.md +++ b/docs/pages/components/popover/en-US/index.md @@ -5,76 +5,55 @@ When the mouse clicks/moves in, the pop-up pop-up box is used to display more co - `` Pop-up box. - `` Monitor triggers, wrap the outside of the listener object, and notify the `Tooltip` when the event is triggered. -## Usage +## Import -```js -import { Popover, Whisper } from 'rsuite'; -``` + ## Examples - +### Default + + + +### Placement + + + +### Triggering events + +`Whisper` provides a `trigger` props, which is used to control the display of `Popover` in different business scenarios. Props values ​​include: + +- `click`: It will be triggered when the element is clicked, and closed when clicked again. +- `focus`: It is generally triggered when the user clicks or taps on an element or selects it with the keyboard's `tab` key. +- `hover`: Will be triggered when the cursor (mouse pointer) is hovering over the element. +- `active`: It is triggered when the element is activated. +- `none`: No trigger event, generally used when it needs to be triggered by a method. + + + +> Note: [Safari ignoring tabindex](https://stackoverflow.com/questions/1848390/safari-ignoring-tabindex) + +### Container and prevent overflow + +Positioned popover components in scrolling container + + + +### Used with Dropdown + + ## Props + + ### `` | Property | Type `(Default)` | Description | | ----------- | -------------------- | -------------------------------------- | -| children \* | React.Node | The content of the component. | +| children \* | ReactNode | The content of the component. | | classPrefix | string `('popover')` | The prefix of the component CSS class. | -| title | React.Node | The title of the component. | +| title | ReactNode | The title of the component. | | visible | boolean | The component is visible by default. | -### `` - -| Property | Type `(Default)` | Description | -| --------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | -| container | HTMLElement or (() => HTMLElement) | Sets the rendering container | -| delay | number | Delay Time | -| delayHide | number | Hidden delay Time | -| delayShow | number | Show Delay Time | -| enterable | boolean | Whether mouse is allowed to enter the floating layer of popover,when the value of `trigger` is set to`hover` | -| full | boolean | The content full the container | -| onBlur | () => void | Lose Focus callback function | -| onClick | () => void | Click on the callback function | -| onClose | () => void | Callback fired when close component | -| onEnter | () => void | Callback fired before the overlay transitions in | -| onEntered | () => void | Callback fired after the overlay finishes transitioning in | -| onEntering | () => void | Callback fired as the overlay begins to transition in | -| onExit | () => void | Callback fired right before the overlay transitions out | -| onExited | () => void | Callback fired after the overlay finishes transitioning out | -| onExiting | () => void | Callback fired as the overlay begins to transition out | -| onFocus | () => void | Callback function to get focus | -| onMouseOut | () => void | Mouse leave callback function | -| onOpen | () => void | Callback fired when open component | -| placement | enum: [PlacementAll](#types) `('right')` | Dispaly placement | -| preventOverflow | boolean | Prevent floating element overflow | -| speaker \* | union: Tooltip, Popover | Displayed component | -| trigger | union: 'click', 'hover', 'focus', 'active', 'none' `(['hover','focus'])` | Triggering events | -| triggerRef | React.Ref | Ref of trigger | - -### Whisper methods - -- open - -Display a Popover. - -```ts -open: (delay?: number) => void -``` - -- close - -Hide a Popover. - -```ts -close: (delay?: number) => void -``` - -## Related components - -- [``](./popover) -- [``](./message) -- [`](./alert) -- [``](./notification) + diff --git a/docs/pages/components/popover/en-US/trigger.md b/docs/pages/components/popover/en-US/trigger.md deleted file mode 100644 index 98cc5dcd48..0000000000 --- a/docs/pages/components/popover/en-US/trigger.md +++ /dev/null @@ -1,72 +0,0 @@ -### Triggering events - -`Whisper` provides a `trigger` props, which is used to control the display of `Popover` in different business scenarios. Props values ​​include: - -- `click`: It will be triggered when the element is clicked, and closed when clicked again. -- `focus`: It is generally triggered when the user clicks or taps on an element or selects it with the keyboard's `tab` key. -- `hover`: Will be triggered when the cursor (mouse pointer) is hovering over the element. -- `active`: It is triggered when the element is activated. -- `none`: No trigger event, generally used when it needs to be triggered by a method. - - - -```js -const speaker = ( - -

This is a default Popover

-

Content

-
- -); - -const TriggerMethod = () => { - const triggerRef = React.createRef(); - const open = () => triggerRef.current.open(); - const close = () => triggerRef.current.close(); - - return ( -
- - Click the `Open` and `Close` buttons. - -
- - - - -
- ); -}; - -const App = () => ( -
- - - - - - - - - - - - - - - - - -
- -
-); - -ReactDOM.render(); -``` - - - -> Note: [Safari ignoring tabindex](https://stackoverflow.com/questions/1848390/safari-ignoring-tabindex) diff --git a/docs/pages/components/popover/zh-CN/basic.md b/docs/pages/components/popover/fragments/basic.md similarity index 95% rename from docs/pages/components/popover/zh-CN/basic.md rename to docs/pages/components/popover/fragments/basic.md index f39905285b..d52b366aec 100644 --- a/docs/pages/components/popover/zh-CN/basic.md +++ b/docs/pages/components/popover/fragments/basic.md @@ -1,5 +1,3 @@ -### 默认 - ```js diff --git a/docs/pages/components/popover/en-US/container.md b/docs/pages/components/popover/fragments/container.md similarity index 88% rename from docs/pages/components/popover/en-US/container.md rename to docs/pages/components/popover/fragments/container.md index e6bdf29a63..4fd24fe22e 100644 --- a/docs/pages/components/popover/en-US/container.md +++ b/docs/pages/components/popover/fragments/container.md @@ -1,7 +1,3 @@ -### Container and prevent overflow - -Positioned popover components in scrolling container - ```js diff --git a/docs/pages/components/popover/fragments/import.md b/docs/pages/components/popover/fragments/import.md new file mode 100644 index 0000000000..f97a25bcfa --- /dev/null +++ b/docs/pages/components/popover/fragments/import.md @@ -0,0 +1,7 @@ +```js +import { Popover, Whisper } from 'rsuite'; + +// or +import Popover from 'rsuite/lib/Popover'; +import Whisper from 'rsuite/lib/Whisper'; +``` diff --git a/docs/pages/components/popover/en-US/placement.md b/docs/pages/components/popover/fragments/placement.md similarity index 95% rename from docs/pages/components/popover/en-US/placement.md rename to docs/pages/components/popover/fragments/placement.md index 6322137804..ac60bf9610 100644 --- a/docs/pages/components/popover/en-US/placement.md +++ b/docs/pages/components/popover/fragments/placement.md @@ -1,16 +1,14 @@ -### Placement - ```js -const Speaker = ({ content, ...props }) => { +const Speaker = React.forwardRef(({ content, ...props }, ref) => { return ( - +

This is a Popover

{content}

); -}; +}); const CustomComponent = ({ placement }) => ( ```js const speaker = ( -

This is a defalut Popover

+

This is a default Popover

Content

link @@ -68,5 +58,3 @@ ReactDOM.render(); ``` - -> 注意: [Safari ignoring tabindex](https://stackoverflow.com/questions/1848390/safari-ignoring-tabindex) diff --git a/docs/pages/components/popover/en-US/with-dropdown.md b/docs/pages/components/popover/fragments/with-dropdown.md similarity index 81% rename from docs/pages/components/popover/en-US/with-dropdown.md rename to docs/pages/components/popover/fragments/with-dropdown.md index 4c1b78d539..e17cfb895c 100644 --- a/docs/pages/components/popover/en-US/with-dropdown.md +++ b/docs/pages/components/popover/fragments/with-dropdown.md @@ -1,10 +1,8 @@ -### Used with Dropdown - ```js -const MenuPopover = ({ onSelect, ...rest }) => ( - +const MenuPopover = React.forwardRef(({ onSelect, ...rest }, ref) => ( + New File New File with Current Profile @@ -15,19 +13,19 @@ const MenuPopover = ({ onSelect, ...rest }) => ( About -); +)); const WithDropdown = () => { - const triggerRef = React.createRef(); + const ref = React.useRef(); function handleSelectMenu(eventKey, event) { console.log(eventKey); - triggerRef.current.hide(); + ref.current.close(); } return ( } > diff --git a/docs/pages/components/popover/index.tsx b/docs/pages/components/popover/index.tsx index 4d74816a9a..03156ed46c 100644 --- a/docs/pages/components/popover/index.tsx +++ b/docs/pages/components/popover/index.tsx @@ -6,7 +6,6 @@ import PreventOverflowContainer from '@/components/PreventOverflowContainer'; export default function Page() { return ( ); diff --git a/docs/pages/components/popover/zh-CN/container.md b/docs/pages/components/popover/zh-CN/container.md deleted file mode 100644 index 399e030651..0000000000 --- a/docs/pages/components/popover/zh-CN/container.md +++ /dev/null @@ -1,39 +0,0 @@ -### 容器与防止溢出 - -`Popover` 会渲染在容器内部,跟随按钮一起滚动。 - - - -```js -/** - * PreventOverflowContainer from - * https://github.com/rsuite/rsuite/blob/master/docs/components/PreventOverflowContainer.tsx - */ - -const speaker = ( - -

This is a defalut Popover

-

Content

-
-); - -const App = () => ( - - {getContainer => ( - - - - )} - -); - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/popover/zh-CN/index.md b/docs/pages/components/popover/zh-CN/index.md index 8e0ca388e7..8421083f29 100644 --- a/docs/pages/components/popover/zh-CN/index.md +++ b/docs/pages/components/popover/zh-CN/index.md @@ -7,13 +7,41 @@ ## 获取组件 -```js -import { Popover, Whisper } from 'rsuite'; -``` + ## 演示 - +### 默认 + + + +### 位置 + + + +### 触发事件 + +`Whisper` 提供了一个 `trigger` 属性,用于在各种场景下控制 `Popover` 显示。属性值包括: + +- `click`: 当用户点击元素时会被触发,再点击会关闭。 +- `focus`: 当用户点击或触摸元素或通过键盘的 `tab` 键选择它时会被触发。 +- `hover`: 鼠标悬停到元素上时触发,鼠标离开则关闭。 +- `active`: 激活元素时会被触发。 +- `none`: 无触发事件,一般用于需要通过方法触发时候使用。 + + + +> 注意: [Safari ignoring tabindex](https://stackoverflow.com/questions/1848390/safari-ignoring-tabindex) + +### 容器与防止溢出 + +`Popover` 会渲染在容器内部,跟随按钮一起滚动。 + + + +### 与 Dropdown 组合使用 + + ## Props @@ -21,60 +49,9 @@ import { Popover, Whisper } from 'rsuite'; | 属性名称 | 类型 | 描述 | | ----------- | -------------------- | ----------------- | -| children \* | React.Node | 组件的内容 | +| children \* | ReactNode | 组件的内容 | | classPrefix | string `('popover')` | 组件 CSS 类的前缀 | -| title | React.Node | 标题 | +| title | ReactNode | 标题 | | visible | boolean | 组件默认可见的 | -### `` - -| 属性名称 | 类型 `(默认值)` | 描述 | -| --------------- | ------------------------------------------------------------------------ | --------------------------------------------------------- | -| container | HTMLElement or (() => HTMLElement) | 设置渲染的容器 | -| delay | number | 延迟时间 | -| delayHide | number | 隐藏的延迟时间 | -| delayShow | number | 展示的延迟时间 | -| enterable | boolean | 当 `trigger` 值为 `hover`时候,鼠标是否可进入提示框浮层中 | -| full | boolean | 撑满容器 | -| onBlur | () => void | 失去焦点回调函数 | -| onClick | () => void | 点击的回调函数 | -| onClose | () => void | 关闭回调函数 | -| onEnter | () => void | 显示前动画过渡的回调函数 | -| onEntered | () => void | 显示后动画过渡的回调函数 | -| onEntering | () => void | 显示中动画过渡的回调函数 | -| onExit | () => void | 退出前动画过渡的回调函数 | -| onExited | () => void | 退出后动画过渡的回调函数 | -| onExiting | () => void | 退出中动画过渡的回调函数 | -| onFocus | () => void | 获取焦点的回调函数 | -| onMouseOut | () => void | 鼠标离开的回调函数 | -| onOpen | () => void | 打开回调函数 | -| placement | enum: [PlacementAll](#types) `('right')` | 显示位置 | -| preventOverflow | boolean | 防止浮动元素溢出 | -| speaker \* | union: Tooltip, Popover | 展示的元素 | -| trigger | union: 'click', 'hover', 'focus', 'active', 'none' `(['hover','focus'])` | 触发事件,可以通过数组配置多事件 | -| triggerRef | React.Ref | trigger 的 ref | - -### Whisper methods - -- open - -显示一个 Popover。 - -```ts -open: (delay?: number) => void -``` - -- close - -隐藏一个 Popover。 - -```ts -close: (delay?: number) => void -``` - -## 相关组件 - -- [``](./tooltip) 文字提示 -- [``](./message) 消息框 -- [`](./alert) 提醒框 -- [``](./notification) 通知框 + diff --git a/docs/pages/components/popover/zh-CN/overlay.md b/docs/pages/components/popover/zh-CN/overlay.md deleted file mode 100644 index c30382f30e..0000000000 --- a/docs/pages/components/popover/zh-CN/overlay.md +++ /dev/null @@ -1,37 +0,0 @@ -### 自定义浮层 - - - -```js -const Overlay = React.forwardRef(({ style, ...rest }, ref) => { - const styles = { - ...style, - background: '#000', - padding: 20, - borderRadius: 4, - position: 'absolute', - boxShadow: '0 3px 6px -2px rgba(0, 0, 0, 0.6)' - }; - return ( -
- Overlay -
- ); -}); - -const App = () => ( - { - const { className, left, top } = props; - return ; - }} - > - - -); - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/popover/zh-CN/with-dropdown.md b/docs/pages/components/popover/zh-CN/with-dropdown.md deleted file mode 100644 index dcef198d2c..0000000000 --- a/docs/pages/components/popover/zh-CN/with-dropdown.md +++ /dev/null @@ -1,41 +0,0 @@ -### 与 Dropdown 组合使用 - - - -```js -const MenuPopover = ({ onSelect, ...rest }) => ( - - - New File - New File with Current Profile - Download As... - Export PDF - Export HTML - Settings - About - - -); - -const WithDropdown = () => { - const triggerRef = React.createRef(); - function handleSelectMenu(eventKey, event) { - console.log(eventKey); - triggerRef.current.hide(); - } - return ( - } - > - - - ); -}; - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/portal/en-US/basic.md b/docs/pages/components/portal/en-US/basic.md deleted file mode 100644 index cd71d2ef2c..0000000000 --- a/docs/pages/components/portal/en-US/basic.md +++ /dev/null @@ -1,54 +0,0 @@ -### Portal - - - -```js -class PortalDemo extends React.Component { - constructor(props) { - super(props); - this.handleToggle = this.handleToggle.bind(this); - this.state = { - show: false - }; - } - - handleToggle() { - this.setState({ - show: !this.state.show - }); - } - - render() { - const { show } = this.state; - return ( -
- -
-
{ - this.container = ref; - }} - > - container -
- {show ? ( - { - return this.container; - }} - onRendered={() => { - console.log('onRendered'); - }} - > -
Element
-
- ) : null} -
- ); - } -} - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/portal/en-US/index.md b/docs/pages/components/portal/en-US/index.md deleted file mode 100644 index 9854278aeb..0000000000 --- a/docs/pages/components/portal/en-US/index.md +++ /dev/null @@ -1,25 +0,0 @@ -# Portal - -The Portal appends the child components to the specified container, such as Modal, Picker, and so on, to render the component outside the triggering source DOM. - -The React 16 provides a `reactdom.createportal ()` method to implement this feature, and if you are currently using a react version of 15 (or less than 15), you can implement this requirement directly through the Portal component. - -## Usage - -```js -import { Portal } from 'rsuite'; -``` - -## Examples - - - -## Props - -### `` - -| Property | Type `(Default)` | Description | -| ---------- | ----------------------------- | ------------------------------ | -| children | React.Node | Subcomponents | -| container | HTMLElement,() => HTMLElement | Render subcomponents Container | -| onRendered | () => void | Rendered callback function | diff --git a/docs/pages/components/portal/index.tsx b/docs/pages/components/portal/index.tsx deleted file mode 100644 index 8fffc4feb4..0000000000 --- a/docs/pages/components/portal/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import * as React from 'react'; -import { Portal, Button } from 'rsuite'; -import DefaultPage from '@/components/Page'; - -export default function Page() { - return ; -} diff --git a/docs/pages/components/portal/zh-CN/basic.md b/docs/pages/components/portal/zh-CN/basic.md deleted file mode 100644 index cd71d2ef2c..0000000000 --- a/docs/pages/components/portal/zh-CN/basic.md +++ /dev/null @@ -1,54 +0,0 @@ -### Portal - - - -```js -class PortalDemo extends React.Component { - constructor(props) { - super(props); - this.handleToggle = this.handleToggle.bind(this); - this.state = { - show: false - }; - } - - handleToggle() { - this.setState({ - show: !this.state.show - }); - } - - render() { - const { show } = this.state; - return ( -
- -
-
{ - this.container = ref; - }} - > - container -
- {show ? ( - { - return this.container; - }} - onRendered={() => { - console.log('onRendered'); - }} - > -
Element
-
- ) : null} -
- ); - } -} - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/portal/zh-CN/index.md b/docs/pages/components/portal/zh-CN/index.md deleted file mode 100644 index 50d9d7dcd7..0000000000 --- a/docs/pages/components/portal/zh-CN/index.md +++ /dev/null @@ -1,25 +0,0 @@ -# Portal 入口 - -Portal 是将子级组件将追加到指定的容器中, 比如 Modal, Picker 等组件就需要把组件渲染到触发源 DOM 的外部。 - -在 React 16 中的提供了一个 `ReactDOM.createPortal()` 方法可以实现该功能, 如果您当前使用的是 React 版本是 15(或者 15 以下),那可以直接通过 Portal 组件来实现这个需求。 - -## 获取组件 - -```js -import { Portal } from 'rsuite'; -``` - -## 演示 - - - -## Props - -### `` - -| 属性名称 | 类型 `(默认值)` | 描述 | -| ---------- | ----------------------------- | ---------------- | -| children | React.Node | 子组件 | -| container | HTMLElement,() => HTMLElement | 渲染子组件的容器 | -| onRendered | () => void | 渲染后的回调函数 | diff --git a/docs/pages/components/tooltip/en-US/basic.md b/docs/pages/components/tooltip/en-US/basic.md deleted file mode 100644 index 8717409f49..0000000000 --- a/docs/pages/components/tooltip/en-US/basic.md +++ /dev/null @@ -1,16 +0,0 @@ -### Default - - - -```js -const instance = ( -
- - This is a tooltip . - -
-); -ReactDOM.render(instance); -``` - - diff --git a/docs/pages/components/tooltip/en-US/container.md b/docs/pages/components/tooltip/en-US/container.md deleted file mode 100644 index f6f4491256..0000000000 --- a/docs/pages/components/tooltip/en-US/container.md +++ /dev/null @@ -1,40 +0,0 @@ -### Container and prevent overflow - - - -```js -/** - * PreventOverflowContainer from - * https://github.com/rsuite/rsuite/blob/master/docs/components/PreventOverflowContainer.tsx - */ - -const speaker = ( - - This is a tooltip . - -); - -class Demo extends React.Component { - render() { - return ( - - {getContainer => ( - - - - )} - - ); - } -} - -ReactDOM.render(); -``` - - diff --git a/docs/pages/components/tooltip/en-US/disabled-elements.md b/docs/pages/components/tooltip/en-US/disabled-elements.md deleted file mode 100644 index ae482add23..0000000000 --- a/docs/pages/components/tooltip/en-US/disabled-elements.md +++ /dev/null @@ -1,18 +0,0 @@ -### Disabled elements - -Elements with the disabled attribute aren’t interactive, meaning users cannot hover or click them to trigger a popover (or tooltip). As a workaround, you’ll want to trigger the overlay from a wrapper `
` or ``. - - - -```js -const instance = ( - Tooltip!}> - - - - -); -ReactDOM.render(instance); -``` - - diff --git a/docs/pages/components/tooltip/en-US/index.md b/docs/pages/components/tooltip/en-US/index.md index ae01257ea2..07f4d55640 100644 --- a/docs/pages/components/tooltip/en-US/index.md +++ b/docs/pages/components/tooltip/en-US/index.md @@ -5,70 +5,59 @@ A text tip for secondary, which replaces the default title property of an HTML e - `` Text tip. - `` Monitor triggers, wrap the outside of the listener object, and notify the `Tooltip` when the event is triggered. -## Usage +## Import -```js -import { Tooltip, Whisper } from 'rsuite'; -``` + ## Examples - +### Default + + + +### Placement + +- `left` , `top` , `right` , `bottom` is in 4 directions, indicating the location of the display. +- `leftStart` , A start is added to the left, and here start is a logical way, indicating that the alignment is the beginning of the Y axis. + +> For a description of start and end, refer to W3C first public working draft about [CSS Logical Properties and Values Level 1](https://www.w3.org/TR/2017/WD-css-logical-1-20170518/). + + + +### Triggering events + +`Whisper` provides a `trigger` props, which is used to control the display of `Tooltip` in different business scenarios. Props values ​​include: + +- `click`: It will be triggered when the element is clicked, and closed when clicked again. +- `focus`: It is generally triggered when the user clicks or taps on an element or selects it with the keyboard's `tab` key. +- `hover`: Will be triggered when the cursor (mouse pointer) is hovering over the element. +- `active`: It is triggered when the element is activated. +- `none`: No trigger event, generally used when it needs to be triggered by a method. + + + +> Note: [Safari ignoring tabindex](https://stackoverflow.com/questions/1848390/safari-ignoring-tabindex) + +### Container and prevent overflow + + + +### Disabled elements + +Elements with the disabled attribute aren’t interactive, meaning users cannot hover or click them to trigger a popover (or tooltip). As a workaround, you’ll want to trigger the overlay from a wrapper `
` or ``. + + ## Props + + ### `` | Property | Type `(Default)` | Description | | ----------- | -------------------- | ------------------------------------- | -| children \* | React.Node | The content of the component. | +| children \* | ReactNode | The content of the component. | | classPrefix | string `('tooltip')` | The prefix of the component CSS class | | visible | boolean | The component is visible by default | -### `` - -| Property | Type `(Default)` | Description | -| --------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------- | -| container | HTMLElement or (() => HTMLElement) | Sets the rendering container | -| delay | number | Delay Time | -| delayHide | number | Hidden delay Time | -| delayShow | number | Show Delay Time | -| onBlur | () => void | Lose Focus callback function | -| onClick | () => void | Click on the callback function | -| onEnter | () => void | Callback fired before the overlay transitions in | -| onEntered | () => void | Callback fired after the overlay finishes transitioning in | -| onEntering | () => void | Callback fired as the overlay begins to transition in | -| onExit | () => void | Callback fired right before the overlay transitions out | -| onExited | () => void | Callback fired after the overlay finishes transitioning out | -| onExiting | () => void | Callback fired as the overlay begins to transition out | -| onFocus | () => void | Callback function to get focus | -| onMouseOut | () => void | Mouse leave callback function | -| placement | enum: [PlacementAll](#types) `('right')` | Dispaly placement | -| preventOverflow | boolean | Prevent floating element overflow | -| speaker \* | union: Tooltip, Popover | Displayed component | -| trigger | union: 'click', 'hover', 'focus', 'active', 'none' `(['hover','focus'])` | Triggering events | - -### Whisper methods - -- open - -Display a Tooltip. - -```ts -open: (delay?: number) => void -``` - -- close - -Hide a Tooltip. - -```ts -close: (delay?: number) => void -``` - -## Related components - -- [``](./popover) -- [``](./message) -- [`](./alert) -- [``](./notification) + diff --git a/docs/pages/components/tooltip/en-US/placement.md b/docs/pages/components/tooltip/en-US/placement.md deleted file mode 100644 index 479f1676b1..0000000000 --- a/docs/pages/components/tooltip/en-US/placement.md +++ /dev/null @@ -1,106 +0,0 @@ -### Placement - -- `left` , `top` , `right` , `bottom` is in 4 directions, indicating the location of the display. -- `leftStart` , A start is added to the left, and here start is a logical way, indicating that the alignment is the beginning of the Y axis. - -> For a description of start and end, refer to W3C first public working draft about [CSS Logical Properties and Values Level 1](https://www.w3.org/TR/2017/WD-css-logical-1-20170518/). - - - -```js -const CustomComponent = ({ placement }) => ( - This is a ToolTip for simple text hints. It can replace the title property - } - > - - -); - -const instance = ( -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - -
- - - - - - -
- - - - - - -
- - - - - - -
- - - - - - - -
-
- - -
- - - - -
- - - -
-); -ReactDOM.render(instance); -``` - - diff --git a/docs/pages/components/tooltip/en-US/trigger.md b/docs/pages/components/tooltip/en-US/trigger.md deleted file mode 100644 index 6dd3b12f90..0000000000 --- a/docs/pages/components/tooltip/en-US/trigger.md +++ /dev/null @@ -1,40 +0,0 @@ -### Triggering events - -`Whisper` provides a `trigger` props, which is used to control the display of `Tooltip` in different business scenarios. Props values ​​include: - -- `click`: It will be triggered when the element is clicked, and closed when clicked again. -- `focus`: It is generally triggered when the user clicks or taps on an element or selects it with the keyboard's `tab` key. -- `hover`: Will be triggered when the cursor (mouse pointer) is hovering over the element. -- `active`: It is triggered when the element is activated. -- `none`: No trigger event, generally used when it needs to be triggered by a method. - - - -```js -const tooltip = ( - - This is a help tooltip . - -); -const instance = ( - - - - - - - - - - - - - - -); -ReactDOM.render(instance); -``` - - - -> Note: [Safari ignoring tabindex](https://stackoverflow.com/questions/1848390/safari-ignoring-tabindex) diff --git a/docs/pages/components/tooltip/zh-CN/basic.md b/docs/pages/components/tooltip/fragments/basic.md similarity index 94% rename from docs/pages/components/tooltip/zh-CN/basic.md rename to docs/pages/components/tooltip/fragments/basic.md index d43109f137..1f4fb76b18 100644 --- a/docs/pages/components/tooltip/zh-CN/basic.md +++ b/docs/pages/components/tooltip/fragments/basic.md @@ -1,5 +1,3 @@ -### 默认 - ```js diff --git a/docs/pages/components/tooltip/zh-CN/container.md b/docs/pages/components/tooltip/fragments/container.md similarity index 96% rename from docs/pages/components/tooltip/zh-CN/container.md rename to docs/pages/components/tooltip/fragments/container.md index 1edb99f5d4..55967be698 100644 --- a/docs/pages/components/tooltip/zh-CN/container.md +++ b/docs/pages/components/tooltip/fragments/container.md @@ -1,5 +1,3 @@ -### 容器与防止溢出 - ```js diff --git a/docs/pages/components/tooltip/zh-CN/disabled-elements.md b/docs/pages/components/tooltip/fragments/disabled-elements.md similarity index 51% rename from docs/pages/components/tooltip/zh-CN/disabled-elements.md rename to docs/pages/components/tooltip/fragments/disabled-elements.md index 3dd93476fb..e57f7f27b2 100644 --- a/docs/pages/components/tooltip/zh-CN/disabled-elements.md +++ b/docs/pages/components/tooltip/fragments/disabled-elements.md @@ -1,7 +1,3 @@ -### 禁用的元素 - -具有禁用属性的元素禁用后无法将鼠标悬停或单击它们来触发弹出 `Tooltip`。 解决方法是,您要可以通过包装 `
` 或 `` 触发叠加层。 - ```js diff --git a/docs/pages/components/tooltip/fragments/import.md b/docs/pages/components/tooltip/fragments/import.md new file mode 100644 index 0000000000..47cbd7a5d4 --- /dev/null +++ b/docs/pages/components/tooltip/fragments/import.md @@ -0,0 +1,7 @@ +```js +import { Tooltip, Whisper } from 'rsuite'; + +// or +import Tooltip from 'rsuite/lib/Tooltip'; +import Whisper from 'rsuite/lib/Whisper'; +``` diff --git a/docs/pages/components/popover/zh-CN/placement.md b/docs/pages/components/tooltip/fragments/placement.md similarity index 89% rename from docs/pages/components/popover/zh-CN/placement.md rename to docs/pages/components/tooltip/fragments/placement.md index 60a99388b7..ae780f9065 100644 --- a/docs/pages/components/popover/zh-CN/placement.md +++ b/docs/pages/components/tooltip/fragments/placement.md @@ -1,22 +1,13 @@ -### 位置 - ```js -const Speaker = ({ content, ...props }) => { - return ( - -

This is a Popover

-

{content}

-
- ); -}; - const CustomComponent = ({ placement }) => ( } + placement={placement} + speaker={ + This is a ToolTip for simple text hints. It can replace the title property + } > @@ -95,6 +86,7 @@ const instance = ( +
diff --git a/docs/pages/components/tooltip/zh-CN/trigger.md b/docs/pages/components/tooltip/fragments/trigger.md similarity index 51% rename from docs/pages/components/tooltip/zh-CN/trigger.md rename to docs/pages/components/tooltip/fragments/trigger.md index db21be1156..46eafa205a 100644 --- a/docs/pages/components/tooltip/zh-CN/trigger.md +++ b/docs/pages/components/tooltip/fragments/trigger.md @@ -1,13 +1,3 @@ -### 触发事件 - -`Whisper` 提供了一个 `trigger` 属性,用于在各种场景下控制 `Tooltip` 显示。属性值包括: - -- `click`: 当用户点击元素时会被触发,再点击会关闭。 -- `focus`: 当用户点击或触摸元素或通过键盘的 `tab` 键选择它时会被触发。 -- `hover`: 鼠标悬停到元素上时触发,鼠标离开则关闭。 -- `active`: 激活元素时会被触发。 -- `none`: 无触发事件,一般用于需要通过方法触发时候使用。 - ```js @@ -27,7 +17,6 @@ const instance = ( - @@ -37,5 +26,3 @@ ReactDOM.render(instance); ``` - -> 注意: [Safari ignoring tabindex](https://stackoverflow.com/questions/1848390/safari-ignoring-tabindex) diff --git a/docs/pages/components/tooltip/index.tsx b/docs/pages/components/tooltip/index.tsx index 6e3049b330..ec6f2eaf82 100644 --- a/docs/pages/components/tooltip/index.tsx +++ b/docs/pages/components/tooltip/index.tsx @@ -6,7 +6,6 @@ import PreventOverflowContainer from '@/components/PreventOverflowContainer'; export default function Page() { return ( ); diff --git a/docs/pages/components/tooltip/zh-CN/index.md b/docs/pages/components/tooltip/zh-CN/index.md index 658b11edca..b774397a9b 100644 --- a/docs/pages/components/tooltip/zh-CN/index.md +++ b/docs/pages/components/tooltip/zh-CN/index.md @@ -7,13 +7,50 @@ ## 获取组件 -```js -import { Tooltip, Whisper } from 'rsuite'; -``` + ## 演示 - +### 默认 + + + +### 位置 + +- `left` , `top` , `right` , `bottom` 是物理中的 4 个方向, 表示显示的位置。 +- `leftStart` , 在 left 后面加了一个 start, 这里的 start 是逻辑方式,表示对齐方式是 Y 轴的开始。 + +> 有关 `start` 和 `end` 的描述可参照 W3C 关于 [CSS 逻辑属性和值(CSS Logical Properties and Values Level 1)](https://www.w3.org/TR/2017/WD-css-logical-1-20170518/) 的首份工作草案(First Public Working Draft) + + + +### 触发事件 + +`Whisper` 提供了一个 `trigger` 属性,用于在各种场景下控制 `Tooltip` 显示。属性值包括: + +- `click`: 当用户点击元素时会被触发,再点击会关闭。 +- `focus`: 当用户点击或触摸元素或通过键盘的 `tab` 键选择它时会被触发。 +- `hover`: 鼠标悬停到元素上时触发,鼠标离开则关闭。 +- `active`: 激活元素时会被触发。 +- `none`: 无触发事件,一般用于需要通过方法触发时候使用。 + + + +> 注意: [Safari ignoring tabindex](https://stackoverflow.com/questions/1848390/safari-ignoring-tabindex) + +### 容器与防止溢出 + + + +### 禁用的元素 + +具有禁用属性的元素禁用后无法将鼠标悬停或单击它们来触发弹出 `Tooltip`。 解决方法是,您要可以通过包装 `
` 或 `` 触发叠加层。 + + + +## Props + + ## Props @@ -21,54 +58,8 @@ import { Tooltip, Whisper } from 'rsuite'; | 属性名称 | 类型 `(默认值)` | 描述 | | ----------- | -------------------- | ----------------- | -| children \* | React.Node | 组件的内容 | +| children \* | ReactNode | 组件的内容 | | classPrefix | string `('tooltip')` | 组件 CSS 类的前缀 | | visible | boolean | 组件默认可见的 | -### `` - -| 属性名称 | 类型 `(默认值)` | 描述 | | -| --------------- | ------------------------------------------------------------------------ | ------------------------------- | --- | -| container | HTMLElement or (() => HTMLElement) | 设置渲染的容器 | -| delay | number | 延迟时间 | | -| delayHide | number | 隐藏的延迟时间 | | -| delayShow | number | 展示的延迟时间 | | -| onBlur | () => void | 失去焦点回调函数 | | -| onClick | () => void | 点击的回调函数 | | -| onEnter | () => void | 显示前动画过渡的回调函数 | -| onEntered | () => void | 显示后动画过渡的回调函数 | -| onEntering | () => void | 显示中动画过渡的回调函数 | -| onExit | () => void | 退出前动画过渡的回调函数 | -| onExited | () => void | 退出后动画过渡的回调函数 | -| onExiting | () => void | 退出中动画过渡的回调函数 | -| onFocus | () => void | 获取焦点的回调函数 | | -| onMouseOut | () => void | 鼠标离开的回调函数 | | -| placement | enum: [PlacementAll](#types) `('right')` | 显示位置 | | -| preventOverflow | boolean | 防止浮动元素溢出 | -| speaker \* | union: Tooltip, Popover | 展示的元素 | | -| trigger | union: 'click', 'hover', 'focus', 'active', 'none' `(['hover','focus'])` | 触发事件,可以通过数组配置多事件 | | - -### Whisper methods - -- open - -显示一个 Tooltip。 - -```ts -open: (delay?: number) => void -``` - -- close - -隐藏一个 Tooltip。 - -```ts -close: (delay?: number) => void -``` - -## 相关组件 - -- [``](./popover) 弹出框 -- [``](./message) 消息框 -- [`](./alert) 提醒框 -- [``](./notification) 通知框 + diff --git a/docs/pages/components/tooltip/zh-CN/placement.md b/docs/pages/components/tooltip/zh-CN/placement.md deleted file mode 100644 index 8a5f0d8fd2..0000000000 --- a/docs/pages/components/tooltip/zh-CN/placement.md +++ /dev/null @@ -1,106 +0,0 @@ -### 位置 - -- `left` , `top` , `right` , `bottom` 是物理中的 4 个方向, 表示显示的位置。 -- `leftStart` , 在 left 后面加了一个 start, 这里的 start 是逻辑方式,表示对齐方式是 Y 轴的开始。 - -> 有关 `start` 和 `end` 的描述可参照 W3C 关于 [CSS 逻辑属性和值(CSS Logical Properties and Values Level 1)](https://www.w3.org/TR/2017/WD-css-logical-1-20170518/) 的首份工作草案(First Public Working Draft) - - - -```js -const CustomComponent = ({ placement }) => ( - This is a ToolTip for simple text hints. It can replace the title property - } - > - - -); - -const instance = ( -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - -
- - - - - - -
- - - - - - -
- - - - - - -
- - - - - - - -
-
- - -
- - - - -
- - - -
-); -ReactDOM.render(instance); -``` - - diff --git a/docs/pages/components/whisper/en-US/index.md b/docs/pages/components/whisper/en-US/index.md new file mode 100644 index 0000000000..e11565a081 --- /dev/null +++ b/docs/pages/components/whisper/en-US/index.md @@ -0,0 +1,17 @@ +# Whisper + +Bind an event to an element, and display a overlay when triggered. + +## Import + + + +## Examples + +### Overlay + + + +## Props + + diff --git a/docs/pages/components/whisper/en-US/props.md b/docs/pages/components/whisper/en-US/props.md new file mode 100644 index 0000000000..baea76d0f5 --- /dev/null +++ b/docs/pages/components/whisper/en-US/props.md @@ -0,0 +1,61 @@ +### `` + +```ts +type Trigger = + | Array<'click' | 'hover' | 'focus' | 'active'> + | 'click' + | 'hover' + | 'focus' + | 'active' + | 'none'; +``` + +| Property | Type `(Default)` | Description | +| --------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| container | HTMLElement | (() => HTMLElement) | Sets the rendering container | +| delay | number | Delay time (ms) Time | +| delayClose | number | Delay close time (ms) Time | +| delayOpen | number | Delay open time (ms) Time | +| enterable | boolean | Whether mouse is allowed to enter the floating layer of popover,when the value of `trigger` is set to`hover` | +| full | boolean | The content full the container | +| onBlur | () => void | Lose Focus callback function | +| onClick | () => void | Click on the callback function | +| onClose | () => void | Callback fired when close component | +| onEnter | () => void | Callback fired before the overlay transitions in | +| onEntered | () => void | Callback fired after the overlay finishes transitioning in | +| onEntering | () => void | Callback fired as the overlay begins to transition in | +| onExit | () => void | Callback fired right before the overlay transitions out | +| onExited | () => void | Callback fired after the overlay finishes transitioning out | +| onExiting | () => void | Callback fired as the overlay begins to transition out | +| onFocus | () => void | Callback function to get focus | +| onOpen | () => void | Callback fired when open component | +| placement | Placement `('right')` | Dispaly placement | +| preventOverflow | boolean | Prevent floating element overflow | +| speaker \* | Tooltip |Popover | ReactElement | Displayed component | +| trigger | Trigger `(['hover','focus'])` | Triggering events | + +### Whisper methods + +- open + +Open a overlay. + +```ts +open: (delay?: number) => void +``` + +- close + +Close a overlay. + +```ts +close: (delay?: number) => void +``` + +Update overlay position + +- updatePosition + +```ts +updatePosition: () => void +``` diff --git a/docs/pages/components/whisper/fragments/import.md b/docs/pages/components/whisper/fragments/import.md new file mode 100644 index 0000000000..ea9aa4f4a0 --- /dev/null +++ b/docs/pages/components/whisper/fragments/import.md @@ -0,0 +1,6 @@ +```js +import { Whisper } from 'rsuite'; + +// or +import Whisper from 'rsuite/lib/Whisper'; +``` diff --git a/docs/pages/components/popover/en-US/overlay.md b/docs/pages/components/whisper/fragments/overlay.md similarity index 98% rename from docs/pages/components/popover/en-US/overlay.md rename to docs/pages/components/whisper/fragments/overlay.md index da0984cfbd..93dcbe6426 100644 --- a/docs/pages/components/popover/en-US/overlay.md +++ b/docs/pages/components/whisper/fragments/overlay.md @@ -1,5 +1,3 @@ -### Overlay - ```js diff --git a/docs/pages/components/whisper/index.tsx b/docs/pages/components/whisper/index.tsx new file mode 100644 index 0000000000..7823975a48 --- /dev/null +++ b/docs/pages/components/whisper/index.tsx @@ -0,0 +1,7 @@ +import * as React from 'react'; +import { ButtonToolbar, Button, Whisper } from 'rsuite'; +import DefaultPage from '@/components/Page'; + +export default function Page() { + return ; +} diff --git a/docs/pages/components/whisper/zh-CN/index.md b/docs/pages/components/whisper/zh-CN/index.md new file mode 100644 index 0000000000..66c59aa2c9 --- /dev/null +++ b/docs/pages/components/whisper/zh-CN/index.md @@ -0,0 +1,17 @@ +# Whisper 弹窗触发器 + +为一个元素绑定事件,触发后显示一个浮层。 + +## 获取组件 + + + +## 演示 + +### 自定义浮层 + + + +## Props + + diff --git a/docs/pages/components/whisper/zh-CN/props.md b/docs/pages/components/whisper/zh-CN/props.md new file mode 100644 index 0000000000..a343f2f221 --- /dev/null +++ b/docs/pages/components/whisper/zh-CN/props.md @@ -0,0 +1,61 @@ +### `` + +```ts +type Trigger = + | Array<'click' | 'hover' | 'focus' | 'active'> + | 'click' + | 'hover' + | 'focus' + | 'active' + | 'none'; +``` + +| 属性名称 | 类型 `(默认值)` | 描述 | +| --------------- | ----------------------------------------- | --------------------------------------------------------- | +| container | HTMLElement | (() => HTMLElement) | 设置渲染的容器 | +| delay | number | 延迟时间 (ms) | +| delayClose | number | 延迟关闭时间 (ms) | +| delayOpen | number | 延迟打开时间 (ms) | +| enterable | boolean | 当 `trigger` 值为 `hover`时候,鼠标是否可进入提示框浮层中 | +| full | boolean | 撑满容器 | +| onBlur | () => void | 失去焦点回调函数 | +| onClick | () => void | 点击的回调函数 | +| onClose | () => void | 关闭回调函数 | +| onEnter | () => void | 显示前动画过渡的回调函数 | +| onEntered | () => void | 显示后动画过渡的回调函数 | +| onEntering | () => void | 显示中动画过渡的回调函数 | +| onExit | () => void | 退出前动画过渡的回调函数 | +| onExited | () => void | 退出后动画过渡的回调函数 | +| onExiting | () => void | 退出中动画过渡的回调函数 | +| onFocus | () => void | 获取焦点的回调函数 | +| onOpen | () => void | 打开回调函数 | +| placement | Placement `('right')` | 显示位置 | +| preventOverflow | boolean | 防止浮动元素溢出 | +| speaker \* | Tooltip |Popover | ReactElement | 展示的元素 | +| trigger | Trigger `(['hover','focus'])` | 触发事件,可以通过数组配置多事件 | + +### Whisper methods + +- open + +打开一个浮层 + +```ts +open: (delay?: number) => void +``` + +- close + +关闭一个浮层 + +```ts +close: (delay?: number) => void +``` + +更新浮层位置 + +- updatePosition + +```ts +updatePosition: () => void +``` diff --git a/docs/utils/component.config.json b/docs/utils/component.config.json index 6e71107638..29f61ef1ae 100644 --- a/docs/utils/component.config.json +++ b/docs/utils/component.config.json @@ -132,6 +132,22 @@ "name": "Navigation", "title": "导航" }, + { + "id": "affix", + "name": "Affix", + "title": "固定位置", + "components": ["Affix"] + }, + { + "id": "breadcrumb", + "name": "Breadcrumb", + "title": "面包屑", + "components": ["Breadcrumb", "Breadcrumb.Item"], + "designHash": { + "default": "23", + "dark": "23" + } + }, { "id": "dropdown", "name": "Dropdown", @@ -192,16 +208,6 @@ "dark": "29" } }, - { - "id": "breadcrumb", - "name": "Breadcrumb", - "title": "面包屑", - "components": ["Breadcrumb", "Breadcrumb.Item"], - "designHash": { - "default": "23", - "dark": "23" - } - }, { "group": true, "id": "layout", @@ -540,7 +546,7 @@ "dark": "72" } }, - + { "group": true, "id": "utils", @@ -553,11 +559,12 @@ "title": "数据模型", "apis": ["Schema.Model", "Schema.Types"] }, + { - "id": "affix", - "name": "Affix", - "title": "固定位置", - "components": ["Affix"] + "id": "whisper", + "name": "Whisper", + "title": "弹窗触发器", + "components": ["Whisper"] }, { "id": "animation", @@ -571,12 +578,6 @@ "Animation.Collapse" ] }, - { - "id": "portal", - "name": "Portal", - "title": "入口", - "components": ["Portal"] - }, { "id": "dom-helper", "name": "DOMHelper", diff --git a/package.json b/package.json index 108ea2a125..e3e393b953 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ ], "dependencies": { "@babel/runtime": "^7.8.4", + "@types/react-is": "^16.7.1", "classnames": ">=2.0.0", "date-fns": "^2.13.0", "date-fns-timezone": "^0.1.4", @@ -62,6 +63,7 @@ "element-resize-event": "^3.0.3", "lodash": "^4.17.11", "prop-types": "^15.7.2", + "react-is": "^16.13.1", "react-lifecycles-compat": "^3.0.4", "react-virtualized": "^9.21.0", "recompose": "^0.30.0", diff --git a/src/@types/common.ts b/src/@types/common.ts index 704317a5ab..089135d577 100644 --- a/src/@types/common.ts +++ b/src/@types/common.ts @@ -34,22 +34,22 @@ export interface RsRefForwardingComponent void; + onEnter?: (node?: null | Element | Text) => void; /** Callback fired as the Modal begins to transition in */ - onEntering?: (node: null | Element | Text) => void; + onEntering?: (node?: null | Element | Text) => void; /** Callback fired after the Modal finishes transitioning in */ - onEntered?: (node: null | Element | Text) => void; + onEntered?: (node?: null | Element | Text) => void; /** Callback fired right before the Modal transitions out */ - onExit?: (node: null | Element | Text) => void; + onExit?: (node?: null | Element | Text) => void; /** Callback fired as the Modal begins to transition out */ - onExiting?: (node: null | Element | Text) => void; + onExiting?: (node?: null | Element | Text) => void; /** Callback fired after the Modal finishes transitioning out */ - onExited?: (node: null | Element | Text) => void; + onExited?: (node?: null | Element | Text) => void; } export interface PickerBaseProps extends WithAsProps, AnimationEventProps { diff --git a/src/Icon/Icon.d.ts b/src/@types/icons.ts similarity index 93% rename from src/Icon/Icon.d.ts rename to src/@types/icons.ts index cbc3f1131e..399343ee2f 100644 --- a/src/Icon/Icon.d.ts +++ b/src/@types/icons.ts @@ -1,7 +1,3 @@ -import * as React from 'react'; - -import { SVGIcon, StandardProps } from '../@types/common'; - export type IconNames = | '500px' | 'address-book' @@ -802,43 +798,3 @@ export type IconNames = | 'youtube' | 'youtube-play' | 'youtube-square'; - -export interface IconProps extends StandardProps { - /** You can use a custom element for this component */ - as?: React.ElementType; - - /** Icon name */ - icon: IconNames | SVGIcon; - - /** Sets the icon size */ - size?: 'lg' | '2x' | '3x' | '4x' | '5x'; - - /** Flip the icon */ - flip?: 'horizontal' | 'vertical'; - - /** Combine multiple icons */ - stack?: '1x' | '2x'; - - /** Rotate the icon */ - rotate?: number; - - /** Fixed icon width because there are many icons with uneven size */ - fixedWidth?: boolean; - - /** Set SVG style when using custom SVG Icon */ - svgStyle?: React.CSSProperties; - - /** Dynamic rotation icon */ - - spin?: boolean; - - /** Use pulse to have it rotate with 8 steps */ - pulse?: boolean; - - /** Inverse color */ - inverse?: boolean; -} - -declare const Icon: React.ComponentType; - -export default Icon; diff --git a/src/Animation/Transition.tsx b/src/Animation/Transition.tsx index be942a7c8c..e284a75d3d 100644 --- a/src/Animation/Transition.tsx +++ b/src/Animation/Transition.tsx @@ -6,7 +6,7 @@ import isFunction from 'lodash/isFunction'; import omit from 'lodash/omit'; import getDOMNode from '../utils/getDOMNode'; import { AnimationEventProps } from '../@types/common'; -import { getAnimationEnd, getAnimationPropTypes } from './utils'; +import { getAnimationEnd, animationPropTypes } from './utils'; export enum STATUS { UNMOUNTED = 0, @@ -55,7 +55,7 @@ interface TransitionState { } export const transitionPropTypes = { - ...getAnimationPropTypes(), + ...animationPropTypes, animation: PropTypes.bool, children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), className: PropTypes.string, @@ -303,6 +303,7 @@ class Transition extends React.Component { return React.cloneElement(child, { ...childProps, + ref: this.childRef, className: classNames(className, child.props?.className, transitionClassName) }); } diff --git a/src/Animation/utils.ts b/src/Animation/utils.ts index 4b22464759..01acce1dac 100644 --- a/src/Animation/utils.ts +++ b/src/Animation/utils.ts @@ -9,13 +9,11 @@ export function getAnimationEnd() { return 'animationend'; } -export function getAnimationPropTypes() { - return { - onEnter: PropTypes.func, - onEntering: PropTypes.func, - onEntered: PropTypes.func, - onExit: PropTypes.func, - onExiting: PropTypes.func, - onExited: PropTypes.func - }; -} +export const animationPropTypes = { + onEnter: PropTypes.func, + onEntering: PropTypes.func, + onEntered: PropTypes.func, + onExit: PropTypes.func, + onExiting: PropTypes.func, + onExited: PropTypes.func +}; diff --git a/src/AutoComplete/AutoComplete.tsx b/src/AutoComplete/AutoComplete.tsx index 4f661ed602..fd5daaaa6d 100644 --- a/src/AutoComplete/AutoComplete.tsx +++ b/src/AutoComplete/AutoComplete.tsx @@ -2,7 +2,7 @@ import React, { useState, useImperativeHandle, useRef, useCallback } from 'react import PropTypes from 'prop-types'; import pick from 'lodash/pick'; import Input from '../Input'; -import { refType, useClassNames, useControlled } from '../utils'; +import { refType, useClassNames, useControlled, PLACEMENT, mergeRefs } from '../utils'; import { PickerToggleTrigger, onMenuKeyDown, @@ -11,9 +11,8 @@ import { MenuWrapper, useFocusItemValue } from '../Picker'; -import { pickerToggleTriggerProps } from '../Picker/PickerToggleTrigger'; -import { PLACEMENT } from '../constants'; -import { getAnimationPropTypes } from '../Animation/utils'; +import { pickerToggleTriggerProps, PositionChildProps } from '../Picker/PickerToggleTrigger'; +import { animationPropTypes } from '../Animation/utils'; import { WithAsProps, RsRefForwardingComponent, @@ -203,13 +202,13 @@ const AutoComplete: AutoCompleteComponent = React.forwardRef((props: AutoComplet } handleClose(); }, - [handleSelect, handleChangeValue, handleClose, setFocusItemValue, value] + [value, setValue, handleSelect, handleChangeValue, handleClose, setFocusItemValue] ); const handleInputFocus = useCallback( (event: React.SyntheticEvent) => { - handleOpen(); onFocus?.(event); + handleOpen(); }, [onFocus, handleOpen] ); @@ -235,6 +234,32 @@ const AutoComplete: AutoCompleteComponent = React.forwardRef((props: AutoComplet close: handleClose })); + const renderDropdownMenu = (positionProps: PositionChildProps, speakerRef) => { + const { left, top, className } = positionProps; + const styles = { left, top }; + + return ( + + + + ); + }; + return ( - - - } + speaker={renderDropdownMenu} > { assert.ok(instance.querySelector('input')); }); - it('Should render 2 `listitem` when set `open` and `defaultValue`', () => { + it('Should render 2 `menuitemradio` when set `open` and `defaultValue`', () => { const instance = getInstance(); - assert.equal(instance.menu.querySelectorAll('[role="listitem"]').length, 2); + assert.equal(instance.menu.querySelectorAll('[role="menuitemradio"]').length, 2); }); it('Should be a `top-end` for placement', () => { @@ -217,7 +217,7 @@ describe('AutoComplete', () => { const instance = getInstance( ); - assert.include(instance.menu.querySelector('[role="list"]').className, 'custom'); + assert.include(instance.menu.querySelector('[role="menu"]').className, 'custom'); }); it('Should have a custom style', () => { @@ -236,13 +236,13 @@ describe('AutoComplete', () => { true} /> ); - assert.equal(instance1.menu.querySelectorAll('[role="listitem"]').length, 3); + assert.equal(instance1.menu.querySelectorAll('[role="menuitemradio"]').length, 3); const instance2 = getInstance( false} /> ); - assert.equal(instance2.menu.querySelectorAll('[role="listitem"]').length, 0); + assert.equal(instance2.menu.querySelectorAll('[role="menuitemradio"]').length, 0); const instance3 = getInstance( { /> ); - assert.equal(instance3.menu.querySelectorAll('[role="listitem"]').length, 3); + assert.equal(instance3.menu.querySelectorAll('[role="menuitemradio"]').length, 3); const instance4 = getInstance( { /> ); - assert.equal(instance4.menu.querySelectorAll('[role="listitem"]').length, 1); + assert.equal(instance4.menu.querySelectorAll('[role="menuitemradio"]').length, 1); }); describe('ref testing', () => { diff --git a/src/Breadcrumb/test/BreadcrumbItemStylesSpec.js b/src/Breadcrumb/test/BreadcrumbItemStylesSpec.js index 7ba4bf0912..5b60a1ed36 100644 --- a/src/Breadcrumb/test/BreadcrumbItemStylesSpec.js +++ b/src/Breadcrumb/test/BreadcrumbItemStylesSpec.js @@ -1,7 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import Breadcrumb from '../index'; -import { createTestContainer, getDOMNode, getStyle, toRGB, inChrome } from '@test/testUtils'; +import { createTestContainer, getStyle, toRGB, inChrome } from '@test/testUtils'; import '../styles/index'; diff --git a/src/CheckPicker/CheckPicker.tsx b/src/CheckPicker/CheckPicker.tsx index b690fcc92c..17b0cd0bee 100644 --- a/src/CheckPicker/CheckPicker.tsx +++ b/src/CheckPicker/CheckPicker.tsx @@ -13,7 +13,9 @@ import { useClassNames, shallowEqual, useCustom, - useControlled + useControlled, + KEY_CODE, + mergeRefs } from '../utils'; import { DropdownMenu, @@ -29,11 +31,14 @@ import { useSearch } from '../Picker'; import { PickerLocaleType, PickerComponent } from '../Picker/types'; -import { pickerToggleTriggerProps } from '../Picker/PickerToggleTrigger'; +import { + pickerToggleTriggerProps, + OverlayTriggerInstance, + PositionChildProps +} from '../Picker/PickerToggleTrigger'; import { ItemDataType, FormControlPickerProps } from '../@types/common'; import { listPickerPropTypes } from '../Picker/propTypes'; import { SelectProps } from '../SelectPicker'; -import { KEY_CODE } from '../constants'; export type ValueType = (number | string)[]; export interface CheckPickerProps @@ -111,8 +116,7 @@ const CheckPicker: PickerComponent = React.forwardRef( ...rest } = props; - const rootRef = useRef(); - const triggerRef = useRef(); + const triggerRef = useRef(); const positionRef = useRef(); const toggleRef = useRef(); const menuRef = useRef(); @@ -172,12 +176,12 @@ const CheckPicker: PickerComponent = React.forwardRef( }; const handleClose = useCallback(() => { - triggerRef.current?.hide?.(); + triggerRef.current?.close(); setFocusItemValue(value ? value[0] : undefined); }, [triggerRef, setFocusItemValue, value]); const handleOpen = useCallback(() => { - triggerRef.current?.show?.(); + triggerRef.current?.open(); }, [triggerRef]); const handleToggleDropdown = () => { @@ -203,7 +207,7 @@ const CheckPicker: PickerComponent = React.forwardRef( setValue([]); handleChangeValue([], event); }, - [disabled, cleanable, handleChangeValue] + [disabled, cleanable, setValue, handleChangeValue] ); const selectFocusMenuItem = (event: React.KeyboardEvent) => { @@ -269,7 +273,7 @@ const CheckPicker: PickerComponent = React.forwardRef( handleSelect(nextValue, item, event); handleChangeValue(nextValue, event); }, - [value, handleSelect, handleChangeValue, setFocusItemValue] + [value, setValue, handleSelect, handleChangeValue, setFocusItemValue] ); const handleExited = useCallback(() => { @@ -285,7 +289,9 @@ const CheckPicker: PickerComponent = React.forwardRef( }, [onOpen]); useImperativeHandle(ref, () => ({ - root: rootRef.current, + get root() { + return triggerRef.current.child; + }, get menu() { return menuRef.current; }, @@ -325,8 +331,10 @@ const CheckPicker: PickerComponent = React.forwardRef( selectedElement = renderValue(value, selectedItems, selectedElement); } - const renderDropdownMenu = () => { - const classes = merge(prefix('check-menu'), menuClassName); + const renderDropdownMenu = (positionProps: PositionChildProps, speakerRef) => { + const { left, top, className } = positionProps; + const classes = merge(className, menuClassName, prefix('check-menu')); + const styles = { ...menuStyle, left, top }; let items = filteredData; let filteredStickyItems = []; @@ -370,10 +378,10 @@ const CheckPicker: PickerComponent = React.forwardRef( return ( toggleRef.current} getPositionInstance={() => positionRef.current} @@ -402,9 +410,9 @@ const CheckPicker: PickerComponent = React.forwardRef( onEnter={createChainedFunction(initStickyItems, onEnter)} onEntered={createChainedFunction(handleEntered, onEntered)} onExited={createChainedFunction(handleExited, onExited)} - speaker={renderDropdownMenu()} + speaker={renderDropdownMenu} > - + { it('Should clean selected default value', () => { - const instance = getDOMNode(); - - ReactTestUtils.Simulate.click(instance.querySelector(cleanClassName)); - expect(instance.querySelector(placeholderClassName).innerText).to.equal('Select'); + const instance = getInstance(); + ReactTestUtils.Simulate.click(instance.root.querySelector(cleanClassName)); + expect(instance.root.querySelector(placeholderClassName).innerText).to.equal('Select'); }); it('Should not clean selected value', () => { diff --git a/src/Col/Col.tsx b/src/Col/Col.tsx index 898f7bc916..b09ef3f918 100644 --- a/src/Col/Col.tsx +++ b/src/Col/Col.tsx @@ -1,8 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import omit from 'lodash/omit'; -import { useClassNames } from '../utils'; -import { SIZE } from '../constants'; +import { useClassNames, SIZE } from '../utils'; import { WithAsProps, RsRefForwardingComponent } from '../@types/common'; export interface ColProps extends WithAsProps { diff --git a/src/DOMHelper/index.ts b/src/DOMHelper/index.ts index 0e1a6180c5..0e148eff8c 100644 --- a/src/DOMHelper/index.ts +++ b/src/DOMHelper/index.ts @@ -1,6 +1,6 @@ import * as helpers from 'dom-lib'; - -type DOMElement = Element | Window; +export * from 'dom-lib'; +type DOMElement = HTMLElement | Window | Document | Element; export interface DOMOffset { top: number; @@ -9,7 +9,7 @@ export interface DOMOffset { width: number; } -export interface DOMHelperAPI { +export interface DOMHelper { /** classes */ hasClass(node: Element, className: string): boolean; addClass(node: Element, className: string): Element; @@ -44,23 +44,38 @@ export interface DOMHelperAPI { /** query */ canUseDOM: boolean; - activeElement(): Element; + activeElement(doc?: Document): Element; getHeight(node: DOMElement, client?: DOMElement): number; getWidth(node: DOMElement, client?: DOMElement): number; getOffset(node: DOMElement): DOMOffset | DOMRect; getOffsetParent(node: Element | Document): Element; getPosition(node: Element, offsetParent?: Element): DOMOffset; getWindow(node: Element): Window; + getContainer(node: Element | (() => Element), defaultContainer: Element): Element; nodeName(node: Element): string; ownerDocument(node: Element): Document; ownerWindow(node: Element): Window; - contains(context: Element, node: Element): boolean; + contains(context: DOMElement, node: DOMElement): boolean; scrollLeft(node: DOMElement): number; scrollLef(node: DOMElement, val: number): void; scrollTop(node: DOMElement): number; scrollTop(node: DOMElement, val: number): void; + isElement(node: DOMElement): boolean; + transition(): { + end: string; + backfaceVisibility: string; + transform: string; + property: string; + timing: string; + delay: string; + duration: string; + }; } -const DOMHelper: DOMHelperAPI = helpers; - +const DOMHelper: DOMHelper = { + ...helpers, + isElement: (node: HTMLElement) => { + return node?.nodeType && typeof node?.nodeName === 'string'; + } +}; export default DOMHelper; diff --git a/src/Drawer/Drawer.d.ts b/src/Drawer/Drawer.d.ts deleted file mode 100644 index b650a76d9b..0000000000 --- a/src/Drawer/Drawer.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -import * as React from 'react'; - -import { TypeAttributes } from '../@types/common'; -import { ModalProps } from '../Modal/Modal.d'; -import { ModalBodyProps } from '../Modal/ModalBody.d'; -import { ModalHeaderProps } from '../Modal/ModalHeader.d'; -import { ModalTitleProps } from '../Modal/ModalTitle.d'; -import { ModalFooterProps } from '../Modal/ModalFooter.d'; - -declare const DrawerFooter: React.ComponentType; -declare const DrawerTitle: React.ComponentType; -declare const DrawerHeader: React.ComponentType; -declare const DrawerBody: React.ComponentType; - -export interface DrawerProps extends ModalProps { - /** The placement of Drawer */ - placement?: TypeAttributes.Placement4; -} - -interface DrawerComponent extends React.ComponentClass { - Body: typeof DrawerBody; - Header: typeof DrawerHeader; - Title: typeof DrawerTitle; - Footer: typeof DrawerFooter; -} - -declare const Drawer: DrawerComponent; - -export default Drawer; diff --git a/src/Drawer/Drawer.tsx b/src/Drawer/Drawer.tsx index 87ff9dbc5d..f4eb4f9bea 100644 --- a/src/Drawer/Drawer.tsx +++ b/src/Drawer/Drawer.tsx @@ -1,52 +1,98 @@ -import * as React from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; -import classNames from 'classnames'; import Slide from '../Animation/Slide'; -import Modal from '../Modal/Modal'; - -import { prefix, defaultProps } from '../utils'; -import { DrawerProps } from './Drawer.d'; - -class Drawer extends React.Component { - static propTypes = { - classPrefix: PropTypes.string, - placement: PropTypes.oneOf(['top', 'right', 'bottom', 'left']), - show: PropTypes.bool, - full: PropTypes.bool, - children: PropTypes.node, - className: PropTypes.string - }; - static defaultProps = { - placement: 'right' - }; +import Modal, { + ModalProps, + ModalBodyProps, + ModalHeaderProps, + ModalFooterProps, + ModalTitleProps +} from '../Modal'; +import { TypeAttributes, RsRefForwardingComponent } from '../@types/common'; +import { useClassNames } from '../utils'; - render() { - const { show, full, className, placement, classPrefix, ...props } = this.props; - const addPrefix = prefix(classPrefix); - const classes = classNames(addPrefix(placement), className, { - [addPrefix('full')]: full - }); - - const animationProps = { - placement - }; - - return ( - - ); - } +export interface DrawerProps extends ModalProps { + /** The placement of Drawer */ + placement?: TypeAttributes.Placement4; } -const EnhancedDrawer = defaultProps({ - classPrefix: 'drawer' -})(Drawer); +const defaultProps: Partial = { + classPrefix: 'drawer', + placement: 'right', + animation: Slide +}; + +const DrawerBody: RsRefForwardingComponent< + 'div', + ModalBodyProps +> = React.forwardRef((props, ref) => ); + +const DrawerHeader: RsRefForwardingComponent< + 'div', + ModalHeaderProps +> = React.forwardRef((props, ref) => ( + +)); + +const DrawerFooter: RsRefForwardingComponent< + 'div', + ModalFooterProps +> = React.forwardRef((props, ref) => ( + +)); + +const DrawerTitle: RsRefForwardingComponent< + 'div', + ModalTitleProps +> = React.forwardRef((props, ref) => ( + +)); + +interface DrawerComponent extends React.FC { + Body?: typeof DrawerBody; + Header?: typeof DrawerHeader; + Title?: typeof DrawerTitle; + Footer?: typeof DrawerFooter; +} + +const Drawer: DrawerComponent = React.forwardRef((props: DrawerProps, ref) => { + const { className, placement, classPrefix, ...rest } = props; + const { merge, prefix } = useClassNames(classPrefix); + const classes = merge(className, prefix(placement)); + + const animationProps = { + placement + }; + + return ( + + ); +}); + +DrawerBody.displayName = 'DrawerBody'; +DrawerHeader.displayName = 'DrawerHeader'; +DrawerFooter.displayName = 'DrawerFooter'; +DrawerTitle.displayName = 'DrawerTitle'; + +Drawer.Body = DrawerBody; +Drawer.Header = DrawerHeader; +Drawer.Footer = DrawerFooter; +Drawer.Title = DrawerTitle; + +Drawer.displayName = 'Drawer'; +Drawer.defaultProps = defaultProps; +Drawer.propTypes = { + classPrefix: PropTypes.string, + placement: PropTypes.oneOf(['top', 'right', 'bottom', 'left']), + children: PropTypes.node, + className: PropTypes.string +}; -export default EnhancedDrawer; +export default Drawer; diff --git a/src/Drawer/index.d.ts b/src/Drawer/index.d.ts deleted file mode 100644 index 262151cb20..0000000000 --- a/src/Drawer/index.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './Drawer'; -export * from './Drawer'; diff --git a/src/Drawer/index.tsx b/src/Drawer/index.tsx index dab97a9abe..e331de3352 100644 --- a/src/Drawer/index.tsx +++ b/src/Drawer/index.tsx @@ -1,18 +1,3 @@ -import setStatic from 'recompose/setStatic'; -import setDisplayName from 'recompose/setDisplayName'; - -import { defaultProps } from '../utils'; import Drawer from './Drawer'; -import ModalBody from '../Modal/ModalBody'; -import ModalHeader from '../Modal/ModalHeader'; -import ModalTitle from '../Modal/ModalTitle'; -import ModalFooter from '../Modal/ModalFooter'; - -const EnhancedBody = defaultProps({ classPrefix: 'drawer-body' })(ModalBody); - -setStatic('Body', setDisplayName('Body')(EnhancedBody))(Drawer); -setStatic('Header', defaultProps({ classPrefix: 'drawer-header' })(ModalHeader))(Drawer); -setStatic('Title', defaultProps({ classPrefix: 'drawer-title' })(ModalTitle))(Drawer); -setStatic('Footer', defaultProps({ classPrefix: 'drawer-footer' })(ModalFooter))(Drawer); - +export type { DrawerProps } from './Drawer'; export default Drawer; diff --git a/src/Drawer/styles/common.less b/src/Drawer/styles/common.less index 67941a2bb8..d4d8a0d466 100644 --- a/src/Drawer/styles/common.less +++ b/src/Drawer/styles/common.less @@ -13,6 +13,7 @@ overflow: hidden; position: fixed; z-index: @zindex-drawer; + box-shadow: @drawer-box-shadow; // Prevent Chrome on Windows from adding a focus outline. For details, see // https://github.com/twbs/bootstrap/pull/10951. outline: 0; @@ -110,9 +111,14 @@ } .@{ns}drawer-dialog { + transition: transform 0.3s ease-out; position: relative; width: 100%; height: 100%; + + .@{ns}drawer-shake & { + transform: translate(0, -10px); + } } .@{ns}drawer-content { @@ -121,7 +127,6 @@ outline: 0; width: 100%; height: 100%; - box-shadow: @drawer-box-shadow; } .@{ns}drawer-backdrop { diff --git a/src/Drawer/test/DrawerSpec.js b/src/Drawer/test/DrawerSpec.js index a53bef154b..b76a09de81 100644 --- a/src/Drawer/test/DrawerSpec.js +++ b/src/Drawer/test/DrawerSpec.js @@ -6,7 +6,7 @@ import Drawer from '../Drawer'; describe('Drawer', () => { it('Should render a drawer', () => { const instance = getDOMNode( - +

message

); @@ -15,7 +15,7 @@ describe('Drawer', () => { it('Should be full', () => { const instance = getDOMNode( - +

message

); @@ -24,7 +24,7 @@ describe('Drawer', () => { it('Should have a `top` className for placement', () => { const instance = getDOMNode( - +

message

); @@ -32,18 +32,18 @@ describe('Drawer', () => { }); it('Should have a custom className', () => { - const instance = getDOMNode(); + const instance = getDOMNode(); assert.ok(instance.querySelector('.rs-drawer.custom')); }); it('Should have a custom style', () => { const fontSize = '12px'; - const instance = getDOMNode(); + const instance = getDOMNode(); assert.equal(instance.querySelector('.rs-drawer').style.fontSize, fontSize); }); it('Should have a custom className prefix', () => { - const instance = getDOMNode(); + const instance = getDOMNode(); assert.ok(instance.querySelector('.fade').className.match(/\bcustom-prefix\b/)); }); }); diff --git a/src/Drawer/test/DrawerStylesSpec.js b/src/Drawer/test/DrawerStylesSpec.js index f31d277922..78e88b704e 100644 --- a/src/Drawer/test/DrawerStylesSpec.js +++ b/src/Drawer/test/DrawerStylesSpec.js @@ -1,26 +1,22 @@ import React from 'react'; import ReactDOM from 'react-dom'; import Drawer from '../index'; -import { createTestContainer, getStyle, getDOMNode, toRGB } from '@test/testUtils'; +import { createTestContainer, getStyle, toRGB } from '@test/testUtils'; import '../styles/index'; describe('Drawer styles', () => { it('Should render the correct styles', () => { const instanceRef = React.createRef(); - ReactDOM.render(, createTestContainer()); - const dom = getDOMNode(instanceRef.current); + ReactDOM.render(, createTestContainer()); + const dom = instanceRef.current; const backdropDom = dom.querySelector('.rs-drawer-backdrop'); const drawerDom = dom.querySelector('.rs-drawer'); - assert.equal(getStyle(dom, 'position'), 'fixed', 'Drawer wrapper position'); - assert.equal(getStyle(dom, 'zIndex'), '1050', 'Drawer wrapper z-index'); - assert.equal(getStyle(drawerDom, 'position'), 'fixed', 'Drawer position'); - assert.equal(getStyle(drawerDom, 'zIndex'), '1050', 'Drawer z-index'); - assert.equal(getStyle(drawerDom, 'overflow'), 'visible', 'Drawer visible'); - assert.equal( - getStyle(backdropDom, 'backgroundColor'), - toRGB('#272c36'), - 'Drawer backDrop background-color' - ); + assert.equal(getStyle(dom, 'position'), 'fixed'); + assert.equal(getStyle(dom, 'zIndex'), '1050'); + assert.equal(getStyle(drawerDom, 'position'), 'fixed'); + assert.equal(getStyle(drawerDom, 'zIndex'), '1050'); + assert.equal(getStyle(drawerDom, 'overflow'), 'visible'); + assert.equal(getStyle(backdropDom, 'backgroundColor'), toRGB('#272c36')); }); }); diff --git a/src/Dropdown/Dropdown.d.ts b/src/Dropdown/Dropdown.d.ts deleted file mode 100644 index 24e04066dc..0000000000 --- a/src/Dropdown/Dropdown.d.ts +++ /dev/null @@ -1,79 +0,0 @@ -import * as React from 'react'; - -import { TypeAttributes, StandardProps } from '../@types/common'; -import { IconProps } from '../Icon/Icon.d'; -import DropdownMenu from './DropdownMenu'; -import DropdownMenuItem from './DropdownMenuItem'; - -export type DropdownTrigger = 'click' | 'hover' | 'contextMenu'; - -export interface DropdownProps extends StandardProps { - /** Primary content */ - children?: React.ReactNode; - - /** Define the title as a submenu */ - title?: React.ReactNode; - - /** Set the icon */ - icon?: React.ReactElement; - - /** The option to activate the state, corresponding to the eventkey in the Dropdown.item */ - activeKey?: T; - - /** Triggering events */ - trigger?: DropdownTrigger | DropdownTrigger[]; - - /** The placement of Menu */ - placement?: TypeAttributes.Placement8; - - /** Whether or not component is disabled */ - disabled?: boolean; - - /** The callback function that the menu closes */ - onClose?: () => void; - - /** Menu Pop-up callback function */ - onOpen?: () => void; - - /** Callback function for menu state switching */ - onToggle?: (open?: boolean) => void; - - /** Selected callback function */ - onSelect?: (eventKey: T, event: React.MouseEvent) => void; - - /** The style of the menu */ - menuStyle?: React.CSSProperties; - - /** A css class to apply to the Toggle DOM node */ - toggleClassName?: string; - - /** Custom title */ - renderTitle?: (children?: React.ReactNode) => React.ReactNode; - - /** The value of the current option */ - eventKey?: T; - - /** You can use a custom element type for this component */ - as?: React.ElementType; - - /** You can use a custom element type for this toggle component */ - toggleAs?: React.ElementType; - - /** No caret variation */ - noCaret?: boolean; - - /** Open the menu and control it */ - open?: boolean; - - /** Whether Dropdown menu shows header */ - showHeader?: boolean; -} - -interface DropdownComponent extends React.ComponentClass { - Menu: typeof DropdownMenu; - Item: typeof DropdownMenuItem; -} - -declare const Dropdown: DropdownComponent; - -export default Dropdown; diff --git a/src/Dropdown/Dropdown.tsx b/src/Dropdown/Dropdown.tsx index 23ba7e61eb..b9e38ddf5f 100644 --- a/src/Dropdown/Dropdown.tsx +++ b/src/Dropdown/Dropdown.tsx @@ -1,261 +1,313 @@ -import * as React from 'react'; +import React, { useRef, useContext, useCallback } from 'react'; import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import _ from 'lodash'; -import setStatic from 'recompose/setStatic'; -import RootCloseWrapper from '../Overlay/RootCloseWrapper'; -import shallowEqual from '../utils/shallowEqual'; - +import kebabCase from 'lodash/kebabCase'; import DropdownToggle from './DropdownToggle'; import DropdownMenu from './DropdownMenu'; import DropdownMenuItem from './DropdownMenuItem'; import { + shallowEqual, createChainedFunction, - prefix, isOneOf, - getUnhandledProps, - defaultProps, - placementPolyfill + useClassNames, + placementPolyfill, + PLACEMENT_8, + useRootClose, + useControlled } from '../utils'; -import { SidenavContext } from '../Sidenav/Sidenav'; -import { PLACEMENT_8 } from '../constants'; -import { DropdownProps } from './Dropdown.d'; +import { SidenavContext, SidenavContextType } from '../Sidenav/Sidenav'; +import { TypeAttributes, WithAsProps, RsRefForwardingComponent } from '../@types/common'; +import { IconProps } from '../Icon'; + +export type DropdownTrigger = 'click' | 'hover' | 'contextMenu'; +export interface DropdownProps + extends WithAsProps, + Omit, 'onSelect' | 'title'> { + /** Define the title as a submenu */ + title?: React.ReactNode; + + /** Set the icon */ + icon?: React.ReactElement; + + /** The option to activate the state, corresponding to the eventkey in the Dropdown.item */ + activeKey?: T; + + /** Triggering events */ + trigger?: DropdownTrigger | DropdownTrigger[]; + + /** The placement of Menu */ + placement?: TypeAttributes.Placement8; + + /** Whether or not component is disabled */ + disabled?: boolean; + + /** The style of the menu */ + menuStyle?: React.CSSProperties; + + /** A css class to apply to the Toggle DOM node */ + toggleClassName?: string; + + /** The value of the current option */ + eventKey?: T; + + /** You can use a custom element type for this toggle component */ + toggleAs?: React.ElementType; + + /** No caret variation */ + noCaret?: boolean; -interface DropdownState { + /** Open the menu and control it */ open?: boolean; + + /** Whether Dropdown menu shows header */ + showHeader?: boolean; + + /** Custom title */ + renderTitle?: (children?: React.ReactNode) => React.ReactNode; + + /** The callback function that the menu closes */ + onClose?: () => void; + + /** Menu Pop-up callback function */ + onOpen?: () => void; + + /** Callback function for menu state switching */ + onToggle?: (open?: boolean) => void; + + /** Selected callback function */ + onSelect?: (eventKey: T, event: React.MouseEvent) => void; } -interface SidenavContextType { - openKeys: any[]; - sidenav: boolean; - expanded: boolean; +export interface DropdownComponent extends RsRefForwardingComponent<'div', DropdownProps> { + Item?: typeof DropdownMenuItem; + Menu?: typeof DropdownMenu; } -class Dropdown extends React.Component { - static displayName = 'Dropdown'; - static contextType = SidenavContext; - static propTypes = { - activeKey: PropTypes.any, - classPrefix: PropTypes.string, - trigger: PropTypes.oneOfType([ - PropTypes.array, - PropTypes.oneOf(['click', 'hover', 'contextMenu']) - ]), - placement: PropTypes.oneOf(PLACEMENT_8), - title: PropTypes.node, - disabled: PropTypes.bool, - icon: PropTypes.node, - menuStyle: PropTypes.object, - className: PropTypes.string, - toggleClassName: PropTypes.string, - children: PropTypes.node, - tabIndex: PropTypes.number, - open: PropTypes.bool, - eventKey: PropTypes.any, - as: PropTypes.elementType, - toggleAs: PropTypes.elementType, - noCaret: PropTypes.bool, - showHeader: PropTypes.bool, - style: PropTypes.object, - onClose: PropTypes.func, - onOpen: PropTypes.func, - onToggle: PropTypes.func, - onSelect: PropTypes.func, - onMouseEnter: PropTypes.func, - onMouseLeave: PropTypes.func, - onContextMenu: PropTypes.func, - onClick: PropTypes.func, - renderTitle: PropTypes.func - }; - static defaultProps = { - placement: 'bottomStart', - trigger: 'click', - tabIndex: 0 - }; +const defaultProps: Partial = { + as: 'div', + classPrefix: 'dropdown', + placement: 'bottomStart', + trigger: 'click', + tabIndex: 0 +}; - constructor(props: DropdownProps) { - super(props); - this.state = { - open: props.open - }; - } +const Dropdown: DropdownComponent = React.forwardRef((props: DropdownProps, ref) => { + const { + as: Component, + title, + children, + className, + menuStyle, + disabled, + renderTitle, + classPrefix, + placement, + activeKey, + tabIndex, + toggleClassName, + trigger, + icon, + eventKey, + toggleAs, + noCaret, + style, + open: openProp, + showHeader, + onClick, + onMouseEnter, + onMouseLeave, + onContextMenu, + onSelect, + onOpen, + onClose, + onToggle, + ...rest + } = props; - getOpen() { - const { open } = this.props; - if (_.isUndefined(open)) { - return this.state.open; - } - return open; - } + const { onOpenChange, openKeys = [], sidenav, expanded } = + useContext(SidenavContext) || {}; + const overlayTarget = useRef(); + const triggerTarget = useRef(); + const [open, setOpen] = useControlled(openProp, false); + const menuExpanded = openKeys.some(key => shallowEqual(key, eventKey)); + const { merge, withClassPrefix, prefix } = useClassNames(classPrefix); + const collapsible = sidenav && expanded; - toggle = (isOpen?: boolean) => { - const { onOpen, onClose, onToggle } = this.props; - const open = _.isUndefined(isOpen) ? !this.getOpen() : isOpen; - const handleToggle = open ? onOpen : onClose; + const handleToggle = useCallback( + (isOpen?: boolean) => { + const nextOpen = typeof isOpen === 'undefined' ? !open : isOpen; + const fn = nextOpen ? onOpen : onClose; - this.setState({ open }); - handleToggle?.(); - onToggle?.(open); - }; + fn?.(); + setOpen(nextOpen); + onToggle?.(nextOpen); + }, + [onClose, onOpen, onToggle, open, setOpen] + ); - handleClick = (event: React.SyntheticEvent) => { - event.preventDefault(); - if (this.props.disabled) { - return; - } - this.toggle(); - }; + const handleOpenChange = useCallback( + (event: React.MouseEvent) => { + onOpenChange?.(eventKey, event); + }, + [eventKey, onOpenChange] + ); - handleOpenChange = (event: React.SyntheticEvent) => { - const { eventKey } = this.props; - this.context?.onOpenChange?.(eventKey, event); - }; + const handleToggleChange = useCallback( + (eventKey: any, event: React.SyntheticEvent) => { + onOpenChange?.(eventKey, event); + }, + [onOpenChange] + ); - handleToggleChange = (eventKey: any, event: React.SyntheticEvent) => { - this.context?.onOpenChange?.(eventKey, event); - }; + const handleClick = useCallback( + (event: React.MouseEvent) => { + event.preventDefault(); + if (disabled) { + return; + } + handleToggle(); + }, + [disabled, handleToggle] + ); - handleMouseEnter = () => { - if (!this.props.disabled) { - this.toggle(true); + const handleMouseEnter = useCallback(() => { + if (!disabled) { + handleToggle(true); } - }; + }, [disabled, handleToggle]); - handleMouseLeave = () => { - if (!this.props.disabled) { - this.toggle(false); + const handleMouseLeave = useCallback(() => { + if (!disabled) { + handleToggle(false); } - }; + }, [disabled, handleToggle]); - handleSelect = (eventKey: any, event: React.MouseEvent) => { - this.props.onSelect?.(eventKey, event); - this.toggle(false); + const handleSelect = (eventKey: any, event: React.MouseEvent) => { + onSelect?.(eventKey, event); + handleToggle(false); }; - render() { - const { - title, - children, - className, - menuStyle, - disabled, - renderTitle, - classPrefix, - placement, - activeKey, - tabIndex, - toggleClassName, - trigger, - icon, - onClick, - onMouseEnter, - onMouseLeave, - onContextMenu, - eventKey, - as: Component, - toggleAs, - noCaret, - style, - showHeader, - ...props - } = this.props; - - const { openKeys = [], sidenav, expanded }: SidenavContextType = this.context || {}; - const menuExpanded = openKeys.some(key => shallowEqual(key, eventKey)); - const addPrefix = prefix(classPrefix); - const open = this.getOpen(); - const collapsible = sidenav && expanded; - const unhandled = getUnhandledProps(Dropdown, props); - const toggleProps = { - ...unhandled, - onClick: createChainedFunction(this.handleOpenChange, onClick), - onContextMenu - }; - - const dropdownProps = { - onMouseEnter, - onMouseLeave - }; - - /** - * Bind event of trigger, - * not used in in the expanded state of '' - */ - if (!collapsible) { - if (isOneOf('click', trigger)) { - toggleProps.onClick = createChainedFunction(this.handleClick, toggleProps.onClick); - } + useRootClose(() => handleToggle(), { + triggerTarget, + overlayTarget, + disabled: !open + }); - if (isOneOf('contextMenu', trigger)) { - toggleProps.onContextMenu = createChainedFunction(this.handleClick, onContextMenu); - } + const toggleProps = { + onClick: createChainedFunction(handleOpenChange, onClick), + onContextMenu + }; - if (isOneOf('hover', trigger)) { - dropdownProps.onMouseEnter = createChainedFunction(this.handleMouseEnter, onMouseEnter); - dropdownProps.onMouseLeave = createChainedFunction(this.handleMouseLeave, onMouseLeave); - } + const dropdownProps = { + onMouseEnter, + onMouseLeave + }; + + /** + * Bind event of trigger, + * not used in in the expanded state of '' + */ + if (!collapsible) { + if (isOneOf('click', trigger)) { + toggleProps.onClick = createChainedFunction(handleClick, toggleProps.onClick); } - const menuProps = { - collapsible, - activeKey, - openKeys, - expanded: menuExpanded, - style: menuStyle, - onSelect: this.handleSelect, - onToggle: this.handleToggleChange - }; - let menu = {children}; - - if (open) { - menu = ( - - {(props, ref) => ( - - {showHeader &&
  • {title}
  • } - {children} -
    - )} -
    - ); + + if (isOneOf('contextMenu', trigger)) { + toggleProps.onContextMenu = createChainedFunction(handleClick, onContextMenu); } - const toggle = ( - - {title} - - ); - - const classes = classNames(classPrefix, className, { - [addPrefix(`placement-${_.kebabCase(placementPolyfill(placement))}`)]: placement, - [addPrefix('disabled')]: disabled, - [addPrefix('no-caret')]: noCaret, - [addPrefix('open')]: open, - [addPrefix(menuExpanded ? 'expand' : 'collapse')]: sidenav - }); - - return ( - - {menu} - {toggle} - - ); + if (isOneOf('hover', trigger)) { + dropdownProps.onMouseEnter = createChainedFunction(handleMouseEnter, onMouseEnter); + dropdownProps.onMouseLeave = createChainedFunction(handleMouseLeave, onMouseLeave); + } } -} + const menuElement = ( + + {showHeader &&
  • {title}
  • } + {children} +
    + ); -const EnhancedDropdown = defaultProps({ - as: 'div', - classPrefix: 'dropdown' -})(Dropdown); + const toggleElement = ( + + {title} + + ); + + const classes = merge( + className, + withClassPrefix({ + [`placement-${kebabCase(placementPolyfill(placement))}`]: placement, + [menuExpanded ? 'expand' : 'collapse']: sidenav, + disabled, + open, + 'no-caret': noCaret + }) + ); + + return ( + + {menuElement} + {toggleElement} + + ); +}); + +Dropdown.Item = DropdownMenuItem; +Dropdown.Menu = DropdownMenu; -setStatic('Item', DropdownMenuItem)(EnhancedDropdown); -setStatic('Menu', DropdownMenu)(EnhancedDropdown); +Dropdown.displayName = 'Dropdown'; +Dropdown.defaultProps = defaultProps; +Dropdown.propTypes = { + activeKey: PropTypes.any, + classPrefix: PropTypes.string, + trigger: PropTypes.oneOfType([ + PropTypes.array, + PropTypes.oneOf(['click', 'hover', 'contextMenu']) + ]), + placement: PropTypes.oneOf(PLACEMENT_8), + title: PropTypes.node, + disabled: PropTypes.bool, + icon: PropTypes.node, + menuStyle: PropTypes.object, + className: PropTypes.string, + toggleClassName: PropTypes.string, + children: PropTypes.node, + tabIndex: PropTypes.number, + open: PropTypes.bool, + eventKey: PropTypes.any, + as: PropTypes.elementType, + toggleAs: PropTypes.elementType, + noCaret: PropTypes.bool, + showHeader: PropTypes.bool, + style: PropTypes.object, + onClose: PropTypes.func, + onOpen: PropTypes.func, + onToggle: PropTypes.func, + onSelect: PropTypes.func, + onMouseEnter: PropTypes.func, + onMouseLeave: PropTypes.func, + onContextMenu: PropTypes.func, + onClick: PropTypes.func, + renderTitle: PropTypes.func +}; -export default EnhancedDropdown; +export default Dropdown; diff --git a/src/Dropdown/DropdownMenu.d.ts b/src/Dropdown/DropdownMenu.d.ts deleted file mode 100644 index 36ae8a4d16..0000000000 --- a/src/Dropdown/DropdownMenu.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as React from 'react'; - -import { StandardProps } from '../@types/common'; -import { IconProps } from '../Icon/Icon.d'; - -export interface DropdownMenuProps extends StandardProps { - /** Primary content */ - children?: React.ReactNode; - - /** Define the title as a submenu */ - title?: React.ReactNode; - - /** The submenu expands from the left and defaults to the right */ - pullLeft?: boolean; - - /** The value of the current option */ - eventKey?: T; - - /** Set the icon */ - icon?: React.ReactElement; -} - -declare const DropdownMenu: React.ComponentType; - -export default DropdownMenu; diff --git a/src/Dropdown/DropdownMenu.tsx b/src/Dropdown/DropdownMenu.tsx index 8c692ce72f..a3305b78ec 100644 --- a/src/Dropdown/DropdownMenu.tsx +++ b/src/Dropdown/DropdownMenu.tsx @@ -1,77 +1,119 @@ -import * as React from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import Collapse from '../Animation/Collapse'; -import shallowEqual from '../utils/shallowEqual'; import DropdownMenuItem from './DropdownMenuItem'; -import { DropdownMenuProps } from './DropdownMenu.d'; import Icon from '../Icon'; import Ripple from '../Ripple'; import { createChainedFunction, - prefix, ReactChildren, - getUnhandledProps, - defaultProps + shallowEqual, + mergeRefs, + useClassNames } from '../utils'; -import mergeRefs from '../utils/mergeRefs'; - -class DropdownMenu extends React.Component { - static displayName = 'DropdownMenu'; - static propTypes = { - activeKey: PropTypes.any, - className: PropTypes.string, - children: PropTypes.node, - icon: PropTypes.node, - classPrefix: PropTypes.string, - pullLeft: PropTypes.bool, - onSelect: PropTypes.func, - title: PropTypes.node, - open: PropTypes.bool, - trigger: PropTypes.oneOfType([PropTypes.array, PropTypes.oneOf(['click', 'hover'])]), - eventKey: PropTypes.any, - openKeys: PropTypes.array, - expanded: PropTypes.bool, - collapsible: PropTypes.bool, - onToggle: PropTypes.func + +import { IconProps } from '../Icon'; +import { StandardProps } from '../@types/common'; + +export interface DropdownMenuProps extends StandardProps { + /** Define the title as a submenu */ + title?: React.ReactNode; + + /** The submenu expands from the left and defaults to the right */ + pullLeft?: boolean; + + /** The value of the current option */ + eventKey?: T; + + /** Set the icon */ + icon?: React.ReactElement; + + open?: boolean; + openKeys?: T[]; + collapsible?: boolean; + expanded?: boolean; + active?: boolean; + activeKey?: T; + trigger?: 'hover' | 'click'; + onSelect?: (eventKey: T, event: React.SyntheticEvent) => void; + onToggle?: (eventKey: T, event: React.SyntheticEvent) => void; +} + +const defaultProps: Partial = { + openKeys: [], + classPrefix: 'dropdown-menu' +}; + +const DropdownMenu = React.forwardRef((props: DropdownMenuProps, ref) => { + const { + children, + className, + classPrefix, + collapsible, + expanded, + activeKey, + openKeys, + onSelect, + onToggle, + ...rest + } = props; + + const { withClassPrefix, merge, prefix } = useClassNames(classPrefix); + const handleToggleChange = (eventKey: string, event: React.MouseEvent) => { + onToggle?.(eventKey, event); + }; + + const isActive = (props: DropdownMenuProps, activeKey: string) => { + if ( + props.active || + (typeof activeKey !== 'undefined' && shallowEqual(props.eventKey, activeKey)) + ) { + return true; + } + + if (ReactChildren.some(props.children, child => isActive(child.props, activeKey))) { + return true; + } + + return props.active; }; - getMenuItemsAndStatus(children?: React.ReactNode): { items: any[]; active: boolean } { + + const getMenuItemsAndStatus = (children?: React.ReactNode): { items: any[]; active: boolean } => { let hasActiveItem: boolean; - const { activeKey, onSelect, classPrefix, openKeys = [] } = this.props; const items = React.Children.map(children, (item: any, index: number) => { if (!item) { return null; } - const displayName: string = item?.type?.displayName; + const displayName = item?.type?.displayName; let active: boolean; - if (~displayName?.indexOf('(DropdownMenuItem)') || ~displayName?.indexOf('(DropdownMenu)')) { - active = this.isActive(item.props, activeKey); + if (displayName === 'DropdownMenuItem' || displayName === 'DropdownMenu') { + active = isActive(item.props, activeKey); if (active) { hasActiveItem = true; } } - if (~displayName?.indexOf('(DropdownMenuItem)')) { + if (displayName === 'DropdownMenuItem') { const { onSelect: onItemSelect } = item.props; return React.cloneElement(item, { key: index, active, onSelect: createChainedFunction(onSelect, onItemSelect) }); - } else if (~displayName?.indexOf('(DropdownMenu)')) { - const itemsAndStatus = this.getMenuItemsAndStatus(item.props.children); + } else if (displayName === 'DropdownMenu') { + const itemsAndStatus = getMenuItemsAndStatus(item.props.children); const { icon, open, trigger, pullLeft, eventKey, title, className } = item.props; const expanded = openKeys.some(key => shallowEqual(key, eventKey)); - const itemClassName = classNames( + const itemClassName = merge( className, - this.addPrefix(`pull-${pullLeft ? 'left' : 'right'}`), - { - [this.addPrefix('item-focus')]: this.isActive(item.props, activeKey) - } + prefix(`pull-${pullLeft ? 'left' : 'right'}`, { + 'item-focus': isActive(item.props, activeKey) + }) ); return ( @@ -82,30 +124,30 @@ class DropdownMenu extends React.Component { expanded={expanded} className={itemClassName} pullLeft={pullLeft} - as="div" + linkAs="div" submenu >
    handleToggleChange(eventKey, e)} role="menu" tabIndex={-1} > {title}
    - {this.renderCollapse((transitionProps, ref) => { + {renderCollapse((transitionProps, ref) => { const { className, ...transitionRestProps } = transitionProps || {}; return (
      {itemsAndStatus.items}
    @@ -122,70 +164,63 @@ class DropdownMenu extends React.Component { items, active: hasActiveItem }; - } - - handleToggleChange = (eventKey: any, event: React.SyntheticEvent) => { - this.props.onToggle?.(eventKey, event); }; - isActive(props: any, activeKey: any) { - if ( - props.active || - (typeof activeKey !== 'undefined' && shallowEqual(props.eventKey, activeKey)) - ) { - return true; - } - - if (ReactChildren.some(props.children, child => this.isActive(child.props, activeKey))) { - return true; - } - - return props.active; - } - - addPrefix = (name: string) => prefix(this.props.classPrefix)(name); - renderCollapse(children, expanded?: boolean) { - return this.props.collapsible ? ( + const renderCollapse = (children, expanded?: boolean) => { + return collapsible ? ( {children} ) : ( children() ); - } - - render() { - const { children, className, classPrefix, expanded, htmlElementRef, ...props } = this.props; - const { items, active } = this.getMenuItemsAndStatus(children); - const unhandled = getUnhandledProps(DropdownMenu, props); - const classes = classNames(classPrefix, className, { - [this.addPrefix('active')]: active - }); + }; - return this.renderCollapse((transitionProps, ref) => { - const { className: transitionClassName, ...transitionRestProps } = transitionProps || {}; - - return ( -
      - {items} -
    - ); - }, expanded); - } -} + const { items, active } = getMenuItemsAndStatus(children); + const classes = merge(className, withClassPrefix({ active })); -export default defaultProps({ - classPrefix: 'dropdown-menu' -})(DropdownMenu); + return renderCollapse((transitionProps, transitionRef) => { + const { className: transitionClassName, ...transitionRestProps } = transitionProps || {}; + + return ( +
      + {items} +
    + ); + }, expanded); +}); + +DropdownMenu.displayName = 'DropdownMenu'; +DropdownMenu.defaultProps = defaultProps; +DropdownMenu.propTypes = { + active: PropTypes.bool, + activeKey: PropTypes.any, + className: PropTypes.string, + children: PropTypes.node, + icon: PropTypes.any, + classPrefix: PropTypes.string, + pullLeft: PropTypes.bool, + title: PropTypes.node, + open: PropTypes.bool, + trigger: PropTypes.oneOf(['click', 'hover']), + eventKey: PropTypes.any, + openKeys: PropTypes.array, + expanded: PropTypes.bool, + collapsible: PropTypes.bool, + onSelect: PropTypes.func, + onToggle: PropTypes.func +}; + +export default DropdownMenu; diff --git a/src/Dropdown/DropdownMenuItem.d.ts b/src/Dropdown/DropdownMenuItem.d.ts deleted file mode 100644 index 54fb52420e..0000000000 --- a/src/Dropdown/DropdownMenuItem.d.ts +++ /dev/null @@ -1,40 +0,0 @@ -import * as React from 'react'; - -import { StandardProps } from '../@types/common'; -import { IconProps } from '../Icon/Icon.d'; - -export interface DropdownMenuItemProps extends StandardProps { - /** Active the current option */ - active?: boolean; - - /** Primary content */ - children?: React.ReactNode; - - /** You can use a custom element for this component */ - as?: React.ElementType; - - /** Whether to display the divider */ - divider?: boolean; - - /** Disable the current option */ - disabled?: boolean; - - /** The value of the current option */ - eventKey?: T; - - /** Displays a custom panel */ - panel?: boolean; - - /** Set the icon */ - icon?: React.ReactElement; - - /** Select the callback function for the current option */ - onSelect?: (eventKey: T, event: React.SyntheticEvent) => void; - - /** Custom rendering item */ - renderItem?: (item: React.ReactNode) => React.ReactNode; -} - -declare const DropdownMenuItem: React.ComponentType; - -export default DropdownMenuItem; diff --git a/src/Dropdown/DropdownMenuItem.tsx b/src/Dropdown/DropdownMenuItem.tsx index 90b63bd937..7b8a02dc17 100644 --- a/src/Dropdown/DropdownMenuItem.tsx +++ b/src/Dropdown/DropdownMenuItem.tsx @@ -1,89 +1,76 @@ -import * as React from 'react'; +import React, { useContext, useCallback } from 'react'; import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import _ from 'lodash'; - import SafeAnchor from '../SafeAnchor'; -import { prefix, isOneOf, createChainedFunction, defaultProps, getUnhandledProps } from '../utils'; +import { isOneOf, createChainedFunction, useClassNames, useControlled } from '../utils'; import { SidenavContext } from '../Sidenav/Sidenav'; -import { DropdownMenuItemProps } from './DropdownMenuItem.d'; +import { WithAsProps, RsRefForwardingComponent } from '../@types/common'; +import { IconProps } from '../Icon'; -interface DropdownMenuItemState { - open?: boolean; -} +export interface DropdownMenuItemProps + extends WithAsProps, + Omit, 'onSelect'> { + /** Active the current option */ + active?: boolean; -class DropdownMenuItem extends React.Component { - static displayName = 'DropdownMenuItem'; - static contextType = SidenavContext; - static propTypes = { - divider: PropTypes.bool, - panel: PropTypes.bool, - trigger: PropTypes.oneOfType([PropTypes.array, PropTypes.oneOf(['click', 'hover'])]), - open: PropTypes.bool, - expanded: PropTypes.bool, - active: PropTypes.bool, - disabled: PropTypes.bool, - pullLeft: PropTypes.bool, - submenu: PropTypes.bool, - onSelect: PropTypes.func, - onClick: PropTypes.func, - icon: PropTypes.node, - eventKey: PropTypes.any, - className: PropTypes.string, - style: PropTypes.object, - children: PropTypes.node, - classPrefix: PropTypes.string, - tabIndex: PropTypes.number, - as: PropTypes.elementType, - renderItem: PropTypes.func - }; - static defaultProps = { - tabIndex: -1, - trigger: 'hover' - }; - - constructor(props: DropdownMenuItemProps) { - super(props); - this.state = { - open: props.open - }; - } + /** Primary content */ + children?: React.ReactNode; - getOpen() { - const { open } = this.props; - if (_.isUndefined(open)) { - return this.state.open; - } - return open; - } + /** You can use a custom element for this component */ + as?: React.ElementType; - toggle = (_event: React.SyntheticEvent, isOpen?: boolean) => { - const open = _.isUndefined(isOpen) ? !this.getOpen() : isOpen; - this.setState({ open }); - }; + /** Whether to display the divider */ + divider?: boolean; - handleClick = (event: React.SyntheticEvent) => { - const { onSelect, eventKey, disabled, onClick } = this.props; + /** Disable the current option */ + disabled?: boolean; - if (disabled) { - event.preventDefault(); - return; - } + /** The value of the current option */ + eventKey?: T; + + /** Displays a custom panel */ + panel?: boolean; - onSelect?.(eventKey, event); - onClick?.(event); - }; + /** Set the icon */ + icon?: React.ReactElement; - handleMouseOver = (event: React.SyntheticEvent) => { - this.toggle(event, true); - }; + /** Whether it is a submenu. */ + submenu?: boolean; - handleMouseOut = (event: React.SyntheticEvent) => { - this.toggle(event, false); - }; + /** The sub-level menu appears from the right side by default, and when `pullLeft` is set, it appears from the left. */ + pullLeft?: boolean; - render() { + /** Triggering event for submenu expansion. */ + trigger?: 'hover' | 'click'; + + /** Whether the submenu is opened. */ + open?: boolean; + + /** Whether the submenu is expanded, used in Sidenav. */ + expanded?: boolean; + + /** You can use a custom element for this link */ + linkAs?: React.ElementType; + + /** Select the callback function for the current option */ + onSelect?: (eventKey: T, event: React.SyntheticEvent) => void; + + /** Custom rendering item */ + renderItem?: (item: React.ReactNode) => React.ReactNode; +} + +const defaultProps: Partial = { + linkAs: SafeAnchor, + as: 'li', + classPrefix: 'dropdown-item', + tabIndex: -1, + trigger: 'hover' +}; + +const DropdownMenuItem: RsRefForwardingComponent<'li', DropdownMenuItemProps> = React.forwardRef( + (props: DropdownMenuItemProps, ref) => { const { + as: Component, + linkAs: Link, children, divider, panel, @@ -97,77 +84,130 @@ class DropdownMenuItem extends React.Component) => { + if (disabled) { + event.preventDefault(); + return; + } + + if (isOneOf('click', trigger) && submenu) { + setOpen(!open); + } + + onSelect?.(eventKey, event); + }, + [disabled, open, eventKey, trigger, submenu, setOpen, onSelect] + ); - if (isOneOf('click', trigger) && submenu) { - itemToggleProps.onClick = createChainedFunction(this.handleClick, this.toggle); + const handleMouseOver = useCallback(() => { + setOpen(true); + }, [setOpen]); + + const handleMouseOut = useCallback(() => { + setOpen(false); + }, [setOpen]); + + const itemEventProps: React.LiHTMLAttributes = {}; + + if (isOneOf('hover', trigger) && submenu && !expanded) { + itemEventProps.onMouseOver = createChainedFunction(handleMouseOver, onMouseOver); + itemEventProps.onMouseOut = createChainedFunction(handleMouseOut, onMouseOut); } if (divider) { return ( -
  • ); } if (panel) { return ( -
  • + {children} -
  • +
    ); } const item = ( - - {icon && React.cloneElement(icon, { className: addPrefix('menu-icon') })} + {icon && React.cloneElement(icon, { className: prefix('menu-icon') })} {children} - + ); return ( -
  • + {renderItem ? renderItem(item) : item} -
  • +
    ); } -} - -export default defaultProps({ - classPrefix: 'dropdown-item', - as: SafeAnchor -})(DropdownMenuItem); +); + +DropdownMenuItem.displayName = 'DropdownMenuItem'; +DropdownMenuItem.defaultProps = defaultProps; +DropdownMenuItem.propTypes = { + as: PropTypes.elementType, + divider: PropTypes.bool, + panel: PropTypes.bool, + trigger: PropTypes.oneOfType([PropTypes.array, PropTypes.oneOf(['click', 'hover'])]), + open: PropTypes.bool, + expanded: PropTypes.bool, + active: PropTypes.bool, + disabled: PropTypes.bool, + pullLeft: PropTypes.bool, + submenu: PropTypes.bool, + onSelect: PropTypes.func, + onClick: PropTypes.func, + icon: PropTypes.node, + eventKey: PropTypes.any, + className: PropTypes.string, + style: PropTypes.object, + children: PropTypes.node, + classPrefix: PropTypes.string, + tabIndex: PropTypes.number, + renderItem: PropTypes.func +}; + +export default DropdownMenuItem; diff --git a/src/Dropdown/DropdownToggle.tsx b/src/Dropdown/DropdownToggle.tsx index 8f606fa2c2..3042d658a2 100644 --- a/src/Dropdown/DropdownToggle.tsx +++ b/src/Dropdown/DropdownToggle.tsx @@ -1,73 +1,73 @@ -import * as React from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; -import classNames from 'classnames'; - import Ripple from '../Ripple'; import Button from '../Button'; -import { prefix, defaultProps } from '../utils'; -import { IconProps } from '../Icon/Icon.d'; +import { useClassNames } from '../utils'; +import { IconProps } from '../Icon'; +import { WithAsProps, RsRefForwardingComponent } from '../@types/common'; -export interface DorpdownToggleProps { - className?: string; - classPrefix?: string; - children?: React.ReactNode; +export interface DorpdownToggleProps extends WithAsProps { icon?: React.ReactElement; noCaret?: boolean; - as: React.ElementType; renderTitle?: (children?: React.ReactNode) => React.ReactNode; } -class DorpdownToggle extends React.Component { - static propTypes = { - className: PropTypes.string, - children: PropTypes.node, - icon: PropTypes.node, - classPrefix: PropTypes.string, - noCaret: PropTypes.bool, - as: PropTypes.elementType, - renderTitle: PropTypes.func - }; - render() { +const defaultProps: Partial = { + as: Button, + classPrefix: 'dropdown-toggle' +}; + +const DorpdownToggle: RsRefForwardingComponent<'a', DorpdownToggleProps> = React.forwardRef( + (props: DorpdownToggleProps, ref) => { const { + as: Component, className, classPrefix, renderTitle, children, icon, noCaret, - as: Component, - ...props - } = this.props; - const addPrefix = prefix(classPrefix); + ...rest + } = props; + const { prefix, withClassPrefix, merge } = useClassNames(classPrefix); + const classes = merge( + className, + withClassPrefix({ + 'custom-title': typeof renderTitle === 'function' + }) + ); if (renderTitle) { return ( - + {renderTitle(children)} - +
    ); } - let buttonProps = {}; - if (Component === Button) { - buttonProps = { - as: 'a', - appearance: 'subtle' - }; - } + const buttonProps = Component === Button ? { as: 'a', appearance: 'subtle' } : null; return ( - + {icon} {children} - {noCaret ? null : } + {noCaret ? null : } ); } -} +); -export default defaultProps({ - as: Button, - classPrefix: 'dropdown-toggle' -})(DorpdownToggle); +DorpdownToggle.displayName = 'DorpdownToggle'; +DorpdownToggle.defaultProps = defaultProps; +DorpdownToggle.propTypes = { + className: PropTypes.string, + children: PropTypes.node, + icon: PropTypes.node, + classPrefix: PropTypes.string, + noCaret: PropTypes.bool, + as: PropTypes.elementType, + renderTitle: PropTypes.func +}; + +export default DorpdownToggle; diff --git a/src/Dropdown/index.d.ts b/src/Dropdown/index.d.ts deleted file mode 100644 index e78c947943..0000000000 --- a/src/Dropdown/index.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { default } from './Dropdown'; -export * from './Dropdown'; -export * from './DropdownMenu'; -export * from './DropdownMenuItem'; diff --git a/src/Dropdown/index.tsx b/src/Dropdown/index.tsx index 3cb55749df..569cd53135 100644 --- a/src/Dropdown/index.tsx +++ b/src/Dropdown/index.tsx @@ -1,3 +1,3 @@ import Dropdown from './Dropdown'; - +export type { DropdownProps } from './Dropdown'; export default Dropdown; diff --git a/src/Dropdown/test/DropdownMenuItemSpec.js b/src/Dropdown/test/DropdownMenuItemSpec.js index 7b66a628de..edf3ea1355 100644 --- a/src/Dropdown/test/DropdownMenuItemSpec.js +++ b/src/Dropdown/test/DropdownMenuItemSpec.js @@ -10,25 +10,26 @@ describe('DropdownMenuItem', () => { it('Should render a li', () => { const title = 'Test'; const instance = getDOMNode({title}); + assert.equal(instance.tagName, 'LI'); assert.equal(innerText(instance), title); }); it('Should render a Button', () => { const title = 'Test'; - const instance = getDOMNode({title}); + const instance = getDOMNode({title}); assert.equal(instance.children[0].tagName, 'BUTTON'); assert.equal(innerText(instance), title); }); it('Should render a divider', () => { const instance = getDOMNode(); - assert.equal(instance.className, 'rs-dropdown-item-divider'); + assert.include(instance.className, 'rs-dropdown-item-divider'); }); it('Should render a panel', () => { const instance = getDOMNode(); - assert.equal(instance.className, 'rs-dropdown-item-panel'); + assert.include(instance.className, 'rs-dropdown-item-panel'); }); it('Should be active', () => { diff --git a/src/FormHelpText/FormHelpText.tsx b/src/FormHelpText/FormHelpText.tsx index 916dcb0a9f..86c2da6a5f 100644 --- a/src/FormHelpText/FormHelpText.tsx +++ b/src/FormHelpText/FormHelpText.tsx @@ -27,8 +27,8 @@ const FormHelpText: RsRefForwardingComponent<'span', FormHelpTextProps> = React. if (tooltip) { return ( - {children}}> - + {children}}> + diff --git a/src/FormHelpText/test/FormHelpTextSpec.js b/src/FormHelpText/test/FormHelpTextSpec.js index 6da34a4404..cfa12ef5cf 100644 --- a/src/FormHelpText/test/FormHelpTextSpec.js +++ b/src/FormHelpText/test/FormHelpTextSpec.js @@ -1,6 +1,5 @@ import React from 'react'; -import ReactTestUtils from 'react-dom/test-utils'; -import { getDOMNode, getInstance } from '@test/testUtils'; +import { getDOMNode } from '@test/testUtils'; import FormHelpText from '../FormHelpText'; import { assert } from 'chai'; diff --git a/src/Icon/Icon.tsx b/src/Icon/Icon.tsx index 4604449bdd..94a1237481 100644 --- a/src/Icon/Icon.tsx +++ b/src/Icon/Icon.tsx @@ -1,77 +1,118 @@ -import * as React from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import { defaultProps, prefix } from '../utils'; -import { IconProps } from './Icon.d'; -import { SVGIcon } from '../@types/common'; - -class Icon extends React.Component { - static propTypes = { - icon: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), - className: PropTypes.string, - classPrefix: PropTypes.string, - as: PropTypes.elementType, - size: PropTypes.oneOf(['lg', '2x', '3x', '4x', '5x']), - flip: PropTypes.oneOf(['horizontal', 'vertical']), - stack: PropTypes.oneOf(['1x', '2x']), - rotate: PropTypes.number, - fixedWidth: PropTypes.bool, - svgStyle: PropTypes.object, - spin: PropTypes.bool, - pulse: PropTypes.bool, - inverse: PropTypes.bool - }; - render() { - const { - className, - classPrefix, - icon, - size, - fixedWidth, - spin, - pulse, - rotate, - flip, - stack, - inverse, - style, - svgStyle, - as: Component, - ...props - } = this.props; - - const addPrefix = prefix(classPrefix); - const isSvgIcon = typeof icon === 'object' && icon.id && icon.viewBox; - - const classes = classNames(className, classPrefix, { - [addPrefix(typeof icon === 'string' ? icon : '')]: !isSvgIcon, - [addPrefix('fw')]: fixedWidth, - [addPrefix('spin')]: spin, - [addPrefix('pulse')]: pulse, - [addPrefix(`size-${size || ''}`)]: size, - [addPrefix(`flip-${flip || ''}`)]: flip, - [addPrefix(`stack-${stack || ''}`)]: stack, - [addPrefix('inverse')]: inverse - }); - - const styles = rotate ? { transform: `rotate(${rotate}deg)`, ...style } : style; - - if (isSvgIcon) { - const svgIcon = icon as SVGIcon; - return ( - - - - - - ); - } - - return ; - } +import { useClassNames } from '../utils'; +import { SVGIcon, WithAsProps, RsRefForwardingComponent } from '../@types/common'; +import { IconNames } from '../@types/icons'; + +export interface IconProps extends WithAsProps { + /** You can use a custom element for this component */ + as?: React.ElementType; + + /** Icon name */ + icon: IconNames | SVGIcon; + + /** Sets the icon size */ + size?: 'lg' | '2x' | '3x' | '4x' | '5x'; + + /** Flip the icon */ + flip?: 'horizontal' | 'vertical'; + + /** Combine multiple icons */ + stack?: '1x' | '2x'; + + /** Rotate the icon */ + rotate?: number; + + /** Fixed icon width because there are many icons with uneven size */ + fixedWidth?: boolean; + + /** Set SVG style when using custom SVG Icon */ + svgStyle?: React.CSSProperties; + + /** Dynamic rotation icon */ + + spin?: boolean; + + /** Use pulse to have it rotate with 8 steps */ + pulse?: boolean; + + /** Inverse color */ + inverse?: boolean; } -export default defaultProps({ +const defaultProps: Partial = { as: 'i', classPrefix: 'icon' -})(Icon); +}; + +const Icon: RsRefForwardingComponent<'i', IconProps> = React.forwardRef((props: IconProps, ref) => { + const { + className, + classPrefix, + icon, + size, + fixedWidth, + spin, + pulse, + rotate, + flip, + stack, + inverse, + style, + svgStyle, + as: Component, + ...rest + } = props; + + const { withClassPrefix, merge } = useClassNames(classPrefix); + const isSvgIcon = typeof icon === 'object' && icon.id && icon.viewBox; + + const classes = merge( + className, + withClassPrefix({ + [typeof icon === 'string' ? icon : '']: !isSvgIcon, + [`size-${size}`]: size, + [`flip-${flip}`]: flip, + [`stack-${stack}`]: stack, + fw: fixedWidth, + inverse, + spin, + pulse + }) + ); + + const styles = rotate ? { transform: `rotate(${rotate}deg)`, ...style } : style; + + if (isSvgIcon) { + const svgIcon = icon as SVGIcon; + return ( + + + + + + ); + } + + return ; +}); + +Icon.displayName = 'Icon'; +Icon.defaultProps = defaultProps; +Icon.propTypes = { + icon: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + className: PropTypes.string, + classPrefix: PropTypes.string, + as: PropTypes.elementType, + size: PropTypes.oneOf(['lg', '2x', '3x', '4x', '5x']), + flip: PropTypes.oneOf(['horizontal', 'vertical']), + stack: PropTypes.oneOf(['1x', '2x']), + rotate: PropTypes.number, + fixedWidth: PropTypes.bool, + svgStyle: PropTypes.object, + spin: PropTypes.bool, + pulse: PropTypes.bool, + inverse: PropTypes.bool +}; + +export default Icon; diff --git a/src/Icon/index.d.ts b/src/Icon/index.d.ts deleted file mode 100644 index 63363a356a..0000000000 --- a/src/Icon/index.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './Icon'; -export * from './Icon'; diff --git a/src/Icon/index.tsx b/src/Icon/index.tsx index 311b1a2397..0f5d80ed49 100644 --- a/src/Icon/index.tsx +++ b/src/Icon/index.tsx @@ -1,3 +1,3 @@ import Icon from './Icon'; - +export type { IconProps } from './Icon'; export default Icon; diff --git a/src/Icon/test/IconSpec.js b/src/Icon/test/IconSpec.js index d153c32afa..4317c49ecb 100644 --- a/src/Icon/test/IconSpec.js +++ b/src/Icon/test/IconSpec.js @@ -1,7 +1,5 @@ import React from 'react'; -import ReactTestUtils from 'react-dom/test-utils'; - -import { getDOMNode, getInstance, getStyle } from '@test/testUtils'; +import { getDOMNode, getStyle } from '@test/testUtils'; import Icon from '../Icon'; describe('Icon', () => { @@ -23,9 +21,9 @@ describe('Icon', () => { '', node: {} }; - const instance = getInstance(); + const instance = getDOMNode(); - ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'svg'); + assert.ok(instance.querySelector('svg')); }); it('Should have a class prefix rsuite-icon', () => { diff --git a/src/Input/Input.tsx b/src/Input/Input.tsx index 976fede743..29b0d5038d 100644 --- a/src/Input/Input.tsx +++ b/src/Input/Input.tsx @@ -1,16 +1,22 @@ import React, { useCallback, useContext } from 'react'; import PropTypes from 'prop-types'; import isUndefined from 'lodash/isUndefined'; -import { createChainedFunction, refType, mergeRefs, useClassNames, useCustom } from '../utils'; import { FormGroupContext } from '../FormGroup/FormGroup'; import { InputGroupContext } from '../InputGroup/InputGroup'; +import { + createChainedFunction, + refType, + mergeRefs, + useClassNames, + useCustom, + KEY_CODE +} from '../utils'; import { WithAsProps, RsRefForwardingComponent, TypeAttributes, FormControlBaseProps } from '../@types/common'; -import { KEY_CODE } from '../constants'; export interface LocaleType { emptyPlaintext: string; diff --git a/src/InputPicker/InputPicker.tsx b/src/InputPicker/InputPicker.tsx index 846715e6ca..457647cd7b 100644 --- a/src/InputPicker/InputPicker.tsx +++ b/src/InputPicker/InputPicker.tsx @@ -17,7 +17,8 @@ import { getDataGroupBy, useClassNames, useCustom, - useControlled + useControlled, + mergeRefs } from '../utils'; import { @@ -33,7 +34,11 @@ import { useSearch } from '../Picker'; import { PickerComponent, PickerLocaleType } from '../Picker/types'; -import { pickerToggleTriggerProps } from '../Picker/PickerToggleTrigger'; +import { + pickerToggleTriggerProps, + OverlayTriggerInstance, + PositionChildProps +} from '../Picker/PickerToggleTrigger'; import Tag, { TagProps } from '../Tag'; import { ItemDataType, FormControlPickerProps } from '../@types/common'; import { listPickerPropTypes } from '../Picker/propTypes'; @@ -147,11 +152,10 @@ const InputPicker: PickerComponent = React.forwardRef( throw Error('`groupBy` can not be equal to `valueKey` and `labelKey`'); } - const rootRef = useRef(); const menuRef = useRef(); const positionRef = useRef(); const toggleRef = useRef(); - const triggerRef = useRef(); + const triggerRef = useRef(); const inputRef = useRef(); const { locale } = useCustom(['Picker', 'InputPicker'], overrideLocale); @@ -176,14 +180,14 @@ const InputPicker: PickerComponent = React.forwardRef( multi ? defaultValue || [] : defaultValue ); - const cloneValue = () => (multi ? clone(value) || [] : value); + const cloneValue = useCallback(() => (multi ? clone(value) || [] : value), [multi, value]); const handleClose = useCallback(() => { - triggerRef?.current?.hide?.(); + triggerRef?.current?.close(); }, [triggerRef]); const handleOpen = useCallback(() => { - triggerRef.current?.show?.(); + triggerRef.current?.open(); }, [triggerRef]); // Used to hover the focuse item when trigger `onKeydown` @@ -299,7 +303,7 @@ const InputPicker: PickerComponent = React.forwardRef( setValue(val); handleChange(val, event); }, - [cloneValue, handleChange] + [setValue, cloneValue, handleChange] ); const handleSelect = useCallback( @@ -376,6 +380,7 @@ const InputPicker: PickerComponent = React.forwardRef( handleChange(val, event); }, [ + setValue, cloneValue, getAllData, handleChange, @@ -415,6 +420,7 @@ const InputPicker: PickerComponent = React.forwardRef( handleClose(); }, [ + setValue, disabledItemValues, controlledData, focusItemValue, @@ -430,7 +436,9 @@ const InputPicker: PickerComponent = React.forwardRef( ); useImperativeHandle(ref, () => ({ - root: rootRef.current, + get root() { + return triggerRef.current.child; + }, get menu() { return menuRef.current; }, @@ -460,7 +468,7 @@ const InputPicker: PickerComponent = React.forwardRef( setValue(val); handleChange(val, event); }, - [focusInput, handleChange, cloneValue] + [setValue, focusInput, handleChange, cloneValue] ); const handleClean = useCallback( @@ -595,9 +603,11 @@ const InputPicker: PickerComponent = React.forwardRef( return tagElements; }; - const renderDropdownMenu = () => { + const renderDropdownMenu = (positionProps: PositionChildProps, speakerRef) => { + const { left, top, className } = positionProps; const menuClassPrefix = multi ? 'picker-check-menu' : 'picker-select-menu'; - const classes = merge(prefix(menuClassPrefix), menuClassName); + const classes = merge(className, menuClassName, prefix(menuClassPrefix)); + const styles = { ...menuStyle, left, top }; let items = filterNodesOfTree(getAllData(), checkShouldDisplay); @@ -641,10 +651,10 @@ const InputPicker: PickerComponent = React.forwardRef( return ( toggleRef.current} getPositionInstance={() => positionRef.current} onKeyDown={handleKeyDown} @@ -693,15 +703,9 @@ const InputPicker: PickerComponent = React.forwardRef( onEntered={createChainedFunction(onEntered, onOpen)} onExit={createChainedFunction(handleExit, onExit)} onExited={createChainedFunction(handleExited, onExited)} - speaker={renderDropdownMenu()} + speaker={renderDropdownMenu} > - + { { inChrome && assert.equal(getStyle(dom, 'border'), `1px solid ${H700}`, 'Picker active border'); @@ -108,5 +107,6 @@ describe('InputPicker styles', () => { createTestContainer() ); dom = instanceRef.current.root; + instanceRef.current.open(); }); }); diff --git a/src/Message/Message.tsx b/src/Message/Message.tsx index f76e9f30ec..c158ae5114 100644 --- a/src/Message/Message.tsx +++ b/src/Message/Message.tsx @@ -1,8 +1,7 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import Icon from '../Icon'; -import { STATUS_ICON_NAMES, STATUS } from '../constants'; -import { useClassNames, useTimeout } from '../utils'; +import { useClassNames, useTimeout, STATUS_ICON_NAMES, STATUS } from '../utils'; import { WithAsProps, TypeAttributes, RsRefForwardingComponent } from '../@types/common'; import CloseButton from '../CloseButton'; diff --git a/src/Modal/BaseModal.tsx b/src/Modal/BaseModal.tsx deleted file mode 100644 index 6806685ec3..0000000000 --- a/src/Modal/BaseModal.tsx +++ /dev/null @@ -1,286 +0,0 @@ -import * as React from 'react'; -import classNames from 'classnames'; -import { ownerDocument, activeElement, contains, getContainer, on } from 'dom-lib'; -import canUseDom from 'dom-lib/lib/query/canUseDOM'; -import Portal from '../Portal'; -import ModalManager from './ModalManager'; -import Fade from '../Animation/Fade'; -import { ModalProps } from './Modal.d'; -import getDOMNode from '../utils/getDOMNode'; -import mergeRefs from '../utils/mergeRefs'; - -class RefHolder extends React.Component { - render() { - return this.props.children || null; - } -} - -interface BaseModalProps extends ModalProps { - container?: HTMLElement | (() => HTMLElement); - onRendered?: any; - transition: React.ElementType; - onEscapeKeyUp?: React.KeyboardEventHandler; - onBackdropClick?: React.MouseEventHandler; - containerClassName?: string; - dialogTransitionTimeout?: number; - backdropTransitionTimeout?: number; - role?: string; - animationProps?: any; -} - -interface BaseModalState { - exited?: boolean; -} - -const modalManager = new ModalManager(); - -class BaseModal extends React.Component { - static manager = modalManager; - static defaultProps = { - backdrop: true, - keyboard: true, - autoFocus: true, - enforceFocus: true - }; - - mountNode = null; - modalNodeRef = null; - backdropRef = null; - dialogRef: React.RefObject = null; - lastFocus = null; - onDocumentKeyupListener = null; - onFocusinListener = null; - - constructor(props: BaseModalProps) { - super(props); - this.state = { exited: !props.show }; - this.backdropRef = React.createRef(); - this.modalNodeRef = React.createRef(); - this.dialogRef = React.createRef(); - } - componentDidMount() { - if (this.props.show) { - this.onShow(); - } - } - - static getDerivedStateFromProps(nextProps: BaseModalProps) { - if (nextProps.show) { - return { exited: false }; - } else if (!nextProps.transition) { - // Otherwise let handleHidden take care of marking exited. - return { exited: true }; - } - return null; - } - - getSnapshotBeforeUpdate(prevProps: BaseModalProps) { - if (this.props.show && !prevProps.show) { - this.checkForFocus(); - } - return null; - } - - componentDidUpdate(prevProps: BaseModalProps) { - const { transition } = this.props; - - if (prevProps.show && !this.props.show && !transition) { - // Otherwise handleHidden will call this. - this.onHide(); - } else if (!prevProps.show && this.props.show) { - this.onShow(); - } - } - - componentWillUnmount() { - const { show, transition } = this.props; - - if (show || (transition && !this.state.exited)) { - this.onHide(); - } - } - - onShow() { - const doc = ownerDocument(this); - const container = getContainer(this.props.container, doc.body); - const { containerClassName } = this.props; - - modalManager.add(this, container, containerClassName); - - this.onDocumentKeyupListener = on(doc, 'keyup', this.handleDocumentKeyUp); - this.onFocusinListener = on(doc, 'focus', this.enforceFocus); - this.props.onShow?.(); - } - - onHide() { - modalManager.remove(this); - this.onDocumentKeyupListener?.off(); - this.onFocusinListener?.off(); - this.restoreLastFocus(); - } - - getDialogElement(): any { - return getDOMNode(this.dialogRef.current); - } - - setMountNodeRef = (ref: any) => { - this.mountNode = ref?.getMountNode?.(); - }; - - isTopModal() { - return modalManager.isTopModal(this); - } - - handleHidden = (args: any) => { - this.setState({ exited: true }); - this.onHide(); - this.props.onExited?.(args); - }; - - handleBackdropClick = (event: React.MouseEvent) => { - if (event.target !== event.currentTarget) { - return; - } - - const { onBackdropClick, backdrop, onHide } = this.props; - - onBackdropClick?.(event); - backdrop && onHide?.(event); - }; - - handleDocumentKeyUp = (event: React.KeyboardEvent) => { - const { keyboard, onHide, onEscapeKeyUp } = this.props; - if (keyboard && event.keyCode === 27 && this.isTopModal()) { - onEscapeKeyUp?.(event); - onHide?.(event); - } - }; - - checkForFocus() { - if (canUseDom) { - this.lastFocus = activeElement(); - } - } - - restoreLastFocus() { - // Support: <=IE11 doesn't support `focus()` on svg elements - if (this.lastFocus) { - this.lastFocus.focus?.(); - this.lastFocus = null; - } - } - - enforceFocus = () => { - const { enforceFocus } = this.props; - - if (!enforceFocus || !this.isTopModal()) { - return; - } - - const active = activeElement(ownerDocument(this)); - const modal = this.getDialogElement(); - - if (modal && modal !== active && !contains(modal, active)) { - modal.focus(); - } - }; - - renderBackdrop() { - const { - transition, - backdrop, - backdropTransitionTimeout, - backdropStyle, - backdropClassName - } = this.props; - - const backdropPorps = { - style: backdropStyle, - onClick: backdrop === true ? this.handleBackdropClick : undefined - }; - - if (transition) { - return ( - - {(props, ref) => { - const { className, ...rest } = props; - return ( -
    - ); - }} - - ); - } - - return
    ; - } - - render() { - const { - children, - transition: Transition, - backdrop, - dialogTransitionTimeout, - style, - className, - container, - animationProps, - onExit, - onExiting, - onEnter, - onEntering, - onEntered, - ...rest - } = this.props; - - const show = !!rest.show; - const mountModal = show || (Transition && !this.state.exited); - - if (!mountModal) { - return null; - } - - let dialog = children; - - if (Transition) { - dialog = ( - - {dialog} - - ); - } - - return ( - -
    - {backdrop && this.renderBackdrop()} - {dialog} -
    -
    - ); - } -} - -export default BaseModal; diff --git a/src/Modal/Modal.d.ts b/src/Modal/Modal.d.ts deleted file mode 100644 index 34ebe41514..0000000000 --- a/src/Modal/Modal.d.ts +++ /dev/null @@ -1,78 +0,0 @@ -import * as React from 'react'; - -import { StandardProps, TypeAttributes, AnimationEventProps } from '../@types/common'; -import ModalBody from './ModalBody'; -import ModalHeader from './ModalHeader'; -import ModalTitle from './ModalTitle'; -import ModalFooter from './ModalFooter'; - -export interface ModalProps extends StandardProps, AnimationEventProps { - /** A modal can have different sizes */ - size?: TypeAttributes.Size; - - /** Primary content */ - children?: React.ReactNode; - - /** CSS class applied to Dialog DOM nodes */ - dialogClassName?: string; - - /** - * Add an optional extra class name to .modal-backdrop - * It could end up looking like class="modal-backdrop foo-modal-backdrop in" - */ - backdropClassName?: string; - - /** CSS style applied to dialog DOM nodes */ - dialogStyle?: React.CSSProperties; - - /** CSS style applied to backdrop DOM nodes */ - backdropStyle?: React.CSSProperties; - - /** Show modal */ - show?: boolean; - - /** Full screen */ - full?: boolean; - - /** - * When set to true, the Modal will display the background when it is opened. - * Clicking on the background will close the Modal. If you do not want to close the Modal, - * set it to 'static'. - */ - backdrop?: boolean | 'static'; - - /** Close Modal when esc key is pressed */ - keyboard?: boolean; - - /** - * When set to true, the Modal is opened and is automatically focused on its own, - * accessible to screen readers - */ - autoFocus?: boolean; - - /** - * When set to true, Modal will prevent the focus from leaving when opened, - * making it easier for the secondary screen reader to access - */ - enforceFocus?: boolean; - - /** You can use a custom element type for Dialog */ - dialogAs?: React.ElementType; - - /** Called when Modal is displayed */ - onShow?: () => void; - - /** Called when Modal is closed */ - onHide?: (event: React.SyntheticEvent) => void; -} - -interface ModalComponent extends React.ComponentClass { - Body: typeof ModalBody; - Header: typeof ModalHeader; - Title: typeof ModalTitle; - Footer: typeof ModalFooter; -} - -declare const Modal: ModalComponent; - -export default Modal; diff --git a/src/Modal/Modal.tsx b/src/Modal/Modal.tsx index 62080d6031..bf6e917bc2 100644 --- a/src/Modal/Modal.tsx +++ b/src/Modal/Modal.tsx @@ -1,272 +1,204 @@ -import * as React from 'react'; +import React, { useRef, useMemo, useState, useEffect, useCallback } from 'react'; import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import _ from 'lodash'; -import setStatic from 'recompose/setStatic'; -import bindElementResize, { unbind as unbindElementResize } from 'element-resize-event'; -import BaseModal from './BaseModal'; +import pick from 'lodash/pick'; +import BaseModal, { BaseModalProps, modalPropTypes } from '../Overlay/Modal'; import Bounce from '../Animation/Bounce'; -import { on, getHeight } from 'dom-lib'; -import { prefix, defaultProps, createChainedFunction } from '../utils'; +import { createChainedFunction, useClassNames, mergeRefs, SIZE } from '../utils'; import ModalDialog, { modalDialogPropTypes } from './ModalDialog'; import ModalBody from './ModalBody'; import ModalHeader from './ModalHeader'; import ModalTitle from './ModalTitle'; import ModalFooter from './ModalFooter'; -import { ModalProps } from './Modal.d'; -import { SIZE } from '../constants'; -import ModalContext from './ModalContext'; -import mergeRefs from '../utils/mergeRefs'; +import helper from '../DOMHelper'; +import { useBodyStyles } from './utils'; +import { TypeAttributes, RsRefForwardingComponent } from '../@types/common'; -const BACKDROP_TRANSITION_DURATION = 150; +export interface ModalProps extends BaseModalProps { + /** A modal can have different sizes */ + size?: TypeAttributes.Size; -interface ModalState { - bodyStyles?: React.CSSProperties; -} - -class Modal extends React.Component { - static propTypes = { - classPrefix: PropTypes.string, - size: PropTypes.oneOf(SIZE), - container: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), - onRendered: PropTypes.func, - className: PropTypes.string, - children: PropTypes.node, - dialogClassName: PropTypes.string, - backdropClassName: PropTypes.string, - style: PropTypes.object, - dialogStyle: PropTypes.object, - backdropStyle: PropTypes.object, - show: PropTypes.bool, - full: PropTypes.bool, - backdrop: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), - keyboard: PropTypes.bool, - transition: PropTypes.elementType, - dialogTransitionTimeout: PropTypes.number, - backdropTransitionTimeout: PropTypes.number, - autoFocus: PropTypes.bool, - enforceFocus: PropTypes.bool, - overflow: PropTypes.bool, - drawer: PropTypes.bool, - dialogAs: PropTypes.elementType, - animation: PropTypes.any, - animationProps: PropTypes.object, - animationTimeout: PropTypes.number, - onEscapeKeyUp: PropTypes.func, - onBackdropClick: PropTypes.func, - onShow: PropTypes.func, - onHide: PropTypes.func, - onEnter: PropTypes.func, - onEntering: PropTypes.func, - onEntered: PropTypes.func, - onExit: PropTypes.func, - onExiting: PropTypes.func, - onExited: PropTypes.func - }; - - static defaultProps = { - size: 'sm', - backdrop: true, - keyboard: true, - autoFocus: true, - enforceFocus: true, - animation: Bounce, - animationTimeout: 300, - dialogAs: ModalDialog, - overflow: true - }; - - dialogElement: HTMLDivElement; + /** Set the duration of the animation */ + animationTimeout: number; - // for test - modalRef: React.Ref; + /** Set an animation effect for Modal, the default is Bounce. */ + animation: React.ElementType; - constructor(props) { - super(props); - this.state = { - bodyStyles: {} - }; + /** CSS class applied to Dialog DOM nodes */ + dialogClassName?: string; - this.modalRef = React.createRef(); - } - - componentWillUnmount() { - this.destroyEvent(); - } - - getBodyStylesByDialog(dialogElement?: HTMLElement) { - const { overflow, drawer } = this.props; - const node = dialogElement || this.dialogElement; - const scrollHeight = node ? node.scrollHeight : 0; - - if (!overflow) { - return {}; - } + /** CSS style applied to dialog DOM nodes */ + dialogStyle?: React.CSSProperties; - const bodyStyles: React.CSSProperties = { - overflow: 'auto' - }; + /** Full screen */ + full?: boolean; - if (node) { - // default margin - let headerHeight = 46; - let footerHeight = 46; - let contentHeight = 30; + /** You can use a custom element type for Dialog */ + dialogAs?: React.ElementType; - const headerDOM = node.querySelector(`.${this.addPrefix('header')}`); - const footerDOM = node.querySelector(`.${this.addPrefix('footer')}`); - const contentDOM = node.querySelector(`.${this.addPrefix('content')}`); + /** Automatically sets the height when the body content is too long. */ + overflow: boolean; - headerHeight = headerDOM ? getHeight(headerDOM) + headerHeight : headerHeight; - footerHeight = footerDOM ? getHeight(footerDOM) + headerHeight : headerHeight; - contentHeight = contentDOM ? getHeight(contentDOM) + contentHeight : contentHeight; - - if (drawer) { - bodyStyles.height = contentHeight - (headerHeight + footerHeight); - } else { - /** - * Header height + Footer height + Dialog margin - */ - const excludeHeight = headerHeight + footerHeight + 60; - const bodyHeight = getHeight(window) - excludeHeight; - const maxHeight = scrollHeight >= bodyHeight ? bodyHeight : scrollHeight; - bodyStyles.maxHeight = maxHeight; - } - } - - return bodyStyles; - } - - windowResizeListener = null; - contentElement = null; - getBodyStyles = () => { - return this.state.bodyStyles; - }; - bindDialogRef = ref => { - this.dialogElement = ref; - }; - - handleShow = () => { - const dialogElement = this.dialogElement; + /** Render Modal as Drawer */ + drawer: boolean; +} - this.updateModalStyles(dialogElement); - this.contentElement = dialogElement.querySelector(`.${this.addPrefix('content')}`); - this.windowResizeListener = on(window, 'resize', this.handleResize); - bindElementResize(this.contentElement, this.handleResize); - }; - handleShowing = () => { - this.updateModalStyles(this.dialogElement); - }; - handleHide = () => { - this.destroyEvent(); - }; - handleDialogClick = (event: React.MouseEvent) => { - if (event.target !== event.currentTarget) { - return; - } +const defaultProps: Partial = { + classPrefix: 'modal', + size: 'sm', + animation: Bounce, + animationTimeout: 300, + dialogAs: ModalDialog, + overflow: true +}; + +export interface ModalContextProps { + /** Pass the close event callback to the header close button. */ + onModalClose: (event: React.MouseEvent) => void; + + /** Pass the latest style to body. */ + getBodyStyles?: () => React.CSSProperties; +} - this.props?.onHide?.(event); - }; +export const ModalContext = React.createContext(null); - handleResize = () => { - this.updateModalStyles(this.dialogElement); - }; +interface ModalComponent extends RsRefForwardingComponent<'div', ModalProps> { + Body?: typeof ModalBody; + Header?: typeof ModalHeader; + Title?: typeof ModalTitle; + Footer?: typeof ModalFooter; + Dialog?: typeof ModalDialog; +} - destroyEvent() { - this.windowResizeListener?.off?.(); - if (this.contentElement) { - unbindElementResize(this.contentElement); +const Modal: ModalComponent = React.forwardRef((props: ModalProps, ref) => { + const { + className, + children, + classPrefix, + dialogClassName, + backdropClassName, + dialogStyle, + animation, + open, + size, + full, + dialogAs: Dialog, + animationProps, + animationTimeout, + overflow, + drawer, + onClose, + onEntered, + onEntering, + onExited, + backdrop, + ...rest + } = props; + + const inClass = { in: open && !animation }; + const { merge, prefix } = useClassNames(classPrefix); + const classes = merge(className, prefix(size, { full })); + + const dialogRef = useRef(); + const transitionEndListener = useRef<{ off: () => void }>(); + + // The style of the Modal body will be updated with the size of the window or container. + const [bodyStyles, onChangeBodyStyles, onDestroyEvents] = useBodyStyles(dialogRef, { + overflow, + drawer, + prefix + }); + + const modalContextValue = useMemo( + () => ({ + onModalClose: onClose, + getBodyStyles: () => bodyStyles + }), + [onClose, bodyStyles] + ); + + const [shake, setShake] = useState(false); + const handleBackdropClick = useCallback(() => { + // When the value of `backdrop` is `static`, a jitter animation will be added to the dialog when clicked. + if (backdrop === 'static') { + setShake(true); + console.log('vvvv', dialogRef.current); + transitionEndListener.current = helper.on(dialogRef.current, helper.transition().end, () => { + console.log('----vvvv--'); + setShake(false); + }); } - } - - updateModalStyles(dialogElement: HTMLElement) { - this.setState({ bodyStyles: this.getBodyStylesByDialog(dialogElement) }); - } - - addPrefix = (name: string) => prefix(this.props.classPrefix)(name); - - render() { - const { - className, - children, - dialogClassName, - backdropClassName, - dialogStyle, - animation, - classPrefix, - show, - size, - full, - dialogAs, - animationProps, - animationTimeout, - onHide, - ...rest - } = this.props; - - const inClass = { in: show && !animation }; - const Dialog: React.ElementType = dialogAs; - - const classes = classNames(this.addPrefix(size), className, { - [this.addPrefix('full')]: full - }); - - return ( - { + onDestroyEvents(); + transitionEndListener.current?.off(); + }, [onDestroyEvents]); + + useEffect(() => { + transitionEndListener.current?.off(); + }, []); + + return ( + + - - {(transitionProps, ref) => { - const { className: transitionClassName, ...transitionRest } = transitionProps; - return ( - - {children} - - ); - }} - - - ); - } -} - -const EnhancedModal = defaultProps({ - classPrefix: 'modal' -})(Modal); - -setStatic('Body', ModalBody)(EnhancedModal); -setStatic('Header', ModalHeader)(EnhancedModal); -setStatic('Title', ModalTitle)(EnhancedModal); -setStatic('Footer', ModalFooter)(EnhancedModal); -setStatic('Dialog', ModalDialog)(EnhancedModal); - -export default EnhancedModal; + {(transitionProps, transitionRef) => { + const { className: transitionClassName, ...transitionRest } = transitionProps; + return ( + + {children} + + ); + }} + + + ); +}); + +Modal.Body = ModalBody; +Modal.Header = ModalHeader; +Modal.Title = ModalTitle; +Modal.Footer = ModalFooter; +Modal.Dialog = ModalDialog; + +Modal.displayName = 'Modal'; +Modal.defaultProps = defaultProps; +Modal.propTypes = { + ...modalPropTypes, + animation: PropTypes.any, + animationTimeout: PropTypes.number, + classPrefix: PropTypes.string, + dialogClassName: PropTypes.string, + size: PropTypes.oneOf(SIZE), + dialogStyle: PropTypes.object, + dialogAs: PropTypes.elementType, + full: PropTypes.bool, + overflow: PropTypes.bool, + drawer: PropTypes.bool +}; + +export default Modal; diff --git a/src/Modal/ModalBody.d.ts b/src/Modal/ModalBody.d.ts deleted file mode 100644 index d8656826fd..0000000000 --- a/src/Modal/ModalBody.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as React from 'react'; - -import { StandardProps } from '../@types/common'; - -export interface ModalBodyProps extends StandardProps { - /** Primary content */ - children?: React.ReactNode; -} - -declare const ModalBody: React.ComponentType; - -export default ModalBody; diff --git a/src/Modal/ModalBody.tsx b/src/Modal/ModalBody.tsx index 74adfe6ced..8239b508e8 100644 --- a/src/Modal/ModalBody.tsx +++ b/src/Modal/ModalBody.tsx @@ -1,31 +1,46 @@ -import * as React from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; -import classNames from 'classnames'; +import { useClassNames } from '../utils'; +import { ModalContext } from './Modal'; +import { WithAsProps, RsRefForwardingComponent } from '../@types/common'; -import { defaultProps } from '../utils'; -import { ModalBodyProps } from './ModalBody.d'; -import ModalContext from './ModalContext'; +export type ModalBodyProps = WithAsProps; -class ModalBody extends React.Component { - static propTypes = { - classPrefix: PropTypes.string, - className: PropTypes.string - }; - render() { - const { classPrefix, className, style, ...props } = this.props; - const classes = classNames(classPrefix, className); +const defaultProps: Partial = { + as: 'div', + classPrefix: 'modal-body' +}; + +const ModalBody: RsRefForwardingComponent<'div', ModalBodyProps> = React.forwardRef( + (props: ModalBodyProps, ref) => { + const { as: Component, classPrefix, className, style, ...rest } = props; + const { withClassPrefix, merge } = useClassNames(classPrefix); + const classes = merge(className, withClassPrefix()); return ( {context => { - const bodyStyles = context ? context.getBodyStyles() : {}; - return
    ; + const bodyStyles = context?.getBodyStyles?.(); + return ( + + ); }} ); } -} +); -export default defaultProps({ - classPrefix: 'modal-body' -})(ModalBody); +ModalBody.displayName = 'ModalBody'; +ModalBody.defaultProps = defaultProps; +ModalBody.propTypes = { + as: PropTypes.elementType, + classPrefix: PropTypes.string, + className: PropTypes.string +}; + +export default ModalBody; diff --git a/src/Modal/ModalContext.d.ts b/src/Modal/ModalContext.d.ts deleted file mode 100644 index 6872918a6e..0000000000 --- a/src/Modal/ModalContext.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface ModalContextProps { - onModalHide: (event: React.MouseEvent) => void; - getBodyStyles?: () => React.CSSProperties; -} - -declare const ModalContext: React.Context; - -export default ModalContext; diff --git a/src/Modal/ModalContext.tsx b/src/Modal/ModalContext.tsx deleted file mode 100644 index a80b35a26a..0000000000 --- a/src/Modal/ModalContext.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { createContext } from '../utils'; -import { ModalContextProps } from './ModalContext.d'; - -const ModalContext = createContext(null); - -export default ModalContext; diff --git a/src/Modal/ModalDialog.d.ts b/src/Modal/ModalDialog.d.ts deleted file mode 100644 index e776919789..0000000000 --- a/src/Modal/ModalDialog.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as React from 'react'; - -import { StandardProps } from '../@types/common'; - -export interface ModalDialogProps extends StandardProps { - /** Primary content */ - children?: React.ReactNode; - - dialogClassName?: string; - dialogStyle?: React.CSSProperties; - - dialogRef?: (instance: HTMLDivElement) => void; -} - -declare const ModalDialog: React.ComponentType; - -export default ModalDialog; diff --git a/src/Modal/ModalDialog.tsx b/src/Modal/ModalDialog.tsx index 4d83d433b9..ab5bfc2411 100644 --- a/src/Modal/ModalDialog.tsx +++ b/src/Modal/ModalDialog.tsx @@ -1,74 +1,65 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import compose from 'recompose/compose'; +import { useClassNames, SIZE } from '../utils'; +import { WithAsProps, RsRefForwardingComponent, TypeAttributes } from '../@types/common'; -import { withStyleProps, defaultProps, prefix, refType } from '../utils'; -import { ModalDialogProps } from './ModalDialog.d'; -import mergeRefs from '../utils/mergeRefs'; +export interface ModalDialogProps extends WithAsProps { + /** A modal can have different sizes */ + size?: TypeAttributes.Size; + dialogClassName?: string; + dialogStyle?: React.CSSProperties; +} export const modalDialogPropTypes = { + size: PropTypes.oneOf(SIZE), className: PropTypes.string, classPrefix: PropTypes.string, dialogClassName: PropTypes.string, style: PropTypes.object, dialogStyle: PropTypes.object, - children: PropTypes.node, - dialogRef: refType + children: PropTypes.node }; -class ModalDialog extends React.Component { - static propTypes = modalDialogPropTypes; +const defaultProps: Partial = { + as: 'div', + classPrefix: 'modal' +}; - htmlElement: HTMLDivElement = null; - getHTMLElement() { - return this.htmlElement; - } - bindHtmlRef = ref => { - this.htmlElement = ref; - }; - render() { +const ModalDialog: RsRefForwardingComponent<'div', ModalDialogProps> = React.forwardRef( + (props: ModalDialogProps, ref) => { const { + as: Component, style, children, dialogClassName, dialogStyle, classPrefix, className, - dialogRef, - ...props - } = this.props; + size, + ...rest + } = props; const modalStyle = { display: 'block', ...style }; - const addPrefix = prefix(classPrefix); - const dialogClasses = classNames(addPrefix('dialog'), dialogClassName); + const { merge, withClassPrefix, prefix } = useClassNames(classPrefix); + const classes = merge(className, withClassPrefix(size)); + const dialogClasses = merge(dialogClassName, prefix('dialog')); return ( -
    -
    -
    {children}
    + +
    +
    {children}
    -
    + ); } -} +); + +ModalDialog.displayName = 'ModalDialog'; +ModalDialog.defaultProps = defaultProps; +ModalDialog.propTypes = modalDialogPropTypes; -export default compose( - withStyleProps({ - hasSize: true - }), - defaultProps({ - classPrefix: 'modal' - }) -)(ModalDialog); +export default ModalDialog; diff --git a/src/Modal/ModalFooter.d.ts b/src/Modal/ModalFooter.d.ts deleted file mode 100644 index 42739d252e..0000000000 --- a/src/Modal/ModalFooter.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as React from 'react'; - -import { StandardProps } from '../@types/common'; - -export interface ModalFooterProps extends StandardProps { - /** Primary content */ - children?: React.ReactNode; -} - -declare const ModalFooter: React.ComponentType; - -export default ModalFooter; diff --git a/src/Modal/ModalFooter.tsx b/src/Modal/ModalFooter.tsx index dd5fdd7c68..ba567a4a1b 100644 --- a/src/Modal/ModalFooter.tsx +++ b/src/Modal/ModalFooter.tsx @@ -1,22 +1,6 @@ -import * as React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; +import createComponent, { ComponentProps } from '../utils/createComponent'; +export type ModalFooterProps = ComponentProps; -import { defaultProps } from '../utils'; -import { ModalFooterProps } from './ModalFooter.d'; +const ModalFooter = createComponent({ name: 'ModalFooter' }); -class ModalFooter extends React.Component { - static propTypes = { - classPrefix: PropTypes.string, - className: PropTypes.string - }; - render() { - const { classPrefix, className, ...props } = this.props; - const classes = classNames(classPrefix, className); - return
    ; - } -} - -export default defaultProps({ - classPrefix: 'modal-footer' -})(ModalFooter); +export default ModalFooter; diff --git a/src/Modal/ModalHeader.d.ts b/src/Modal/ModalHeader.d.ts deleted file mode 100644 index 5641c11188..0000000000 --- a/src/Modal/ModalHeader.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as React from 'react'; - -import { StandardProps } from '../@types/common'; - -export interface ModalHeaderProps extends StandardProps { - /** Primary content */ - children?: React.ReactNode; - - /** Display close button */ - closeButton?: boolean; - - /** Called when Modal is hidden */ - onHide?: (event: React.MouseEvent) => void; -} - -declare const ModalHeader: React.ComponentType; - -export default ModalHeader; diff --git a/src/Modal/ModalHeader.tsx b/src/Modal/ModalHeader.tsx index 3a7b7bd9ab..4530bc2198 100644 --- a/src/Modal/ModalHeader.tsx +++ b/src/Modal/ModalHeader.tsx @@ -1,55 +1,70 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -import { createChainedFunction, defaultProps, prefix } from '../utils'; -import ModalContext from './ModalContext'; -import { ModalHeaderProps } from './ModalHeader.d'; - -class ModalHeader extends React.Component { - static propTypes = { - classPrefix: PropTypes.string, - className: PropTypes.string, - closeButton: PropTypes.bool, - children: PropTypes.node, - onHide: PropTypes.func - }; - static defaultProps = { - closeButton: true - }; - - render() { - const { classPrefix, onHide, className, closeButton, children, ...props } = this.props; - const classes = classNames(classPrefix, className); - const addPrefix = prefix(classPrefix); +import { createChainedFunction, useClassNames } from '../utils'; +import { ModalContext } from './Modal'; +import CloseButton from '../CloseButton'; +import { WithAsProps, RsRefForwardingComponent } from '../@types/common'; + +export interface ModalHeaderProps extends WithAsProps { + /** Primary content */ + children?: React.ReactNode; + + /** Display close button */ + closeButton?: boolean; + + /** Called when Modal is hidden */ + onClose?: (event: React.MouseEvent) => void; +} + +const defaultProps: Partial = { + as: 'div', + closeButton: true, + classPrefix: 'modal-header' +}; + +const ModalHeader: RsRefForwardingComponent<'div', ModalHeaderProps> = React.forwardRef( + (props: ModalHeaderProps, ref) => { + const { + as: Component, + classPrefix, + className, + closeButton, + children, + onClose, + ...rest + } = props; + const { merge, withClassPrefix, prefix } = useClassNames(classPrefix); + const classes = merge(className, withClassPrefix()); const buttonElement = ( {context => ( - + )} ); return ( -
    + {closeButton && buttonElement} {children} -
    + ); } -} +); -export default defaultProps({ - classPrefix: 'modal-header' -})(ModalHeader); +ModalHeader.displayName = 'ModalHeader'; +ModalHeader.defaultProps = defaultProps; +ModalHeader.propTypes = { + as: PropTypes.elementType, + classPrefix: PropTypes.string, + className: PropTypes.string, + closeButton: PropTypes.bool, + children: PropTypes.node, + onHide: PropTypes.func +}; + +export default ModalHeader; diff --git a/src/Modal/ModalTitle.d.ts b/src/Modal/ModalTitle.d.ts deleted file mode 100644 index 9e36b66568..0000000000 --- a/src/Modal/ModalTitle.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as React from 'react'; - -import { StandardProps } from '../@types/common'; - -export interface ModalTitleProps extends StandardProps { - /** Primary content */ - children?: React.ReactNode; -} - -declare const ModalTitle: React.ComponentType; - -export default ModalTitle; diff --git a/src/Modal/ModalTitle.tsx b/src/Modal/ModalTitle.tsx index 1e94631280..d7e330735e 100644 --- a/src/Modal/ModalTitle.tsx +++ b/src/Modal/ModalTitle.tsx @@ -1,26 +1,6 @@ -import * as React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import { defaultProps } from '../utils'; -import { ModalTitleProps } from './ModalTitle.d'; +import createComponent, { ComponentProps } from '../utils/createComponent'; +export type ModalTitleProps = ComponentProps; -class ModalTitle extends React.Component { - static propTypes = { - className: PropTypes.string, - classPrefix: PropTypes.string, - children: PropTypes.node - }; - render() { - const { className, classPrefix, children, ...props } = this.props; - const classes = classNames(classPrefix, className); - return ( -

    - {children} -

    - ); - } -} +const ModalTitle = createComponent({ name: 'ModalTitle', componentAs: 'h4' }); -export default defaultProps({ - classPrefix: 'modal-title' -})(ModalTitle); +export default ModalTitle; diff --git a/src/Modal/index.d.ts b/src/Modal/index.d.ts deleted file mode 100644 index 5e286081c2..0000000000 --- a/src/Modal/index.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './Modal'; -export * from './Modal'; diff --git a/src/Modal/index.tsx b/src/Modal/index.tsx index 8144af51b5..d2ead081a0 100644 --- a/src/Modal/index.tsx +++ b/src/Modal/index.tsx @@ -1,3 +1,8 @@ import Modal from './Modal'; - +export type { ModalProps } from './Modal'; +export type { ModalBodyProps } from './ModalBody'; +export type { ModalDialogProps } from './ModalDialog'; +export type { ModalFooterProps } from './ModalFooter'; +export type { ModalHeaderProps } from './ModalHeader'; +export type { ModalTitleProps } from './ModalTitle'; export default Modal; diff --git a/src/Modal/styles/common.less b/src/Modal/styles/common.less index 8925707ced..e6bdc87050 100644 --- a/src/Modal/styles/common.less +++ b/src/Modal/styles/common.less @@ -60,6 +60,14 @@ } } +.@{ns}modal-dialog { + transition: transform 0.3s ease-out; +} + +.@{ns}modal-shake .@{ns}modal-dialog { + transform: scale(1.02); +} + // Actual modal .@{ns}modal-content { position: relative; diff --git a/src/Modal/test/ModalHeaderSpec.js b/src/Modal/test/ModalHeaderSpec.js index f6677b7027..e664cb270c 100644 --- a/src/Modal/test/ModalHeaderSpec.js +++ b/src/Modal/test/ModalHeaderSpec.js @@ -19,11 +19,11 @@ describe('ModalHeader', () => { assert.ok(!instance.querySelector('button')); }); - it('Should call onHide callback', done => { + it('Should call onClose callback', done => { const doneOp = () => { done(); }; - const instance = getDOMNode(); + const instance = getDOMNode(); ReactTestUtils.Simulate.click(instance.querySelector('.rs-modal-header-close')); }); diff --git a/src/Modal/test/ModalSpec.js b/src/Modal/test/ModalSpec.js index 4098ffd695..c1384fa685 100644 --- a/src/Modal/test/ModalSpec.js +++ b/src/Modal/test/ModalSpec.js @@ -7,12 +7,12 @@ import Modal from '../Modal'; describe('Modal', () => { it('Should render the modal content', () => { const instance = getInstance( - +

    message

    ); - assert.equal(instance.modalRef.current.getDialogElement().querySelectorAll('p').length, 1); + assert.equal(instance.querySelectorAll('p').length, 1); }); it('Should close the modal when the modal dialog is clicked', done => { @@ -20,40 +20,38 @@ describe('Modal', () => { done(); }; - const instance = getInstance(); - const dialog = instance.modalRef.current.getDialogElement(); + const instance = getInstance(); - ReactTestUtils.Simulate.click(dialog); + ReactTestUtils.Simulate.click(instance.querySelector('.rs-modal-backdrop')); }); it('Should not close the modal when the "static" dialog is clicked', () => { - const onHideSpy = sinon.spy(); - const instance = getInstance(); - const dialog = instance.modalRef.current.getDialogElement(); - ReactTestUtils.Simulate.click(dialog); + const onCloseSpy = sinon.spy(); + const instance = getInstance(); + ReactTestUtils.Simulate.click(instance); - assert.ok(!onHideSpy.calledOnce); + assert.ok(!onCloseSpy.calledOnce); }); it('Should be automatic height', () => { const instance = getInstance( - + ); - assert.ok(instance.dialogElement.querySelector('.rs-modal-body').style.overflow, 'auto'); + assert.ok(instance.querySelector('.rs-modal-body').style.overflow, 'auto'); }); - it('Should call onHide callback', done => { + it('Should call onClose callback', done => { const doneOp = () => { done(); }; const instance = getInstance( - + ); - const closeButton = instance.dialogElement.querySelector('.rs-modal-header-close'); + const closeButton = instance.querySelector('.rs-modal-header-close'); ReactTestUtils.Simulate.click(closeButton); }); @@ -62,28 +60,28 @@ describe('Modal', () => { done(); }; + const ref = React.createRef(); + class Demo extends React.Component { constructor(props) { super(props); this.state = { - show: true + open: true }; this.handleClose = this.handleClose.bind(this); } handleClose() { this.setState({ - show: false + open: false }); } render() { return ( { - this.demo = ref; - }} - show={this.state.show} - onHide={this.handleClose} + ref={ref} + open={this.state.open} + onClose={this.handleClose} onExited={doneOp} > @@ -91,24 +89,24 @@ describe('Modal', () => { ); } } - const instance = getInstance(); - const closeButton = instance.demo.dialogElement.querySelector('.rs-modal-header-close'); + getInstance(); + const closeButton = ref.current.querySelector('.rs-modal-header-close'); ReactTestUtils.Simulate.click(closeButton); }); it('Should have a custom className', () => { - const instance = getInstance(); - assert.ok(instance.dialogElement.className.match(/\bcustom\b/)); + const instance = getInstance(); + assert.ok(instance.querySelector('.custom')); }); it('Should have a custom style', () => { const fontSize = '12px'; - const instance = getInstance(); - assert.equal(instance.dialogElement.style.fontSize, fontSize); + const instance = getInstance(); + assert.equal(instance.style.fontSize, fontSize); }); it('Should have a custom className prefix', () => { - const instance = getInstance(); - assert.ok(instance.dialogElement.className.match(/\bcustom-prefix\b/)); + const instance = getInstance(); + assert.ok(instance.className.match(/\bcustom-prefix\b/)); }); }); diff --git a/src/Modal/test/ModalStyleSpec.js b/src/Modal/test/ModalStyleSpec.js index 8ac4b84fc8..6c0085cf6b 100644 --- a/src/Modal/test/ModalStyleSpec.js +++ b/src/Modal/test/ModalStyleSpec.js @@ -1,15 +1,15 @@ import React from 'react'; import ReactDOM from 'react-dom'; import Modal from '../index'; -import { createTestContainer, getStyle, getDOMNode, toRGB } from '@test/testUtils'; +import { createTestContainer, getStyle, toRGB } from '@test/testUtils'; import '../styles/index'; describe('Modal styles', () => { it('Should render the correct styles', () => { const instanceRef = React.createRef(); - ReactDOM.render(, createTestContainer()); - const dom = getDOMNode(instanceRef.current); + ReactDOM.render(, createTestContainer()); + const dom = instanceRef.current; const backdropDom = dom.querySelector('.rs-modal-backdrop'); const drawerDom = dom.querySelector('.rs-modal'); assert.equal(getStyle(dom, 'position'), 'fixed', 'Modal wrapper position'); diff --git a/src/Modal/utils.ts b/src/Modal/utils.ts new file mode 100644 index 0000000000..cf76b1a8d7 --- /dev/null +++ b/src/Modal/utils.ts @@ -0,0 +1,73 @@ +import { useState, useRef, useCallback, useEffect } from 'react'; +import bindElementResize, { unbind as unbindElementResize } from 'element-resize-event'; +import helper from '../DOMHelper'; + +export const useBodyStyles = ( + ref: React.RefObject, + options: { overflow: boolean; drawer: boolean; prefix: (...classes: any) => string } +): [React.CSSProperties, () => void, () => void] => { + const [bodyStyles, setBodyStyles] = useState({}); + const { overflow, drawer, prefix } = options; + const windowResizeListener = useRef(); + const contentElement = useRef(); + + const updateBodyStyles = useCallback(() => { + const dialog = ref.current; + const scrollHeight = dialog ? dialog.scrollHeight : 0; + + const styles: React.CSSProperties = { + overflow: 'auto' + }; + + if (dialog) { + // default margin + let headerHeight = 46; + let footerHeight = 46; + let contentHeight = 30; + + const headerDOM = dialog.querySelector(`.${prefix('header')}`); + const footerDOM = dialog.querySelector(`.${prefix('footer')}`); + const contentDOM = dialog.querySelector(`.${prefix('content')}`); + + headerHeight = headerDOM ? helper.getHeight(headerDOM) + headerHeight : headerHeight; + footerHeight = footerDOM ? helper.getHeight(footerDOM) + headerHeight : headerHeight; + contentHeight = contentDOM ? helper.getHeight(contentDOM) + contentHeight : contentHeight; + + if (drawer) { + styles.height = contentHeight - (headerHeight + footerHeight); + } else { + /** + * Header height + Footer height + Dialog margin + */ + const excludeHeight = headerHeight + footerHeight + 60; + const bodyHeight = helper.getHeight(window) - excludeHeight; + const maxHeight = scrollHeight >= bodyHeight ? bodyHeight : scrollHeight; + styles.maxHeight = maxHeight; + } + } + + setBodyStyles(styles); + }, [drawer, prefix, ref]); + + const onDestroyEvents = useCallback(() => { + windowResizeListener.current?.off?.(); + if (contentElement.current) { + unbindElementResize(contentElement.current); + } + }, []); + + const onChangeBodyStyles = useCallback(() => { + if (overflow) { + updateBodyStyles(); + contentElement.current = ref.current?.querySelector(`.${prefix('content')}`); + windowResizeListener.current = helper.on(window, 'resize', updateBodyStyles); + bindElementResize(contentElement.current, updateBodyStyles); + } + }, [overflow, prefix, ref, updateBodyStyles]); + + useEffect(() => { + onDestroyEvents(); + }, [onDestroyEvents]); + + return [bodyStyles, onChangeBodyStyles, onDestroyEvents]; +}; diff --git a/src/Notification/Notification.tsx b/src/Notification/Notification.tsx index cb01409a62..8574afcd7a 100644 --- a/src/Notification/Notification.tsx +++ b/src/Notification/Notification.tsx @@ -1,9 +1,8 @@ import React, { useCallback } from 'react'; import PropTypes from 'prop-types'; import Icon from '../Icon'; -import { useClassNames, useTimeout } from '../utils'; +import { useClassNames, useTimeout, STATUS_ICON_NAMES } from '../utils'; import { WithAsProps, RsRefForwardingComponent } from '../@types/common'; -import { STATUS_ICON_NAMES } from '../constants'; import CloseButton from '../CloseButton'; export type MessageType = 'info' | 'success' | 'warning' | 'error'; diff --git a/src/Overlay/BaseOverlay.tsx b/src/Overlay/BaseOverlay.tsx deleted file mode 100644 index 7f8fbf38b8..0000000000 --- a/src/Overlay/BaseOverlay.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import * as React from 'react'; -import Portal from '../Portal'; -import Position from './Position'; -import RootCloseWrapper from './RootCloseWrapper'; -import { TypeAttributes, AnimationEventProps } from '../@types/common'; - -export interface BaseOverlayProps extends AnimationEventProps { - container?: HTMLElement | (() => HTMLElement); - onRendered?: any; - children?: React.ReactNode; - className?: string; - containerPadding?: number; - placement?: TypeAttributes.Placement; - shouldUpdatePosition?: boolean; - preventOverflow?: boolean; - show?: boolean; - rootClose?: boolean; - transition?: React.ElementType; - positionRef?: React.Ref; - target?: () => HTMLElement; - onHide?: () => void; -} - -interface BaseOverlayState { - exited?: boolean; -} - -class BaseOverlay extends React.Component { - constructor(props: BaseOverlayProps) { - super(props); - this.state = { exited: !props.show }; - } - - static getDerivedStateFromProps(nextProps: BaseOverlayProps) { - if (nextProps.show) { - return { exited: false }; - } else if (!nextProps.transition) { - return { exited: true }; - } - return null; - } - - handleHidden = (args: any) => { - this.setState({ exited: true }); - this.props.onExited?.(args); - }; - - render() { - const { - container, - containerPadding, - target, - placement, - shouldUpdatePosition, - rootClose, - children, - transition: Transition, - show, - onHide, - positionRef, - preventOverflow, - ...props - } = this.props; - - const mountOverlay = show || (Transition && !this.state.exited); - - if (!mountOverlay) { - return null; - } - - let child = children; - - const positionProps = { - container, - containerPadding, - target, - placement, - shouldUpdatePosition, - preventOverflow - }; - - child = ( - - {child} - - ); - - if (Transition) { - const { onExit, onExiting, onEnter, onEntering, onEntered } = props; - child = ( - - {child} - - ); - } - - if (rootClose) { - child = ( - - {child} - - ); - } - - return {child}; - } -} - -export default BaseOverlay; diff --git a/src/Overlay/Modal.tsx b/src/Overlay/Modal.tsx new file mode 100644 index 0000000000..bf35781dc4 --- /dev/null +++ b/src/Overlay/Modal.tsx @@ -0,0 +1,329 @@ +import React, { useRef, useEffect, useState, useCallback } from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import helper from '../DOMHelper'; +import ModalManager from './ModalManager'; +import Fade from '../Animation/Fade'; +import { animationPropTypes } from '../Animation/utils'; +import { mergeRefs, getDOMNode, usePortal, createChainedFunction } from '../utils'; +import { WithAsProps, AnimationEventProps, RsRefForwardingComponent } from '../@types/common'; + +export interface BaseModalProps extends WithAsProps, AnimationEventProps { + /** Animation-related properties */ + animationProps?: any; + + /** Primary content */ + children?: React.ReactNode; + + /** + * Add an optional extra class name to .modal-backdrop + * It could end up looking like class="modal-backdrop foo-modal-backdrop in" + */ + backdropClassName?: string; + + /** CSS style applied to backdrop DOM nodes */ + backdropStyle?: React.CSSProperties; + + /** Open modal */ + open?: boolean; + + /** + * When set to true, the Modal will display the background when it is opened. + * Clicking on the background will close the Modal. If you do not want to close the Modal, + * set it to 'static'. + */ + backdrop?: boolean | 'static'; + + /** Close Modal when esc key is pressed */ + keyboard?: boolean; + + /** + * When set to true, the Modal is opened and is automatically focused on its own, + * accessible to screen readers + */ + autoFocus?: boolean; + + /** + * When set to true, Modal will prevent the focus from leaving when opened, + * making it easier for the secondary screen reader to access + */ + enforceFocus?: boolean; + + /** Called when Modal is displayed */ + onOpen?: () => void; + + /** Called when Modal is closed */ + onClose?: (event: React.SyntheticEvent) => void; +} + +interface ModalProps extends BaseModalProps { + children: (props, ref) => React.ReactElement; + container?: HTMLElement | (() => HTMLElement); + containerClassName?: string; + backdropTransitionTimeout?: number; + dialogTransitionTimeout?: number; + transition: React.ElementType; + onEscapeKeyUp?: React.KeyboardEventHandler; + onBackdropClick?: React.MouseEventHandler; +} + +const modalManager = new ModalManager(); + +const defaultProps: Partial = { + as: 'div', + backdrop: true, + keyboard: true, + autoFocus: true, + enforceFocus: true +}; + +const Modal: RsRefForwardingComponent<'div', ModalProps> = React.forwardRef( + (props: ModalProps, ref) => { + const { + as: Component, + children, + transition: Transition, + dialogTransitionTimeout, + style, + className, + container, + animationProps, + containerClassName, + keyboard, + enforceFocus, + backdrop, + backdropTransitionTimeout, + backdropStyle, + backdropClassName, + open, + autoFocus, + onBackdropClick, + onEscapeKeyUp, + onExit, + onExiting, + onExited, + onEnter, + onEntering, + onEntered, + onClose, + onOpen, + ...rest + } = props; + + const [exited, setExited] = useState(!open); + const { Portal } = usePortal({ container }); + + if (open) { + if (exited) setExited(false); + } else if (!Transition && !exited) { + setExited(true); + } + + const mountModal = open || (Transition && !exited); + + const rootRef = useRef(); + const lastFocus = useRef(); + + const isTopModal = () => { + return modalManager.isTopModal(rootRef.current); + }; + + const handleDocumentKeyUp = useCallback( + (event: React.KeyboardEvent) => { + if (keyboard && event.keyCode === 27 && isTopModal()) { + onEscapeKeyUp?.(event); + onClose?.(event); + } + }, + [keyboard, onEscapeKeyUp, onClose] + ); + + const checkForFocus = () => { + if (helper.canUseDOM) { + lastFocus.current = helper.activeElement() as HTMLElement; + } + }; + + const restoreLastFocus = () => { + if (lastFocus.current) { + lastFocus.current.focus?.(); + lastFocus.current = null; + } + }; + + const getDialogElement = () => { + return getDOMNode(rootRef.current) as HTMLElement; + }; + + const handleEnforceFocus = useCallback(() => { + if (!enforceFocus || !isTopModal()) { + return; + } + + const currentActiveElement = helper.activeElement(); + const dialog = getDialogElement(); + + if ( + dialog && + dialog !== currentActiveElement && + !helper.contains(dialog, currentActiveElement) + ) { + dialog.focus(); + } + }, [enforceFocus]); + + const handleBackdropClick = (event: React.MouseEvent) => { + if (event.target !== event.currentTarget) { + return; + } + + onBackdropClick?.(event); + + if (backdrop === true) { + onClose?.(event); + } + }; + const documentKeyupListener = useRef<{ off: () => void }>(); + const docusinListener = useRef<{ off: () => void }>(); + const handleOpen = useCallback(() => { + const dialog = getDialogElement(); + const containerElement = helper.getContainer(container, document.body); + modalManager.add(rootRef.current, containerElement, containerClassName); + + documentKeyupListener.current = helper.on(document, 'keydown', handleDocumentKeyUp); + docusinListener.current = helper.on(document, 'focus', handleEnforceFocus, true); + onOpen?.(); + checkForFocus(); + + if (autoFocus) { + dialog?.focus(); + } + }, [autoFocus, container, containerClassName, handleDocumentKeyUp, handleEnforceFocus, onOpen]); + + const handleClose = useCallback(() => { + modalManager.remove(rootRef.current); + documentKeyupListener.current?.off(); + docusinListener.current?.off(); + restoreLastFocus(); + }, []); + + useEffect(() => { + if (exited) { + handleClose(); + } else if (open) { + handleOpen(); + } + }, [open, exited, handleOpen, handleClose]); + + useEffect(() => { + return () => { + handleClose(); + }; + }, [handleClose]); + + const handleExited = useCallback(() => { + setExited(true); + }, []); + + if (!mountModal) { + return null; + } + + const renderBackdrop = () => { + const backdropPorps = { + style: backdropStyle, + onClick: handleBackdropClick + }; + + if (Transition) { + return ( + + {(fadeProps, ref) => { + const { className, ...rest } = fadeProps; + return ( +
    + ); + }} + + ); + } + + return
    ; + }; + + const dialogElement = Transition ? ( + + {children} + + ) : ( + children + ); + + return ( + + + {backdrop && renderBackdrop()} + {dialogElement} + + + ); + } +); + +export const modalPropTypes = { + as: PropTypes.elementType, + className: PropTypes.string, + backdropClassName: PropTypes.string, + style: PropTypes.object, + backdropStyle: PropTypes.object, + open: PropTypes.bool, + backdrop: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), + keyboard: PropTypes.bool, + autoFocus: PropTypes.bool, + enforceFocus: PropTypes.bool, + animationProps: PropTypes.object, + onOpen: PropTypes.func, + onClose: PropTypes.func +}; + +Modal.displayName = 'OverlayModal'; +Modal.defaultProps = defaultProps; +Modal.propTypes = { + ...animationPropTypes, + ...modalPropTypes, + children: PropTypes.func, + container: PropTypes.any, + containerClassName: PropTypes.string, + dialogTransitionTimeout: PropTypes.number, + backdropTransitionTimeout: PropTypes.number, + transition: PropTypes.any, + onEscapeKeyUp: PropTypes.func, + onBackdropClick: PropTypes.func +}; + +export default Modal; diff --git a/src/Modal/ModalManager.ts b/src/Overlay/ModalManager.ts similarity index 100% rename from src/Modal/ModalManager.ts rename to src/Overlay/ModalManager.ts diff --git a/src/Overlay/Overlay.tsx b/src/Overlay/Overlay.tsx index ade051963f..107e7c2aa8 100644 --- a/src/Overlay/Overlay.tsx +++ b/src/Overlay/Overlay.tsx @@ -1,51 +1,43 @@ -import * as React from 'react'; -import PropTypes from 'prop-types'; +import React, { useState, useRef, useCallback } from 'react'; import classNames from 'classnames'; - -import BaseOverlay, { BaseOverlayProps } from './BaseOverlay'; +import PropTypes from 'prop-types'; +import Position, { PositionProps } from './Position'; +import { TypeAttributes, AnimationEventProps } from '../@types/common'; +import { refType, mergeRefs, useRootClose } from '../utils'; import Fade from '../Animation/Fade'; -import refType from '../utils/refType'; -export interface OverlayProps extends BaseOverlayProps { - animation?: boolean; +export interface OverlayProps extends AnimationEventProps { + container?: HTMLElement | (() => HTMLElement); + children?: React.ReactElement | ((props: any, ref) => React.ReactElement); + childrenProps?: React.HTMLAttributes; + className?: string; + containerPadding?: number; + placement?: TypeAttributes.Placement; + preventOverflow?: boolean; + open?: boolean; + rootClose?: boolean; + transition?: React.ElementType; + triggerTarget?: React.RefObject; + onClose?: () => void; } -const Overlay = ({ animation = true, children, transition = Fade, ...rest }: OverlayProps) => { - let child = children as React.DetailedReactHTMLElement; - if (!animation) { - transition = undefined; - } - - if (!transition) { - child = React.Children.only(child); - child = React.cloneElement(child, { - className: classNames('in', child.props.className) - }); - } - - return ( - - {child} - - ); +const defaultProps: Partial = { + transition: Fade }; -Overlay.propTypes = { - animation: PropTypes.bool, - container: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), - children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), - onRendered: PropTypes.func, +export const overlayPropTypes = { + container: PropTypes.any, + children: PropTypes.any, + childrenProps: PropTypes.object, className: PropTypes.string, containerPadding: PropTypes.number, - placement: PropTypes.string, - shouldUpdatePosition: PropTypes.bool, + placement: PropTypes.any, preventOverflow: PropTypes.bool, - show: PropTypes.bool, + open: PropTypes.bool, rootClose: PropTypes.bool, - transition: PropTypes.elementType, - positionRef: refType, - target: PropTypes.func, - onHide: PropTypes.func, + transition: PropTypes.any, + triggerTarget: refType, + onClose: PropTypes.func, onEnter: PropTypes.func, onEntering: PropTypes.func, onEntered: PropTypes.func, @@ -54,4 +46,105 @@ Overlay.propTypes = { onExited: PropTypes.func }; +const Overlay = React.forwardRef((props: OverlayProps, ref) => { + const { + container, + containerPadding, + placement, + rootClose, + children, + childrenProps, + transition: Transition, + open, + preventOverflow, + triggerTarget, + onClose, + onExited, + onExit, + onExiting, + onEnter, + onEntering, + onEntered + } = props; + + const [exited, setExited] = useState(!open); + const overlayTarget = useRef(); + + if (open) { + if (exited) setExited(false); + } else if (!Transition && !exited) { + setExited(true); + } + + const mountOverlay = open || (Transition && !exited); + + const handleExited = useCallback( + (args: any) => { + setExited(true); + onExited?.(args); + }, + [onExited] + ); + + useRootClose(onClose, { triggerTarget, overlayTarget, disabled: !rootClose || !mountOverlay }); + + if (!mountOverlay) { + return null; + } + + const positionProps: PositionProps = { + container, + containerPadding, + triggerTarget, + placement, + preventOverflow + }; + + const childWithPosition = ( + + {(childProps, childRef) => { + // overlayTarget is the ref on the DOM of the Overlay. + if (typeof children === 'function') { + return children( + Object.assign(childProps, childrenProps), + mergeRefs(childRef, overlayTarget) + ); + } + + // Position will return coordinates and className + const { left, top, className } = childProps; + return React.cloneElement(children, { + ...childrenProps, + className: classNames(className, children.props.className), + style: { left, top, ...children.props.style }, + ref: mergeRefs(childRef, overlayTarget) + }); + }} + + ); + + if (Transition) { + return ( + + {childWithPosition} + + ); + } + + return childWithPosition; +}); + +Overlay.displayName = 'Overlay'; +Overlay.defaultProps = defaultProps; +Overlay.propTypes = overlayPropTypes; + export default Overlay; diff --git a/src/Overlay/OverlayTrigger.d.ts b/src/Overlay/OverlayTrigger.d.ts deleted file mode 100644 index 924e9bfc5e..0000000000 --- a/src/Overlay/OverlayTrigger.d.ts +++ /dev/null @@ -1,80 +0,0 @@ -import * as React from 'react'; -import { AnimationEventProps, StandardProps, TypeAttributes } from '../@types/common'; - -export type OverlayTriggerTrigger = 'click' | 'hover' | 'focus' | 'active' | 'none'; - -export interface TriggerProps extends AnimationEventProps, StandardProps { - /** Triggering events */ - trigger?: OverlayTriggerTrigger | OverlayTriggerTrigger[]; - - /** Display placement */ - placement?: TypeAttributes.Placement; - - /** Delay Time */ - delay?: number; - - /** Show delay Time */ - delayShow?: number; - - /** Hidden delay Time */ - delayHide?: number; - - /** Sets the rendering container */ - container?: HTMLElement | (() => HTMLElement); - - /** display element */ - speaker?: - | React.ReactElement - | ((props: any, ref: React.RefObject) => React.ReactElement); - - /** Prevent floating element overflow */ - preventOverflow?: boolean; - - /** Show speaker */ - open?: boolean; - - /** Whether mouse is allowed to enter the floating layer of popover, whose default value is false. */ - enterable?: boolean; - - /** Position of ref */ - positionRef?: React.Ref; - - /** Lose Focus callback function */ - onBlur?: () => void; - - /** Click on the callback function */ - onClick?: () => void; - - /** Callback function to get focus */ - onFocus?: () => void; - - /** Mouse leave callback function */ - onMouseOut?: () => void; - - /** Mouse over callback function */ - onMouseOver?: () => void; - - /** Callback fired when open component */ - onOpen?: () => void; - - /** Callback fired when close component */ - onClose?: () => void; -} - -export interface OverlayTriggerProps extends TriggerProps { - containerPadding?: number; - show?: boolean; - rootClose?: boolean; - onHide?: () => void; - transition?: React.ElementType; - animation?: React.ElementType | boolean; - delay?: number; - delayShow?: number; - delayHide?: number; - defaultOpen?: boolean; - open?: boolean; - disabled?: boolean; -} - -declare const OverlayTrigger: React.ComponentType; -export default OverlayTrigger; diff --git a/src/Overlay/OverlayTrigger.tsx b/src/Overlay/OverlayTrigger.tsx index 84fb3b90e5..90b4b1fda3 100644 --- a/src/Overlay/OverlayTrigger.tsx +++ b/src/Overlay/OverlayTrigger.tsx @@ -1,286 +1,359 @@ -import * as React from 'react'; +import React, { useRef, useEffect, useImperativeHandle, useCallback } from 'react'; import get from 'lodash/get'; -import pick from 'lodash/pick'; import isNil from 'lodash/isNil'; import { contains } from 'dom-lib'; import Overlay, { OverlayProps } from './Overlay'; -import createChainedFunction from '../utils/createChainedFunction'; +import { createChainedFunction, usePortal, useControlled } from '../utils'; import isOneOf from '../utils/isOneOf'; -import getDOMNode from '../utils/getDOMNode'; -import Portal from '../Portal'; -import { OverlayTriggerProps } from './OverlayTrigger.d'; -import { TypeAttributes } from '../@types/common'; -function onMouseEventHandler(handler: React.MouseEventHandler, event: React.MouseEvent) { - const target = event.currentTarget; - const related = event.relatedTarget || get(event, ['nativeEvent', 'toElement']); +import { AnimationEventProps, StandardProps, TypeAttributes } from '../@types/common'; +import { PositionInstance } from './Position'; +import { isUndefined } from 'lodash'; +export type OverlayTriggerTrigger = 'click' | 'hover' | 'focus' | 'active' | 'none'; - if ((!related || related !== target) && !contains(target, related)) { - handler(event); - } -} +function mergeEvents(events = {}, props = {}) { + const nextEvents = {}; -interface TriggerProps { - 'aria-describedby': string; - key: string; - onMouseOver?: React.MouseEventHandler; - onMouseOut?: React.MouseEventHandler; - onBlur?: React.MouseEventHandler; - onClick?: React.MouseEventHandler; - onFocus?: React.MouseEventHandler; + Object.keys(events).forEach(eventName => { + if (events[eventName]) { + nextEvents[eventName] = createChainedFunction(events[eventName], props?.[eventName]); + } + }); + return nextEvents; } -interface SpeakerProps { - placement: TypeAttributes.Placement | TypeAttributes.Placement4; - onMouseEnter?: () => void; - onMouseLeave?: () => void; -} +export interface OverlayTriggerProps extends StandardProps, AnimationEventProps { + /** Triggering events */ + trigger?: OverlayTriggerTrigger | OverlayTriggerTrigger[]; -interface OverlayTriggerState { - isOverlayShown?: boolean; - isOnSpeaker?: boolean; -} + /** Display placement */ + placement?: TypeAttributes.Placement; -class OverlayTrigger extends React.Component { - static defaultProps = { - trigger: ['hover', 'focus'], - delayHide: 200, - placement: 'bottomStart', - rootClose: true - }; + /** Delay time */ + delay?: number; - onMouseOverListener; - onMouseOutListener; + /** Open delay time */ + delayOpen?: number; - delayShowTimer; - delayHideTimer; + /** Close delay time */ + delayClose?: number; - mouseEnteredToSpeaker = false; - mouseEnteredToTrigger = false; + /** Sets the rendering container */ + container?: HTMLElement | (() => HTMLElement); - constructor(props: OverlayTriggerProps) { - super(props); + /** Container padding */ + containerPadding?: number; - if (props.trigger !== 'none') { - this.onMouseOverListener = e => onMouseEventHandler(this.handleDelayedShow, e); - this.onMouseOutListener = e => onMouseEventHandler(this.handleDelayedHide, e); - } - this.state = { isOverlayShown: props.defaultOpen }; - } + /** display element */ + speaker?: React.ReactElement | ((props: any, ref: React.RefObject) => React.ReactElement); - componentWillUnmount() { - clearTimeout(this.delayShowTimer); - clearTimeout(this.delayHideTimer); - } + /** Prevent floating element overflow */ + preventOverflow?: boolean; - getOverlayTarget = () => getDOMNode(this); + /** Opern overlay */ + open?: boolean; - handleSpeakerMouseEnter = () => { - this.mouseEnteredToSpeaker = true; - }; + /** The overlay is open by default */ + defaultOpen?: boolean; - handleSpeakerMouseLeave = () => { - const { trigger } = this.props; - this.mouseEnteredToSpeaker = false; - if (!isOneOf('click', trigger) && !isOneOf('active', trigger)) { - this.hideWithCheck(); - } - }; + /** Whether mouse is allowed to enter the floating layer of popover, whose default value is false. */ + enterable?: boolean; - open = (delay?: number) => { - this.show(delay); - }; + /** For the monitored component, the event will be bound to this component. */ + children?: React.ReactElement | ((props: any, ref) => React.ReactElement); - close = (delay?: number) => { - this.hide(delay); - }; + /** Whether to allow clicking document to close the overlay */ + rootClose?: boolean; - show = (delay?: number) => { - if (delay) { - return (this.delayShowTimer = setTimeout(() => { - this.delayShowTimer = null; - this.setState({ isOverlayShown: true }); - }, delay)); - } - this.setState({ isOverlayShown: true }); - }; + /** Once disabled, the event cannot be triggered. */ + disabled?: boolean; - hide = (delay?: number) => { - if (delay) { - return (this.delayHideTimer = setTimeout(() => { - this.delayHideTimer = null; - this.setState({ isOverlayShown: false }); - }, delay)); - } + /** Lose Focus callback function */ + onBlur?: () => void; - this.setState({ isOverlayShown: false }); - }; + /** Click on the callback function */ + onClick?: () => void; - hideWithCheck = (delay?: number) => { - if (!this.mouseEnteredToSpeaker && !this.mouseEnteredToTrigger) { - this.hide(delay); - } - }; + /** Callback function to get focus */ + onFocus?: () => void; + + /** Mouse leave callback function */ + onMouseOut?: () => void; + + /** Mouse over callback function */ + onMouseOver?: () => void; + + /** Callback fired when open component */ + onOpen?: () => void; + + /** Callback fired when close component */ + onClose?: () => void; +} + +/** + * Useful for mouseover and mouseout. + * In order to resolve the node entering the mouseover element, a mouseout event and a mouseover event will be triggered. + * https://javascript.info/mousemove-mouseover-mouseout-mouseenter-mouseleave + * @param handler + * @param event + */ +function onMouseEventHandler( + handler: (event: React.MouseEvent, delay?: number) => void, + event: React.MouseEvent, + delay?: number +) { + const target = event.currentTarget; + const related = event.relatedTarget || get(event, ['nativeEvent', 'toElement']); + + if ((!related || related !== target) && !contains(target, related)) { + handler(event, delay); + } +} - toggleHideAndShow = () => { - const { delayShow, delay, delayHide } = this.props; - if (this.state.isOverlayShown) { - this.hideWithCheck(isNil(delayHide) ? delay : delayHide); +const defaultProps: Partial = { + trigger: ['hover', 'focus'], + placement: 'bottomStart', + rootClose: true +}; + +export interface OverlayTriggerInstance { + child: Element; + updatePosition?: () => void; + open?: () => void; + close?: () => void; +} + +const OverlayTrigger = React.forwardRef((props: OverlayTriggerProps, ref) => { + const { + children, + container, + defaultOpen, + trigger, + disabled, + open: openProp, + delay, + delayOpen: delayOpenProp, + delayClose: delayCloseProp, + enterable, + placement, + speaker, + onClick, + onMouseOver, + onMouseOut, + onFocus, + onBlur, + onClose, + ...rest + } = props; + + const { Portal } = usePortal({ container }); + + const triggerRef = useRef(); + const overlayRef = useRef(); + const [open, setOpen] = useControlled(openProp, defaultOpen); + + // Delay the timer to close/open the overlay + // When the cursor moves from the trigger to the overlay, the overlay will be closed. + // In order to keep the overlay open, a timer is used to delay the closing. + const delayOpenTimer = useRef>(); + const delayCloseTimer = useRef>(); + + const delayOpen = isNil(delayOpenProp) ? delay : delayOpenProp; + const delayClose = isNil(delayCloseProp) ? delay : delayCloseProp; + + // Whether the cursor is on the overlay + const isOnOverlay = useRef(false); + + // Whether the cursor is on the trigger + const isOnTrigger = useRef(false); + + useEffect(() => { + return () => { + clearTimeout(delayOpenTimer.current); + clearTimeout(delayCloseTimer.current); + }; + }, []); + + const handleOpen = useCallback( + (delay?: number) => { + const ms = isUndefined(delay) ? delayOpen : delay; + if (ms) { + return (delayOpenTimer.current = setTimeout(() => { + delayOpenTimer.current = null; + setOpen(true); + }, ms)); + } + setOpen(true); + }, + [delayOpen, setOpen] + ); + + const handleClose = useCallback( + (delay?: number) => { + const ms = isUndefined(delay) ? delayClose : delay; + if (ms) { + return (delayCloseTimer.current = setTimeout(() => { + delayCloseTimer.current = null; + setOpen(false); + }, ms)); + } + setOpen(false); + }, + [delayClose, setOpen] + ); + + useImperativeHandle(ref, () => ({ + get child() { + return triggerRef.current; + }, + open: handleOpen, + close: handleClose, + updatePosition: () => { + overlayRef.current?.updatePosition?.(); + } + })); + + /** + * Close after the cursor leaves. + */ + const handleCloseWhenLeave = useCallback(() => { + // When the cursor is not on the overlay and not on the trigger, it is closed. + if (!isOnOverlay.current && !isOnTrigger.current) { + handleClose(); + } + }, [handleClose]); + + /** + * Toggle open and closed state. + */ + const handleOpenState = useCallback(() => { + if (open) { + handleCloseWhenLeave(); } else { - this.show(isNil(delayShow) ? delay : delayShow); + handleOpen(); } - }; - - handleDelayedShow = () => { - const { delayShow, enterable } = this.props; - const delay = isNil(delayShow) ? this.props.delay : delayShow; + }, [open, handleCloseWhenLeave, handleOpen]); + const handleDelayedOpen = useCallback(() => { if (!enterable) { - return this.show(delay); + return handleOpen(); } - this.mouseEnteredToTrigger = true; - if (!isNil(this.delayHideTimer)) { - clearTimeout(this.delayHideTimer); - this.delayHideTimer = null; - return this.show(delay); + isOnTrigger.current = true; + if (!isNil(delayCloseTimer.current)) { + clearTimeout(delayCloseTimer.current); + delayCloseTimer.current = null; + return handleOpen(); } - if (this.state.isOverlayShown) { + if (open) { return; } - this.show(delay); - }; - - handleDelayedHide = () => { - const { delayHide, enterable } = this.props; - const delay = isNil(delayHide) ? this.props.delay : delayHide; + handleOpen(); + }, [enterable, open, handleOpen]); + const handleDelayedClose = useCallback(() => { if (!enterable) { - this.hide(delay); + handleClose(); } - this.mouseEnteredToTrigger = false; - if (!isNil(this.delayShowTimer)) { - clearTimeout(this.delayShowTimer); - this.delayShowTimer = null; + isOnTrigger.current = false; + if (!isNil(delayOpenTimer.current)) { + clearTimeout(delayOpenTimer.current); + delayOpenTimer.current = null; return; } - if (!this.state.isOverlayShown || !isNil(this.delayHideTimer)) { + if (!open || !isNil(delayCloseTimer.current)) { return; } - if (!delay) { - return this.hideWithCheck(); - } + delayCloseTimer.current = setTimeout(() => { + clearTimeout(delayCloseTimer.current); + delayCloseTimer.current = null; + handleCloseWhenLeave(); + }, 200); + }, [enterable, open, handleClose, handleCloseWhenLeave]); - this.delayHideTimer = setTimeout(() => { - if (this.state.isOnSpeaker) { - return; - } + const handleSpeakerMouseEnter = useCallback(() => { + isOnOverlay.current = true; + }, []); - clearTimeout(this.delayHideTimer); - this.delayHideTimer = null; - this.hideWithCheck(); - }, delay); - }; + const handleSpeakerMouseLeave = useCallback(() => { + isOnOverlay.current = false; + if (!isOneOf('click', trigger) && !isOneOf('active', trigger)) { + handleCloseWhenLeave(); + } + }, [handleCloseWhenLeave, trigger]); - renderOverlay() { - const { open, speaker, trigger, onHide } = this.props; - const { isOverlayShown } = this.state; - const overlayProps: OverlayProps = { - ...pick(this.props, Object.keys(Overlay.propTypes)), - show: typeof open === 'undefined' ? isOverlayShown : open, - target: this.getOverlayTarget - }; + const triggerEvents = { onClick, onMouseOver, onMouseOut, onFocus, onBlur }; + if (!disabled) { if (isOneOf('click', trigger)) { - overlayProps.onHide = createChainedFunction(this.hide, onHide); - } else if (isOneOf('active', trigger)) { - overlayProps.onHide = createChainedFunction(this.hide, onHide); + triggerEvents.onClick = createChainedFunction(handleOpenState, triggerEvents.onClick); } - const speakerProps: SpeakerProps = { - placement: overlayProps.placement - }; - - if (trigger !== 'none') { - speakerProps.onMouseEnter = this.handleSpeakerMouseEnter; - speakerProps.onMouseLeave = this.handleSpeakerMouseLeave; + if (isOneOf('active', trigger)) { + triggerEvents.onClick = createChainedFunction(handleDelayedOpen, triggerEvents.onClick); } - if (typeof speaker === 'function') { - return {speaker}; + if (isOneOf('hover', trigger)) { + let onMouseOverListener = null; + let onMouseOutListener = null; + + if (trigger !== 'none') { + onMouseOverListener = e => onMouseEventHandler(handleDelayedOpen, e); + onMouseOutListener = e => onMouseEventHandler(handleDelayedClose, e); + } + triggerEvents.onMouseOver = createChainedFunction(onMouseOverListener, onMouseOver); + triggerEvents.onMouseOut = createChainedFunction(onMouseOutListener, onMouseOut); } - return {React.cloneElement(speaker, speakerProps)}; + if (isOneOf('focus', trigger)) { + triggerEvents.onFocus = createChainedFunction(handleDelayedOpen, onFocus); + triggerEvents.onBlur = createChainedFunction(handleDelayedClose, onBlur); + } } - render() { - const { - children, - speaker, - onClick, - trigger, - onMouseOver, - onMouseOut, - onFocus, - onBlur, - disabled - } = this.props; - - const triggerComponent = React.Children.only(children) as React.DetailedReactHTMLElement< - any, - HTMLElement - >; - - const triggerProps = triggerComponent.props; - - const props: TriggerProps = { - key: 'triggerComponent', - onClick: createChainedFunction(triggerProps.onClick, onClick), - 'aria-describedby': get(speaker, ['props', 'id']) + const renderOverlay = () => { + const overlayProps: OverlayProps = { + ...rest, + triggerTarget: triggerRef, + onClose: trigger !== 'none' ? createChainedFunction(handleClose, onClose) : undefined, + placement, + container, + open }; - if (!disabled) { - if (isOneOf('click', trigger)) { - props.onClick = createChainedFunction(this.toggleHideAndShow, props.onClick); - } - - if (isOneOf('active', trigger)) { - props.onClick = createChainedFunction(this.handleDelayedShow, props.onClick); - } - - if (isOneOf('hover', trigger)) { - props.onMouseOver = createChainedFunction( - this.onMouseOverListener, - triggerProps.onMouseOver, - onMouseOver - ); - props.onMouseOut = createChainedFunction( - this.onMouseOutListener, - triggerProps.onMouseOut, - onMouseOut - ); - } - - if (isOneOf('focus', trigger)) { - props.onFocus = createChainedFunction( - this.handleDelayedShow, - triggerProps.onFocus, - onFocus - ); - - props.onBlur = createChainedFunction(this.handleDelayedHide, triggerProps.onBlur, onBlur); - } - } + // The purpose of adding mouse entry and exit events to the Overlay is to record whether the current cursor is on the Overlay. + // When `trigger` is equal to `hover`, if the cursor leaves the `triggerTarget` and stays on the Overlay, + // the Overlay will continue to remain open. + const speakerProps = + trigger !== 'none' && enterable + ? { onMouseEnter: handleSpeakerMouseEnter, onMouseLeave: handleSpeakerMouseLeave } + : null; + + return ( + + {speaker} + + ); + }; - return [ - React.cloneElement(triggerComponent, props), - {this.renderOverlay()} - ]; - } -} + return ( + <> + {typeof children === 'function' + ? children({ ...triggerEvents, 'aria-expanded': open }, triggerRef) + : React.cloneElement(children, { + ref: triggerRef, + ...mergeEvents(triggerEvents, children.props) + })} + {renderOverlay()} + + ); +}); + +OverlayTrigger.displayName = 'OverlayTrigger'; +OverlayTrigger.defaultProps = defaultProps; export default OverlayTrigger; diff --git a/src/Overlay/Position.tsx b/src/Overlay/Position.tsx index eecc2e1c05..c2aee44659 100644 --- a/src/Overlay/Position.tsx +++ b/src/Overlay/Position.tsx @@ -1,183 +1,166 @@ -import * as React from 'react'; +import React, { + useState, + useEffect, + useRef, + useMemo, + useCallback, + useImperativeHandle +} from 'react'; +import PropTypes from 'prop-types'; import classNames from 'classnames'; -import _ from 'lodash'; -import { ownerDocument, getContainer, on } from 'dom-lib'; -import positionUtils from './positionUtils'; -import shallowEqual from '../utils/shallowEqual'; -import getDOMNode from '../utils/getDOMNode'; +import helper from '../DOMHelper'; +import positionUtils, { PositionType } from './positionUtils'; +import { getDOMNode, refType } from '../utils'; import { TypeAttributes } from '../@types/common'; -interface PositionProps { - children?: React.ReactNode; +export interface PositionChildProps { + className: string; + left: number; + top: number; +} + +export interface PositionProps { + children?: (props: PositionChildProps, ref) => React.ReactElement; className?: string; - target?: (props: PositionProps) => HTMLElement; container?: HTMLElement | (() => HTMLElement); containerPadding?: number; placement?: TypeAttributes.Placement; - shouldUpdatePosition?: boolean; preventOverflow?: boolean; + triggerTarget?: React.RefObject; } -interface PositionState { - positionLeft?: number; - positionTop?: number; - arrowOffsetLeft?: null | number; - arrowOffsetTop?: null | number; - positionClassName?: string; -} - -class Position extends React.Component { - static displayName = 'Position'; - static defaultProps = { - containerPadding: 0, - placement: 'right', - shouldUpdatePosition: false +const defaultProps: Partial = { + containerPadding: 0, + placement: 'right' +}; + +const usePosition = ( + props: PositionProps, + ref: React.RefObject +): [PositionType, (placementChanged?: any) => void] => { + const { placement, preventOverflow, containerPadding, container, triggerTarget } = props; + const containerRef = useRef(); + const containerScrollListenerRef = useRef<{ off: () => void }>(); + const lastTargetRef = useRef(); + const defaultPosition = { + positionLeft: 0, + positionTop: 0, + arrowOffsetLeft: null, + arrowOffsetTop: null }; + const [position, setPosition] = useState(defaultPosition); + const utils = useMemo( + () => + positionUtils({ + placement, + preventOverflow, + padding: containerPadding + }), + [placement, preventOverflow, containerPadding] + ); + + const updatePosition = useCallback( + (placementChanged = true) => { + if (!triggerTarget?.current) { + return; + } + const targetElement = getDOMNode(triggerTarget); - utils = null; - lastTarget: any = false; - needsFlush = null; - container = null; - containerScrollListener = null; - childRef: React.RefObject; - - constructor(props: PositionProps) { - super(props); - this.state = { - positionLeft: 0, - positionTop: 0, - arrowOffsetLeft: null, - arrowOffsetTop: null - }; - this.utils = positionUtils({ - placement: props.placement, - preventOverflow: props.preventOverflow, - padding: props.containerPadding - }); - this.childRef = React.createRef(); - } + if (!helper.isElement(targetElement)) { + throw new Error('`target` should return an HTMLElement'); + } - getHTMLElement() { - /** - * findDOMNode is deprecated in StrictMode. - * Replace findDOMNode with ref. Provided for `Transition` calls. - * https://fb.me/react-strict-mode-find-node - */ - return getDOMNode(this.childRef.current); - } + /** + * If the target and placement do not change, the position is not updated. + */ + if (targetElement === lastTargetRef.current && !placementChanged) { + return; + } - componentDidMount() { - this.updatePosition(false); - if (this.container && this.props.preventOverflow) { - this.containerScrollListener = on( - this.container.tagName === 'BODY' ? window : this.container, - 'scroll', - this.updatePosition + const overlay = getDOMNode(ref.current); + const containerElement = helper.getContainer( + typeof container === 'function' ? container() : container, + helper.ownerDocument(ref.current).body ); - } - } - shouldComponentUpdate(nextProps: PositionProps, nextState: PositionState) { - if (!shallowEqual(nextProps, this.props)) { - this.needsFlush = true; - return true; - } - - if (!shallowEqual(nextState, this.state)) { - return true; - } + setPosition(utils.calcOverlayPosition(overlay, targetElement, containerElement)); - return false; - } + containerRef.current = containerElement; + lastTargetRef.current = targetElement; + }, + [container, ref, triggerTarget, utils] + ); - componentDidUpdate(prevProps: PositionProps) { - if (this.needsFlush) { - this.needsFlush = false; - this.updatePosition(prevProps.placement !== this.props.placement); + useEffect(() => { + updatePosition(false); + if (containerRef.current && preventOverflow) { + containerScrollListenerRef.current = helper.on( + containerRef.current?.tagName === 'BODY' ? window : containerRef.current, + 'scroll', + () => updatePosition() + ); } - } + return () => { + lastTargetRef.current = null; + containerScrollListenerRef.current?.off(); + }; + }, [preventOverflow, updatePosition]); - componentWillUnmount() { - this.lastTarget = null; - this.containerScrollListener?.off(); - } + useEffect(() => updatePosition(), [updatePosition, placement]); - getTargetSafe() { - const { target } = this.props; - if (!target) { - return null; - } + return [position, updatePosition]; +}; - const targetSafe = target(this.props); +export interface PositionInstance { + updatePosition?: () => void; + child?: Element; +} - if (!targetSafe) { - return null; - } +const Position = React.forwardRef((props: PositionProps, ref) => { + const { children, className } = props; + const childRef = React.useRef(); + + const [position, updatePosition] = usePosition(props, childRef); + const { + positionClassName, + arrowOffsetLeft, + arrowOffsetTop, + positionLeft, + positionTop + } = position; + + useImperativeHandle(ref, () => ({ + get child() { + return childRef.current; + }, + updatePosition + })); + + if (typeof children === 'function') { + const childProps = { + className: classNames(className, positionClassName), + arrowOffsetLeft, + arrowOffsetTop, + left: positionLeft, + top: positionTop + }; - return targetSafe; + return children(childProps, childRef); } - updatePosition = (placementChanged = true) => { - const target = this.getTargetSafe(); - const { shouldUpdatePosition } = this.props; - - /** - * 如果 target 没有变化,同时不允许更新位置,placement 位置也没有改变,则返回 - */ - if (target === this.lastTarget && !shouldUpdatePosition && !placementChanged) { - return; - } - - this.lastTarget = target; - - if (!target) { - this.setState({ - positionLeft: 0, - positionTop: 0, - arrowOffsetLeft: null, - arrowOffsetTop: null - }); - return; - } - - const overlay = getDOMNode(this); - const container = getContainer(this.props.container, ownerDocument(this).body); - const nextPosition = this.utils.calcOverlayPosition(overlay, target, container); - - this.container = container; - this.setState(nextPosition); - }; - - render() { - const { children, className, ...rest } = this.props; - const { positionLeft, positionTop, positionClassName, ...arrowPosition } = this.state; - - if (typeof children === 'function') { - return children( - { - className: classNames(className, positionClassName), - left: positionLeft, - top: positionTop - }, - this.childRef - ); - } - - const child = React.Children.only(children) as React.DetailedReactHTMLElement; - - return React.cloneElement(child, { - ..._.omit(rest, ['target', 'container', 'containerPadding', 'preventOverflow']), - ...arrowPosition, - positionLeft, - positionTop, - className: classNames(className, positionClassName, child.props.className), - style: { - ...child.props.style, - left: positionLeft, - top: positionTop - } - }); - } -} + return children; +}); + +Position.displayName = 'Position'; +Position.defaultProps = defaultProps; +Position.propTypes = { + className: PropTypes.string, + children: PropTypes.func, + container: PropTypes.oneOfType([PropTypes.func, PropTypes.any]), + containerPadding: PropTypes.number, + placement: PropTypes.any, + preventOverflow: PropTypes.bool, + triggerTarget: refType +}; export default Position; diff --git a/src/Overlay/RootCloseWrapper.tsx b/src/Overlay/RootCloseWrapper.tsx deleted file mode 100644 index f60fd9aed1..0000000000 --- a/src/Overlay/RootCloseWrapper.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import * as React from 'react'; -import { on, contains } from 'dom-lib'; -import getDOMNode from '../utils/getDOMNode'; - -function isLeftClickEvent(event: React.MouseEvent) { - return event?.button === 0; -} - -function isModifiedEvent(event: React.MouseEvent) { - return !!(event.metaKey || event.altKey || event.ctrlKey || event?.shiftKey); -} - -interface RootCloseWrapperProps { - children: React.ReactNode; - onRootClose?: () => void; - target?: () => HTMLElement; -} - -class RootCloseWrapper extends React.Component { - onDocumentClickListener = null; - onDocumentKeyupListener = null; - childRef: React.RefObject; - - constructor(props) { - super(props); - this.childRef = React.createRef(); - } - - componentDidMount() { - const doc = window.document; - this.onDocumentClickListener = on(doc, 'click', this.handleDocumentClick, true); - this.onDocumentKeyupListener = on(doc, 'keyup', this.handleDocumentKeyUp); - } - - componentWillUnmount() { - this.onDocumentClickListener?.off(); - this.onDocumentKeyupListener?.off(); - } - - handleDocumentClick = (event: React.MouseEvent) => { - if (contains(getDOMNode(this.childRef.current || this), event.target)) { - return; - } - if (isModifiedEvent(event) || !isLeftClickEvent(event)) { - return; - } - - const { target } = this.props; - if (target) { - if (contains(target(), event.target)) { - return; - } - } - - this.props.onRootClose?.(); - }; - - handleDocumentKeyUp = (event: React.KeyboardEvent) => { - if (event.keyCode === 27) { - this.props.onRootClose?.(); - } - }; - - render() { - const { children } = this.props; - - if (typeof children === 'function') { - return children({}, this.childRef); - } - - return children; - } -} - -export default RootCloseWrapper; diff --git a/src/Overlay/positionUtils.ts b/src/Overlay/positionUtils.ts index fed08ee2d7..1e8733ce85 100644 --- a/src/Overlay/positionUtils.ts +++ b/src/Overlay/positionUtils.ts @@ -2,15 +2,41 @@ import maxBy from 'lodash/maxBy'; import minBy from 'lodash/minBy'; import kebabCase from 'lodash/kebabCase'; import { ownerDocument, getOffset, getPosition, scrollTop, scrollLeft } from 'dom-lib'; +import { TypeAttributes } from '../@types/common'; + +export interface PositionType { + positionLeft?: number; + positionTop?: number; + arrowOffsetLeft?: null | number; + arrowOffsetTop?: null | number; + positionClassName?: string; +} + +export interface UtilProps { + placement: TypeAttributes.Placement; + preventOverflow: boolean; + padding: number; +} -const AutoPlacement = { +export const AutoPlacement = { left: 'Start', right: 'End', top: 'Start', bottom: 'End' }; -function getContainerDimensions(containerNode) { +export interface Dimensions { + width: number; + height: number; + scrollX: number; + scrollY: number; +} + +/** + * Get the external dimensions of the container + * @param containerNode + */ +function getContainerDimensions(containerNode: HTMLElement): Dimensions { let width; let height; let scrollX; @@ -28,7 +54,7 @@ function getContainerDimensions(containerNode) { return { width, height, scrollX, scrollY }; } -export default props => { +export default (props: UtilProps) => { const { placement, preventOverflow, padding } = props; function getTopDelta(top, overlayHeight, container) { @@ -149,9 +175,9 @@ export default props => { return `${direction.key}${AutoPlacement[align.key]}`; }, - // 计算浮层的位置 - calcOverlayPosition(overlayNode, target, container) { + // Calculate the position of the overlay + calcOverlayPosition(overlayNode, target, container): PositionType { const childOffset = this.getPosition(target, container); const { height: overlayHeight, width: overlayWidth } = getOffset(overlayNode); const { top, left } = childOffset; diff --git a/src/Picker/DropdownMenu.tsx b/src/Picker/DropdownMenu.tsx index 12b0b94055..52f3ba8816 100644 --- a/src/Picker/DropdownMenu.tsx +++ b/src/Picker/DropdownMenu.tsx @@ -228,7 +228,7 @@ const DropdownMenu = React.forwardRef( }; return (
    void; } -const DropdownMenuCheckItem = React.forwardRef( - (props: DropdownMenuCheckItemProps, ref: React.Ref) => { - const { - active, - as: Component = 'div', - checkboxAs: CheckboxItem = Checkbox, - classPrefix = 'check-item', - checkable = true, - disabled, - value, - focus, - children, - className, - indeterminate, - onKeyDown, - onSelect, - onCheck, - onSelectItem, - ...rest - } = props; +const DropdownMenuCheckItem: RsRefForwardingComponent< + 'div', + DropdownMenuCheckItemProps +> = React.forwardRef((props: DropdownMenuCheckItemProps, ref) => { + const { + active, + as: Component = 'div', + checkboxAs: CheckboxItem = Checkbox, + classPrefix = 'check-item', + checkable = true, + disabled, + value, + focus, + children, + className, + indeterminate, + onKeyDown, + onSelect, + onCheck, + onSelectItem, + ...rest + } = props; - const handleChange = useCallback( - (value: any, checked: boolean, event: React.SyntheticEvent) => { - onSelect?.(value, event, checked); - }, - [onSelect] - ); + const handleChange = useCallback( + (value: any, checked: boolean, event: React.SyntheticEvent) => { + onSelect?.(value, event, checked); + }, + [onSelect] + ); - const handleCheck = useCallback( - (event: React.SyntheticEvent) => { - if (!disabled) { - onCheck?.(value, event, !active); - } - }, - [value, disabled, onCheck, active] - ); + const handleCheck = useCallback( + (event: React.SyntheticEvent) => { + if (!disabled) { + onCheck?.(value, event, !active); + } + }, + [value, disabled, onCheck, active] + ); - const handleSelectItem = useCallback( - (event: React.SyntheticEvent) => { - if (!disabled) { - onSelectItem?.(value, event, !active); - } - }, - [value, disabled, onSelectItem, active] - ); + const handleSelectItem = useCallback( + (event: React.SyntheticEvent) => { + if (!disabled) { + onSelectItem?.(value, event, !active); + } + }, + [value, disabled, onSelectItem, active] + ); - const { withClassPrefix } = useClassNames(classPrefix); - const checkboxItemClasses = withClassPrefix({ focus }); + const { withClassPrefix } = useClassNames(classPrefix); + const checkboxItemClasses = withClassPrefix({ focus }); - return ( - + - - {children} - - - ); - } -); + {children} + + + ); +}); DropdownMenuCheckItem.displayName = 'DropdownMenuCheckItem'; DropdownMenuCheckItem.propTypes = { diff --git a/src/Picker/DropdownMenuItem.tsx b/src/Picker/DropdownMenuItem.tsx index 3e6a9eb657..fbb2a6dbd3 100644 --- a/src/Picker/DropdownMenuItem.tsx +++ b/src/Picker/DropdownMenuItem.tsx @@ -1,11 +1,9 @@ import React, { useCallback } from 'react'; import PropTypes from 'prop-types'; import { useClassNames } from '../utils'; -import { StandardProps } from '../@types/common'; +import { WithAsProps, RsRefForwardingComponent } from '../@types/common'; -export interface DropdownMenuItemProps - extends StandardProps, - Omit, 'onSelect'> { +export interface DropdownMenuItemProps extends WithAsProps { active?: boolean; disabled?: boolean; value?: any; @@ -16,8 +14,8 @@ export interface DropdownMenuItemProps renderItem?: (value: any) => React.ReactNode; } -const DropdownMenuItem = React.forwardRef( - (props: DropdownMenuItemProps, ref: React.Ref) => { +const DropdownMenuItem: RsRefForwardingComponent<'div', DropdownMenuItemProps> = React.forwardRef( + (props: DropdownMenuItemProps, ref) => { const { as: Component = 'div', active, @@ -48,21 +46,17 @@ const DropdownMenuItem = React.forwardRef( return ( - - {renderItem ? renderItem(value) : children} - + {renderItem ? renderItem(value) : children} ); } diff --git a/src/Picker/MenuWrapper.tsx b/src/Picker/MenuWrapper.tsx index ac52244fef..7aa32144b2 100644 --- a/src/Picker/MenuWrapper.tsx +++ b/src/Picker/MenuWrapper.tsx @@ -4,11 +4,10 @@ import { addStyle, getWidth } from 'dom-lib'; import { useElementResize, useClassNames } from '../utils'; import getDOMNode from '../utils/getDOMNode'; import mergeRefs from '../utils/mergeRefs'; -import { StandardProps } from '../@types/common'; +import { WithAsProps, RsRefForwardingComponent } from '../@types/common'; const omitProps = [ 'placement', - 'shouldUpdatePosition', 'arrowOffsetLeft', 'arrowOffsetTop', 'positionLeft', @@ -29,7 +28,7 @@ const resizePlacement = [ 'autoHorizontalEnd' ]; -export interface MenuWrapperProps extends StandardProps { +export interface MenuWrapperProps extends WithAsProps { placement?: string; autoWidth?: boolean; children?: React.ReactNode; @@ -38,48 +37,50 @@ export interface MenuWrapperProps extends StandardProps { onKeyDown?: (event: React.KeyboardEvent) => void; } -const MenuWrapper = React.forwardRef((props: MenuWrapperProps, ref: React.Ref) => { - const { - as: Component = 'div', - classPrefix = 'picker-menu', - autoWidth, - className, - placement, - getPositionInstance, - getToggleInstance, - ...rest - } = props; +const MenuWrapper: RsRefForwardingComponent<'div', MenuWrapperProps> = React.forwardRef( + (props: MenuWrapperProps, ref) => { + const { + as: Component = 'div', + classPrefix = 'picker-menu', + autoWidth, + className, + placement, + getPositionInstance, + getToggleInstance, + ...rest + } = props; - const menuElementRef = useRef(); - const handleResize = useCallback(() => { - const instance = getPositionInstance?.(); - if (instance && resizePlacement.includes(placement)) { - instance.updatePosition(true); - } - }, [getPositionInstance, placement]); + const menuElementRef = useRef(); + const handleResize = useCallback(() => { + const instance = getPositionInstance?.(); + if (instance && resizePlacement.includes(placement)) { + instance.updatePosition(true); + } + }, [getPositionInstance, placement]); - useElementResize(() => menuElementRef.current, handleResize); - useEffect(() => { - const toggle = getToggleInstance?.(); - if (autoWidth && toggle) { - // Get the width value of the button, - // and then set it to the menu to make their width consistent. - const width = getWidth(getDOMNode(toggle)); - addStyle(menuElementRef.current, 'min-width', `${width}px`); - } - }, [autoWidth, getToggleInstance, menuElementRef]); + useElementResize(() => menuElementRef.current, handleResize); + useEffect(() => { + const toggle = getToggleInstance?.(); + if (autoWidth && toggle) { + // Get the width value of the button, + // and then set it to the menu to make their width consistent. + const width = getWidth(getDOMNode(toggle)); + addStyle(menuElementRef.current, 'min-width', `${width}px`); + } + }, [autoWidth, getToggleInstance, menuElementRef]); - const { withClassPrefix, merge } = useClassNames(classPrefix); - const classes = merge(className, withClassPrefix()); + const { withClassPrefix, merge } = useClassNames(classPrefix); + const classes = merge(className, withClassPrefix()); - return ( - - ); -}); + return ( + + ); + } +); MenuWrapper.displayName = 'MenuWrapper'; diff --git a/src/Picker/PickerToggleTrigger.tsx b/src/Picker/PickerToggleTrigger.tsx index 909c089019..26d09d1218 100644 --- a/src/Picker/PickerToggleTrigger.tsx +++ b/src/Picker/PickerToggleTrigger.tsx @@ -1,18 +1,21 @@ import React from 'react'; import pick from 'lodash/pick'; -import OverlayTrigger from '../Overlay/OverlayTrigger'; +import OverlayTrigger, { OverlayTriggerInstance } from '../Overlay/OverlayTrigger'; +import { PositionChildProps } from '../Overlay/Position'; import { placementPolyfill } from '../utils'; import { CustomConsumer } from '../CustomProvider'; import { TypeAttributes } from '../@types/common'; type TriggerType = 'click' | 'hover' | 'focus' | 'active'; +export type { OverlayTriggerInstance, PositionChildProps }; + export interface PickerToggleTriggerProps { placement?: TypeAttributes.Placement; pickerProps: any; open?: boolean; trigger?: TriggerType | TriggerType[]; - children: React.ReactNode; - speaker: React.ReactElement; + children: React.ReactElement | ((props: any, ref) => React.ReactElement); + speaker: React.ReactElement | ((props: any, ref: React.RefObject) => React.ReactElement); positionRef?: React.Ref; onEnter?: (node: null | Element | Text) => void; onEntered?: (node: null | Element | Text) => void; @@ -39,19 +42,18 @@ export const pickerToggleTriggerProps = [ const PickerToggleTrigger = React.forwardRef( (props: PickerToggleTriggerProps, ref: React.Ref) => { - const { pickerProps, speaker, placement, trigger = 'click', open, ...rest } = props; + const { pickerProps, speaker, placement, trigger = 'click', ...rest } = props; return ( {context => ( )} diff --git a/src/Picker/SearchBar.tsx b/src/Picker/SearchBar.tsx index 93ac698e33..e65524b619 100644 --- a/src/Picker/SearchBar.tsx +++ b/src/Picker/SearchBar.tsx @@ -2,48 +2,48 @@ import React, { useCallback } from 'react'; import get from 'lodash/get'; import PropTypes from 'prop-types'; import { useClassNames } from '../utils'; -import { StandardProps } from '../@types/common'; +import { WithAsProps, RsRefForwardingComponent } from '../@types/common'; -export interface SearchBarProps - extends StandardProps, - Omit, 'onChange'> { +export interface SearchBarProps extends WithAsProps { value?: string; placeholder?: string; className?: string; onChange?: (value: string, event: React.SyntheticEvent) => void; } -const SearchBar = React.forwardRef((props: SearchBarProps, ref: React.Ref) => { - const { - as: Component = 'div', - classPrefix = 'picker-search-bar', - value, - children, - className, - placeholder, - onChange, - ...rest - } = props; - const { withClassPrefix, merge, prefix } = useClassNames(classPrefix); - const classes = merge(className, withClassPrefix()); - const handleChange = useCallback( - (event: React.SyntheticEvent) => { - onChange?.(get(event, 'target.value'), event); - }, - [onChange] - ); - return ( - - - {children} - - ); -}); +const SearchBar: RsRefForwardingComponent<'div', SearchBarProps> = React.forwardRef( + (props: SearchBarProps, ref) => { + const { + as: Component = 'div', + classPrefix = 'picker-search-bar', + value, + children, + className, + placeholder, + onChange, + ...rest + } = props; + const { withClassPrefix, merge, prefix } = useClassNames(classPrefix); + const classes = merge(className, withClassPrefix()); + const handleChange = useCallback( + (event: React.SyntheticEvent) => { + onChange?.(get(event, 'target.value'), event); + }, + [onChange] + ); + return ( + + + {children} + + ); + } +); SearchBar.displayName = 'SearchBar'; SearchBar.propTypes = { diff --git a/src/Picker/propTypes.ts b/src/Picker/propTypes.ts index 3e1f89f0bd..14a9495c62 100644 --- a/src/Picker/propTypes.ts +++ b/src/Picker/propTypes.ts @@ -1,10 +1,9 @@ import PropTypes from 'prop-types'; -import { PLACEMENT } from '../constants'; -import { refType } from '../utils'; -import { getAnimationPropTypes } from '../Animation/utils'; +import { refType, PLACEMENT } from '../utils'; +import { animationPropTypes } from '../Animation/utils'; export const pickerPropTypes = { - ...getAnimationPropTypes(), + ...animationPropTypes, classPrefix: PropTypes.string, className: PropTypes.string, style: PropTypes.object, diff --git a/src/Picker/test/DropdownMenuCheckItemSpec.js b/src/Picker/test/DropdownMenuCheckItemSpec.js index e0a75b692e..0ef58c17de 100644 --- a/src/Picker/test/DropdownMenuCheckItemSpec.js +++ b/src/Picker/test/DropdownMenuCheckItemSpec.js @@ -73,7 +73,7 @@ describe('picker - DropdownMenuCheckItem', () => { it('Should have a role', () => { const instance = getDOMNode(item); - assert.equal(instance.getAttribute('role'), 'listitem'); + assert.equal(instance.getAttribute('role'), 'menuitemcheckbox'); }); it('Should have a custom className', () => { diff --git a/src/Picker/test/DropdownMenuItemSpec.js b/src/Picker/test/DropdownMenuItemSpec.js index c137dba95b..a37c3e2ed6 100644 --- a/src/Picker/test/DropdownMenuItemSpec.js +++ b/src/Picker/test/DropdownMenuItemSpec.js @@ -51,7 +51,7 @@ describe('picker - DropdownMenuItem', () => { it('Should have a role', () => { const instance = getDOMNode(item); - assert.equal(instance.getAttribute('role'), 'listitem'); + assert.equal(instance.getAttribute('role'), 'menuitemradio'); }); it('Should have a custom className', () => { diff --git a/src/Picker/types.ts b/src/Picker/types.ts index 2623b4bf76..9c60a365f8 100644 --- a/src/Picker/types.ts +++ b/src/Picker/types.ts @@ -1,8 +1,8 @@ import { RsRefForwardingComponent } from '../@types/common'; export interface PickerInstance { - root?: HTMLDivElement; - menu?: HTMLDivElement; + root?: Element; + menu?: Element; toggle?: HTMLButtonElement; open?: () => void; close?: () => void; diff --git a/src/Picker/utils.ts b/src/Picker/utils.ts index c2649483ec..5388234581 100644 --- a/src/Picker/utils.ts +++ b/src/Picker/utils.ts @@ -7,11 +7,7 @@ import isUndefined from 'lodash/isUndefined'; import omit from 'lodash/omit'; import find from 'lodash/find'; import { findNodeOfTree, filterNodesOfTree } from '../utils/treeUtils'; -import placementPolyfill from '../utils/placementPolyfill'; -import reactToString from '../utils/reactToString'; -import shallowEqual from '../utils/shallowEqual'; -import useClassNames from '../utils/useClassNames'; -import { KEY_CODE } from '../constants'; +import { KEY_CODE, useClassNames, shallowEqual, reactToString, placementPolyfill } from '../utils'; import { TypeAttributes, ItemDataType } from '../@types/common'; interface NodeKeys { diff --git a/src/Popover/Popover.d.ts b/src/Popover/Popover.d.ts deleted file mode 100644 index 2438715391..0000000000 --- a/src/Popover/Popover.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import * as React from 'react'; -import { StandardProps } from '../@types/common'; - -export interface PopoverProps extends StandardProps { - /** Primary content */ - children?: React.ReactNode; - - /** The title of the component. */ - title?: React.ReactNode; -} - -declare const Popover: React.ComponentType; - -export default Popover; diff --git a/src/Popover/Popover.tsx b/src/Popover/Popover.tsx index 80b2a36017..4f7616da9c 100644 --- a/src/Popover/Popover.tsx +++ b/src/Popover/Popover.tsx @@ -1,23 +1,29 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import _ from 'lodash'; -import { prefix, defaultProps } from '../utils'; -import { PopoverProps } from './Popover.d'; -import { overlayProps } from '../Whisper/Whisper'; - -class Popover extends React.Component { - static propTypes = { - classPrefix: PropTypes.string, - children: PropTypes.node, - title: PropTypes.node, - style: PropTypes.object, - visible: PropTypes.bool, - className: PropTypes.string, - full: PropTypes.bool - }; - render() { +import { useClassNames } from '../utils'; +import { WithAsProps, RsRefForwardingComponent } from '../@types/common'; + +export interface PopoverProps extends WithAsProps { + /** The title of the component. */ + title?: React.ReactNode; + + /** The component is visible by default. */ + visible?: boolean; + + /** The content full the container */ + + full?: boolean; +} + +const defaultProps: Partial = { + as: 'div', + classPrefix: 'popover' +}; + +const Popover: RsRefForwardingComponent<'div', PopoverProps> = React.forwardRef( + (props: PopoverProps, ref) => { const { + as: Component, classPrefix, title, children, @@ -25,13 +31,11 @@ class Popover extends React.Component { visible, className, full, - htmlElementRef, ...rest - } = this.props; - const addPrefix = prefix(classPrefix); - const classes = classNames(classPrefix, className, { - [addPrefix('full')]: full - }); + } = props; + + const { withClassPrefix, merge, prefix } = useClassNames(classPrefix); + const classes = merge(className, withClassPrefix({ full })); const styles = { display: 'block', @@ -40,15 +44,25 @@ class Popover extends React.Component { }; return ( -
    -
    - {title ?

    {title}

    : null} -
    {children}
    -
    + +
    + {title &&

    {title}

    } +
    {children}
    + ); } -} +); -export default defaultProps({ - classPrefix: 'popover' -})(Popover); +Popover.displayName = 'Popover'; +Popover.defaultProps = defaultProps; +Popover.propTypes = { + as: PropTypes.elementType, + classPrefix: PropTypes.string, + children: PropTypes.node, + title: PropTypes.node, + style: PropTypes.object, + visible: PropTypes.bool, + className: PropTypes.string, + full: PropTypes.bool +}; +export default Popover; diff --git a/src/Popover/index.d.ts b/src/Popover/index.d.ts deleted file mode 100644 index fc3074b834..0000000000 --- a/src/Popover/index.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './Popover'; -export * from './Popover'; diff --git a/src/Popover/index.tsx b/src/Popover/index.tsx index 497a8666bb..a1b719b12a 100644 --- a/src/Popover/index.tsx +++ b/src/Popover/index.tsx @@ -1,3 +1,3 @@ import Popover from './Popover'; - +export type { PopoverProps } from './Popover'; export default Popover; diff --git a/src/Popover/test/PopoverSpec.js b/src/Popover/test/PopoverSpec.js index 01c8b36cc2..78706689c1 100644 --- a/src/Popover/test/PopoverSpec.js +++ b/src/Popover/test/PopoverSpec.js @@ -1,6 +1,5 @@ import React from 'react'; -import ReactTestUtils from 'react-dom/test-utils'; -import { getDOMNode, getInstance } from '@test/testUtils'; +import { getDOMNode } from '@test/testUtils'; import Popover from '../Popover'; import { innerText } from '@test/testUtils'; @@ -14,8 +13,8 @@ describe('Popover', () => { }); it('Should be full', () => { - const instance = getInstance(Test); - assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'rs-popover-full')); + const instance = getDOMNode(Test); + assert.include(instance.className, 'rs-popover-full'); }); it('Should have a id', () => { diff --git a/src/Popover/test/PopoverStylesSpec.js b/src/Popover/test/PopoverStylesSpec.js index 0d807c41f6..e8e4c3a930 100644 --- a/src/Popover/test/PopoverStylesSpec.js +++ b/src/Popover/test/PopoverStylesSpec.js @@ -16,7 +16,7 @@ describe('Popover styles', () => { , createTestContainer() ); - const dom = getDOMNode(instanceRef.current); + const dom = instanceRef.current; assert.equal(getStyle(dom, 'backgroundColor'), toRGB('#fff'), 'Popover background-color'); }); diff --git a/src/SelectPicker/SelectPicker.tsx b/src/SelectPicker/SelectPicker.tsx index fccaf76f1f..87731f7a85 100644 --- a/src/SelectPicker/SelectPicker.tsx +++ b/src/SelectPicker/SelectPicker.tsx @@ -12,7 +12,9 @@ import { getDataGroupBy, useCustom, useClassNames, - useControlled + useControlled, + KEY_CODE, + mergeRefs } from '../utils'; import { DropdownMenu, @@ -27,11 +29,14 @@ import { useSearch } from '../Picker'; import { PickerComponent, PickerLocaleType } from '../Picker/types'; -import { pickerToggleTriggerProps } from '../Picker/PickerToggleTrigger'; +import { + pickerToggleTriggerProps, + OverlayTriggerInstance, + PositionChildProps +} from '../Picker/PickerToggleTrigger'; import { listPickerPropTypes } from '../Picker/propTypes'; import { FormControlPickerProps, ItemDataType } from '../@types/common'; import { ListProps } from 'react-virtualized/dist/commonjs/List'; -import { KEY_CODE } from '../constants'; export type ValueType = number | string; export interface SelectProps { @@ -150,8 +155,7 @@ const SelectPicker: PickerComponent = React.forwardRef( ...rest } = props; - const rootRef = useRef(); - const triggerRef = useRef(); + const triggerRef = useRef(); const positionRef = useRef(); const toggleRef = useRef(); const menuRef = useRef(); @@ -184,77 +188,66 @@ const SelectPicker: PickerComponent = React.forwardRef( // Use component active state to support keyboard events. const [active, setActive] = useState(false); - const handleKeyDown = (event: React.KeyboardEvent) => { - // enter - if ((!focusItemValue || !active) && event.keyCode === KEY_CODE.ENTER) { - handleToggleDropdown(); - } + const handleClose = useCallback(() => { + triggerRef.current?.close?.(); + }, []); - // delete - if (event.keyCode === KEY_CODE.BACKSPACE && event.target === toggleRef?.current) { - handleClean(event); - } + const handleOpen = useCallback(() => { + triggerRef.current?.open?.(); + }, []); - if (!menuRef.current) { - return; - } - - onKeyDown(event); - onMenuKeyDown(event, { - enter: selectFocusMenuItem, - esc: handleClose - }); - }; - - const handleToggleDropdown = () => { + const handleToggleDropdown = useCallback(() => { if (active) { handleClose(); return; } handleOpen(); - }; - - const selectFocusMenuItem = (event: React.SyntheticEvent) => { - if (!focusItemValue) { - return; - } - - // Find active `MenuItem` by `value` - const focusItem = findNodeOfTree(data, item => shallowEqual(item[valueKey], focusItemValue)); + }, [active, handleOpen, handleClose]); - setValue(focusItemValue); - handleSelect(focusItemValue, focusItem, event); - handleChangeValue(focusItemValue, event); - handleClose(); - }; + const handleSelect = useCallback( + (value: any, item: ItemDataType, event: React.SyntheticEvent) => { + onSelect?.(value, item, event); + toggleRef.current?.focus(); + }, + [onSelect] + ); - const handleItemSelect = (value: any, item: ItemDataType, event: React.SyntheticEvent) => { - setValue(value); - setFocusItemValue(value); + const handleChangeValue = useCallback( + (value: any, event: React.SyntheticEvent) => { + onChange?.(value, event); + }, + [onChange] + ); - handleSelect(value, item, event); - handleChangeValue(value, event); - handleClose(); - }; + const selectFocusMenuItem = useCallback( + (event: React.SyntheticEvent) => { + if (!focusItemValue) { + return; + } - const handleSelect = (value: any, item: ItemDataType, event: React.SyntheticEvent) => { - onSelect?.(value, item, event); - toggleRef.current?.focus(); - }; + // Find active `MenuItem` by `value` + const focusItem = findNodeOfTree(data, item => + shallowEqual(item[valueKey], focusItemValue) + ); - const handleClose = () => { - triggerRef.current?.hide?.(); - }; + setValue(focusItemValue); + handleSelect(focusItemValue, focusItem, event); + handleChangeValue(focusItemValue, event); + handleClose(); + }, + [data, focusItemValue, valueKey, setValue, handleSelect, handleChangeValue, handleClose] + ); - const handleOpen = () => { - triggerRef.current?.show?.(); - }; + const handleItemSelect = useCallback( + (value: any, item: ItemDataType, event: React.SyntheticEvent) => { + setValue(value); + setFocusItemValue(value); - const handleChangeValue = useCallback( - (value: any, event: React.SyntheticEvent) => { - onChange?.(value, event); + handleSelect(value, item, event); + handleChangeValue(value, event); + handleClose(); }, - [onChange] + [setValue, setFocusItemValue, handleSelect, handleChangeValue, handleClose] ); const handleClean = useCallback( @@ -266,23 +259,58 @@ const SelectPicker: PickerComponent = React.forwardRef( setFocusItemValue(value); handleChangeValue(null, event); }, - [value, disabled, cleanable, handleChangeValue, setFocusItemValue] + [value, disabled, cleanable, setValue, handleChangeValue, setFocusItemValue] ); - const handleExited = () => { + const handleKeyDown = useCallback( + (event: React.KeyboardEvent) => { + // enter + if ((!focusItemValue || !active) && event.keyCode === KEY_CODE.ENTER) { + handleToggleDropdown(); + } + + // delete + if (event.keyCode === KEY_CODE.BACKSPACE && event.target === toggleRef?.current) { + handleClean(event); + } + + if (!menuRef.current) { + return; + } + + onKeyDown(event); + onMenuKeyDown(event, { + enter: selectFocusMenuItem, + esc: handleClose + }); + }, + [ + active, + focusItemValue, + handleClean, + handleClose, + handleToggleDropdown, + onKeyDown, + selectFocusMenuItem + ] + ); + + const handleExited = useCallback(() => { setSearchKeyword(''); setActive(false); onClose?.(); - }; + }, [onClose, setSearchKeyword]); - const handleEntered = () => { + const handleEntered = useCallback(() => { setActive(true); setFocusItemValue(value); onOpen?.(); - }; + }, [onOpen, setFocusItemValue, value]); useImperativeHandle(ref, () => ({ - root: rootRef.current, + get root() { + return triggerRef.current.child; + }, get menu() { return menuRef.current; }, @@ -314,8 +342,10 @@ const SelectPicker: PickerComponent = React.forwardRef( selectedElement = renderValue(value, activeItem, selectedElement); } - const renderDropdownMenu = () => { - const classes = merge(prefix('check-menu'), menuClassName); + const renderDropdownMenu = (positionProps: PositionChildProps, speakerRef) => { + const { left, top, className } = positionProps; + const classes = merge(className, menuClassName, prefix('select-menu')); + const styles = { ...menuStyle, left, top }; let items = filteredData; // Create a tree structure data when set `groupBy` @@ -351,10 +381,10 @@ const SelectPicker: PickerComponent = React.forwardRef( return ( toggleRef.current} getPositionInstance={() => positionRef.current} @@ -387,9 +417,9 @@ const SelectPicker: PickerComponent = React.forwardRef( placement={placement} onEntered={createChainedFunction(handleEntered, onEntered)} onExited={createChainedFunction(handleExited, onExited)} - speaker={renderDropdownMenu()} + speaker={renderDropdownMenu} > - + { }); it('Select picker group should render correct styles', () => { - const instanceRef = React.createRef(); + const ref = React.createRef(); ReactDOM.render( - , + , createTestContainer() ); - const secondItemGroup = instanceRef.current.menu.querySelectorAll( + + const secondItemGroup = ref.current.menu.querySelectorAll( '.group-test-menu .rs-picker-menu-group' )[1]; inChrome && diff --git a/src/Sidenav/Sidenav.tsx b/src/Sidenav/Sidenav.tsx index 9973968686..bc6e322c9f 100644 --- a/src/Sidenav/Sidenav.tsx +++ b/src/Sidenav/Sidenav.tsx @@ -17,6 +17,13 @@ interface SidenavState { openKeys?: any[]; } +export interface SidenavContextType { + openKeys: T[]; + sidenav: boolean; + expanded: boolean; + onOpenChange: (openKeys: T[], event: React.SyntheticEvent) => void; +} + class Sidenav extends React.Component { static propTypes = { classPrefix: PropTypes.string, diff --git a/src/Tooltip/Tooltip.d.ts b/src/Tooltip/Tooltip.d.ts deleted file mode 100644 index 9d3a2a338f..0000000000 --- a/src/Tooltip/Tooltip.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as React from 'react'; -import { TypeAttributes, StandardProps } from '../@types/common'; - -export interface TooltipProps extends StandardProps { - /** Dispaly placement */ - placement?: TypeAttributes.Placement; - - /** Wheather visible */ - visible?: boolean; - - /** Primary content */ - children?: React.ReactNode; -} - -declare const Tooltip: React.ComponentType; - -export default Tooltip; diff --git a/src/Tooltip/Tooltip.tsx b/src/Tooltip/Tooltip.tsx index 10f1edb136..2011939ec1 100644 --- a/src/Tooltip/Tooltip.tsx +++ b/src/Tooltip/Tooltip.tsx @@ -1,52 +1,52 @@ -import * as React from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import _ from 'lodash'; -import { prefix, defaultProps } from '../utils'; -import { TooltipProps } from './Tooltip.d'; -import { overlayProps } from '../Whisper/Whisper'; - -class Tooltip extends React.Component { - static propTypes = { - visible: PropTypes.bool, - classPrefix: PropTypes.string, - className: PropTypes.string, - style: PropTypes.object, - children: PropTypes.node - }; - render() { - const { - className, - classPrefix, - children, - style, - visible, - htmlElementRef, - ...rest - } = this.props; - - const addPrefix = prefix(classPrefix); - const classes = classNames(classPrefix, className); +import { useClassNames } from '../utils'; +import { TypeAttributes, WithAsProps, RsRefForwardingComponent } from '../@types/common'; + +export interface TooltipProps extends WithAsProps { + /** Dispaly placement */ + placement?: TypeAttributes.Placement; + + /** Wheather visible */ + visible?: boolean; + + /** Primary content */ + children?: React.ReactNode; +} + +const defaultProps: Partial = { + as: 'div', + classPrefix: 'tooltip' +}; + +const Tooltip: RsRefForwardingComponent<'div', TooltipProps> = React.forwardRef( + (props: TooltipProps, ref) => { + const { as: Component, className, classPrefix, children, style, visible, ...rest } = props; + + const { merge, withClassPrefix, prefix } = useClassNames(classPrefix); + const classes = merge(className, withClassPrefix()); const styles = { opacity: visible ? 1 : undefined, ...style }; return ( -
    -
    -
    {children}
    -
    + +
    +
    {children}
    + ); } -} +); -export default defaultProps({ - classPrefix: 'tooltip' -})(Tooltip); +Tooltip.displayName = 'Tooltip'; +Tooltip.defaultProps = defaultProps; +Tooltip.propTypes = { + visible: PropTypes.bool, + classPrefix: PropTypes.string, + className: PropTypes.string, + style: PropTypes.object, + children: PropTypes.node +}; + +export default Tooltip; diff --git a/src/Tooltip/index.d.ts b/src/Tooltip/index.d.ts deleted file mode 100644 index de73a9e2b6..0000000000 --- a/src/Tooltip/index.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './Tooltip'; -export * from './Tooltip'; diff --git a/src/Tooltip/index.tsx b/src/Tooltip/index.tsx index d8851cb0a0..caf4dab8bb 100644 --- a/src/Tooltip/index.tsx +++ b/src/Tooltip/index.tsx @@ -1,3 +1,3 @@ import Tooltip from './Tooltip'; - +export type { TooltipProps } from './Tooltip'; export default Tooltip; diff --git a/src/Whisper/Whisper.tsx b/src/Whisper/Whisper.tsx index 103f1320e6..44fe211b72 100644 --- a/src/Whisper/Whisper.tsx +++ b/src/Whisper/Whisper.tsx @@ -1,31 +1,11 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import OverlayTrigger from '../Overlay/OverlayTrigger'; -import { createChainedFunction, placementPolyfill, refType, mergeRefs } from '../utils'; +import { createChainedFunction, placementPolyfill, PLACEMENT } from '../utils'; import { CustomConsumer } from '../CustomProvider'; -import { PLACEMENT } from '../constants'; -import { TriggerProps } from '../Overlay/OverlayTrigger.d'; -import { TooltipProps } from '../Tooltip/Tooltip.d'; -import { PopoverProps } from '../Popover/Popover.d'; +import { OverlayTriggerProps } from '../Overlay/OverlayTrigger'; -export const overlayProps = [ - 'placement', - 'shouldUpdatePosition', - 'arrowOffsetLeft', - 'arrowOffsetTop', - 'positionLeft', - 'positionTop' -]; - -export interface WhisperProps extends TriggerProps { - /** display element */ - speaker?: - | React.ReactElement - | ((props: any, ref: React.RefObject) => React.ReactElement); - - /** @deprecated Use `ref` instead */ - triggerRef?: React.Ref; -} +export type WhisperProps = OverlayTriggerProps; export interface WhisperInstance extends React.Component { open: (delay?: number) => void; @@ -34,7 +14,6 @@ export interface WhisperInstance extends React.Component { const Whisper = React.forwardRef((props: WhisperProps, ref) => { const { - triggerRef, onOpen, onClose, onEntered, @@ -47,12 +26,12 @@ const Whisper = React.forwardRef((props: WhisperProps, ref) => { {context => ( )} @@ -61,7 +40,6 @@ const Whisper = React.forwardRef((props: WhisperProps, ref) => { Whisper.displayName = 'Whisper'; Whisper.propTypes = { - triggerRef: refType, onOpen: PropTypes.func, onClose: PropTypes.func, onEntered: PropTypes.func, diff --git a/src/Whisper/test/WhisperSpec.js b/src/Whisper/test/WhisperSpec.js index 2201516f36..374512647f 100644 --- a/src/Whisper/test/WhisperSpec.js +++ b/src/Whisper/test/WhisperSpec.js @@ -1,6 +1,6 @@ import React from 'react'; import ReactTestUtils from 'react-dom/test-utils'; -import { getDOMNode } from '@test/testUtils'; +import { getDOMNode, getInstance } from '@test/testUtils'; import Whisper from '../Whisper'; import Tooltip from '../../Tooltip'; @@ -68,13 +68,13 @@ describe('Whisper', () => { const doneOp = () => { done(); }; - const triggerRef = React.createRef(); - getDOMNode( - }> + const instance = getInstance( + }> ); - triggerRef.current.open(); + + instance.open(); }); it('Should call onOpen callback', done => { diff --git a/src/constants.ts b/src/constants.ts index 9436af114e..e2440baa7a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -30,8 +30,10 @@ export const PLACEMENT_8 = [ ]; export const PLACEMENT_AUTO = [ 'auto', + 'autoVertical', 'autoVerticalStart', 'autoVerticalEnd', + 'autoHorizontal', 'autoHorizontalStart', 'autoHorizontalEnd' ]; diff --git a/src/utils/constants.ts b/src/utils/constants.ts new file mode 100644 index 0000000000..e2440baa7a --- /dev/null +++ b/src/utils/constants.ts @@ -0,0 +1,89 @@ +export const STATUS_ICON_NAMES: any = { + info: 'info', + success: 'check-circle', + error: 'close-circle', + warning: 'remind' +}; + +export enum PAGINATION_ICON_NAMES { + more = 'more', + prev = 'page-previous', + next = 'page-next', + first = 'page-top', + last = 'page-end' +} + +export const SIZE = ['lg', 'md', 'sm', 'xs']; +export const STATUS = ['success', 'warning', 'error', 'info']; +export const COLOR = ['red', 'orange', 'yellow', 'green', 'cyan', 'blue', 'violet']; + +export const PLACEMENT_4 = ['top', 'bottom', 'right', 'left']; +export const PLACEMENT_8 = [ + 'bottomStart', + 'bottomEnd', + 'topStart', + 'topEnd', + 'leftStart', + 'rightStart', + 'leftEnd', + 'rightEnd' +]; +export const PLACEMENT_AUTO = [ + 'auto', + 'autoVertical', + 'autoVerticalStart', + 'autoVerticalEnd', + 'autoHorizontal', + 'autoHorizontalStart', + 'autoHorizontalEnd' +]; + +export const PLACEMENT = [].concat(PLACEMENT_4, PLACEMENT_8, PLACEMENT_AUTO); + +/** + * Check Tree Node State + */ +export enum CHECK_STATE { + UNCHECK = 0, + CHECK = 1, + INDETERMINATE = 2 +} + +export type CheckStateType = CHECK_STATE.UNCHECK | CHECK_STATE.CHECK | CHECK_STATE.INDETERMINATE; + +export const TREE_NODE_PADDING = 16; +export const TREE_NODE_ROOT_PADDING = 12; + +/** + * Tree Node Drag Type + */ +export enum TREE_NODE_DROP_POSITION { + DRAG_OVER = 0, // drag node in tree node + DRAG_OVER_TOP = 1, // drag node on tree node + DRAG_OVER_BOTTOM = 2 // drag node under tree node +} + +export const KEY_CODE = { + /** value: 8 */ + BACKSPACE: 8, + /** value: 13 */ + ENTER: 13, + /** value: 27 */ + ESC: 27, + /** value: 37 */ + LEFT: 37, + /** value: 38 */ + UP: 38, + /** value: 39 */ + RIGHT: 39, + /** value: 40 */ + DOWN: 40, + /** value: 9 */ + TAB: 9 +}; + +export enum DATERANGE_DISABLED_TARGET { + CALENDAR = 'CALENDAR', + TOOLBAR_BUTTON_OK = 'TOOLBAR_BUTTON_OK', + TOOLBAR_SHORTCUT = 'TOOLBAR_SHORTCUT' +} diff --git a/src/utils/getDOMNode.ts b/src/utils/getDOMNode.ts index fc88678c45..426eb5aae9 100644 --- a/src/utils/getDOMNode.ts +++ b/src/utils/getDOMNode.ts @@ -1,26 +1,19 @@ import { findDOMNode } from 'react-dom'; -export default function getDOMNode(element: any) { - /** - * Native HTML elements - */ +const getRefTarget = (ref: React.RefObject | Element | null | undefined) => { + return ref && ('current' in ref ? ref.current : ref); +}; +export default function getDOMNode(elementOrRef) { + // If elementOrRef is an instance of Position, child is returned. [PositionInstance] + const element = elementOrRef?.child ? elementOrRef?.child : getRefTarget(elementOrRef); + + // Native HTML elements if (element?.nodeType && typeof element?.nodeName === 'string') { return element; } - /** - * The component provides the `getHTMLElement` method. - */ - - const htmlElement = element?.getHTMLElement?.(); - if (htmlElement) { - return htmlElement; - } - - /** - * If you can't get the native HTML element, you can only get it through findDOMNode. - */ + // If you can't get the native HTML element, you can only get it through findDOMNode. // eslint-disable-next-line react/no-find-dom-node - return findDOMNode(element); + return findDOMNode(element) as Element; } diff --git a/src/utils/htmlPropsUtils.ts b/src/utils/htmlPropsUtils.ts index e31851f89a..d17df25d9e 100644 --- a/src/utils/htmlPropsUtils.ts +++ b/src/utils/htmlPropsUtils.ts @@ -3,7 +3,8 @@ * https://github.com/Semantic-Org/Semantic-UI-React/blob/master/src/lib/htmlPropsUtils.js */ -import _ from 'lodash'; +import forEach from 'lodash/forEach'; +import includes from 'lodash/includes'; export const htmlInputAttrs = [ // REACT @@ -99,9 +100,9 @@ export const partitionHTMLProps = ( const inputProps: { [key: string]: string } = {}; const rest = {}; - _.forEach(props, (val, prop) => { + forEach(props, (val, prop) => { const possibleAria = includeAria && (/^aria-.*$/.test(prop) || prop === 'role'); - const target: any = _.includes(htmlProps, prop) || possibleAria ? inputProps : rest; + const target: any = includes(htmlProps, prop) || possibleAria ? inputProps : rest; target[prop] = val; }); diff --git a/src/utils/index.ts b/src/utils/index.ts index d073417f9b..ba114f54d2 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,8 +1,8 @@ export * from './BrowserDetection'; export * from './htmlPropsUtils'; +export * from './constants'; export { default as withStyleProps } from './withStyleProps'; - export { default as guid } from './guid'; export { default as prefix, defaultClassPrefix, getClassNamePrefix, globalKey } from './prefix'; export { default as createChainedFunction } from './createChainedFunction'; @@ -19,10 +19,12 @@ export { default as findNodesOfTree } from './findNodesOfTree'; export { default as createContext } from './createContext'; export { default as placementPolyfill } from './placementPolyfill'; export { default as getMonthView } from './getMonthView'; +export { default as getDOMNode } from './getDOMNode'; export { default as isRTL } from './directionUtil'; export { default as refType } from './refType'; export { default as mergeRefs } from './mergeRefs'; export { default as shallowEqual } from './shallowEqual'; +export { default as reactToString } from './reactToString'; export { default as useClassNames } from './useClassNames'; export { default as useEventListener } from './useEventListener'; export { default as useElementResize } from './useElementResize'; @@ -31,3 +33,4 @@ export { default as usePortal } from './usePortal'; export { default as createComponent } from './createComponent'; export { default as useTimeout } from './useTimeout'; export { default as useControlled } from './useControlled'; +export { default as useRootClose } from './useRootClose'; diff --git a/src/utils/usePortal.tsx b/src/utils/usePortal.tsx index d5f5d0ea85..e68b2eacbc 100644 --- a/src/utils/usePortal.tsx +++ b/src/utils/usePortal.tsx @@ -7,31 +7,23 @@ interface PortalProps { container?: HTMLElement | (() => HTMLElement); } -function usePortal({ id, container }: PortalProps) { - const rootElemRef = useRef( - helper.canUseDOM ? document.createElement('div') : null - ) as React.MutableRefObject; +function usePortal(props: PortalProps = {}) { + const { id, container } = props; + const rootElemRef = useRef(helper.canUseDOM ? document.body : null); useEffect(() => { const containerElement = typeof container === 'function' ? container() : container; - const root = rootElemRef.current; - // Look for existing target dom element to append to const existingParent = id && document.querySelector(`#${id}`); // Parent is either a new root or the existing dom element const parentElement = containerElement || existingParent || document.body; - // Add the detached element to the parent - parentElement.appendChild(root); - - return () => { - root?.remove(); - }; + rootElemRef.current = parentElement; }, [rootElemRef, container, id]); - const Portal = useCallback( + const Portal: React.FC = useCallback( ({ children }: { children: React.ReactNode }) => { if (rootElemRef.current != null) return createPortal(children, rootElemRef.current); return null; diff --git a/src/utils/useRootClose.ts b/src/utils/useRootClose.ts new file mode 100644 index 0000000000..8937039ce8 --- /dev/null +++ b/src/utils/useRootClose.ts @@ -0,0 +1,79 @@ +import { useEffect, useCallback } from 'react'; +import helper from '../DOMHelper'; +import { getDOMNode, KEY_CODE } from './'; + +function isLeftClickEvent(event: React.MouseEvent) { + return event?.button === 0; +} + +function isModifiedEvent(event: React.MouseEvent) { + return !!(event.metaKey || event.altKey || event.ctrlKey || event?.shiftKey); +} + +type TargetType = React.RefObject | Element | null | undefined; + +interface Options { + disabled: boolean; + triggerTarget: TargetType; + overlayTarget: TargetType; +} + +/** + * A hook that listens to the document click event and closes the overlay. + * @param onRootClose + * @param param1 + */ +function useRootClose( + onRootClose: (e: Event) => void, + { disabled, triggerTarget, overlayTarget }: Options +) { + const handleDocumentKeyUp = useCallback( + (event: KeyboardEvent) => { + if (event.keyCode === KEY_CODE.ESC) { + onRootClose?.(event); + } + }, + [onRootClose] + ); + + const handleDocumentClick = useCallback( + event => { + const triggerElement = getDOMNode(triggerTarget); + const overlayElement = getDOMNode(overlayTarget); + + // Check if the clicked element is a trigger. + if (helper.contains(triggerElement, event.target)) { + return; + } + + // Check if the clicked element is a overlay. + if (helper.contains(overlayElement, event.target)) { + return; + } + + if (isModifiedEvent(event) || !isLeftClickEvent(event)) { + return; + } + + onRootClose?.(event); + }, + [onRootClose, triggerTarget, overlayTarget] + ); + + useEffect(() => { + const currentTarget = getDOMNode(triggerTarget); + + if (disabled || !currentTarget) return; + + const doc = helper.ownerDocument(currentTarget); + const onDocumentClickListener = helper.on(doc, 'click', handleDocumentClick, true); + const onDocumentKeyupListener = helper.on(doc, 'keyup', handleDocumentKeyUp); + + return () => { + onDocumentClickListener?.off(); + onDocumentKeyupListener?.off(); + }; + }, [triggerTarget, disabled, onRootClose, handleDocumentClick, handleDocumentKeyUp]); +} + +export default useRootClose; diff --git a/src/utils/withStyleProps.tsx b/src/utils/withStyleProps.tsx index d1cc423a7f..0cda6e84be 100644 --- a/src/utils/withStyleProps.tsx +++ b/src/utils/withStyleProps.tsx @@ -6,12 +6,9 @@ import classNames from 'classnames'; import setDisplayName from 'recompose/setDisplayName'; import wrapDisplayName from 'recompose/wrapDisplayName'; import setPropTypes from 'recompose/setPropTypes'; - import { TypeAttributes } from '../@types/common'; -import prefix from './prefix'; import extendReactStatics from './extendReactStatics'; -import { SIZE, STATUS, COLOR } from '../constants'; -import refType from './refType'; +import { prefix, refType, SIZE, STATUS, COLOR } from '.'; export interface RequiredProps { className?: string; diff --git a/test/testUtils.js b/test/testUtils.js index f9bc6cd375..208e0f12b2 100644 --- a/test/testUtils.js +++ b/test/testUtils.js @@ -3,7 +3,6 @@ import React from 'react'; import { findDOMNode } from 'react-dom'; import ReactTestUtils from 'react-dom/test-utils'; -import _ from 'lodash'; import getPalette from './getPalette'; import tinycolor from 'tinycolor2'; export { getStyle } from 'dom-lib'; @@ -63,6 +62,10 @@ export function getDOMNode(children) { return children; } + if (isDOMElement(children.child)) { + return children.child; + } + if (ReactTestUtils.isCompositeComponent(children)) { return findDOMNode(children); } @@ -75,6 +78,10 @@ export function getDOMNode(children) { return getInstance(children).root; } + if (getInstance(children) && isDOMElement(getInstance(children).child)) { + return getInstance(children).child; + } + return findDOMNode(getInstance(children)); } diff --git a/yarn.lock b/yarn.lock index 349c2e7329..109ee00dd1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1221,6 +1221,13 @@ dependencies: "@types/react" "*" +"@types/react-is@^16.7.1": + version "16.7.1" + resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-16.7.1.tgz#d3f1c68c358c00ce116b55ef5410cf486dd08539" + integrity sha512-dMLFD2cCsxtDgMkTydQCM0PxDq8vwc6uN5M/jRktDfYvH3nQj6pjC9OrCXS2lKlYoYTNJorI/dI8x9dpLshexQ== + dependencies: + "@types/react" "*" + "@types/react-virtualized@^9.21.10": version "9.21.10" resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.21.10.tgz#cd072dc9c889291ace2c4c9de8e8c050da8738b7"

    - link -