likes
comments
collection
share

如何在React中实现可调整大小的抽屉组件

作者站长头像
站长
· 阅读数 4

业务背景

在现代的Web应用程序中,抽屉组件(Drawer)是一种常见的UI元素,用于在不离开当前页面的情况下显示从边缘滑入的附加信息或选项。Ant Design(Antd)是一个流行的React UI框架,它提供了一个功能丰富的抽屉组件。然而,Antd的抽屉组件默认情况下并不支持拖动来调整大小。本文将介绍如何为Antd的抽屉组件添加这一功能,使用户能够通过拖动来调整抽屉的宽度。

思路

为了实现一个可调整大小的抽屉组件,我们将需要以下几个关键点:

  1. 可调整大小的容器:使用第三方库如react-resizable来提供拖动调整大小的功能。
  2. 抽屉的状态管理:使用React的状态(useState)来跟踪抽屉的开关状态和宽度。
  3. 拖动把手:自定义拖动把手组件来提供一个可拖动的界面元素。

实现

引入依赖

我们将使用Ant Design(Antd)提供的Drawer组件以及react-resizable库来创建一个可调整大小的抽屉。首先,确保安装了所有必要的依赖:

npm install antd react-resizable

我们在下面会使用到 react-resizable 的Resizable组件

Resizablereact-resizable 库提供的一个组件,它允许你的 React 组件具有可调整大小的功能。它接受一系列的 props 来定制其行为和样式。以下是代码中使用的 Resizable 组件的 API :

  • onResize: 这是一个回调函数,当调整大小操作发生时被调用。它接收两个参数:事件对象和包含新的尺寸信息的数据对象。你可以使用这个函数来更新你的组件状态,以反映新的尺寸。
  • width: 定义 Resizable 组件的初始宽度。这个值通常来自组件的状态,以便它可以在用户调整大小后更新。
  • height: 定义 Resizable 组件的初始高度。与 width 类似,这也是一个可以动态更新的值。
  • handle: 允许你提供一个自定义的句柄组件,用户可以通过拖动这个句柄来调整大小。这个 prop 接收一个 JSX 元素。
  • axis: 一个字符串,定义了调整大小的方向。可选值为 "x""y""both",分别表示只能水平调整大小、只能垂直调整大小或两者皆可。
  • resizeHandles: 一个数组,定义了哪些边缘可以被用来调整大小。数组中的字符串 "e""w""n""s" 分别代表东(右)、西(左)、北(上)和南(下)方向的调整句柄。
  • minConstraints: 一个数组,定义了组件的最小宽度和高度。在你的例子中,它只设置了一个最小宽度(minWidth),意味着组件的宽度不能小于这个值。

除了你已经使用的这些 API,react-resizable 还提供了其他一些 props 来进一步定制 Resizable 组件的行为:

  • maxConstraints: 类似于 minConstraints,但这是用来设置组件的最大宽度和高度的。
  • onResizeStart: 当调整大小操作开始时被调用的回调函数。
  • onResizeStop: 当调整大小操作结束时被调用的回调函数。
  • draggableOpts: 可以提供给 react-draggable 库用来定制拖拽行为的选项对象。
  • lockAspectRatio: 布尔值,用于锁定调整大小时的宽高比。

自定义拖动把手

为了给用户提供一个可视的拖动界面,我们将创建一个自定义的拖动把手组件ResizeHandle,并将其用作Resizablehandle属性:

// 定义 ResizeHandle 的 props 类型
interface ResizeHandleProps {
  handleAxis: 'x' | 'y';
}

// 使用 React.forwardRef 并添加泛型参数 <HTMLDivElement, ResizeHandleProps>
const ResizeHandle = forwardRef<HTMLDivElement, ResizeHandleProps>(
  (props, ref) => {
    const { handleAxis, ...restProps } = props; // 使用结构分离 handleAxis 和剩余 props
    const style: React.CSSProperties = {
      position: 'absolute',
      top: 0,
      left: 0,
      width: '10px',
      height: '100%',
      cursor: 'ew-resize',
      boxSizing: 'border-box',
      zIndex: 100001,
      userSelect: 'none',
      background: 'red',
    };
    return (
      <div
        ref={ref}
        {...restProps}
        style={style}
        className={`handle-${handleAxis}`}
      ></div>
    );
  },
);

集成抽屉和可调整大小的容器

