开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情
背景
之前在工作中遇到一个需求:存在多个select下拉选择框,当下拉弹框出现时,鼠标经过每一个子项时,需要展示该子项的详细内容。参照同组大佬的代码,利用ReactDOM.createPortal和定位来实现了该需求。
ReactDOM.createPortal
function createPortal(children: React.ReactNode, container: Element, key?: string | null | undefined): React.ReactPortal
第一个children参数表示需要被渲染的子组件或元素,第二个参数container表示该渲染子组件需要挂载的元素节点,代表容器节点。
ReactDOM.createPortal()方法能够让子组件脱离父组件,在父组件以外的地方进行挂载,所以利用它能够实现脱离父组件的子组件定位,它经常用于创建Modal弹框,实现更加自由的弹框定位。
问题与解决方式
在开发过程中遇到了几个问题,提供了解决方案,这里也提供给大家:
所遇问题记录:
- 由于下拉弹框不定宽,导致获取其宽度是个难题:最先开始直接使用.ant-select-selector类名获取select输入框元素,用getBoundingClientRect()方法计算其left、top、width等属性值去定位描述面板,但是会出现下拉框比输入框宽,属性面板遮盖住部分下拉框。
- 添加几项后,获取下拉框类名.ant-select-dropdown元素,下拉框的getBoundingClientRect()或该HTMLElement元素的width、height、left、top等属性值都变为了0
解决方式:
为select添加dropdownClassName属性,每一个Select选择框都有唯一类名作为每个下拉框的类名,从而替换掉.ant-select-dropdown类名,之后就能获取其clientWidth宽度了。
实现思路:
主要实现方式就是在组件中传入dropdownClassName用于后续获取当前下拉框宽高和位置,便于为容器节点定位,当鼠标经过或离开下拉弹框每一项时,使用onMouseEnter和onMouseLeave设置提示详情子组件的显示与否和详情信息。
主要代码如下:
<Select
dropdownClassName={dropdownClassName[0]}
onClick={(e: any) =>
registerFunc(nodeClassname[0], e, dropdownClassName[0])
}
onBlur={() => resetRoots(nodeClassname[0])}
defaultValue='test1'
dropdownMatchSelectWidth={false}
style={{ minWidth: 100 }}
>
{options.map((item) => (
<Select.Option key={item.value} value={item.value}>
<div
onMouseEnter={(e: any) => {
setOpen(true);
setCurDesc(item.value);
}}
onMouseLeave={(e: any) => {
setOpen(false);
setCurDesc("");
}}
>
{item.label}
</div>
</Select.Option>
))}
</Select>
当点击select选择框显示弹出框时,调用注册容器节点registerFunc方法创建容器节点,因为存在多个Select选择框,所以需要定义一个节点对象,用于存储不同的容器节点,然后就利用getBoundingClient()方法获取定位值,最后使用组件即可 注册容器节点registerFunc代码如下:
const selectDropdownEl =
document.getElementsByClassName(dropdownClassName)[0] as HTMLElement;
const containerWidth =
(selectDropdownEl as HTMLElement)?.clientWidth || 0;
const containerRoot = document.createElement("div");
containerRoot.style.position = "absolute";
containerRoot.style.left = `${(selectDropdownEl?.getBoundingClientRect()?.left || 0) + containerWidth}px`;
containerRoot.style.top = `${
(selectDropdownEl?.getBoundingClientRect()?.top || 0)
}px`;
newRoot.style.zIndex = "999";
if (!roots[nodeClassname]) {
document.body.append(containerRoot);
} else {
document.body.replaceChild(containerRoot, roots[nodeClassname]);
}
roots[nodeClassname] = containerRoot;
提示详情子组件代码为:
const ChildPortal: FC<{
info: any;
isOpen: boolean;
nodeClassName: string;
visible?: boolean;
}> = ({ info, isOpen, nodeClassName, visible = false }) => {
if (!isOpen || !info || !roots[nodeClassName]) {
return null;
}
return ReactDOM.createPortal(
<div visible={visible}>
<div
className="content"
style={{
background: "#fff",
minWidth: 200,
maxWidth: 500,
minHeight: 150,
borderRadius: "4px",
boxShadow: "rgba(0,0,0,0.1) 0px 0px 4px",
padding: "10px",
}}
>
<h4>描述</h4>
<p>{info}</p>
</div>
</div>,
roots[nodeClassName]
);
};
demo实现效果
这里实现了一个类似的demo,实现效果如下所示: