@react-stately/table 3.0.0-beta.0 → 3.0.0-nightly-641446f65-240905

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/Cell.main.js +38 -0
  2. package/dist/Cell.main.js.map +1 -0
  3. package/dist/Cell.mjs +33 -0
  4. package/dist/Cell.module.js +33 -0
  5. package/dist/Cell.module.js.map +1 -0
  6. package/dist/Column.main.js +75 -0
  7. package/dist/Column.main.js.map +1 -0
  8. package/dist/Column.mjs +66 -0
  9. package/dist/Column.module.js +66 -0
  10. package/dist/Column.module.js.map +1 -0
  11. package/dist/Row.main.js +97 -0
  12. package/dist/Row.main.js.map +1 -0
  13. package/dist/Row.mjs +88 -0
  14. package/dist/Row.module.js +88 -0
  15. package/dist/Row.module.js.map +1 -0
  16. package/dist/TableBody.main.js +61 -0
  17. package/dist/TableBody.main.js.map +1 -0
  18. package/dist/TableBody.mjs +52 -0
  19. package/dist/TableBody.module.js +52 -0
  20. package/dist/TableBody.module.js.map +1 -0
  21. package/dist/TableCollection.main.js +288 -0
  22. package/dist/TableCollection.main.js.map +1 -0
  23. package/dist/TableCollection.mjs +282 -0
  24. package/dist/TableCollection.module.js +282 -0
  25. package/dist/TableCollection.module.js.map +1 -0
  26. package/dist/TableColumnLayout.main.js +113 -0
  27. package/dist/TableColumnLayout.main.js.map +1 -0
  28. package/dist/TableColumnLayout.mjs +108 -0
  29. package/dist/TableColumnLayout.module.js +108 -0
  30. package/dist/TableColumnLayout.module.js.map +1 -0
  31. package/dist/TableHeader.main.js +56 -0
  32. package/dist/TableHeader.main.js.map +1 -0
  33. package/dist/TableHeader.mjs +47 -0
  34. package/dist/TableHeader.module.js +47 -0
  35. package/dist/TableHeader.module.js.map +1 -0
  36. package/dist/TableUtils.main.js +182 -0
  37. package/dist/TableUtils.main.js.map +1 -0
  38. package/dist/TableUtils.mjs +175 -0
  39. package/dist/TableUtils.module.js +175 -0
  40. package/dist/TableUtils.module.js.map +1 -0
  41. package/dist/import.mjs +37 -0
  42. package/dist/main.js +39 -593
  43. package/dist/main.js.map +1 -1
  44. package/dist/module.js +22 -564
  45. package/dist/module.js.map +1 -1
  46. package/dist/types.d.ts +163 -9
  47. package/dist/types.d.ts.map +1 -1
  48. package/dist/useTableColumnResizeState.main.js +109 -0
  49. package/dist/useTableColumnResizeState.main.js.map +1 -0
  50. package/dist/useTableColumnResizeState.mjs +104 -0
  51. package/dist/useTableColumnResizeState.module.js +104 -0
  52. package/dist/useTableColumnResizeState.module.js.map +1 -0
  53. package/dist/useTableState.main.js +71 -0
  54. package/dist/useTableState.main.js.map +1 -0
  55. package/dist/useTableState.mjs +66 -0
  56. package/dist/useTableState.module.js +66 -0
  57. package/dist/useTableState.module.js.map +1 -0
  58. package/dist/useTreeGridState.main.js +207 -0
  59. package/dist/useTreeGridState.main.js.map +1 -0
  60. package/dist/useTreeGridState.mjs +202 -0
  61. package/dist/useTreeGridState.module.js +202 -0
  62. package/dist/useTreeGridState.module.js.map +1 -0
  63. package/package.json +18 -10
  64. package/src/Cell.ts +4 -1
  65. package/src/Column.ts +12 -2
  66. package/src/Row.ts +50 -10
  67. package/src/TableBody.ts +6 -2
  68. package/src/TableCollection.ts +99 -15
  69. package/src/TableColumnLayout.ts +127 -0
  70. package/src/TableHeader.ts +11 -2
  71. package/src/TableUtils.ts +253 -0
  72. package/src/index.ts +15 -6
  73. package/src/useTableColumnResizeState.ts +147 -0
  74. package/src/useTableState.ts +43 -16
  75. package/src/useTreeGridState.ts +277 -0
  76. package/LICENSE +0 -201
