如何横向调整MUI Drawer宽度并适配其余布局?
MUI左侧Drawer拖拽调整宽度实现及异常修复
问题概述
需要实现左侧MUI Drawer打开后可横向拖拽调整宽度,其余布局自动适配,且无法使用Grid组件。当前实现存在异常:鼠标抬起后抽屉有时仍持续缩放,需点击页面后重新操作才能恢复。
异常原因分析
- 事件监听移除逻辑失效:
handleMouseUp未用useCallback包裹,组件每次渲染都会生成新的函数引用,导致removeEventListener无法匹配到对应的监听函数,事件未被正确移除。 - 宽度计算不准确:依赖
document.body.offsetLeft计算宽度,当页面存在滚动或布局偏移时,结果易出现偏差,干扰拖拽逻辑。
解决方案
关键优化点
- 用
useCallback包裹handleMouseUp和handleMouseMove,保证函数引用稳定,确保事件能被正确移除。 - 基于Drawer元素自身的位置计算拖拽宽度,替代
document.body.offsetLeft,提升计算准确性。 - 统一使用事件冒泡阶段(移除
addEventListener的第三个参数true),避免捕获逻辑干扰。 - 处理Drawer关闭时的状态重置,避免残留宽度影响下次打开后的拖拽体验。
完整实现代码
const useStyles = makeStyles((theme) => ({ dragger: { width: "5px", backgroundColor: "#f4f7f9", cursor: "ew-resize", padding: "4px 0 0", position: "absolute", top: 0, right: 0, bottom: 0, zIndex: 100 } })); export default function MiniDrawer() { const theme = useTheme(); const classes = useStyles(); const drawerRef = React.useRef(null); const [open, setOpen] = React.useState(false); const minDrawerWidth = 50; const maxDrawerWidth = 1000; const [drawerWidth, setDrawerWidth] = React.useState(minDrawerWidth); // 稳定函数引用,确保事件能被正确移除 const handleMouseMove = React.useCallback((e) => { if (!drawerRef.current) return; // 基于Drawer元素的offsetLeft计算宽度,更准确 const newWidth = e.clientX - drawerRef.current.offsetLeft; if (newWidth > minDrawerWidth && newWidth < maxDrawerWidth) { setDrawerWidth(newWidth); } }, []); const handleMouseUp = React.useCallback(() => { document.removeEventListener("mouseup", handleMouseUp); document.removeEventListener("mousemove", handleMouseMove); }, [handleMouseMove]); const handleMouseDown = (e) => { e.preventDefault(); // 阻止默认行为,避免页面选中文字等干扰 document.addEventListener("mouseup", handleMouseUp); document.addEventListener("mousemove", handleMouseMove); }; const handleDrawerOpen = () => { setOpen(true); // 打开时重置宽度为最小宽度(可根据需求调整) setDrawerWidth(minDrawerWidth); }; const handleDrawerClose = () => { setOpen(false); }; const drawer = ( <div> <DrawerHeader> <IconButton onClick={handleDrawerClose}> {theme.direction === "rtl" ? ( <ChevronRightIcon /> ) : ( <ChevronLeftIcon /> )} </IconButton> </DrawerHeader> <Divider /> <List> {["Inbox", "Starred", "Send email", "Drafts"].map((text, index) => ( <ListItem key={text} disablePadding sx={{ display: "block" }}> <ListItemButton sx={{ minHeight: 48, justifyContent: open ? "initial" : "center", px: 2.5 }} > <ListItemIcon sx={{ minWidth: 0, mr: open ? 3 : "auto", justifyContent: "center" }} > {index % 2 === 0 ? <InboxIcon /> : <MailIcon />} </ListItemIcon> <ListItemText primary={text} sx={{ opacity: open ? 1 : 0 }} /> </ListItemButton> </ListItem> ))} </List> <Divider /> <List> {["All mail", "Trash", "Spam"].map((text, index) => ( <ListItem key={text} disablePadding sx={{ display: "block" }}> <ListItemButton sx={{ minHeight: 48, justifyContent: open ? "initial" : "center", px: 2.5 }} > <ListItemIcon sx={{ minWidth: 0, mr: open ? 3 : "auto", justifyContent: "center" }} > {index % 2 === 0 ? <InboxIcon /> : <MailIcon />} </ListItemIcon> <ListItemText primary={text} sx={{ opacity: open ? 1 : 0 }} /> </ListItemButton> </ListItem> ))} </List> </div> ); return ( <Box sx={{ display: "flex" }}> <CssBaseline /> <AppBar position="fixed" open={open}> <Toolbar> <IconButton color="inherit" aria-label="open drawer" onClick={handleDrawerOpen} edge="start" sx={{ marginRight: 5, ...(open && { display: "none" }) }} > <MenuIcon /> </IconButton> <Typography variant="h6" noWrap component="div"> Mini variant drawer </Typography> </Toolbar> </AppBar> <Drawer ref={drawerRef} variant="permanent" open={open} PaperProps={{ style: { width: open ? drawerWidth : minDrawerWidth, transition: open ? 'width 0.3s ease' : 'none' // 添加过渡动画,提升体验 } }} > {/* 仅在Drawer打开时显示拖拽条 */} {open && ( <div id="dragger" onMouseDown={handleMouseDown} className={classes.dragger} /> )} {drawer} </Drawer> <Box component="main" sx={{ flexGrow: 1, p: 3, transition: 'margin 0.3s ease' }}> <DrawerHeader /> <Typography paragraph> Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Rhoncus dolor purus non enim praesent elementum facilisis leo vel. Risus at ultrices mi tempus imperdiet. Semper risus in hendrerit gravida rutrum quisque non tellus. Convallis convallis tellus id interdum velit laoreet id donec ultrices. Odio morbi quis commodo odio aenean sed adipiscing. Amet nisl suscipit adipiscing bibendum est ultricies integer quis. Cursus euismod quis viverra nibh cras. Metus vulputate eu scelerisque felis imperdiet proin fermentum leo. Mauris commodo quis imperdiet massa tincidunt. Cras tincidunt lobortis feugiat vivamus at augue. At augue eget arcu dictum varius duis at consectetur lorem. Velit sed ullamcorper morbi tincidunt. Lorem donec massa sapien faucibus et molestie ac. </Typography> <Typography paragraph> Consequat mauris nunc congue nisi vitae suscipit. Fringilla est ullamcorper eget nulla facilisi etiam dignissim diam. Pulvinar elementum integer enim neque volutpat ac tincidunt. Ornare suspendisse sed nisi lacus sed viverra tellus. Purus sit amet volutpat consequat mauris. Elementum eu facilisis sed odio morbi. Euismod lacinia at quis risus sed vulputate odio. Morbi tincidunt ornare massa eget egestas purus viverra accumsan in. In hendrerit gravida rutrum quisque non tellus orci ac. Pellentesque nec nam aliquam sem et tortor. Habitant morbi tristique senectus et. Adipiscing elit duis tristique sollicitudin nibh sit. Ornare aenean euismod elementum nisi quis eleifend. Commodo viverra maecenas accumsan lacus vel facilisis. Nulla posuere sollicitudin aliquam ultrices sagittis orci a. </Typography> </Box> </Box> ); }
额外优化说明
- 添加
drawerRef获取Drawer元素位置,确保宽度计算精准。 - 拖拽条仅在Drawer打开时显示,避免关闭状态下的无效交互。
- 为Drawer和主内容区添加过渡动画,提升拖拽时的视觉流畅度。
- 在
handleMouseDown中调用e.preventDefault(),避免拖拽时选中页面文字等干扰行为。
内容的提问来源于stack exchange,提问作者Ikura
相关产品推荐
相关产品推荐

