mirror of
https://github.com/ant-design/ant-design.git
synced 2024-11-24 02:59:58 +08:00
docs: Modal hooks bug (#39708)
This commit is contained in:
parent
78592a5596
commit
f1c6350119
166
docs/blog/modal-hook-order.en-US.md
Normal file
166
docs/blog/modal-hook-order.en-US.md
Normal file
@ -0,0 +1,166 @@
|
||||
---
|
||||
title: Funny Modal hook BUG
|
||||
date: 2022-12-21
|
||||
author: zombieJ
|
||||
---
|
||||
|
||||
Recently we encountered an [issue](https://github.com/ant-design/ant-design/issues/39427), saying that when `contextHolder` of `Modal.useModal` is placed in different positions, `modal.confirm` popup location will be different:
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import { Button, Modal } from 'antd';
|
||||
|
||||
export default () => {
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Modal open>
|
||||
<Button
|
||||
onClick={() => {
|
||||
modal.confirm({ title: 'Hello World' });
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
|
||||
{/* 🚨 BUG when put here */}
|
||||
{contextHolder}
|
||||
</Modal>
|
||||
|
||||
{/* ✅ Work as expect when put here */}
|
||||
{/* {contextHolder} */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
Workable version:
|
||||
|
||||
![Normal](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*VJJUTL88uM4AAAAAAAAAAAAADrJ8AQ/original)
|
||||
|
||||
Bug version:
|
||||
|
||||
![BUG](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*a_ulS7EaylkAAAAAAAAAAAAADrJ8AQ/original)
|
||||
|
||||
From the figure above, we can see that when `contextHolder` is placed inside `Modal`, the pop-up position of the hooks call is incorrect.
|
||||
|
||||
### Why?
|
||||
|
||||
antd's Modal internal calls the `rc-dialog` component library, which accepts a `mousePosition` attribute to control the pop-up position([Dialog/Content/index.tsx](https://github.com/react-component/dialog/blob/79649e187ee512be6b3eb3b76e4a6b618b67ebc7/src/Dialog/Content/index.tsx#L43)):
|
||||
|
||||
```tsx
|
||||
// pseudocode
|
||||
const elementOffset = offset(dialogElement);
|
||||
const transformOrigin = `${mousePosition.x - elementOffset.left}px ${
|
||||
mousePosition.y - elementOffset.top
|
||||
}px`;
|
||||
```
|
||||
|
||||
The `offset` method is used to obtain the coordinate position of the form itself([util.ts](https://github.com/react-component/dialog/blob/79649e187ee512be6b3eb3b76e4a6b618b67ebc7/src/util.ts#L28)):
|
||||
|
||||
```tsx
|
||||
// pseudocode
|
||||
function offset(el: Element) {
|
||||
const { left, top } = el.getBoundingClientRect();
|
||||
return { left, top };
|
||||
}
|
||||
```
|
||||
|
||||
Through breakpoint debugging, we can find that the value of `mousePosition` is correct, but the value of `rect` obtained in `offset` is wrong:
|
||||
|
||||
```json
|
||||
{
|
||||
"left": 0,
|
||||
"top": 0,
|
||||
"width": 0,
|
||||
"height": 0
|
||||
}
|
||||
```
|
||||
|
||||
This value obviously means that the form component has not been added to the DOM tree at the animation start node, so we need to check the logic added by Dialog.
|
||||
|
||||
### createPortal
|
||||
|
||||
`rc-dialog` creates a node in the document through `rc-portal`, and then renders the component to this node through `ReactDOM.createPortal`. For the different positions of `contextHolder` and different interactive, it can be speculated that there must be a problem with the timing of creating nodes in the document, so we can take a closer look at the part of adding nodes by default in `rc-portal`([useDom.tsx](https://github.com/react-component/portal/blob/85e6e15ee97c70ec260c5409d9d273d6967e3560/src/useDom.tsx#L55)):
|
||||
|
||||
```tsx
|
||||
// pseudocode
|
||||
function append() {
|
||||
// This is not real world code, just for explain
|
||||
document.body.appendChild(document.createElement('div'));
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (queueCreate) {
|
||||
queueCreate(append);
|
||||
} else {
|
||||
append();
|
||||
}
|
||||
}, []);
|
||||
```
|
||||
|
||||
Among them, `queueCreate` is obtained through `context`, the purpose is to prevent the situation that the child element is created before the parent element under the nesting level:
|
||||
|
||||
```tsx
|
||||
<Modal title="Hello 1" open>
|
||||
<Modal title="Hello 2" open>
|
||||
<Modal>
|
||||
<Modal>
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- Child `useLayoutEffect` is run before parent. Which makes inject dom before parent -->
|
||||
<div data-title="Hello 2"></div>
|
||||
<div data-title="Hello 1"></div>
|
||||
```
|
||||
|
||||
Use `queueCreate` to add the `append` of the child element to the queue, and then use `useLayoutEffect` to execute:
|
||||
|
||||
```tsx
|
||||
// pseudocode
|
||||
const [queue, setQueue] = useState<VoidFunction[]>([]);
|
||||
|
||||
function queueCreate(appendFn: VoidFunction) {
|
||||
setQueue((origin) => {
|
||||
const newQueue = [appendFn, ...origin];
|
||||
return newQueue;
|
||||
});
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (queue.length) {
|
||||
queue.forEach((appendFn) => appendFn());
|
||||
setQueue([]);
|
||||
}
|
||||
}, [queue]);
|
||||
```
|
||||
|
||||
### Resolution
|
||||
|
||||
Due to the above queue operation, the dom of the portal will be triggered in the next `useLayoutEffect` under nesting. This causes the `uesLayoutEffect` timing of the animation to start in `rc-dialog` after the node behavior is added, resulting in the element not being in the document and unable to obtain the correct coordinate information.
|
||||
|
||||
Since Modal is already enabled, it does not need to be executed asynchronously through `queue`, so we only need to add a judgment if it is enabled, and execute `append` directly:
|
||||
|
||||
```tsx
|
||||
// pseudocode
|
||||
const appendedRef = useRef(false);
|
||||
|
||||
const queueCreate = !appendedRef.current
|
||||
? (appendFn: VoidFunction) => {
|
||||
// same code
|
||||
}
|
||||
: undefined;
|
||||
|
||||
function append() {
|
||||
// This is not real world code, just for explain
|
||||
document.body.appendChild(document.createElement('div'));
|
||||
appendedRef.current = true;
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
return <PortalContext value={queueCreate}>{children}</PortalContext>;
|
||||
```
|
||||
|
||||
That's all.
|
166
docs/blog/modal-hook-order.zh-CN.md
Normal file
166
docs/blog/modal-hook-order.zh-CN.md
Normal file
@ -0,0 +1,166 @@
|
||||
---
|
||||
title: Modal hook 的有趣 BUG
|
||||
date: 2022-12-21
|
||||
author: zombieJ
|
||||
---
|
||||
|
||||
最近我们遇到了一个 [issue](https://github.com/ant-design/ant-design/issues/39427),说是 `Modal.useModal` 的 `contextHolder` 在放置不同的位置时,`modal.confirm` 弹出位置会不一样:
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import { Button, Modal } from 'antd';
|
||||
|
||||
export default () => {
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Modal open>
|
||||
<Button
|
||||
onClick={() => {
|
||||
modal.confirm({ title: 'Hello World' });
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
|
||||
{/* 🚨 BUG when put here */}
|
||||
{contextHolder}
|
||||
</Modal>
|
||||
|
||||
{/* ✅ Work as expect when put here */}
|
||||
{/* {contextHolder} */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
正常版本:
|
||||
|
||||
![Normal](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*VJJUTL88uM4AAAAAAAAAAAAADrJ8AQ/original)
|
||||
|
||||
有问题版本:
|
||||
|
||||
![BUG](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*a_ulS7EaylkAAAAAAAAAAAAADrJ8AQ/original)
|
||||
|
||||
从上图可以看到当 `contextHolder` 放在 `Modal` 内部时,hooks 调用的弹出位置不正确了。
|
||||
|
||||
### 思路整理
|
||||
|
||||
antd 的 Modal 底层调用的是 `rc-dialog` 组件库,其接受一个 `mousePosition` 属性,用于控制弹出位置([Dialog/Content/index.tsx](https://github.com/react-component/dialog/blob/79649e187ee512be6b3eb3b76e4a6b618b67ebc7/src/Dialog/Content/index.tsx#L43)):
|
||||
|
||||
```tsx
|
||||
// pseudocode
|
||||
const elementOffset = offset(dialogElement);
|
||||
const transformOrigin = `${mousePosition.x - elementOffset.left}px ${
|
||||
mousePosition.y - elementOffset.top
|
||||
}px`;
|
||||
```
|
||||
|
||||
其中 `offset` 方法用于获取窗体本身的坐标位置([util.ts](https://github.com/react-component/dialog/blob/79649e187ee512be6b3eb3b76e4a6b618b67ebc7/src/util.ts#L28)):
|
||||
|
||||
```tsx
|
||||
// pseudocode
|
||||
function offset(el: Element) {
|
||||
const { left, top } = el.getBoundingClientRect();
|
||||
return { left, top };
|
||||
}
|
||||
```
|
||||
|
||||
通过断点调试,我们可以发现 `mousePosition` 的值是正确的,但是 `offset` 中获取的 `rect` 的值是错误的:
|
||||
|
||||
```json
|
||||
{
|
||||
"left": 0,
|
||||
"top": 0,
|
||||
"width": 0,
|
||||
"height": 0
|
||||
}
|
||||
```
|
||||
|
||||
这个值很明显代表窗体组件在动画启动节点尚未添加到 DOM 树中,所以我们需要查看一下 Dialog 添加的逻辑。
|
||||
|
||||
### createPortal
|
||||
|
||||
`rc-dialog` 通过 `rc-portal` 在 document 中创建一个节点,然后通过 `ReactDOM.createPortal` 将组件渲染到这个节点上。对于 `contextHolder` 位置不同而出现表现不同可以推测,一定是在 document 创建节点的时序出现了问题,于是我们可以进一步看一下 `rc-portal` 中默认添加节点的部分([useDom.tsx](https://github.com/react-component/portal/blob/85e6e15ee97c70ec260c5409d9d273d6967e3560/src/useDom.tsx#L55)):
|
||||
|
||||
```tsx
|
||||
// pseudocode
|
||||
function append() {
|
||||
// This is not real world code, just for explain
|
||||
document.body.appendChild(document.createElement('div'));
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (queueCreate) {
|
||||
queueCreate(append);
|
||||
} else {
|
||||
append();
|
||||
}
|
||||
}, []);
|
||||
```
|
||||
|
||||
其中 `queueCreate` 是通过 `context` 获取,目的是为了防止在嵌套层级下,子元素创建先于父元素的情况:
|
||||
|
||||
```tsx
|
||||
<Modal title="Hello 1" open>
|
||||
<Modal title="Hello 2" open>
|
||||
<Modal>
|
||||
<Modal>
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- Child `useLayoutEffect` is run before parent. Which makes inject dom before parent -->
|
||||
<div data-title="Hello 2"></div>
|
||||
<div data-title="Hello 1"></div>
|
||||
```
|
||||
|
||||
通过 `queueCreate` 将子元素的 `append` 加入队列,然后再通过 `useLayoutEffect` 执行:
|
||||
|
||||
```tsx
|
||||
// pseudocode
|
||||
const [queue, setQueue] = useState<VoidFunction[]>([]);
|
||||
|
||||
function queueCreate(appendFn: VoidFunction) {
|
||||
setQueue((origin) => {
|
||||
const newQueue = [appendFn, ...origin];
|
||||
return newQueue;
|
||||
});
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (queue.length) {
|
||||
queue.forEach((appendFn) => appendFn());
|
||||
setQueue([]);
|
||||
}
|
||||
}, [queue]);
|
||||
```
|
||||
|
||||
### 问题分析
|
||||
|
||||
由于上述的队列操作,使得 portal 的 dom 在嵌套下会在下一个 `useLayoutEffect` 触发。这导致添加节点行为后于 `rc-dialog` 启动动画的 `uesLayoutEffect` 时机,导致元素不在 document 中而无法获取正确的坐标信息。
|
||||
|
||||
由于 Modal 已经是开启状态,其实不需要通过 `queue` 异步执行,所以我们只需要加一个判断如果是开启状态,直接执行 `append` 即可:
|
||||
|
||||
```tsx
|
||||
// pseudocode
|
||||
const appendedRef = useRef(false);
|
||||
|
||||
const queueCreate = !appendedRef.current
|
||||
? (appendFn: VoidFunction) => {
|
||||
// same code
|
||||
}
|
||||
: undefined;
|
||||
|
||||
function append() {
|
||||
// This is not real world code, just for explain
|
||||
document.body.appendChild(document.createElement('div'));
|
||||
appendedRef.current = true;
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
return <PortalContext value={queueCreate}>{children}</PortalContext>;
|
||||
```
|
||||
|
||||
以上。
|
@ -1,7 +1,7 @@
|
||||
---
|
||||
title: about antd test library migration
|
||||
date: 2022-12-20
|
||||
author: zombieJ,li-jia-nan
|
||||
author: li-jia-nan,zombieJ
|
||||
---
|
||||
|
||||
Hello, I am **[@li-jia-nan](https://github.com/li-jia-nan)**. It is also a new Collaborator who joined antd in the past few months. Fortunately, as one of the Collaborators, I developed the **[FloatButton component](/components/float-button)** and **[QRCode component](/components/qrcode)**, as well as some other maintenance work. Let me share the migration of the antd test library son~
|
||||
|
@ -1,7 +1,7 @@
|
||||
---
|
||||
title: antd 测试库迁移的那些事儿
|
||||
date: 2022-12-20
|
||||
author: zombieJ,li-jia-nan
|
||||
author: li-jia-nan,zombieJ
|
||||
---
|
||||
|
||||
大家好,我是 **[@li-jia-nan](https://github.com/li-jia-nan)**。也是前几个月新加入 antd 的 Collaborator, 有幸作为 Collaborators 之一,我开发了 **[FloatButton](/components/float-button-cn)** 组件和 **[QRCode](/components/qrcode-cn)** 组件,以及一些其它维护工作,下面分享一下 antd 测试库迁移的那些事儿~
|
||||
|
Loading…
Reference in New Issue
Block a user