-
Notifications
You must be signed in to change notification settings - Fork 221
Expand file tree
/
Copy pathuse-column-widths.tsx
More file actions
207 lines (185 loc) · 7.46 KB
/
use-column-widths.tsx
File metadata and controls
207 lines (185 loc) · 7.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
import { useResizeObserver, useStableCallback } from '@cloudscape-design/component-toolkit/internal';
import { getLogicalBoundingClientRect } from '@cloudscape-design/component-toolkit/internal';
import { ColumnWidthStyle, setElementWidths } from './column-widths-utils';
export const DEFAULT_COLUMN_WIDTH = 120;
export interface ColumnWidthDefinition extends ColumnWidthStyle {
id: PropertyKey;
}
function readWidths(
getCell: (columnId: PropertyKey) => null | HTMLElement,
visibleColumns: readonly ColumnWidthDefinition[]
) {
const result = new Map<PropertyKey, number>();
for (let index = 0; index < visibleColumns.length; index++) {
const column = visibleColumns[index];
let width = (column.width as number) || 0;
const minWidth = (column.minWidth as number) || width || DEFAULT_COLUMN_WIDTH;
if (
!width && // read width from the DOM if it is missing in the config
index !== visibleColumns.length - 1 // skip reading for the last column, because it expands to fully fit the container
) {
const colEl = getCell(column.id);
width = colEl ? getLogicalBoundingClientRect(colEl).inlineSize : DEFAULT_COLUMN_WIDTH;
}
result.set(column.id, Math.max(width, minWidth));
}
return result;
}
function updateWidths(
visibleColumns: readonly ColumnWidthDefinition[],
oldWidths: Map<PropertyKey, number>,
newWidth: number,
columnId: PropertyKey
) {
const column = visibleColumns.find(column => column.id === columnId);
let minWidth = DEFAULT_COLUMN_WIDTH;
if (typeof column?.width === 'number' && column.width < DEFAULT_COLUMN_WIDTH) {
minWidth = column?.width;
}
if (typeof column?.minWidth === 'number') {
minWidth = column?.minWidth;
}
newWidth = Math.max(newWidth, minWidth);
if (oldWidths.get(columnId) === newWidth) {
return oldWidths;
}
const newWidths = new Map(oldWidths);
newWidths.set(columnId, newWidth);
return newWidths;
}
interface WidthsContext {
getColumnStyles(sticky: boolean, columnId: PropertyKey): ColumnWidthStyle;
columnWidths: Map<PropertyKey, number>;
updateColumn: (columnId: PropertyKey, newWidth: number) => void;
setCell: (sticky: boolean, columnId: PropertyKey, node: null | HTMLElement) => void;
}
const WidthsContext = createContext<WidthsContext>({
getColumnStyles: () => ({}),
columnWidths: new Map(),
updateColumn: () => {},
setCell: () => {},
});
interface WidthProviderProps {
visibleColumns: readonly ColumnWidthDefinition[];
resizableColumns: boolean | undefined;
containerRef: React.RefObject<HTMLElement>;
children: React.ReactNode;
}
export function ColumnWidthsProvider({ visibleColumns, resizableColumns, containerRef, children }: WidthProviderProps) {
const visibleColumnsRef = useRef<PropertyKey[] | null>(null);
const containerWidthRef = useRef(0);
const [columnWidths, setColumnWidths] = useState<null | Map<PropertyKey, number>>(null);
const cellsRef = useRef(new Map<PropertyKey, HTMLElement>());
const stickyCellsRef = useRef(new Map<PropertyKey, HTMLElement>());
const getCell = (columnId: PropertyKey): null | HTMLElement => cellsRef.current.get(columnId) ?? null;
const setCell = (sticky: boolean, columnId: PropertyKey, node: null | HTMLElement) => {
const ref = sticky ? stickyCellsRef : cellsRef;
if (node) {
ref.current.set(columnId, node);
} else {
ref.current.delete(columnId);
}
};
const getColumnStyles = (sticky: boolean, columnId: PropertyKey): ColumnWidthStyle => {
const column = visibleColumns.find(column => column.id === columnId);
if (!column) {
return {};
}
if (sticky) {
return {
width:
cellsRef.current.get(column.id)?.getBoundingClientRect().width ||
(columnWidths?.get(column.id) ?? column.width),
};
}
if (resizableColumns && columnWidths) {
const isLastColumn = column.id === visibleColumns[visibleColumns.length - 1]?.id;
const totalWidth = visibleColumns.reduce(
(sum, { id }) => sum + (columnWidths.get(id) || DEFAULT_COLUMN_WIDTH),
0
);
if (isLastColumn && containerWidthRef.current > totalWidth) {
return { width: 'auto', minWidth: column?.minWidth };
} else {
return { width: columnWidths.get(column.id), minWidth: column?.minWidth };
}
}
return {
width: column.width,
minWidth: column.minWidth,
maxWidth: !resizableColumns ? column.maxWidth : undefined,
};
};
// Imperatively sets width style for a cell avoiding React state.
// This allows setting the style as soon container's size change is observed.
const updateColumnWidths = useStableCallback(() => {
for (const { id } of visibleColumns) {
const element = cellsRef.current.get(id);
if (element) {
setElementWidths(element, getColumnStyles(false, id));
}
}
// Sticky column widths must be synchronized once all real column widths are assigned.
for (const { id } of visibleColumns) {
const element = stickyCellsRef.current.get(id);
if (element) {
setElementWidths(element, getColumnStyles(true, id));
}
}
});
// Observes container size and requests an update to the last cell width as it depends on the container's width.
useResizeObserver(containerRef, ({ contentBoxWidth: containerWidth }) => {
containerWidthRef.current = containerWidth;
requestAnimationFrame(() => updateColumnWidths());
});
// The widths of the dynamically added columns (after the first render) if not set explicitly
// will default to the DEFAULT_COLUMN_WIDTH.
useEffect(() => {
updateColumnWidths();
if (!resizableColumns) {
return;
}
let updated = false;
const newColumnWidths = new Map(columnWidths);
const lastVisible = visibleColumnsRef.current;
if (lastVisible) {
for (let index = 0; index < visibleColumns.length; index++) {
const column = visibleColumns[index];
if (!columnWidths?.get(column.id) && lastVisible.indexOf(column.id) === -1) {
updated = true;
const width = (column.width as number) || DEFAULT_COLUMN_WIDTH;
const minWidth = (column.minWidth as number) || width;
newColumnWidths.set(column.id, Math.max(width, minWidth));
}
}
if (updated) {
setColumnWidths(newColumnWidths);
}
}
visibleColumnsRef.current = visibleColumns.map(column => column.id);
}, [columnWidths, resizableColumns, visibleColumns, updateColumnWidths]);
// Read the actual column widths after the first render to employ the browser defaults for
// those columns without explicit width.
useEffect(() => {
if (!resizableColumns) {
return;
}
setColumnWidths(() => readWidths(getCell, visibleColumns));
// This code is intended to run only at the first render and should not re-run when table props change
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
function updateColumn(columnId: PropertyKey, newWidth: number) {
setColumnWidths(columnWidths => updateWidths(visibleColumns, columnWidths ?? new Map(), newWidth, columnId));
}
return (
<WidthsContext.Provider value={{ getColumnStyles, columnWidths: columnWidths ?? new Map(), updateColumn, setCell }}>
{children}
</WidthsContext.Provider>
);
}
export function useColumnWidths() {
return useContext(WidthsContext);
}