@semcore/data-table 2.1.0 → 2.2.0

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.
package/src/Body.tsx CHANGED
@@ -4,11 +4,11 @@ import { Box, Flex, IBoxProps } from '@semcore/flex-box';
4
4
  import ScrollArea from '@semcore/scroll-area';
5
5
  import { getFixedStyle, getScrollOffsetValue } from './utils';
6
6
  import { RowData, Column, NestedCells, PropsLayer, Cell } from './types';
7
- import assignProps from '@semcore/utils/lib/assignProps';
8
- import type ResizeObserverCallback from 'resize-observer-polyfill';
9
-
7
+ import assignProps, { callAllEventHandlers } from '@semcore/utils/lib/assignProps';
8
+ import ResizeObserver from 'resize-observer-polyfill';
10
9
  import scrollStyles from './style/scroll-area.shadow.css';
11
10
  import syncScroll from '@semcore/utils/lib/syncScroll';
11
+ import trottle from '@semcore/utils/lib/rafTrottle';
12
12
 
13
13
  const testEnv = process.env.NODE_ENV === 'test';
14
14
 
@@ -24,35 +24,33 @@ type AsProps = {
24
24
  onResize: ResizeObserverCallback;
25
25
  rowPropsLayers: PropsLayer[];
26
26
  use: 'primary' | 'secondary';
27
+ uniqueKey: string;
28
+ virtualScroll?: boolean | { tollerance?: number; rowHeight?: number };
27
29
  };
28
30
 
