Skip to content

Commit a0a9887

Browse files
authored
Merge pull request #749 from gfazioli/improve-auto-resize
Improve auto resize
2 parents 3ee01b0 + a75f42a commit a0a9887

13 files changed

+759
-265
lines changed

.github/copilot-instructions.md

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# Mantine DataTable - Copilot Instructions
2+
3+
This is a dual-purpose repository containing both the **Mantine DataTable** component package and its documentation website. Understanding this hybrid architecture is crucial for effective development.
4+
5+
## Project Architecture
6+
7+
### Dual Repository Structure
8+
- **Package code**: `package/` - The actual DataTable component exported to npm
9+
- **Documentation site**: `app/`, `components/` - Next.js app with examples and docs
10+
- **Build outputs**: Package builds to `dist/`, docs build for GitHub Pages deployment
11+
12+
### Package Development Flow
13+
```bash
14+
# Core development commands (use pnpm, not yarn despite legacy docs)
15+
pnpm dev # Start Next.js dev server for docs/examples
16+
pnpm build:package # Build package only (tsup + postcss)
17+
pnpm build:docs # Build documentation site
18+
pnpm build # Build both package and docs
19+
pnpm lint # ESLint + TypeScript checks
20+
```
21+
22+
### Component Architecture Pattern
23+
The DataTable follows a **composition-based architecture** with specialized sub-components:
24+
25+
```typescript
26+
// Main component in package/DataTable.tsx
27+
DataTable -> {
28+
DataTableHeader,
29+
DataTableRow[],
30+
DataTableFooter,
31+
DataTablePagination,
32+
DataTableLoader,
33+
DataTableEmptyState
34+
}
35+
```
36+
37+
Each sub-component has its own `.tsx`, `.css`, and sometimes `.module.css` files. Always maintain this parallel structure when adding features.
38+
39+
## Development Conventions
40+
41+
### Import Alias Pattern
42+
Examples use `import { DataTable } from '__PACKAGE__'` - this resolves to the local package during development. Never import from `mantine-datatable` in examples.
43+
44+
### TypeScript Patterns
45+
- **Generic constraints**: `DataTable<T>` where T extends record type
46+
- **Prop composition**: Props inherit from base Mantine components (TableProps, etc.)
47+
- **Accessor pattern**: Use `idAccessor` prop for custom ID fields, defaults to `'id'`
48+
49+
### CSS Architecture
50+
- **Layered imports**: `styles.css` imports all component styles
51+
- **CSS layers**: `@layer mantine, mantine-datatable` for proper specificity
52+
- **Utility classes**: Defined in `utilityClasses.css` for common patterns
53+
- **CSS variables**: Dynamic values injected via `cssVariables.ts`
54+
55+
### Hook Patterns
56+
Custom hooks follow the pattern `useDataTable*` and are located in `package/hooks/`:
57+
- `useDataTableColumns` - Column management and persistence
58+
- `useRowExpansion` - Row expansion state
59+
- `useLastSelectionChangeIndex` - Selection behavior
60+
61+
## Documentation Development
62+
63+
### Example Structure
64+
Each example in `app/examples/` follows this pattern:
65+
```
66+
feature-name/
67+
├── page.tsx # Next.js page with controls
68+
├── FeatureExample.tsx # Actual DataTable implementation
69+
└── FeaturePageContent.tsx # Documentation content
70+
```
71+
72+
### Code Block Convention
73+
Use the `CodeBlock` component for syntax highlighting. Example files should be minimal and focused on demonstrating a single feature clearly.
74+
75+
## Data Patterns
76+
77+
### Record Structure
78+
Examples use consistent data shapes:
79+
- `companies.json` - Basic company data with id, name, address
80+
- `employees.json` - Employee data with departments/relationships
81+
- `async.ts` - Simulated API calls with delay/error simulation
82+
83+
### Selection Patterns
84+
- **Gmail-style additive selection**: Shift+click for range selection
85+
- **Trigger modes**: `'checkbox'` | `'row'` | `'cell'`
86+
- **Custom selection logic**: Use `isRecordSelectable` for conditional selection
87+
88+
## Build System
89+
90+
### Package Build (tsup)
91+
- **ESM**: `tsup.esm.ts` - Modern module format
92+
- **CJS**: `tsup.cjs.ts` - CommonJS compatibility
93+
- **Types**: `tsup.dts.ts` - TypeScript declarations
94+
- **CSS**: PostCSS processes styles to `dist/`
95+
96+
### Documentation Deployment
97+
- **GitHub Pages**: `output: 'export'` in `next.config.js`
98+
- **Base path**: `/mantine-datatable` when `GITHUB_PAGES=true`
99+
- **Environment injection**: Package version, NPM downloads via build-time fetch
100+
101+
## Common Patterns
102+
103+
### Adding New Features
104+
1. Create component in `package/` with `.tsx` and `.css` files
105+
2. Add to main `DataTable.tsx` component composition
106+
3. Export new types from `package/types/index.ts`
107+
4. Create example in `app/examples/new-feature/`
108+
5. Update main navigation in `app/config.ts`
109+
110+
### Styling New Components
111+
- Use CSS custom properties for theming
112+
- Follow existing naming: `.mantine-datatable-component-name`
113+
- Import CSS in `package/styles.css`
114+
- Add utility classes to `utilityClasses.css` if reusable
115+
116+
### TypeScript Integration
117+
- Extend base Mantine props where possible
118+
- Use composition over inheritance for prop types
119+
- Export all public types from `package/types/index.ts`
120+
- Maintain strict null checks and proper generics
121+
122+
## Performance Considerations
123+
124+
- **Virtualization**: Not implemented - DataTable handles reasonable record counts (< 1000s)
125+
- **Memoization**: Use `useMemo` for expensive column calculations
126+
- **CSS-in-JS**: Avoided in favor of CSS modules for better performance
127+
- **Bundle size**: Keep dependencies minimal (only Mantine + React)