package/src/TableBody.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import {PartialNode} from '@react-stately/collections';
14
- import React, {ReactElement} from 'react';
14
+ import React, {JSX, ReactElement} from 'react';
15
15
  import {TableBodyProps} from '@react-types/table';
16
16
 
17
17
  function TableBody<T>(props: TableBodyProps<T>): ReactElement { // eslint-disable-line @typescript-eslint/no-unused-vars
@@ -29,7 +29,7 @@ TableBody.getCollectionNode = function* getCollectionNode<T>(props: TableBodyPro
29
29
  if (!items) {
30
30
  throw new Error('props.children was a function but props.items is missing');
31
31
  }
32
-
32
+
33
33
  for (let item of items) {
34
34
  yield {
35
35
  type: 'item',
@@ -52,6 +52,10 @@ TableBody.getCollectionNode = function* getCollectionNode<T>(props: TableBodyPro
52
52
  };
53
53
  };
54
54
 
55
+ /**
56
+ * A TableBody is a container for the Row elements of a Table. Rows can be statically defined
57
+ * as children, or generated dynamically using a function based on the data passed to the `items` prop.
58
+ */
55
59
  // We don't want getCollectionNode to show up in the type definition
56
60
  let _TableBody = TableBody as <T>(props: TableBodyProps<T>) => JSX.Element;
57
61
  export {_TableBody as TableBody};
@@ -9,18 +9,31 @@
9
9
  * OF ANY KIND, either express or implied. See the License for the specific language
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
+
13
+ import {getFirstItem, getLastItem} from '@react-stately/collections';
12
14
  import {GridCollection} from '@react-stately/grid';
13
15
  import {GridNode} from '@react-types/grid';
14
- import {Key} from 'react';
16
+ import {TableCollection as ITableCollection} from '@react-types/table';
17
+ import {Key} from '@react-types/shared';
15
18
 
16
19
  interface GridCollectionOptions {
17
- showSelectionCheckboxes?: boolean
20
+ showSelectionCheckboxes?: boolean,
21
+ showDragButtons?: boolean
18
22
  }
19
23
 
20
24
  const ROW_HEADER_COLUMN_KEY = 'row-header-column-' + Math.random().toString(36).slice(2);
25
+ let ROW_HEADER_COLUMN_KEY_DRAG = 'row-header-column-' + Math.random().toString(36).slice(2);
26
+ while (ROW_HEADER_COLUMN_KEY === ROW_HEADER_COLUMN_KEY_DRAG) {
27
+ ROW_HEADER_COLUMN_KEY_DRAG = 'row-header-column-' + Math.random().toString(36).slice(2);
28
+ }
29
+
30
+ /** @private */
31
+ export function buildHeaderRows<T>(keyMap: Map<Key, GridNode<T>>, columnNodes: GridNode<T>[]): GridNode<T>[] {
32
+ if (columnNodes.length === 0) {
33
+ return [];
34
+ }
21
35
 
22
- function buildHeaderRows<T>(keyMap: Map<Key, GridNode<T>>, columnNodes: GridNode<T>[]): GridNode<T>[] {
23
- let columns = [];
36
+ let columns: GridNode<T>[][] = [];
24
37
  let seen = new Map();
25
38
  for (let column of columnNodes) {
26
39
  let parentKey = column.parentKey;
@@ -28,6 +41,9 @@ function buildHeaderRows<T>(keyMap: Map<Key, GridNode<T>>, columnNodes: GridNode
28
41
 
29
42
  while (parentKey) {
30
43
  let parent: GridNode<T> = keyMap.get(parentKey);
44
+ if (!parent) {
45
+ break;
46
+ }
31
47
 
32
48
  // If we've already seen this parent, than it is shared
33
49
  // with a previous column. If the current column is taller
@@ -47,6 +63,7 @@ function buildHeaderRows<T>(keyMap: Map<Key, GridNode<T>>, columnNodes: GridNode
47
63
 
48
64
  // Adjust shifted indices
49
65
  for (let i = col.length; i < column.length; i++) {
66
+ // eslint-disable-next-line max-depth
50
67
  if (column[i] && seen.has(column[i])) {
51
68
  seen.get(column[i]).index = i;
52
69
  }
@@ -90,6 +107,7 @@ function buildHeaderRows<T>(keyMap: Map<Key, GridNode<T>>, columnNodes: GridNode
90
107
  textValue: null
91
108
  };
92
109
 
110
+ // eslint-disable-next-line max-depth
93
111
  if (row.length > 0) {
94
112
  row[row.length - 1].nextKey = placeholder.key;
95
113
  placeholder.prevKey = row[row.length - 1].key;
@@ -104,7 +122,7 @@ function buildHeaderRows<T>(keyMap: Map<Key, GridNode<T>>, columnNodes: GridNode
104
122
  }
105
123
 
106
124
  item.level = i;
107
- item.index = colIndex;
125
+ item.colIndex = colIndex;
108
126
  row.push(item);
109
127
  }
110
128
 
@@ -156,17 +174,17 @@ function buildHeaderRows<T>(keyMap: Map<Key, GridNode<T>>, columnNodes: GridNode
156
174
  });
157
175
  }
158
176
 
159
- export class TableCollection<T> extends GridCollection<T> {
177
+ export class TableCollection<T> extends GridCollection<T> implements ITableCollection<T> {
160
178
  headerRows: GridNode<T>[];
161
179
  columns: GridNode<T>[];
162
180
  rowHeaderColumnKeys: Set<Key>;
163
181
  body: GridNode<T>;
182
+ _size: number = 0;
164
183
 
165
- constructor(nodes: Iterable<GridNode<T>>, prev?: TableCollection<T>, opts?: GridCollectionOptions) {
184
+ constructor(nodes: Iterable<GridNode<T>>, prev?: ITableCollection<T>, opts?: GridCollectionOptions) {
166
185
  let rowHeaderColumnKeys: Set<Key> = new Set();
167
186
  let body: GridNode<T>;
168
- let columns = [];
169
-
187
+ let columns: GridNode<T>[] = [];
170
188
  // Add cell for selection checkboxes if needed.
171
189
  if (opts?.showSelectionCheckboxes) {
172
190
  let rowHeaderColumn: GridNode<T> = {
@@ -175,7 +193,7 @@ export class TableCollection<T> extends GridCollection<T> {
175
193
  value: null,
176
194
  textValue: '',
177
195
  level: 0,
178
- index: 0,
196
+ index: opts?.showDragButtons ? 1 : 0,
179
197
  hasChildNodes: false,
180
198
  rendered: null,
181
199
  childNodes: [],
@@ -187,6 +205,26 @@ export class TableCollection<T> extends GridCollection<T> {
187
205
  columns.unshift(rowHeaderColumn);
188
206
  }
189
207
 
208
+ // Add cell for drag buttons if needed.
209
+ if (opts?.showDragButtons) {
210
+ let rowHeaderColumn: GridNode<T> = {
211
+ type: 'column',
212
+ key: ROW_HEADER_COLUMN_KEY_DRAG,
213
+ value: null,
214
+ textValue: '',
215
+ level: 0,
216
+ index: 0,
217
+ hasChildNodes: false,
218
+ rendered: null,
219
+ childNodes: [],
220
+ props: {
221
+ isDragButtonCell: true
222
+ }
223
+ };
224
+
225
+ columns.unshift(rowHeaderColumn);
226
+ }
227
+
190
228
  let rows = [];
191
229
  let columnKeyMap = new Map();
192
230
  let visit = (node: GridNode<T>) => {
@@ -216,6 +254,7 @@ export class TableCollection<T> extends GridCollection<T> {
216
254
  for (let node of nodes) {
217
255
  visit(node);
218
256
  }
257
+
219
258
  let headerRows = buildHeaderRows(columnKeyMap, columns) as GridNode<T>[];
220
259
  headerRows.forEach((row, i) => rows.splice(i, 0, row));
221
260
 
@@ -231,10 +270,11 @@ export class TableCollection<T> extends GridCollection<T> {
231
270
  this.rowHeaderColumnKeys = rowHeaderColumnKeys;
232
271
  this.body = body;
233
272
  this.headerRows = headerRows;
273
+ this._size = [...body.childNodes].length;
234
274
 
235
275
  // Default row header column to the first one.
236
276
  if (this.rowHeaderColumnKeys.size === 0) {
237
- this.rowHeaderColumnKeys.add(this.columns[opts?.showSelectionCheckboxes ? 1 : 0].key);
277
+ this.rowHeaderColumnKeys.add(this.columns.find(column => !column.props?.isDragButtonCell && !column.props?.isSelectionCell).key);
238
278
  }
239
279
  }
240
280
 
@@ -243,7 +283,7 @@ export class TableCollection<T> extends GridCollection<T> {
243
283
  }
244
284
 
245
285
  get size() {
246
- return [...this.body.childNodes].length;
286
+ return this._size;
247
287
  }
248
288
 
249
289
  getKeys() {
@@ -261,15 +301,59 @@ export class TableCollection<T> extends GridCollection<T> {
261
301
  }
262
302
 
263
303
  getFirstKey() {
264
- return [...this.body.childNodes][0]?.key;
304
+ return getFirstItem(this.body.childNodes)?.key;
265
305
  }
266
306
 
267
307
  getLastKey() {
268
- let rows = [...this.body.childNodes];
269
- return rows[rows.length - 1]?.key;
308
+ return getLastItem(this.body.childNodes)?.key;
270
309
  }
271
310
 
272
311
  getItem(key: Key) {
273
312
  return this.keyMap.get(key);
274
313
  }
314
+
315
+ at(idx: number) {
316
+ const keys = [...this.getKeys()];
317
+ return this.getItem(keys[idx]);
318
+ }
319
+
320
+ getChildren(key: Key): Iterable<GridNode<T>> {
321
+ if (key === this.body.key) {
322
+ return this.body.childNodes;
323
+ }
324
+
325
+ return super.getChildren(key);
326
+ }
327
+
328
+ getTextValue(key: Key): string {
329
+ let row = this.getItem(key);
330
+ if (!row) {
331
+ return '';
332
+ }
333
+
334
+ // If the row has a textValue, use that.
335
+ if (row.textValue) {
336
+ return row.textValue;
337
+ }
338
+
339
+ // Otherwise combine the text of each of the row header columns.
340
+ let rowHeaderColumnKeys = this.rowHeaderColumnKeys;
341
+ if (rowHeaderColumnKeys) {
342
+ let text = [];
343
+ for (let cell of row.childNodes) {
344
+ let column = this.columns[cell.index];
345
+ if (rowHeaderColumnKeys.has(column.key) && cell.textValue) {
346
+ text.push(cell.textValue);
347
+ }
348
+
349
+ if (text.length === rowHeaderColumnKeys.size) {
350
+ break;
351
+ }
352
+ }
353
+
354
+ return text.join(' ');
355
+ }
356
+
357
+ return '';
358
+ }
275
359
  }
@@ -0,0 +1,127 @@
1
+ /*
2
+ * Copyright 2022 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ import {
14
+ calculateColumnSizes,
15
+ getMaxWidth,
16
+ getMinWidth
17
+ } from './TableUtils';
18
+ import {ColumnSize, TableCollection} from '@react-types/table';
19
+ import {GridNode} from '@react-types/grid';
20
+ import {Key} from '@react-types/shared';
21
+
22
+ export interface TableColumnLayoutOptions<T> {
23
+ getDefaultWidth?: (column: GridNode<T>) => ColumnSize | null | undefined,
24
+ getDefaultMinWidth?: (column: GridNode<T>) => ColumnSize | null | undefined
25
+ }
26
+
27
+ export class TableColumnLayout<T> {
28
+ getDefaultWidth: (column: GridNode<T>) => ColumnSize | null | undefined;
29
+ getDefaultMinWidth: (column: GridNode<T>) => ColumnSize | null | undefined;
30
+ columnWidths: Map<Key, number> = new Map();
31
+ columnMinWidths: Map<Key, number> = new Map();
32
+ columnMaxWidths: Map<Key, number> = new Map();
33
+
34
+ constructor(options: TableColumnLayoutOptions<T>) {
35
+ this.getDefaultWidth = options?.getDefaultWidth ?? (() => '1fr');
36
+ this.getDefaultMinWidth = options?.getDefaultMinWidth ?? (() => 75);
37
+ }
38
+
39
+ /** Takes an array of columns and splits it into 2 maps of columns with controlled and columns with uncontrolled widths. */
40
+ splitColumnsIntoControlledAndUncontrolled(columns: Array<GridNode<T>>): [Map<Key, GridNode<T>>, Map<Key, GridNode<T>>] {
41
+ return columns.reduce((acc, col) => {
42
+ if (col.props.width != null) {
43
+ acc[0].set(col.key, col);
44
+ } else {
45
+ acc[1].set(col.key, col);
46
+ }
47
+ return acc;
48
+ }, [new Map(), new Map()]);
49
+ }
50
+
51
+ /** Takes uncontrolled and controlled widths and joins them into a single Map. */
52
+ recombineColumns(columns: Array<GridNode<T>>, uncontrolledWidths: Map<Key, ColumnSize>, uncontrolledColumns: Map<Key, GridNode<T>>, controlledColumns: Map<Key, GridNode<T>>): Map<Key, ColumnSize> {
53
+ return new Map(columns.map(col => {
54
+ if (uncontrolledColumns.has(col.key)) {
55
+ return [col.key, uncontrolledWidths.get(col.key)];
56
+ } else {
57
+ return [col.key, controlledColumns.get(col.key).props.width];
58
+ }
59
+ }));
60
+ }
61
+
62
+ /** Used to make an initial Map of the uncontrolled widths based on default widths. */
63
+ getInitialUncontrolledWidths(uncontrolledColumns: Map<Key, GridNode<T>>): Map<Key, ColumnSize> {
64
+ return new Map(Array.from(uncontrolledColumns).map(([key, col]) =>
65
+ [key, col.props.defaultWidth ?? this.getDefaultWidth?.(col) ?? '1fr']
66
+ ));
67
+ }
68
+
69
+ getColumnWidth(key: Key): number {
70
+ return this.columnWidths.get(key) ?? 0;
71
+ }
72
+
73
+ getColumnMinWidth(key: Key): number {
74
+ return this.columnMinWidths.get(key) ?? 0;
75
+ }
76
+
77
+ getColumnMaxWidth(key: Key): number {
78
+ return this.columnMaxWidths.get(key) ?? 0;
79
+ }
80
+
81
+ resizeColumnWidth(collection: TableCollection<T>, uncontrolledWidths: Map<Key, ColumnSize>, col: Key, width: number): Map<Key, ColumnSize> {
82
+ let prevColumnWidths = this.columnWidths;
83
+ let freeze = true;
84
+ let newWidths = new Map<Key, ColumnSize>();
85
+
86
+ width = Math.max(this.getColumnMinWidth(col), Math.min(this.getColumnMaxWidth(col), Math.floor(width)));
87
+
88
+ collection.columns.forEach(column => {
89
+ if (column.key === col) {
90
+ newWidths.set(column.key, width);
91
+ freeze = false;
92
+ } else if (freeze) {
93
+ // freeze columns to the left to their previous pixel value
94
+ newWidths.set(column.key, prevColumnWidths.get(column.key));
95
+ } else {
96
+ newWidths.set(column.key, column.props.width ?? uncontrolledWidths.get(column.key));
97
+ }
98
+ });
99
+
100
+ return newWidths;
101
+ }
102
+
103
+ buildColumnWidths(tableWidth: number, collection: TableCollection<T>, widths: Map<Key, ColumnSize>) {
104
+ this.columnWidths = new Map();
105
+ this.columnMinWidths = new Map();
106
+ this.columnMaxWidths = new Map();
107
+
108
+ // initial layout or table/window resizing
109
+ let columnWidths = calculateColumnSizes(
110
+ tableWidth,
111
+ collection.columns.map(col => ({...col.props, key: col.key})),
112
+ widths,
113
+ (i) => this.getDefaultWidth(collection.columns[i]),
114
+ (i) => this.getDefaultMinWidth(collection.columns[i])
115
+ );
116
+
117
+ // columns going in will be the same order as the columns coming out
118
+ columnWidths.forEach((width, index) => {
119
+ let key = collection.columns[index].key;
120
+ let column = collection.columns[index];
121
+ this.columnWidths.set(key, width);
122
+ this.columnMinWidths.set(key, getMinWidth(column.props.minWidth ?? this.getDefaultMinWidth(column), tableWidth));
123
+ this.columnMaxWidths.set(key, getMaxWidth(column.props.maxWidth, tableWidth));
124
+ });
125
+ return this.columnWidths;
126
+ }
127
+ }
@@ -10,16 +10,21 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
+ import {CollectionBuilderContext} from './useTableState';
13
14
  import {PartialNode} from '@react-stately/collections';
14
- import React, {ReactElement} from 'react';
15
+ import React, {JSX, ReactElement} from 'react';
15
16
  import {TableHeaderProps} from '@react-types/table';
16
17
 
17
18
  function TableHeader<T>(props: TableHeaderProps<T>): ReactElement { // eslint-disable-line @typescript-eslint/no-unused-vars
18
19
  return null;
19
20
  }
20
21
 
21
- TableHeader.getCollectionNode = function* getCollectionNode<T>(props: TableHeaderProps<T>): Generator<PartialNode<T>, void, any> {
22
+ TableHeader.getCollectionNode = function* getCollectionNode<T>(props: TableHeaderProps<T>, context: CollectionBuilderContext<T>): Generator<PartialNode<T>, void, any> {
22
23
  let {children, columns} = props;
24
+
25
+ // Clear columns so they aren't double added in strict mode.
26
+ context.columns = [];
27
+
23
28
  if (typeof children === 'function') {
24
29
  if (!columns) {
25
30
  throw new Error('props.children was a function but props.columns is missing');
@@ -45,6 +50,10 @@ TableHeader.getCollectionNode = function* getCollectionNode<T>(props: TableHeade
45
50
  }
46
51
  };
47
52
 
53
+ /**
54
+ * A TableHeader is a container for the Column elements in a Table. Columns can be statically defined
55
+ * as children, or generated dynamically using a function based on the data passed to the `columns` prop.
56
+ */
48
57
  // We don't want getCollectionNode to show up in the type definition
49
58
  let _TableHeader = TableHeader as <T>(props: TableHeaderProps<T>) => JSX.Element;
50
59
  export {_TableHeader as TableHeader};
@@ -0,0 +1,253 @@
1
+ /*
2
+ * Copyright 2022 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ import {ColumnSize} from '@react-types/table';
14
+ import {Key} from '@react-types/shared';
15
+
16
+ // numbers and percents are considered static. *fr units or a lack of units are considered dynamic.
17
+ export function isStatic(width: number | string): boolean {
18
+ return width != null && (!isNaN(width as number) || (String(width)).match(/^(\d+)(?=%$)/) !== null);
19
+ }
20
+
21
+ export function parseFractionalUnit(width: string): number {
22
+ if (!width) {
23
+ return 1;
24
+ }
25
+ let match = width.match(/^(.+)(?=fr$)/);
26
+ // if width is the incorrect format, just default it to a 1fr
27
+ if (!match) {
28
+ console.warn(`width: ${width} is not a supported format, width should be a number (ex. 150), percentage (ex. '50%') or fr unit (ex. '2fr')`,
29
+ 'defaulting to \'1fr\'');
30
+ return 1;
31
+ }
32
+ return parseFloat(match[0]);
33
+ }
34
+
35
+ export function parseStaticWidth(width: number | string, tableWidth: number): number {
36
+ if (typeof width === 'string') {
37
+ let match = width.match(/^(\d+)(?=%$)/);
38
+ if (!match) {
39
+ throw new Error('Only percentages or numbers are supported for static column widths');
40
+ }
41
+ return tableWidth * (parseFloat(match[0]) / 100);
42
+ }
43
+ return width;
44
+ }
45
+
46
+
47
+ export function getMaxWidth(maxWidth: number | string, tableWidth: number): number {
48
+ return maxWidth != null
49
+ ? parseStaticWidth(maxWidth, tableWidth)
50
+ : Number.MAX_SAFE_INTEGER;
51
+ }
52
+
53
+ // cannot support FR units, we'd need to know everything else in the table to do that
54
+ export function getMinWidth(minWidth: number | string, tableWidth: number): number {
55
+ return minWidth != null
56
+ ? parseStaticWidth(minWidth, tableWidth)
57
+ : 0;
58
+ }
59
+
60
+
61
+ export interface IColumn {
62
+ minWidth?: number | string,
63
+ maxWidth?: number | string,
64
+ width?: number | string,
65
+ defaultWidth?: number | string,
66
+ key?: Key
67
+ }
68
+
69
+ /**
70
+ * Implements the flex algorithm described in https://www.w3.org/TR/css-flexbox-1/#layout-algorithm
71
+ * It makes a few constraint/assumptions:
72
+ * 1. All basis values are 0 unless it is a static width, then the basis is the static width
73
+ * 2. All flex grow and shrink values are equal to the FR specified on the column, grow and shrink for the same column are equal
74
+ * 3. We only have one row
75
+ * An example of the setup can be seen here https://jsfiddle.net/snowystinger/wv0ymjaf/61/ where I let the browser figure out the
76
+ * flex of the columns.
77
+ * Note: We differ in one key aspect, all of our column widths must be whole numbers, so we avoid browser
78
+ * sub pixel rounding errors. To do this, we use a cascading rounding algorithm to ensure that the sum of the widths is maintained
79
+ * while distributing the rounding remainder across the columns.
80
+ *
81
+ * As noted in the chrome source code, this algorithm is very accurate, but has the potential to be quadratic.
82
+ * They have deemed this to be acceptable because the number of elements is usually small and the flex factors
83
+ * are usually not high variance. I believe we can make the same assumptions. Particularly once resizing is
84
+ * started, it will convert all columns to the left to static widths, so it will cut down on the number of FR columns.
85
+ *
86
+ * There are likely faster ways to do this, I've chosen to stick to the spec as closely as possible for readability, accuracy, and for the
87
+ * note that this behaving quadratically is unlikely to be a problem.
88
+ * @param availableWidth - The visible width of the table.
89
+ * @param columns - The table defined columns.
90
+ * @param changedColumns - Any columns we want to override, for example, during resizing.
91
+ * @param getDefaultWidth - A function that returns the default width of a column by its index.
92
+ * @param getDefaultMinWidth - A function that returns the default min width of a column by its index.
93
+ */
94
+ export function calculateColumnSizes(availableWidth: number, columns: IColumn[], changedColumns: Map<Key, ColumnSize>, getDefaultWidth, getDefaultMinWidth) {
95
+ let hasNonFrozenItems = false;
96
+ let flexItems = columns.map((column, index) => {
97
+ let width = changedColumns.get(column.key) != null ? changedColumns.get(column.key) : column.width ?? column.defaultWidth ?? getDefaultWidth?.(index) ?? '1fr';
98
+ let frozen = false;
99
+ let baseSize = 0;
100
+ let flex = 0;
101
+ let targetMainSize = null;
102
+ if (isStatic(width)) {
103
+ baseSize = parseStaticWidth(width, availableWidth);
104
+ frozen = true;
105
+ } else {
106
+ flex = parseFractionalUnit(width);
107
+ if (flex <= 0) {
108
+ frozen = true;
109
+ }
110
+ }
111
+
112
+ let min = getMinWidth(column.minWidth ?? getDefaultMinWidth?.(index) ?? 0, availableWidth);
113
+ let max = getMaxWidth(column.maxWidth, availableWidth);
114
+ let hypotheticalMainSize = Math.max(min, Math.min(baseSize, max));
115
+
116
+ // 9.7.1
117
+ // We don't make use of flex basis, it's always 0, so we are always in 'grow' mode.
118
+ // 9.7.2
119
+ if (frozen) {
120
+ targetMainSize = hypotheticalMainSize;
121
+ } else if (baseSize > hypotheticalMainSize) {
122
+ frozen = true;
123
+ targetMainSize = hypotheticalMainSize;
124
+ }
125
+
126
+ // 9.7.3
127
+ if (!frozen) {
128
+ hasNonFrozenItems = true;
129
+ }
130
+ return {
131
+ frozen,
132
+ baseSize,
133
+ hypotheticalMainSize,
134
+ min,
135
+ max,
136
+ flex,
137
+ targetMainSize,
138
+ violation: 0
139
+ };
140
+ });
141
+
142
+ // 9.7.4
143
+ // 9.7.4.a
144
+ while (hasNonFrozenItems) {
145
+ // 9.7.4.b
146
+ /**
147
+ * Calculate the remaining free space as for initial free space,
148
+ * above (9.7.3). If the sum of the unfrozen flex items’ flex factors is
149
+ * less than one, multiply the initial free space by this sum (of flex factors).
150
+ * If the magnitude of this value is less than the magnitude of
151
+ * the remaining free space, use this as the remaining free space.
152
+ */
153
+ let usedWidth = 0;
154
+ let flexFactors = 0;
155
+ flexItems.forEach(item => {
156
+ if (item.frozen) {
157
+ usedWidth += item.targetMainSize;
158
+ } else {
159
+ usedWidth += item.baseSize;
160
+ flexFactors += item.flex;
161
+ }
162
+ });
163
+
164
+ let remainingFreeSpace = availableWidth - usedWidth;
165
+ // we only support integer FR's, and because of hasNonFrozenItems, we know that flexFactors > 0
166
+ // so no need to check for flexFactors < 1
167
+ // 9.7.4.c
168
+ /**
169
+ * If the remaining free space is zero
170
+ * - Do nothing.
171
+ * Else // remember, we're always in grow mode
172
+ * - Find the ratio of the item’s flex grow factor to the
173
+ * sum of the flex grow factors of all unfrozen items on
174
+ * the line. Set the item’s target main size to its flex
175
+ * base size plus a fraction of the remaining free space
176
+ * proportional to the ratio.
177
+ */
178
+ if (remainingFreeSpace > 0) {
179
+ flexItems.forEach((item) => {
180
+ if (!item.frozen) {
181
+ let ratio = item.flex / flexFactors;
182
+ item.targetMainSize = item.baseSize + (ratio * remainingFreeSpace);
183
+ }
184
+ });
185
+ }
186
+
187
+ // 9.7.4.d
188
+ /**
189
+ * Fix min/max violations. Clamp each non-frozen item’s
190
+ * target main size by its used min and max main sizes
191
+ * and floor its content-box size at zero. If the item’s
192
+ * target main size was made smaller by this, it’s a max
193
+ * violation. If the item’s target main size was made
194
+ * larger by this, it’s a min violation.
195
+ */
196
+ let totalViolation = 0;
197
+ flexItems.forEach(item => {
198
+ item.violation = 0;
199
+ if (!item.frozen) {
200
+ let {min, max, targetMainSize} = item;
201
+ item.targetMainSize = Math.max(min, Math.min(targetMainSize, max));
202
+
203
+ item.violation = item.targetMainSize - targetMainSize;
204
+ totalViolation += item.violation;
205
+ }
206
+ });
207
+
208
+ // 9.7.4.e
209
+ /**
210
+ * Freeze over-flexed items. The total violation is the
211
+ * sum of the adjustments from the previous step
212
+ * ∑(clamped size - unclamped size). If the total violation is:
213
+ * Zero
214
+ * - Freeze all items.
215
+ *
216
+ * Positive
217
+ * - Freeze all the items with min violations.
218
+ *
219
+ * Negative
220
+ * - Freeze all the items with max violations.
221
+ */
222
+ hasNonFrozenItems = false;
223
+ flexItems.forEach(item => {
224
+ if (totalViolation === 0 || Math.sign(totalViolation) === Math.sign(item.violation)) {
225
+ item.frozen = true;
226
+ } else if (!item.frozen) {
227
+ hasNonFrozenItems = true;
228
+ }
229
+ });
230
+ }
231
+
232
+ return cascadeRounding(flexItems);
233
+ }
234
+
235
+ function cascadeRounding(flexItems): number[] {
236
+ /*
237
+ Given an array of floats that sum to an integer, this rounds the floats
238
+ and returns an array of integers with the same sum.
239
+ */
240
+
241
+ let fpTotal = 0;
242
+ let intTotal = 0;
243
+ let roundedArray = [];
244
+ flexItems.forEach(function (item) {
245
+ let float = item.targetMainSize;
246
+ let integer = Math.round(float + fpTotal) - intTotal;
247
+ fpTotal += float;
248
+ intTotal += integer;
249
+ roundedArray.push(integer);
250
+ });
251
+
252
+ return roundedArray;
253
+ }
package/src/index.ts CHANGED
@@ -10,10 +10,19 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- export * from './useTableState';
14
- export * from './TableHeader';
15
- export * from './TableBody';
16
- export * from './Column';
17
- export * from './Row';
18
- export * from './Cell';
13
+ export type {TableColumnResizeState, TableColumnResizeStateProps} from './useTableColumnResizeState';
14
+ export type {TableState, CollectionBuilderContext, TableStateProps} from './useTableState';
15
+ export type {TableHeaderProps, TableBodyProps, ColumnProps, RowProps, CellProps} from '@react-types/table';
16
+ export type {TreeGridState, TreeGridStateProps} from './useTreeGridState';
17
+
18
+ export {useTableColumnResizeState} from './useTableColumnResizeState';
19
+ export {useTableState} from './useTableState';
20
+ export {TableHeader} from './TableHeader';
21
+ export {TableBody} from './TableBody';
22
+ export {Column} from './Column';
23
+ export {Row} from './Row';
24
+ export {Cell} from './Cell';
19
25
  export {Section} from '@react-stately/collections';
26
+ export {TableCollection, buildHeaderRows} from './TableCollection';
27
+ export {TableColumnLayout} from './TableColumnLayout';
28
+ export {UNSTABLE_useTreeGridState} from './useTreeGridState';