- 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 (
}
>
File
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 => (
-
- Click
-
- )}
-
-);
-
-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 ;
- }}
- >
- click me
-
-);
-
-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 (
- }
- >
- File
-
- );
-};
-
-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 (
-
-
toggle
-
-
{
- 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 (
-
-
toggle
-
-
{
- 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 => (
-
- Click
-
- )}
-
- );
- }
-}
-
-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!}>
-
- button
-
-
-);
-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
- }
- >
- {placement}
-
-);
-
-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 = (
-
-
- Click
-
-
- Focus
-
-
- Hover
-
-
- Active
-
-
-);
-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
+ }
>
{placement}
@@ -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 = (
Hover
-
Active
@@ -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
- }
- >
- {placement}
-
-);
-
-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 (
@@ -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 (
-
- );
- }, 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 (
+
+ );
+ }, 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 (
-
-
+
);
}
-}
+);
+
+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 => (
- void>(
- onHide,
- context ? context.onModalHide : null
- )}
- >
- ×
-
+
)}
);
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}
+
);
}
-}
+);
-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(
+ }>
button
);
- 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"