Firefox中React组件右/下边缘拖拽Resize Handle消失问题求助
问题背景
有一个React子组件CommentTextArea,核心为文本输入框,包含相对定位的div及内部通过flex占满空间的textarea,div周边设有8个绝对定位的圆形resize手柄。组件位置(x,y)、宽高由父组件Feedback传入。
拖拽调整组件大小时,父组件逻辑为:仅拖拽顶部/左侧边缘手柄时更新组件位置,所有拖拽操作均更新宽高。但在Firefox中,拖拽底部/右侧边缘手柄(仅需更新宽高,无需修改位置)时,拖拽一小段距离后resize手柄会消失;若强制给组件的originX/Y添加极小值更新位置,手柄则能正常显示,推测是右/下边缘的手柄未被浏览器重绘。
父组件resize处理函数
const applyResizingToTextItem = ( item, resizeHandleType, newLocationOfHandle ) => { if ( resizeHandleType === "top" || resizeHandleType === "top-left" || resizeHandleType === "top-right" ) { item.height += item.topOffset - newLocationOfHandle.y; item.topOffset = newLocationOfHandle.y; } if ( resizeHandleType === "left" || resizeHandleType === "top-left" || resizeHandleType === "bottom-left" ) { item.width += item.leftOffset - newLocationOfHandle.x; item.leftOffset = newLocationOfHandle.x; } if ( resizeHandleType === "bottom" || resizeHandleType === "bottom-left" || resizeHandleType === "bottom-right" ) { item.height = newLocationOfHandle.y - item.topOffset; item.topOffset = item.topOffset - 0.0001; // 临时修复:强制更新位置 } if ( resizeHandleType === "right" || resizeHandleType === "bottom-right" || resizeHandleType === "top-right" ) { item.width = newLocationOfHandle.x - item.leftOffset; // item.leftOffset = item.leftOffset + 0.0001; // 临时修复代码 } };
子组件核心代码片段
export default CommentTextArea = (props, ref) => { const useStyles = makeStyles((theme) => ({ foreignObject: { overflow: "visible", width: "1px", height: "1px" }, textarea: { cursor: "all-scroll", outline: "none" }, disablePageScroll: { "& textarea": { touchAction: "none", }, }, selected: { border: "2px solid #359139", }, resizeHandle: { position: "absolute", width: "8px", height: "8px", backgroundColor: "#359139", borderRadius: "50%", "&.top": { top: "-4px", left: "50%", transform: "translateX(-50%)", cursor: "ns-resize", }, "&.right": { right: "-4px", top: "50%", transform: "translateY(-50%)", cursor: "ew-resize", }, "&.bottom": { bottom: "-4px", left: "50%", transform: "translateX(-50%)", cursor: "ns-resize", }, "&.left": { left: "-4px", top: "50%", transform: "translateY(-50%)", cursor: "ew-resize", }, "&.top-left": { top: "-4px", left: "-4px", cursor: "nw-resize" }, "&.top-right": { top: "-4px", right: "-4px", cursor: "ne-resize" }, "&.bottom-left": { bottom: "-4px", left: "-4px", cursor: "ne-resize" }, "&.bottom-right": { bottom: "-4px", right: "-4px", cursor: "nw-resize" }, }, })); // 省略事件监听等代码... return( <> <foreignObject x={topLeftX} y={topLeftY} className={`${classes.foreignObject} ${ deviceContext.isTouchEnabled && classes.disablePageScroll }`} > <div style={{ position: "relative", display: "flex", width: `${width}px`, height: `${height}px`, }} > <textarea style={{ color: textColor, fontSize: `${textSize}px`, backgroundColor: "transparent", resize: "none", flex: "auto", minWidth: "0px", minHeight: "0px", }} onClick={stopPropagation} onMouseDown={handleDragStart} onTouchStart={handleDragStart} className={`${props.selected ? classes.selected : ""} ${ classes.textarea }`} value={props.value} onChange={(event) => props.onTextAreaChange(event, props.id)} /> {props.selected && ( <> <div className={`${classes.resizeHandle} top`} data-handle="top" onClick={stopPropagation} onMouseDown={handleDragStart} onTouchStart={handleDragStart} /> <button className={`${classes.resizeHandle} right`} data-handle="right" onClick={stopPropagation} onMouseDown={handleDragStart} onTouchStart={handleDragStart} /> <div className={`${classes.resizeHandle} bottom`} data-handle="bottom" onClick={stopPropagation} onMouseDown={handleDragStart} onTouchStart={handleDragStart} /> <div className={`${classes.resizeHandle} left`} data-handle="left" onClick={stopPropagation} onMouseDown={handleDragStart} onTouchStart={handleDragStart} /> <div className={`${classes.resizeHandle} top-left`} data-handle="top-left" onClick={stopPropagation} onMouseDown={handleDragStart} onTouchStart={handleDragStart} /> <div className={`${classes.resizeHandle} top-right`} data-handle="top-right" onClick={stopPropagation} onMouseDown={handleDragStart} onTouchStart={handleDragStart} /> <div className={`${classes.resizeHandle} bottom-left`} data-handle="bottom-left" onClick={stopPropagation} onMouseDown={handleDragStart} onTouchStart={handleDragStart} /> <div className={`${classes.resizeHandle} bottom-right`} data-handle="bottom-right" onClick={stopPropagation} onMouseDown={handleDragStart} onTouchStart={handleDragStart} /> </> )} </div> </foreignObject> </> ); }
修复方案
方案1:通过React Key强制重绘
React依赖props变化触发重绘,当仅修改宽高时,Firefox可能优化了非必要元素的渲染。给子组件添加随resize操作递增的key,强制React重新渲染整个组件,包括resize手柄。
父组件修改:
// 父组件中添加状态 const [resizeRenderKey, setResizeRenderKey] = useState(0); // 在applyResizingToTextItem函数末尾添加 setResizeRenderKey(prev => prev + 1); // 渲染CommentTextArea时传入key <CommentTextArea key={resizeRenderKey} // 其他props... />
方案2:优化ForeignObject布局
子组件中foreignObject的宽高设置为1px,这可能导致Firefox无法正确计算内部元素的渲染范围,超出部分被隐藏。修改foreignObject的样式,使其宽高匹配内部内容:
// 修改子组件的useStyles foreignObject: { overflow: "visible", width: "100%", height: "100%", // 或者直接绑定组件宽高 width: props => `${props.width}px`, height: props => `${props.height}px`, },
方案3:CSS强制触发重绘
利用CSS属性触发浏览器重绘机制,给resize手柄容器添加will-change属性,或微小的transform变化:
修改子组件resizeHandle样式:
resizeHandle: { position: "absolute", width: "8px", height: "8px", backgroundColor: "#359139", borderRadius: "50%", will-change: "transform, top, right, bottom, left", // 告知浏览器元素会变化 // 其他手柄定位样式... },
或者给外层div添加GPU加速属性:
// 子组件中外层div的style <div style={{ position: "relative", display: "flex", width: `${width}px`, height: `${height}px`, transform: "translateZ(0)", // 强制GPU渲染,触发重绘 }} >
方案4:优雅的位置偏移替代方案
保留临时修复的核心逻辑,但不修改业务数据的topOffset/leftOffset,而是在子组件中动态添加微小偏移:
父组件传递resize状态:
// 在applyResizingToTextItem中记录当前是否拖拽右/下边缘 const [isResizingRightOrBottom, setIsResizingRightOrBottom] = useState(false); // 拖拽开始时设置状态,结束时重置 const handleDragStart = (e) => { const handleType = e.target.dataset.handle; setIsResizingRightOrBottom( handleType === "right" || handleType === "bottom" || handleType.includes("right") || handleType.includes("bottom") ); }; // 传递给子组件 <CommentTextArea isResizingRightOrBottom={isResizingRightOrBottom} // 其他props... />
子组件调整位置计算:
// 子组件中计算调整后的位置 const adjustedX = topLeftX + (props.isResizingRightOrBottom ? 0.0001 : 0); const adjustedY = topLeftY + (props.isResizingRightOrBottom ? 0.0001 : 0); // 应用到foreignObject <foreignObject x={adjustedX} y={adjustedY} // 其他props... >
内容的提问来源于stack exchange,提问作者oomer

