HOW - Antd Table 组件实现拖拽变更宽度(结合 tooltip 气泡)实践

一、前言

1.1 demo

Antd - Table 表格 中,最新文档里已经没有拖拽的 demo 展示了。

我们通过谷歌引擎搜索后,发现在 Ant design - How to adjust table column width by dragging? 这条贴子里,有一个 Antd 官方 resizable column demo

因此,我们会基于该示例来实现我们想要的最终效果。

1.2 目标效果

  1. 每一列的内容支持内容过长省略号,并且鼠标悬浮有 tooltip
  2. 每一列支持在表格 header 拖拽改变宽度

1.3 遇到的问题和解决思路

两者独立使用没问题,但结合使用会有如下问题。

  1. 首先,table 组件设置 ellipsis={true} 后会导致 table-layout: fixed。仅实现拖拽时为 table-layout: auto

请添加图片描述

这会在 colum 较多时导致 table 宽度超出视窗。我们可以在 table 外包裹一层 resizableTableContainer div,并设置如下

.resizableTableContainer {
	overflow-x: auto; 
	width: 100%; 
	display: block;
}

请添加图片描述
如此,可以避免上述超出视窗的问题。

  1. 接着,发现由于 table 本身设置了 width: 100%,导致当 column 较少时,每一列的宽度会被自适应初始化变宽,进一步导致拖拽某一列的宽度会影响其他列,即出现多列自适应宽度变化,因此我们需要将其设置为 width: min-content

请添加图片描述
此时,可以实现避免每一列的宽度出现自适应了。

  1. 接着,又发现当 column 较少时,会出现 table 铺不满容器宽度,因此,我们需要通过主动计算,当出现 (所有 column 宽度和 initialWidthsSum < tableWidth 时),手动设置所有列的宽度自适应为剩余宽度的等分宽度,使得所有列能铺满 100% 宽度

  2. 最后,在 handleSize 方法里,实现控制宽度如下:1. 控制每一列最小宽度为原始宽度 1/2 2. 控制整体缩小宽度不能小于 tableWidth

1.4 使用要求

每一列都要设置初始宽度,即在初始化 columns 时要为列设置 width 属性。

二、具体代码

src/components/ResizableTable/index.tsx

import React, { useState, useEffect, useRef } from "react"
import type { ResizeCallbackData } from "react-resizable"
import { Resizable } from "react-resizable"
import styles from "./index.module.less"

interface WithResizableColumnsProps {
    columns: any[]
}

const ResizableTitle = (
    props: React.HTMLAttributes<any> & {
        onResize: (
            e: React.SyntheticEvent<Element>,
            data: ResizeCallbackData
        ) => void
        width: number
    }
) => {
    const { onResize, width, ...restProps } = props
    if (!width) {
        return <th {...restProps} />
    }
    return (
        <Resizable
            width={width}
            height={0}
            handle={
                <span
                    className="react-resizable-handle"
                    onClick={(e) => {
                        e.stopPropagation()
                    }}
                />
            }
            onResize={onResize}
            draggableOpts={{ enableUserSelectHack: false }}
        >
            <th {...restProps} />
        </Resizable>
    )
}

const withResizableColumns = (WrappedComponent: React.ComponentType<any>) => {
    return (props: WithResizableColumnsProps) => {
        const tableRef = useRef<HTMLTableElement>(null)
        const [tableWidth, setTableWidth] = useState<number>(0)
        useEffect(() => {
            if (tableRef.current) {
                setTableWidth(tableRef.current.offsetWidth || 0)
            }
            const handleResizeInner = () => {
                if (tableRef.current) {
                    setTableWidth(tableRef.current.offsetWidth || 0)
                }
            }
            window.addEventListener("resize", handleResizeInner)
            return () => {
                window.removeEventListener("resize", handleResizeInner)
            }
        }, [tableRef])

        const { columns: initialColumns } = props
        const initialColumnsWidths = initialColumns.map((col) => col.width || 0)

        const [columns, setColumns] = useState(initialColumns)

        useEffect(() => {
            // 一旦所有 column 宽度和小于 tableWidth
            // 重新设置最后一列的宽度自适应为剩余宽度
            const initialWidthsSum = initialColumns.reduce(
                (acc, column) => acc + column.width,
                0
            )
            if (tableWidth && initialWidthsSum < tableWidth) {
                const difference = tableWidth - initialWidthsSum
                const numberOfColumns = initialColumns.length
                const additionalWidthPerColumn = Math.round(
                    difference / numberOfColumns
                )
                const newColumns = initialColumns.map((column) => ({
                    ...column,
                    width: column.width + additionalWidthPerColumn,
                }))
                setColumns(newColumns)
                return
            }
            setColumns(initialColumns)
        }, [initialColumns, tableWidth])

        const handleResize =
            (index: number) =>
            (
                _: React.SyntheticEvent<Element>,
                { size }: ResizeCallbackData
            ) => {
                // 宽度控制
                // 1. 控制每一列最小宽度为原始宽度 1/2
                // 2. 控制整体缩小宽度不能小于 tableWidth

                const minWidth = initialColumnsWidths[index] / 2
                const resizedWidth = Math.max(size.width, minWidth)

                // 计算当前所有列的总宽度
                const totalWidth = columns.reduce(
                    (acc, column) => acc + column.width,
                    0
                )

                // 计算调整后列的总宽度
                const newTotalWidth =
                    totalWidth - columns[index].width + resizedWidth

                // 计算需要调整的宽度
                let adjustedWidth = resizedWidth
                if (newTotalWidth < tableWidth) {
                    // 计算差值并调整列宽
                    const difference = tableWidth - newTotalWidth
                    adjustedWidth = resizedWidth + difference
                }

                // 更新列宽
                const newColumns = [...columns]
                newColumns[index] = {
                    ...newColumns[index],
                    width: adjustedWidth,
                }
                setColumns(newColumns)
            }

        const mergedColumns = columns.map((col, index) => ({
            ...col,
            onHeaderCell: (column: any) => ({
                width: column.width,
                onResize: handleResize(index) as React.ReactEventHandler<any>,
            }),
        }))

        return (
            <div ref={tableRef} className={styles.resizableTableContainer}>
                <WrappedComponent
                    {...props}
                    columns={mergedColumns}
                    components={{
                        header: {
                            cell: ResizableTitle,
                        },
                    }}
                />
            </div>
        )
    }
}

export default withResizableColumns

src/components/ResizableTable/index.module.less

.resizableTableContainer {
    overflow-x: auto;
    width: 100%;
    display: block;

    :global {
        .react-resizable {
            position: relative;
            background-clip: padding-box !important;
        }
        .react-resizable-handle {
            position: absolute;
            inset-inline-end: -5px;
            bottom: 0;
            z-index: 1;
            width: 10px;
            height: 100%;
            cursor: col-resize;
        }

        table {
            width: min-content !important; // defaut value: 100%
        }
    }
}

在项目中使用:

src/App.tsx

import { Button, Table } from "antd"
import withResizableColumns from "src/components/ResizableTable"
const ResizableTable = withResizableColumns(Table)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值