29
- class Body extends Component<AsProps> {
30
- renderRow(cells: NestedCells, index: number) {
31
- const SRow = Box;
32
- const { styles, rowPropsLayers } = this.asProps;
33
-
34
- const cellsByColumn = cells.flatRowData || getCellsByColumn(cells);
31
+ type State = {
32
+ rowHeight: number | undefined;
33
+ scrollAreaHeight: undefined | number;
34
+ scrollOffset: number;
35
+ };
35
36
 
36
- let props = {
37
- children: this.renderCells(cells, cellsByColumn, index),
38
- theme: undefined,
39
- active: undefined,
40
- };
37
+ class Body extends Component<AsProps, State> {
38
+ state: State = {
39
+ rowHeight: undefined,
40
+ scrollAreaHeight: undefined,
41
+ scrollOffset: 0,
42
+ };
41
43
 
42
- for (const rowPropsLayer of rowPropsLayers) {
43
- const { childrenPropsGetter = (p) => p, ...other } = rowPropsLayer;
44
- const propsRow = assignProps(other, props);
45
- props = assignProps(childrenPropsGetter(propsRow, cellsByColumn, index), propsRow);
46
- }
44
+ firstRowRef = React.createRef<HTMLElement>();
45
+ firstRowResizeObserver: ResizeObserver | null = null;
47
46
 
48
- return sstyled(styles)(<SRow key={index} {...props} />);
49
- }
47
+ getRowHeight = () => {
48
+ const { virtualScroll } = this.asProps;
49
+ const rowHeightFromProps = typeof virtualScroll === 'object' && virtualScroll?.rowHeight;
50
+ return rowHeightFromProps || this.state.rowHeight;
51
+ };
50
52
 
51
- renderRows(rows: NestedCells[]) {
52
- return rows.map((cells, index) => this.renderRow(cells, index));
53
- }
54
-
55
- renderCells(cells: NestedCells, cellsByColumn: RowData, index: number) {
53
+ renderCells(cells: NestedCells, rowData: RowData, index: number) {
56
54
  const SCell = Flex;
57
55
  const { styles, columns, use } = this.asProps;
58
56
  return cells.map((cell) => {
@@ -73,7 +71,6 @@ class Body extends Component<AsProps> {
73
71
  let props: CellProps = {
74
72
  name: cell.name,
75
73
  children: <>{cell.data}</>,
76
- ['data-data']: JSON.stringify(cell.data),
77
74
  justifyContent: column?.props?.justifyContent,
78
75
  style: {
79
76
  width: vars.length === 1 ? vars[0] : `calc(${vars.join(' + ')})`,
@@ -86,7 +83,7 @@ class Body extends Component<AsProps> {
86
83
  for (const cellPropLayer of cell.cellPropsLayers || []) {
87
84
  const { childrenPropsGetter = (p) => p, ...other } = cellPropLayer;
88
85
  const propsCell = assignProps(other, props);
89
- props = assignProps(childrenPropsGetter(propsCell, cellsByColumn, index), propsCell);
86
+ props = assignProps(childrenPropsGetter(propsCell, rowData, index), propsCell);
90
87
  }
91
88
 
92
89
  return sstyled(styles)(
@@ -96,17 +93,136 @@ class Body extends Component<AsProps> {
96
93
  }, [] as React.ReactElement[]);
97
94
  }
98
95
 
96
+ renderRow(
97
+ cells: NestedCells,
98
+ { dataIndex, topOffset, nested }: { dataIndex: number; topOffset?: number; nested: boolean },
99
+ ) {
100
+ const SRow = Box;
101
+ const { styles, rowPropsLayers, uniqueKey, virtualScroll } = this.asProps;
102
+ const rowHeightFromProps = typeof virtualScroll === 'object' && virtualScroll?.rowHeight;
103
+
104
+ const rowData = cells.flatRowData || getCellsByColumn(cells);
105
+ const key = rowData[uniqueKey] ? String(rowData[uniqueKey]) : `row_${dataIndex}`;
106
+ const needToMeasureHeight = dataIndex === 0 && !nested && !rowHeightFromProps;
107
+
108
+ let props = {
109
+ children: this.renderCells(cells, rowData, dataIndex),
110
+ theme: undefined,
111
+ active: undefined,
112
+ positioned: topOffset !== undefined,
113
+ top: topOffset,
114
+ ref: needToMeasureHeight ? this.firstRowRef : undefined,
115
+ key,
116
+ };
117
+
118
+ for (const rowPropsLayer of rowPropsLayers) {
119
+ const { childrenPropsGetter = (p) => p, ...other } = rowPropsLayer;
120
+ const propsRow = assignProps(other, props);
121
+ props = assignProps(childrenPropsGetter(propsRow, rowData, dataIndex), propsRow);
122
+ }
123
+
124
+ return sstyled(styles)(<SRow {...props} />);
125
+ }
126
+
127
+ renderRows(rows: NestedCells[]) {
128
+ return rows.map((cells, dataIndex) => this.renderRow(cells, { dataIndex, nested: false }));
129
+ }
130
+
131
+ renderVirtualizedRows(rows: NestedCells[]) {
132
+ if (rows.length === 0) return [];
133
+
134
+ const { virtualScroll } = this.asProps;
135
+ const { scrollOffset, scrollAreaHeight } = this.state;
136
+ const rowHeight = this.getRowHeight();
137
+
138
+ const tollerance = (typeof virtualScroll === 'object' ? virtualScroll?.tollerance : 2) ?? 2;
139
+ const startIndex = Math.max(Math.floor(scrollOffset / rowHeight!) - tollerance, 0);
140
+ const lastIndex = Math.min(
141
+ Math.ceil((scrollOffset + scrollAreaHeight!) / rowHeight!) + tollerance,
142
+ rows.length,
143
+ );
144
+
145
+ const rowHeightFromProps = typeof virtualScroll === 'object' && virtualScroll?.rowHeight;
146
+ const needToMeasureFirstRowHeight = !rowHeightFromProps;
147
+
148
+ const firstRow = { cells: rows[0], dataIndex: 0, topOffset: 0 };
149
+ const visibleRows = rowHeight !== undefined ? rows.slice(startIndex, lastIndex) : [];
150
+ const processedVisibleRows = visibleRows.map((cells, index) => ({
151
+ cells,
152
+ dataIndex: startIndex + index,
153
+ topOffset: rowHeight! * (startIndex + index),
154
+ }));
155
+ if (needToMeasureFirstRowHeight && startIndex !== 0) {
156
+ processedVisibleRows.unshift(firstRow);
157
+ }
158
+
159
+ return processedVisibleRows.map(({ cells, dataIndex, topOffset }) =>
160
+ this.renderRow(cells, { dataIndex, topOffset, nested: false }),
161
+ );
162
+ }
163
+
164
+ handleFirstRowResize = trottle((entries: ResizeObserverEntry[]) => {
165
+ const { contentRect } = entries[0];
166
+ const { height } = contentRect;
167
+ this.setState((oldState: State) => {
168
+ if (oldState.rowHeight === height) return oldState;
169
+ return { rowHeight: height };
170
+ });
171
+ });
172
+
173
+ handleScrollAreaResize = trottle((entries: ResizeObserverEntry[]) => {
174
+ const { virtualScroll } = this.asProps;
175
+ if (!virtualScroll) return;
176
+ const { contentRect } = entries[0];
177
+ const { height } = contentRect;
178
+ this.setState((oldState: State) => {
179
+ if (oldState.scrollAreaHeight === height) return oldState;
180
+ return { scrollAreaHeight: height };
181
+ });
182
+ });
183
+
184
+ handleScrollAreaScroll = (event: React.SyntheticEvent<HTMLElement>) => {
185
+ const { scrollTop } = event.target as HTMLElement;
186
+ const { virtualScroll } = this.asProps;
187
+ if (virtualScroll) {
188
+ this.setState((oldState: State) => {
189
+ if (oldState.scrollOffset === scrollTop) return oldState;
190
+ return { scrollOffset: scrollTop };
191
+ });
192
+ }
193
+ };
194
+
195
+ setupRowSizeObserver = () => {
196
+ if (!this.firstRowRef.current) return;
197
+ if (!this.asProps.virtualScroll) return;
198
+ this.firstRowResizeObserver = new ResizeObserver(this.handleFirstRowResize);
199
+ this.firstRowResizeObserver.observe(this.firstRowRef.current);
200
+ };
201
+
202
+ componentWillUnmount() {
203
+ this.firstRowResizeObserver?.disconnect();
204
+ }
205
+
99
206
  render() {
100
207
  const SBody = Root;
101
208
  const SBodyWrapper = Box;
102
209
  const SScrollAreaBar = ScrollArea.Bar;
103
- const { Children, styles, rows, columns, onResize, $scrollRef } = this.asProps;
210
+ const SHeightHold = Box;
211
+ const { Children, styles, rows, columns, $scrollRef, virtualScroll, onResize } = this.asProps;
104
212
 
105
213
  const columnsInitialized = columns.reduce((sum, { width }) => sum + width, 0) > 0 || testEnv;
106
214
 
107
215
  const [offsetLeftSum, offsetRightSum] = getScrollOffsetValue(columns);
108
216
  const offsetSum = offsetLeftSum + offsetRightSum;
109
217
 
218
+ const rowHeight = this.getRowHeight();
219
+ const holdHeight =
220
+ rowHeight !== undefined && virtualScroll ? rowHeight * rows.length : undefined;
221
+
222
+ if (virtualScroll && columnsInitialized && !rowHeight) {
223
+ new Promise(() => this.setupRowSizeObserver());
224
+ }
225
+
110
226
  return sstyled(styles)(
111
227
  <SBodyWrapper>
112
228
  <ScrollArea
@@ -114,10 +230,15 @@ class Body extends Component<AsProps> {
114
230
  styles={scrollStyles}
115
231
  use:left={`${offsetLeftSum}px`}
116
232
  use:right={`${offsetRightSum}px`}
117
- onResize={onResize}
233
+ onResize={callAllEventHandlers(onResize, this.handleScrollAreaResize)}
234
+ onScroll={this.handleScrollAreaScroll}
118
235
  >
119
236
  <ScrollArea.Container ref={$scrollRef}>
120
- <SBody render={Box}>{columnsInitialized ? this.renderRows(rows) : null}</SBody>
237
+ <SBody render={Box}>
238
+ {holdHeight && <SHeightHold hMin={holdHeight} aria-hidden={true} />}
239
+ {columnsInitialized && !virtualScroll ? this.renderRows(rows) : null}
240
+ {columnsInitialized && virtualScroll ? this.renderVirtualizedRows(rows) : null}
241
+ </SBody>
121
242
  </ScrollArea.Container>
122
243
  <SScrollAreaBar
123
244
  orientation="horizontal"
package/src/DataTable.tsx CHANGED
@@ -36,6 +36,7 @@ type AsProps = {
36
36
  use: 'primary' | 'secondary';
37
37
  sort: SortDirection[];
38
38
  data: RowData[];
39
+ uniqueKey: string;
39
40
  };
40
41
 
41
42
  type HeadAsProps = {
@@ -79,6 +80,10 @@ export interface IDataTableProps extends IBoxProps {
79
80
  sort?: DataTableSort;
80
81
  /** Handler call when will request change sort */
81
82
  onSortChange?: (sort: DataTableSort, e?: React.SyntheticEvent) => void;
83
+ /** Field name in one data entity that is unique accross all set of data
84
+ * @default id
85
+ */
86
+ uniqueKey?: string;
82
87
  }
83
88
 
84
89
  export interface IDataTableHeadProps extends IBoxProps {
@@ -106,6 +111,12 @@ export interface IDataTableColumnProps extends IFlexProps {
106
111
  export interface IDataTableBodyProps extends IBoxProps {
107
112
  /** Rows table */
108
113
  rows?: DataTableRow[];
114
+ /** When enabled, only visually acessable rows are rendered.
115
+ * `tollerance` property controls how many rows outside of viewport are render.
116
+ * `rowHeight` fixes the rows height if it known. If not provided, first row node height is measured.
117
+ * @default { tollerance: 2 }
118
+ */
119
+ virtualScroll?: boolean | { tollerance?: number; rowHeight?: number };
109
120
  }
110
121
 
111
122
  export interface IDataTableRowProps extends IBoxProps {
@@ -129,6 +140,7 @@ class RootDefinitionTable extends Component<AsProps> {
129
140
 
130
141
  static defaultProps = {
131
142
  use: 'primary',
143
+ uniqueKey: 'id',
132
144
  sort: [],
133
145
  data: [],
134
146
  } as AsProps;
@@ -260,7 +272,7 @@ class RootDefinitionTable extends Component<AsProps> {
260
272
  }
261
273
 
262
274
  getBodyProps(props: BodyAsProps) {
263
- const { data, use } = this.asProps;
275
+ const { data, use, uniqueKey } = this.asProps;
264
276
 
265
277
  const cellPropsLayers: { [columnName: string]: PropsLayer[] } = {};
266
278
  const rowPropsLayers: PropsLayer[] = [];
@@ -289,13 +301,16 @@ class RootDefinitionTable extends Component<AsProps> {
289
301
  }
290
302
  });
291
303
 
292
- return {
304
+ const result = {
293
305
  columns: this.columns,
294
306
  rows: this.dataToRows(data, cellPropsLayers),
307
+ uniqueKey,
295
308
  use,
296
309
  rowPropsLayers,
297
310
  $scrollRef: this.scrollBodyRef,
298
311
  };
312
+
313
+ return result;
299
314
  }
300
315
 
301
316
  dataToRows(data: RowData[], cellPropsLayers: { [columnName: string]: PropsLayer[] }) {
@@ -209,6 +209,10 @@ SRow[theme='danger']:hover SCell:not([theme]) {
209
209
  background-color: color-mod(var(--red) blend(#fff 85%));
210
210
  }
211
211
 
212
+ SRow[positioned] {
213
+ position: absolute;
214
+ }
215
+
212
216
  SCell {
213
217
  display: flex;
214
218
  flex: 1;
@@ -275,3 +279,10 @@ SScrollAreaBar[orientation='horizontal'] {
275
279
  margin-right: calc(var(--right) + 4px);
276
280
  width: calc(100% - var(--offsetSum) - 8px);
277
281
  }
282
+
283
+ SHeightHold {
284
+ position: absolute;
285
+ top: 0;
286
+ width: 100px;
287
+ /* pointer-events: none; */
288
+ }
package/src/types.ts CHANGED
@@ -33,7 +33,6 @@ export type Column<
33
33
  style: React.CSSProperties;
34
34
  fixed: 'left' | 'right';
35
35
  children: React.ReactNode[];
36
-
37
36
  resizable: boolean;
38
37
  sortable: boolean | SortDirection;
39
38
  sortDirection: SortDirection;