业务背景
在现代的Web应用程序中,抽屉组件(Drawer)是一种常见的UI元素,用于在不离开当前页面的情况下显示从边缘滑入的附加信息或选项。Ant Design(Antd)是一个流行的React UI框架,它提供了一个功能丰富的抽屉组件。然而,Antd的抽屉组件默认情况下并不支持拖动来调整大小。本文将介绍如何为Antd的抽屉组件添加这一功能,使用户能够通过拖动来调整抽屉的宽度。
思路
为了实现一个可调整大小的抽屉组件,我们将需要以下几个关键点:
- 可调整大小的容器 :使用第三方库如
react-resizable
来提供拖动调整大小的功能。 - 抽屉的状态管理 :使用React的状态(
useState
)来跟踪抽屉的开关状态和宽度。 - 拖动把手:自定义拖动把手组件来提供一个可拖动的界面元素。
实现
引入依赖
我们将使用Ant Design(Antd)提供的Drawer
组件以及react-resizable
库来创建一个可调整大小的抽屉。首先,确保安装了所有必要的依赖:
bash
npm install antd react-resizable
我们在下面会使用到 react-resizable 的Resizable
组件
Resizable
是 react-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
,并将其用作Resizable
的handle
属性:
jsx
// 定义 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
,同时设置最小宽度约束:
jsx
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,修改宽度会有动画,详情可参考
css
.ant-drawer .ant-drawer-content-wrapper {
transition: all 0.3s, width 0s;
}
完整代码
注意:
import './style.less'
ini
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
javascript
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;