为什么底层组件重新渲染时 popper 会跳到左上角?
Why does popper jump to top-left corner when underlying component re-renders?
我正在使用 Material-UI Popper 组件(它又使用 popper.js)来创建悬停工具栏。在大多数情况下,它运行良好,除了一种奇怪的行为:
- Select 一些文本:悬停工具栏出现在文本上方 - 正如预期的那样。
- Select 工具栏中的任何按钮:执行适当的操作。但是,工具栏会跳转到 window 的左上角。见下文。
您可以在 my Storybook 中尝试此行为 - 只需 select 一些文本并单击其中一个“T”按钮。
基本问题围绕着 popper 的定位:
- 当用户 select 发送一些文本时,会创建一个伪造的虚拟元素并将其作为锚元素传递给 popper。 Popper 使用此
anchorEl
来定位悬停工具栏。到目前为止一切顺利。
- 当用户单击工具栏中的按钮时,悬停工具栏会跳到 window 的左上角。
我猜这是因为锚元素在底层组件重新呈现时以某种方式丢失了。我不知道为什么,但这只是我的理论。有人可以帮我解决这个问题吗?
计算 anchorEl
的代码位于 React useEffect()
中。我已确保 useEffect
的依赖列表是准确的。我可以看到当工具栏跳转时,useEffect()
没有被调用,这意味着 anchorEl
没有被重新计算。这让我相信工具栏应该在当前位置保持完整,而不是跳到 (0,0)。但这并没有发生:-(.
这是工具栏组件中的 useEffect()
代码。您可以在 my repo 中找到完整代码。任何帮助将不胜感激。
useEffect(() => {
if (editMode === 'toolbar') {
if (isTextSelected) {
const domSelection = window.getSelection();
if (domSelection === null || domSelection.rangeCount === 0) {
return;
}
const domRange = domSelection.getRangeAt(0);
const rect = domRange.getBoundingClientRect();
setAnchorEl({
clientWidth: rect.width,
clientHeight: rect.height,
getBoundingClientRect: () =>
domRange.getBoundingClientRect(),
});
setToolbarOpen(true);
} else {
setToolbarOpen(false);
}
} else {
setToolbarOpen(false);
}
}, [editMode, isTextSelected, selection, selectionStr]);
我相信你的 domRange
在 toggleBlock 完成工作后不再有效(由于 dom 节点被替换),所以 getBoundingClientRect
不再返回任何东西有意义。
您应该能够通过重做获取 anchorEl getBoundingClientRect
范围内的工作来解决此问题。也许像下面这样(我没有尝试执行它,所以不能保证没有小错误):
const getSelectionRange = () => {
const domSelection = window.getSelection();
if (domSelection === null || domSelection.rangeCount === 0) {
return null;
}
return domSelection.getRangeAt(0);
};
useEffect(() => {
if (editMode === "toolbar") {
if (isTextSelected) {
const domRange = getSelectionRange();
if (domRange === null) {
return;
}
const rect = domRange.getBoundingClientRect();
setAnchorEl({
clientWidth: rect.width,
clientHeight: rect.height,
getBoundingClientRect: () => {
const innerDomRange = getSelectionRange();
return innerDomRange === null
? null
: innerDomRange.getBoundingClientRect();
}
});
setToolbarOpen(true);
} else {
setToolbarOpen(false);
}
} else {
setToolbarOpen(false);
}
}, [editMode, isTextSelected, selection, selectionStr]);
我正在使用 Material-UI Popper 组件(它又使用 popper.js)来创建悬停工具栏。在大多数情况下,它运行良好,除了一种奇怪的行为:
- Select 一些文本:悬停工具栏出现在文本上方 - 正如预期的那样。
- Select 工具栏中的任何按钮:执行适当的操作。但是,工具栏会跳转到 window 的左上角。见下文。
您可以在 my Storybook 中尝试此行为 - 只需 select 一些文本并单击其中一个“T”按钮。
基本问题围绕着 popper 的定位:
- 当用户 select 发送一些文本时,会创建一个伪造的虚拟元素并将其作为锚元素传递给 popper。 Popper 使用此
anchorEl
来定位悬停工具栏。到目前为止一切顺利。 - 当用户单击工具栏中的按钮时,悬停工具栏会跳到 window 的左上角。
我猜这是因为锚元素在底层组件重新呈现时以某种方式丢失了。我不知道为什么,但这只是我的理论。有人可以帮我解决这个问题吗?
计算 anchorEl
的代码位于 React useEffect()
中。我已确保 useEffect
的依赖列表是准确的。我可以看到当工具栏跳转时,useEffect()
没有被调用,这意味着 anchorEl
没有被重新计算。这让我相信工具栏应该在当前位置保持完整,而不是跳到 (0,0)。但这并没有发生:-(.
这是工具栏组件中的 useEffect()
代码。您可以在 my repo 中找到完整代码。任何帮助将不胜感激。
useEffect(() => {
if (editMode === 'toolbar') {
if (isTextSelected) {
const domSelection = window.getSelection();
if (domSelection === null || domSelection.rangeCount === 0) {
return;
}
const domRange = domSelection.getRangeAt(0);
const rect = domRange.getBoundingClientRect();
setAnchorEl({
clientWidth: rect.width,
clientHeight: rect.height,
getBoundingClientRect: () =>
domRange.getBoundingClientRect(),
});
setToolbarOpen(true);
} else {
setToolbarOpen(false);
}
} else {
setToolbarOpen(false);
}
}, [editMode, isTextSelected, selection, selectionStr]);
我相信你的 domRange
在 toggleBlock 完成工作后不再有效(由于 dom 节点被替换),所以 getBoundingClientRect
不再返回任何东西有意义。
您应该能够通过重做获取 anchorEl getBoundingClientRect
范围内的工作来解决此问题。也许像下面这样(我没有尝试执行它,所以不能保证没有小错误):
const getSelectionRange = () => {
const domSelection = window.getSelection();
if (domSelection === null || domSelection.rangeCount === 0) {
return null;
}
return domSelection.getRangeAt(0);
};
useEffect(() => {
if (editMode === "toolbar") {
if (isTextSelected) {
const domRange = getSelectionRange();
if (domRange === null) {
return;
}
const rect = domRange.getBoundingClientRect();
setAnchorEl({
clientWidth: rect.width,
clientHeight: rect.height,
getBoundingClientRect: () => {
const innerDomRange = getSelectionRange();
return innerDomRange === null
? null
: innerDomRange.getBoundingClientRect();
}
});
setToolbarOpen(true);
} else {
setToolbarOpen(false);
}
} else {
setToolbarOpen(false);
}
}, [editMode, isTextSelected, selection, selectionStr]);