app/examples/column-resizing/ResizingComplexExample.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ export default function ResizingComplexExample() {
3434
const { effectiveColumns, resetColumnsWidth, resetColumnsOrder, resetColumnsToggle } = useDataTableColumns<Company>({
3535
key,
3636
columns: [
37-
{ accessor: 'name', ...props },
38-
{ accessor: 'streetAddress', ...props },
37+
{ accessor: 'name', ellipsis: true, ...props },
38+
{ accessor: 'streetAddress', ellipsis: true, ...props },
3939
{ accessor: 'city', ellipsis: true, ...props },
4040
{ accessor: 'state', textAlign: 'right', ...props },
4141
],

app/examples/column-resizing/ResizingExample.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,18 @@ import { companies, type Company } from '~/data';
88
export default function ResizingExample() {
99
const key = 'resize-example';
1010

11+
const [resizable, setResizable] = useState<boolean>(true);
1112
const [withTableBorder, setWithTableBorder] = useState<boolean>(true);
1213
const [withColumnBorders, setWithColumnBorders] = useState<boolean>(true);
1314

1415
const { effectiveColumns, resetColumnsWidth } = useDataTableColumns<Company>({
1516
key,
1617
columns: [
17-
{ accessor: 'name', width: 200, resizable: true },
18-
{ accessor: 'streetAddress', resizable: true },
19-
{ accessor: 'city', ellipsis: true, resizable: true },
20-
{ accessor: 'state', textAlign: 'right' },
18+
{ accessor: 'name', resizable },
19+
{ accessor: 'streetAddress', resizable, ellipsis: true },
20+
{ accessor: 'city', resizable, ellipsis: true },
21+
{ accessor: 'state', resizable },
22+
{ accessor: 'missionStatement', resizable, ellipsis: true },
2123
],
2224
});
2325

@@ -32,6 +34,12 @@ export default function ResizingExample() {
3234
/>
3335
<Group grow justify="space-between">
3436
<Group justify="flex-start">
37+
<Switch
38+
checked={resizable}
39+
onChange={(event) => setResizable(event.currentTarget.checked)}
40+
labelPosition="left"
41+
label="Resizable"
42+
/>
3543
<Switch
3644
checked={withTableBorder}
3745
onChange={(event) => setWithTableBorder(event.currentTarget.checked)}

package/DataTable.css

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939

4040
--mantine-datatable-header-height: 0;
4141
--mantine-datatable-footer-height: 0;
42-
--mantine-datatable-selection-column-width: 0;
42+
--mantine-datatable-selection-column-width: 44px;
4343
--mantine-datatable-top-shadow-opacity: 0;
4444
--mantine-datatable-left-shadow-opacity: 0;
4545
--mantine-datatable-bottom-shadow-opacity: 0;
@@ -70,6 +70,31 @@
7070
white-space: nowrap;
7171
}
7272

73+
/* Selection column should always have fixed width */
74+
.mantine-datatable th[data-accessor="__selection__"],
75+
.mantine-datatable td[data-accessor="__selection__"] {
76+
width: 44px !important;
77+
min-width: 44px !important;
78+
max-width: 44px !important;
79+
}
80+
81+
/* When not using fixed layout, allow natural table sizing */
82+
.mantine-datatable:not(.mantine-datatable-resizable-columns) th {
83+
white-space: nowrap;
84+
/* Allow natural width calculation */
85+
width: auto;
86+
min-width: auto;
87+
max-width: none;
88+
}
89+
90+
/* But selection column should still be fixed even in auto layout */
91+
.mantine-datatable:not(.mantine-datatable-resizable-columns) th[data-accessor="__selection__"],
92+
.mantine-datatable:not(.mantine-datatable-resizable-columns) td[data-accessor="__selection__"] {
93+
width: 44px !important;
94+
min-width: 44px !important;
95+
max-width: 44px !important;
96+
}
97+
7398
.mantine-datatable-table {
7499
border-collapse: separate;
75100
border-spacing: 0;

package/DataTable.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Box, Table, type MantineSize } from '@mantine/core';
22
import { useMergedRef } from '@mantine/hooks';
33
import clsx from 'clsx';
4-
import { useCallback, useMemo } from 'react';
4+
import { useCallback, useMemo, useState } from 'react';
55
import { DataTableColumnsProvider } from './DataTableDragToggleProvider';
66
import { DataTableEmptyRow } from './DataTableEmptyRow';
77
import { DataTableEmptyState } from './DataTableEmptyState';
@@ -134,14 +134,9 @@ export function DataTable<T>({
134134
return groups?.flatMap((group) => group.columns) ?? columns!;
135135
}, [columns, groups]);
136136

137-
const hasResizableColumns = useMemo(() => {
138-
return effectiveColumns.some((col) => col.resizable);
139-
}, [effectiveColumns]);
140-
141-
const dragToggle = useDataTableColumns({
142-
key: storeColumnsKey,
143-
columns: effectiveColumns,
144-
});
137+
// When columns are resizable, start with auto layout to let the browser
138+
// compute natural widths, then capture them and switch to fixed layout.
139+
const [fixedLayoutEnabled, setFixedLayoutEnabled] = useState(false);
145140

146141
const { refs, onScroll: handleScrollPositionChange } = useDataTableInjectCssVariables({
147142
scrollCallbacks: {
@@ -154,6 +149,14 @@ export function DataTable<T>({
154149
withRowBorders: otherProps.withRowBorders,
155150
});
156151

152+
const dragToggle = useDataTableColumns({
153+
key: storeColumnsKey,
154+
columns: effectiveColumns,
155+
headerRef: refs.header as any,
156+
scrollViewportRef: refs.scrollViewport as any,
157+
onFixedLayoutChange: setFixedLayoutEnabled,
158+
});
159+
157160
const mergedTableRef = useMergedRef(refs.table, tableRef);
158161
const mergedViewportRef = useMergedRef(refs.scrollViewport, scrollViewportRef);
159162

@@ -267,7 +270,7 @@ export function DataTable<T>({
267270
'mantine-datatable-pin-last-column': pinLastColumn,
268271
'mantine-datatable-selection-column-visible': selectionColumnVisible,
269272
'mantine-datatable-pin-first-column': pinFirstColumn,
270-
'mantine-datatable-resizable-columns': hasResizableColumns,
273+
'mantine-datatable-resizable-columns': dragToggle.hasResizableColumns && fixedLayoutEnabled,
271274
},
272275
classNames?.table
273276
)}

package/DataTableHeaderCell.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export function DataTableHeaderCell<T>({
137137
style={[
138138
{
139139
width,
140-
...(!resizable ? { minWidth: width, maxWidth: width } : { minWidth: '1px' }),
140+
...(!resizable ? { minWidth: width, maxWidth: width } : {}),
141141
},
142142
style,
143143
]}

package/DataTableResizableHeaderHandle.tsx

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export const DataTableResizableHeaderHandle = (props: DataTableResizableHeaderHa
8787
currentCol.style.width = `${finalCurrentWidth}px`;
8888
nextCol.style.width = `${finalNextWidth}px`;
8989

90-
// Force the table layout to recalculate
90+
// Ensure the table maintains fixed layout during resize
9191
currentCol.style.minWidth = `${finalCurrentWidth}px`;
9292
currentCol.style.maxWidth = `${finalCurrentWidth}px`;
9393
nextCol.style.minWidth = `${finalNextWidth}px`;
@@ -141,7 +141,7 @@ export const DataTableResizableHeaderHandle = (props: DataTableResizableHeaderHa
141141
document.addEventListener('mousemove', handleMouseMove);
142142
document.addEventListener('mouseup', handleMouseUp);
143143
},
144-
[accessor, setMultipleColumnWidths]
144+
[accessor, columnRef, setMultipleColumnWidths]
145145
);
146146

147147
const handleDoubleClick = useCallback(() => {
@@ -150,30 +150,29 @@ export const DataTableResizableHeaderHandle = (props: DataTableResizableHeaderHa
150150
const currentColumn = columnRef.current;
151151
const nextColumn = currentColumn.nextElementSibling as HTMLTableCellElement | null;
152152

153-
// Reset styles immediately
153+
// Clear any inline styles that might interfere with natural sizing
154154
currentColumn.style.width = '';
155155
currentColumn.style.minWidth = '';
156156
currentColumn.style.maxWidth = '';
157157

158-
const updates = [{ accessor, width: 'initial' }];
158+
// Reset current column to auto width
159+
const updates = [{ accessor, width: 'auto' }];
159160

160161
if (nextColumn) {
161162
nextColumn.style.width = '';
162163
nextColumn.style.minWidth = '';
163164
nextColumn.style.maxWidth = '';
164165

165166
const nextAccessor = nextColumn.getAttribute('data-accessor');
166-
// Only add to updates if it's not the selection column
167+
// Only reset next column if it's not the selection column
167168
if (nextAccessor && nextAccessor !== '__selection__') {
168-
updates.push({ accessor: nextAccessor, width: 'initial' });
169+
updates.push({ accessor: nextAccessor, width: 'auto' });
169170
}
170171
}
171172

172-
// Use setTimeout to ensure DOM changes are applied before context update
173-
setTimeout(() => {
174-
setMultipleColumnWidths(updates);
175-
}, 0);
176-
}, [accessor, setMultipleColumnWidths]);
173+
// Update context - this will trigger re-measurement of natural widths
174+
setMultipleColumnWidths(updates);
175+
}, [accessor, columnRef, setMultipleColumnWidths]);
177176

178177
return (
179178
<div

package/hooks/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
export * from './useColumnResize';
2+
export * from './useDataTableColumnReorder';
3+
export * from './useDataTableColumnResize';
24
export * from './useDataTableColumns';
5+
export * from './useDataTableColumnToggle';
36
export * from './useDataTableInjectCssVariables';
47
export * from './useIsomorphicLayoutEffect';
58
export * from './useLastSelectionChangeIndex';

package/hooks/useColumnResize.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,14 @@ export const useColumnResize = ({ onColumnResize, minColumnWidth = 50 }: UseColu
2424
event.stopPropagation();
2525

2626
const nextColumn = currentColumn.nextElementSibling as HTMLTableCellElement | null;
27-
if (!nextColumn) return false; // Can't resize without next column
27+
if (!nextColumn) {
28+
return false;
29+
}
2830

2931
const nextAccessor = nextColumn.getAttribute('data-accessor');
30-
if (!nextAccessor) return false; // Need accessor for next column
32+
if (!nextAccessor) {
33+
return false;
34+
}
3135

3236
const currentWidth = currentColumn.getBoundingClientRect().width;
3337
const nextWidth = nextColumn.getBoundingClientRect().width;
@@ -58,11 +62,6 @@ export const useColumnResize = ({ onColumnResize, minColumnWidth = 50 }: UseColu
5862

5963
const deltaX = clientX - resizeState.startX;
6064

61-
// Calculate new widths with constraints
62-
const newCurrentWidth = Math.max(minColumnWidth, resizeState.originalWidths.current + deltaX);
63-
64-
const newNextWidth = Math.max(minColumnWidth, resizeState.originalWidths.next - deltaX);
65-
6665
// Calculate the actual delta we can apply based on constraints
6766
const actualDelta = Math.min(
6867
deltaX,

0 commit comments

Comments
 (0)