You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何横向调整MUI Drawer宽度并适配其余布局?

MUI左侧Drawer拖拽调整宽度实现及异常修复

问题概述

需要实现左侧MUI Drawer打开后可横向拖拽调整宽度,其余布局自动适配,且无法使用Grid组件。当前实现存在异常:鼠标抬起后抽屉有时仍持续缩放,需点击页面后重新操作才能恢复。

异常原因分析

  1. 事件监听移除逻辑失效:handleMouseUp未用useCallback包裹,组件每次渲染都会生成新的函数引用,导致removeEventListener无法匹配到对应的监听函数,事件未被正确移除。
  2. 宽度计算不准确:依赖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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 07:25:00