ResizableDrawer组件中,使用Resizable组件和自定义的ResizeHandle来包裹Drawer,同时设置最小宽度约束:


const minWidth = 200;
const windowHeight = window.innerHeight;

// 定义 ResizableDrawer 的 props 类型
interface ResizableDrawerProps {
  width?: number;
  height?: number;
  children?: React.ReactNode;
  minConstraints?: [number, number];
  open: boolean;
  onClose: () => void;
}

const ResizableDrawer: React.FC<ResizableDrawerProps> = ({
  width: defaultWidth,
  height,
  children,
  minConstraints,
  open,
  onClose,
}) => {
  const [width, setWidth] = useState(defaultWidth ?? minWidth);

  function onResize(e: React.SyntheticEvent, data: any) {
    // 替换 any 为对应的数据类型
    const { size } = data;
    setWidth(size.width);
  }

  return (
    <Resizable
      onResize={onResize}
      width={width}
      height={height ?? windowHeight}
      handle={<ResizeHandle handleAxis="x" />}
      axis="x"
      resizeHandles={['e', 'w']}
      minConstraints={minConstraints ?? [minWidth]}
    >
      <Drawer
        open={open}
        onClose={onClose}
        title="Antd Drawer resize"
        width={width}
        placement="right"
        keyboard={false}
        mask={false}
      >
        {children}
      </Drawer>
    </Resizable>
  );
};

export default ResizableDrawer;

css样式覆盖

这里的样式覆盖主要针对于 Drawer组件有 transition,修改宽度会有动画,详情可参考

.ant-drawer .ant-drawer-content-wrapper {
  transition: all 0.3s, width 0s;
}

完整代码

注意:import './style.less'

import { Drawer, Button } from 'antd';
import React, { useState, forwardRef } from 'react';
import { Resizable } from 'react-resizable';
import './style.less'
// 定义 ResizeHandle 的 props 类型
interface ResizeHandleProps {
  handleAxis: 'x' | 'y';
}

// 使用 React.forwardRef 并添加泛型参数 <HTMLDivElement, ResizeHandleProps>
const ResizeHandle = forwardRef<HTMLDivElement, ResizeHandleProps>(
  (props, ref) => {
    const { handleAxis, ...restProps } = props; // 使用结构分离 handleAxis 和剩余 props
    const style: React.CSSProperties = {
      position: 'absolute',
      top: 0,
      left: 0,
      width: '10px',
      height: '100%',
      cursor: 'ew-resize',
      boxSizing: 'border-box',
      zIndex: 100001,
      userSelect: 'none',
      background: 'red',
    };
    return (
      <div
        ref={ref}
        {...restProps}
        style={style}
        className={`handle-${handleAxis}`}
      ></div>
    );
  },
);
ResizeHandle.displayName = 'ResizeHandle';

const minWidth = 200;
const windowHeight = window.innerHeight;

// 定义 ResizableDrawer 的 props 类型
interface ResizableDrawerProps {
  width?: number;
  height?: number;
  children?: React.ReactNode;
  minConstraints?: [number, number];
  open: boolean;
  onClose: () => void;
}

const ResizableDrawer: React.FC<ResizableDrawerProps> = ({
  width: defaultWidth,
  height,
  children,
  minConstraints,
  open,
  onClose,
}) => {
  const [width, setWidth] = useState(defaultWidth ?? minWidth);

  function onResize(e: React.SyntheticEvent, data: any) {
    // 替换 any 为对应的数据类型
    const { size } = data;
    setWidth(size.width);
  }

  return (
    <Resizable
      onResize={onResize}
      width={width}
      height={height ?? windowHeight}
      handle={<ResizeHandle handleAxis="x" />}
      axis="x"
      resizeHandles={['e', 'w']}
      minConstraints={minConstraints ?? [minWidth]}
    >
      <Drawer
        open={open}
        onClose={onClose}
        title="Antd Drawer resize"
        width={width}
        placement="right"
        keyboard={false}
        mask={false}
      >
        {children}
      </Drawer>
    </Resizable>
  );
};

Demo

const App: React.FC = () => {
    const [open, setOpen] = useState(false);
  
    return (
      <div>
        <Button onClick={() => setOpen(!open)}>Toggle Drawer</Button>
        <ResizableDrawer width={300} open={open} onClose={() => setOpen(false)}>
          <p>This is inside the drawer.</p>
        </ResizableDrawer>
      </div>
    );
  };
  
  export default App;