Clamp column resize so neither column goes below 50px

- Limit drag diff so both the dragged and neighbor column stay >= 50px
- Prevents columns from collapsing or going negative
This commit is contained in:
Adam Moussa 2026-04-16 18:59:24 -04:00
parent 33431ae449
commit 168383b0ee
2 changed files with 163 additions and 70 deletions

View file

@ -1,82 +1,124 @@
import React from "react";
import React, { useState, useCallback, useRef } from "react";
const SharedTable = ({
rows = [],
columns = [],
onEdit,
onDelete,
page,
setPage,
pageSize,
setPageSize,
const SharedTable = ({
rows = [],
columns = [],
onEdit,
onDelete,
page,
setPage,
pageSize,
setPageSize,
totalCount,
loading,
error
error
}) => {
const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
const [colWidths, setColWidths] = useState(null);
const gridRef = useRef(null);
const snapshotWidths = useCallback(() => {
if (!gridRef.current) return null;
const headerCells = gridRef.current.querySelectorAll("[data-col]");
const widths = {};
headerCells.forEach((cell) => {
widths[cell.dataset.col] = cell.getBoundingClientRect().width;
});
return widths;
}, []);
const handleMouseDown = useCallback((e, colKey) => {
e.preventDefault();
const startX = e.clientX;
const currentWidths = snapshotWidths();
if (!currentWidths) return;
setColWidths(currentWidths);
const colIndex = columns.findIndex((c) => c.key === colKey);
const nextColKey = colIndex < columns.length - 1 ? columns[colIndex + 1].key : null;
const startWidth = currentWidths[colKey] || 100;
const nextStartWidth = nextColKey ? (currentWidths[nextColKey] || 100) : 0;
const onMove = (moveEvent) => {
let diff = moveEvent.clientX - startX;
const minCol = 50;
if (startWidth + diff < minCol) diff = minCol - startWidth;
if (nextColKey && nextStartWidth - diff < minCol) diff = nextStartWidth - minCol;
setColWidths((prev) => {
const updated = { ...prev, [colKey]: startWidth + diff };
if (nextColKey) updated[nextColKey] = nextStartWidth - diff;
return updated;
});
};
const onUp = () => {
document.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseup", onUp);
document.body.style.cursor = "";
document.body.style.userSelect = "";
};
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
document.addEventListener("mousemove", onMove);
document.addEventListener("mouseup", onUp);
}, [snapshotWidths]);
if (loading) return <div className="loading">Loading...</div>;
if (error) return <div className="error">{error}</div>;
const gridTemplate = colWidths
? ["40px", ...columns.map((col) => `${colWidths[col.key] || 100}px`), "80px"].join(" ")
: ["40px", ...columns.map(() => "1fr"), "80px"].join(" ");
return (
<div className="tableContainer">
<div className="tableResponsive">
<table className="dataTable">
<thead>
<tr>
<th style={{ width: "1%" }}>
<div className="gridTable" ref={gridRef} style={{ gridTemplateColumns: gridTemplate }}>
<div className="gridCell gridHeader">
<input type="checkbox" />
</div>
{columns.map((col) => (
<div key={col.key} data-col={col.key} className="gridCell gridHeader">
{col.label}
<span
className="colResizeHandle"
onMouseDown={(e) => handleMouseDown(e, col.key)}
/>
</div>
))}
<div className="gridCell gridHeader">Actions</div>
{rows.map((row, idx) => (
<React.Fragment key={row.id || idx}>
<div className="gridCell gridBody">
<input type="checkbox" />
</th>
</div>
{columns.map((col) => (
<th key={col.key} style={col.style}>
{col.label}
</th>
<div key={col.key} className="gridCell gridBody">
{col.render ? col.render(row[col.key], row) : row[col.key]}
</div>
))}
<th style={{ width: "1%" }}>Actions</th>
</tr>
</thead>
<tbody>
{rows.map((row, idx) => (
<tr key={row.id || idx}>
<td>
<input type="checkbox" />
</td>
{columns.map((col) => (
<td key={col.key}>
{col.render
? col.render(row[col.key], row)
: row[col.key]}
</td>
))}
<td>
<div className="actionButtons">
{onEdit && (
<button
className="btnIcon edit"
title="Edit"
onClick={() => onEdit(row)}
>
✎
</button>
)}
{onDelete && (
<button
className="btnIcon delete"
title="Delete"
onClick={() => onDelete(row)}
>
🗑
</button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
<div className="gridCell gridBody">
<div className="actionButtons">
{onEdit && (
<button className="btnIcon edit" title="Edit" onClick={() => onEdit(row)}>
✎
</button>
)}
{onDelete && (
<button className="btnIcon delete" title="Delete" onClick={() => onDelete(row)}>
🗑
</button>
)}
</div>
</div>
</React.Fragment>
))}
</div>
</div>
{/* Pagination */}
<div className="paginationContainer">
<div className="paginationLeft">
<label>
@ -98,17 +140,11 @@ const SharedTable = ({
Showing {Math.min(page * pageSize, totalCount)} of {totalCount}
</span>
<div className="pagination">
<button
onClick={() => setPage(page - 1)}
disabled={page === 1}
>
<button onClick={() => setPage(page - 1)} disabled={page === 1}>
← Previous
</button>
<span>Page {page} of {totalPages}</span>
<button
onClick={() => setPage(page + 1)}
disabled={page >= totalPages}
>
<button onClick={() => setPage(page + 1)} disabled={page >= totalPages}>
Next →
</button>
</div>

View file

@ -137,6 +137,63 @@ body {
padding: 12px;
border-bottom: 1px solid #eee;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.gridTable {
display: grid;
font-size: 14px;
min-width: 100%;
}
.gridCell {
padding: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.gridHeader {
font-weight: 600;
color: #333;
background: #f9f9f9;
border-bottom: 2px solid #ddd;
position: relative;
}
.gridBody {
border-bottom: 1px solid #eee;
color: #333;
}
.gridBody:hover {
background: #f9f9f9;
}
.gridCell a {
color: #007bff;
text-decoration: none;
cursor: pointer;
}
.gridCell a:hover {
text-decoration: underline;
}
.colResizeHandle {
position: absolute;
top: 0;
right: 0;
width: 5px;
height: 100%;
cursor: col-resize;
background: transparent;
}
.colResizeHandle:hover {
background: #007bff;
}
.dataTable tbody tr:hover {