• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SAP / ui5-webcomponents-react / 7209022170

14 Dec 2023 12:43PM CUT coverage: 87.974% (+0.05%) from 87.923%
7209022170

Pull #5349

github

web-flow
Merge 5b807e900 into 30ab28ac5
Pull Request #5349: docs(cypress-commands): improve `clickUi5SelectOption` commands description

2895 of 3854 branches covered (0.0%)

5201 of 5912 relevant lines covered (87.97%)

24077.59 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.11
/packages/main/src/components/AnalyticalTable/index.tsx
1
'use client';
2

3
import { useVirtualizer } from '@tanstack/react-virtual';
4
import {
5
  debounce,
6
  deprecationNotice,
7
  enrichEventWithDetails,
8
  useI18nBundle,
9
  useIsomorphicId,
10
  useIsomorphicLayoutEffect,
11
  useIsRTL,
12
  useSyncRef
13
} from '@ui5/webcomponents-react-base';
14
import { clsx } from 'clsx';
15
import type { CSSProperties, MutableRefObject } from 'react';
16
import React, { forwardRef, useCallback, useEffect, useMemo, useRef } from 'react';
17
import { createUseStyles } from 'react-jss';
18
import {
19
  useColumnOrder,
20
  useExpanded,
21
  useFilters,
22
  useGlobalFilter,
23
  useGroupBy,
24
  useResizeColumns,
25
  useRowSelect,
26
  useSortBy,
27
  useTable
28
} from 'react-table';
29
import {
30
  AnalyticalTableScaleWidthMode,
31
  AnalyticalTableSelectionBehavior,
32
  AnalyticalTableSelectionMode,
33
  AnalyticalTableSubComponentsBehavior,
34
  AnalyticalTableVisibleRowCountMode,
35
  GlobalStyleClasses
36
} from '../../enums/index.js';
37
import {
38
  COLLAPSE_NODE,
39
  COLLAPSE_PRESS_SPACE,
40
  DESELECT_ALL,
41
  EXPAND_NODE,
42
  EXPAND_PRESS_SPACE,
43
  FILTERED,
44
  GROUPED,
45
  INVALID_TABLE,
46
  SELECT_ALL,
47
  SELECT_PRESS_SPACE,
48
  UNSELECT_PRESS_SPACE
49
} from '../../i18n/i18n-defaults.js';
50
import { FlexBox } from '../FlexBox/index.js';
51
import { Text } from '../Text/index.js';
52
import styles from './AnayticalTable.jss.js';
53
import { ColumnHeaderContainer } from './ColumnHeader/ColumnHeaderContainer.js';
54
import { DefaultColumn } from './defaults/Column/index.js';
55
import { DefaultLoadingComponent } from './defaults/LoadingComponent/index.js';
56
import { TablePlaceholder } from './defaults/LoadingComponent/TablePlaceholder.js';
57
import { DefaultNoDataComponent } from './defaults/NoDataComponent/index.js';
58
import { useA11y } from './hooks/useA11y.js';
59
import { useColumnDragAndDrop } from './hooks/useDragAndDrop.js';
60
import { useDynamicColumnWidths } from './hooks/useDynamicColumnWidths.js';
61
import { useKeyboardNavigation } from './hooks/useKeyboardNavigation.js';
62
import { usePopIn } from './hooks/usePopIn.js';
63
import { useResizeColumnsConfig } from './hooks/useResizeColumnsConfig.js';
64
import { useRowHighlight } from './hooks/useRowHighlight.js';
65
import { useRowNavigationIndicators } from './hooks/useRowNavigationIndicator.js';
66
import { useRowSelectionColumn } from './hooks/useRowSelectionColumn.js';
67
import { useSelectionChangeCallback } from './hooks/useSelectionChangeCallback.js';
68
import { useSingleRowStateSelection } from './hooks/useSingleRowStateSelection.js';
69
import { useStyling } from './hooks/useStyling.js';
70
import { useTableScrollHandles } from './hooks/useTableScrollHandles.js';
71
import { useToggleRowExpand } from './hooks/useToggleRowExpand.js';
72
import { useVisibleColumnsWidth } from './hooks/useVisibleColumnsWidth.js';
73
import { VerticalScrollbar } from './scrollbars/VerticalScrollbar.js';
74
import { VirtualTableBody } from './TableBody/VirtualTableBody.js';
75
import { VirtualTableBodyContainer } from './TableBody/VirtualTableBodyContainer.js';
76
import { stateReducer } from './tableReducer/stateReducer.js';
77
import { TitleBar } from './TitleBar/index.js';
78
import type {
79
  AnalyticalTableColumnDefinition,
80
  AnalyticalTableDomRef,
81
  AnalyticalTablePropTypes,
82
  AnalyticalTableState,
83
  DivWithCustomScrollProp
84
} from './types/index.js';
85
import { getRowHeight, getSubRowsByString, tagNamesWhichShouldNotSelectARow } from './util/index.js';
86
import { VerticalResizer } from './VerticalResizer.js';
87

88
// When a sorted column is removed from the visible columns array (e.g. when "popped-in"), it doesn't clean up the sorted columns leading to an undefined `sortType`.
89
const sortTypesFallback = {
421✔
90
  undefined: () => undefined
×
91
};
92

93
const useStyles = createUseStyles(styles, { name: 'AnalyticalTable' });
421✔
94
/**
95
 * The `AnalyticalTable` provides a set of convenient functions for responsive table design, including virtualization of rows and columns, infinite scrolling and customizable columns that will, unless otherwise defined, distribute the available space equally among themselves.
96
 * It also provides several possibilities for working with the data, including sorting, filtering, grouping and aggregation.
97
 */
98
const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTypes>((props, ref) => {
421✔
99
  const {
100
    alternateRowColor,
101
    adjustTableHeightOnPopIn,
102
    className,
103
    columnOrder,
104
    columns,
105
    data: rawData,
106
    extension,
107
    filterable,
108
    globalFilterValue,
109
    groupBy,
110
    groupable,
111
    header,
112
    headerRowHeight,
113
    highlightField,
114
    infiniteScroll,
115
    infiniteScrollThreshold,
116
    isTreeTable,
117
    loading,
118
    markNavigatedRow,
119
    minRows,
120
    noDataText,
121
    overscanCount,
122
    overscanCountHorizontal,
123
    portalContainer,
124
    retainColumnWidth,
125
    reactTableOptions,
126
    renderRowSubComponent,
127
    rowHeight,
128
    scaleWidthMode,
129
    scaleXFactor,
130
    selectedRowIds,
131
    selectionBehavior,
132
    selectionMode,
133
    showOverlay,
134
    sortable,
135
    style,
136
    subComponentsBehavior,
137
    subRowsKey,
138
    tableHooks,
139
    tableInstance,
140
    visibleRowCountMode,
141
    visibleRows,
142
    withNavigationHighlight,
143
    withRowHighlight,
144
    onColumnsReorder,
145
    onGroup,
146
    onLoadMore,
147
    onRowClick,
148
    onRowExpandChange,
149
    onRowSelect,
150
    onSort,
151
    onTableScroll,
152
    LoadingComponent,
153
    NoDataComponent,
154
    alwaysShowSubComponent: _omit,
155
    ...rest
156
  } = props;
35,482✔
157

158
  useEffect(() => {
35,446✔
159
    if (props.alwaysShowSubComponent != undefined) {
1,981!
160
      deprecationNotice(
×
161
        'alwaysShowSubComponent',
162
        '`alwaysShowSubComponent` is deprecated. Please use `subComponentsBehavior` instead!'
163
      );
164
    }
165
  }, [props.alwaysShowSubComponent]);
166

167
  const alwaysShowSubComponent =
168
    subComponentsBehavior === AnalyticalTableSubComponentsBehavior.Visible ||
35,446✔
169
    subComponentsBehavior === AnalyticalTableSubComponentsBehavior.IncludeHeight ||
170
    props.alwaysShowSubComponent;
171

172
  const uniqueId = useIsomorphicId();
35,446✔
173
  const i18nBundle = useI18nBundle('@ui5/webcomponents-react');
35,446✔
174
  const titleBarId = useRef(`titlebar-${uniqueId}`).current;
35,446✔
175
  const invalidTableTextId = useRef(`invalidTableText-${uniqueId}`).current;
35,446✔
176

177
  const classes = useStyles();
35,446✔
178

179
  const tableRef = useRef<DivWithCustomScrollProp>(null);
35,446✔
180
  const parentRef = useRef<DivWithCustomScrollProp>(null);
35,446✔
181
  const verticalScrollBarRef = useRef<DivWithCustomScrollProp>(null);
35,446✔
182

183
  const getSubRows = useCallback((row) => getSubRowsByString(subRowsKey, row) || [], [subRowsKey]);
423,101✔
184

185
  const invalidTableA11yText = i18nBundle.getText(INVALID_TABLE);
35,446✔
186
  const tableInstanceRef = useRef<Record<string, any>>(null);
35,446✔
187
  const scrollContainerRef = useRef<HTMLDivElement>(null);
35,446✔
188
  tableInstanceRef.current = useTable(
35,446✔
189
    {
190
      columns,
191
      data: rawData,
192
      defaultColumn: DefaultColumn,
193
      getSubRows,
194
      stateReducer,
195
      disableFilters: !filterable,
196
      disableSortBy: !sortable,
197
      disableGroupBy: isTreeTable || (!alwaysShowSubComponent && renderRowSubComponent) ? true : !groupable,
126,587✔
198
      selectSubRows: false,
199
      sortTypes: sortTypesFallback,
200
      webComponentsReactProperties: {
201
        translatableTexts: {
202
          selectAllText: i18nBundle.getText(SELECT_ALL),
203
          deselectAllText: i18nBundle.getText(DESELECT_ALL),
204
          expandA11yText: i18nBundle.getText(EXPAND_PRESS_SPACE),
205
          collapseA11yText: i18nBundle.getText(COLLAPSE_PRESS_SPACE),
206
          selectA11yText: i18nBundle.getText(SELECT_PRESS_SPACE),
207
          unselectA11yText: i18nBundle.getText(UNSELECT_PRESS_SPACE),
208
          expandNodeA11yText: i18nBundle.getText(EXPAND_NODE),
209
          collapseNodeA11yText: i18nBundle.getText(COLLAPSE_NODE),
210
          filteredA11yText: i18nBundle.getText(FILTERED),
211
          groupedA11yText: i18nBundle.getText(GROUPED)
212
        },
213
        tagNamesWhichShouldNotSelectARow,
214
        tableRef,
215
        selectionMode,
216
        selectionBehavior,
217
        classes,
218
        onRowSelect: onRowSelect,
219
        onRowClick,
220
        onRowExpandChange,
221
        isTreeTable,
222
        alternateRowColor,
223
        scaleWidthMode,
224
        loading,
225
        withRowHighlight,
226
        highlightField,
227
        withNavigationHighlight,
228
        markNavigatedRow,
229
        renderRowSubComponent,
230
        alwaysShowSubComponent,
231
        showOverlay,
232
        uniqueId,
233
        subRowsKey,
234
        onColumnsReorder
235
      },
236
      ...reactTableOptions
237
    },
238
    useFilters,
239
    useGlobalFilter,
240
    useColumnOrder,
241
    useGroupBy,
242
    useSortBy,
243
    useExpanded,
244
    useRowSelect,
245
    useResizeColumns,
246
    useResizeColumnsConfig,
247
    useRowSelectionColumn,
248
    useSingleRowStateSelection,
249
    useSelectionChangeCallback,
250
    useRowHighlight,
251
    useRowNavigationIndicators,
252
    useDynamicColumnWidths,
253
    useStyling,
254
    useToggleRowExpand,
255
    useA11y,
256
    usePopIn,
257
    useVisibleColumnsWidth,
258
    useKeyboardNavigation,
259
    useColumnDragAndDrop,
260
    ...tableHooks
261
  );
262

263
  const {
264
    getTableProps,
265
    headerGroups,
266
    rows,
267
    prepareRow,
268
    setColumnOrder,
269
    dispatch,
270
    totalColumnsWidth,
271
    visibleColumns,
272
    visibleColumnsWidth,
273
    setGroupBy,
274
    setGlobalFilter
275
  } = tableInstanceRef.current;
35,446✔
276
  const tableState: AnalyticalTableState = tableInstanceRef.current.state;
35,446✔
277
  const { triggerScroll } = tableState;
35,446✔
278

279
  const [componentRef, updatedRef] = useSyncRef<AnalyticalTableDomRef>(ref);
35,446✔
280
  //@ts-expect-error: types are compatible
281
  const isRtl = useIsRTL(updatedRef);
35,446✔
282

283
  const columnVirtualizer = useVirtualizer({
35,446✔
284
    count: visibleColumnsWidth.length,
285
    getScrollElement: () => tableRef.current,
24,542✔
286
    estimateSize: useCallback((index) => visibleColumnsWidth[index], [visibleColumnsWidth]),
25,666✔
287
    horizontal: true,
288
    overscan: isRtl ? Infinity : overscanCountHorizontal,
35,446✔
289
    indexAttribute: 'data-column-index',
290
    // necessary as otherwise values are rounded which leads to wrong total width calculation leading to unnecessary scrollbar
291
    measureElement: !scaleXFactor || scaleXFactor === 1 ? (el) => el.getBoundingClientRect().width : undefined
30,089!
292
  });
293
  const [analyticalTableRef, scrollToRef] = useTableScrollHandles(updatedRef, dispatch);
35,446✔
294

295
  if (parentRef.current) {
35,446✔
296
    scrollToRef.current = {
25,278✔
297
      ...scrollToRef.current,
298
      horizontalScrollToOffset: columnVirtualizer.scrollToOffset,
299
      horizontalScrollToIndex: columnVirtualizer.scrollToIndex
300
    };
301
  }
302
  useEffect(() => {
35,446✔
303
    if (triggerScroll && triggerScroll.direction === 'horizontal') {
1,993✔
304
      if (triggerScroll.type === 'offset') {
6✔
305
        columnVirtualizer.scrollToOffset(...triggerScroll.args);
3✔
306
      } else {
307
        columnVirtualizer.scrollToIndex(...triggerScroll.args);
3✔
308
      }
309
    }
310
  }, [triggerScroll]);
311

312
  const includeSubCompRowHeight =
313
    !!renderRowSubComponent &&
35,446✔
314
    (subComponentsBehavior === AnalyticalTableSubComponentsBehavior.IncludeHeight ||
315
      subComponentsBehavior === AnalyticalTableSubComponentsBehavior.IncludeHeightExpandable) &&
316
    !!tableState.subComponentsHeight &&
317
    !!Object.keys(tableState.subComponentsHeight);
318

319
  if (tableInstance && {}.hasOwnProperty.call(tableInstance, 'current')) {
35,446✔
320
    (tableInstance as MutableRefObject<Record<string, any>>).current = tableInstanceRef.current;
2,053✔
321
  }
322
  if (typeof tableInstance === 'function') {
35,446!
323
    tableInstance(tableInstanceRef.current);
×
324
  }
325

326
  const titleBarRef = useRef(null);
35,446✔
327
  const extensionRef = useRef(null);
35,446✔
328
  const headerRef = useRef(null);
35,446✔
329

330
  const extensionsHeight =
35,446✔
331
    (titleBarRef.current?.offsetHeight ?? 0) +
68,685✔
332
    (extensionRef.current?.offsetHeight ?? 0) +
70,892✔
333
    (headerRef.current?.offsetHeight ?? 0);
45,580✔
334

335
  const internalRowHeight = getRowHeight(rowHeight, tableRef);
35,446✔
336
  const internalHeaderRowHeight = headerRowHeight ?? internalRowHeight;
35,446✔
337
  const popInRowHeight =
338
    tableState?.popInColumns?.length > 0
35,446✔
339
      ? internalRowHeight + tableState.popInColumns.length * (internalRowHeight + 16)
340
      : internalRowHeight;
341

342
  const internalVisibleRowCount = tableState.visibleRows ?? visibleRows;
35,446✔
343

344
  const updateTableClientWidth = useCallback(() => {
35,446✔
345
    if (tableRef.current) {
6,381✔
346
      dispatch({ type: 'TABLE_RESIZE', payload: { tableClientWidth: tableRef.current.clientWidth } });
6,381✔
347
    }
348
  }, [tableRef.current]);
349

350
  const updateRowsCount = useCallback(() => {
35,446✔
351
    if (visibleRowCountMode === AnalyticalTableVisibleRowCountMode.Auto && analyticalTableRef.current?.parentElement) {
11,188✔
352
      const parentElement = analyticalTableRef.current?.parentElement;
559✔
353
      const tableYPosition =
354
        parentElement &&
559!
355
        getComputedStyle(parentElement).position === 'relative' &&
356
        analyticalTableRef.current?.offsetTop
357
          ? analyticalTableRef.current?.offsetTop
358
          : 0;
359
      const parentHeight = parentElement?.getBoundingClientRect().height;
559✔
360
      const tableHeight = parentHeight ? parentHeight - tableYPosition : 0;
559!
361
      const bodyHeight = tableHeight - extensionsHeight;
559✔
362
      let subCompsRowCount = 0;
559✔
363
      if (includeSubCompRowHeight) {
559!
364
        let localBodyHeight = 0;
×
365
        let i = 0;
×
366
        while (localBodyHeight < bodyHeight) {
×
367
          if (tableState.subComponentsHeight[i]) {
×
368
            localBodyHeight += tableState.subComponentsHeight[i].subComponentHeight + popInRowHeight;
×
369
          } else if (rows[i]) {
×
370
            localBodyHeight += popInRowHeight;
×
371
          } else {
372
            break;
×
373
          }
374
          if (localBodyHeight >= bodyHeight) {
×
375
            break;
×
376
          }
377
          subCompsRowCount++;
×
378
          i++;
×
379
        }
380
        dispatch({
×
381
          type: 'VISIBLE_ROWS',
382
          payload: { visibleRows: Math.max(1, subCompsRowCount) }
383
        });
384
      } else {
385
        const rowCount = Math.max(1, Math.floor(bodyHeight / popInRowHeight));
559✔
386
        dispatch({
559✔
387
          type: 'VISIBLE_ROWS',
388
          payload: { visibleRows: rowCount }
389
        });
390
      }
391
    }
392
  }, [
393
    analyticalTableRef.current?.parentElement?.getBoundingClientRect().height,
394
    analyticalTableRef.current?.getBoundingClientRect().y,
395
    extensionsHeight,
396
    popInRowHeight,
397
    visibleRowCountMode,
398
    includeSubCompRowHeight,
399
    tableState.subComponentsHeight
400
  ]);
401

402
  useEffect(() => {
35,446✔
403
    setGlobalFilter(globalFilterValue);
2,580✔
404
  }, [globalFilterValue, setGlobalFilter]);
405

406
  useEffect(() => {
35,446✔
407
    const debouncedWidthObserverFn = debounce(updateTableClientWidth, 60);
8,839✔
408
    const tableWidthObserver = new ResizeObserver(debouncedWidthObserverFn);
8,839✔
409
    tableWidthObserver.observe(tableRef.current);
8,839✔
410

411
    const debouncedHeightObserverFn = debounce(updateRowsCount, 60);
8,839✔
412
    const parentHeightObserver = new ResizeObserver(debouncedHeightObserverFn);
8,839✔
413
    if (analyticalTableRef.current?.parentElement) {
8,839✔
414
      parentHeightObserver.observe(analyticalTableRef.current?.parentElement);
8,839✔
415
    }
416
    return () => {
8,839✔
417
      debouncedHeightObserverFn.cancel();
8,795✔
418
      debouncedWidthObserverFn.cancel();
8,795✔
419
      tableWidthObserver.disconnect();
8,795✔
420
      parentHeightObserver.disconnect();
8,795✔
421
    };
422
  }, [updateTableClientWidth, updateRowsCount]);
423

424
  useIsomorphicLayoutEffect(() => {
35,446✔
425
    dispatch({ type: 'IS_RTL', payload: { isRtl } });
2,089✔
426
  }, [isRtl]);
427

428
  useIsomorphicLayoutEffect(() => {
35,446✔
429
    updateTableClientWidth();
3,962✔
430
  }, [updateTableClientWidth]);
431

432
  useIsomorphicLayoutEffect(() => {
35,446✔
433
    updateRowsCount();
8,839✔
434
  }, [updateRowsCount]);
435

436
  useEffect(() => {
35,446✔
437
    if (tableState.visibleRows !== undefined && visibleRowCountMode === AnalyticalTableVisibleRowCountMode.Fixed) {
2,411!
438
      dispatch({
×
439
        type: 'VISIBLE_ROWS',
440
        payload: { visibleRows: undefined }
441
      });
442
    }
443
  }, [visibleRowCountMode, tableState.visibleRows]);
444

445
  useEffect(() => {
35,446✔
446
    setGroupBy(groupBy);
1,981✔
447
  }, [groupBy, setGroupBy]);
448

449
  useEffect(() => {
35,446✔
450
    dispatch({ type: 'SET_SELECTED_ROW_IDS', payload: { selectedRowIds } });
2,020✔
451
  }, [selectedRowIds]);
452

453
  useEffect(() => {
35,446✔
454
    if (tableState?.interactiveRowsHavePopIn && (!tableState?.popInColumns || tableState?.popInColumns?.length === 0)) {
2,122!
455
      dispatch({ type: 'WITH_POPIN', payload: false });
×
456
    }
457
  }, [tableState?.interactiveRowsHavePopIn, tableState?.popInColumns?.length]);
458

459
  const tableBodyHeight = useMemo(() => {
35,446✔
460
    if (typeof tableState.bodyHeight === 'number') {
4,302✔
461
      return tableState.bodyHeight;
4✔
462
    }
463
    const rowNum = rows.length < internalVisibleRowCount ? Math.max(rows.length, minRows) : internalVisibleRowCount;
4,298✔
464

465
    const rowHeight =
466
      visibleRowCountMode === AnalyticalTableVisibleRowCountMode.Auto ||
4,298✔
467
      tableState.interactiveRowsHavePopIn ||
468
      adjustTableHeightOnPopIn
469
        ? popInRowHeight
470
        : internalRowHeight;
471
    if (includeSubCompRowHeight) {
4,298✔
472
      let initialBodyHeightWithSubComps = 0;
68✔
473
      for (let i = 0; i < rowNum; i++) {
68✔
474
        if (tableState.subComponentsHeight[i]) {
204✔
475
          initialBodyHeightWithSubComps += tableState.subComponentsHeight[i].subComponentHeight + rowHeight;
68✔
476
        } else if (rows[i]) {
136✔
477
          initialBodyHeightWithSubComps += rowHeight;
136✔
478
        }
479
      }
480
      return initialBodyHeightWithSubComps;
68✔
481
    }
482
    return rowHeight * rowNum;
4,230✔
483
  }, [
484
    internalRowHeight,
485
    rows.length,
486
    internalVisibleRowCount,
487
    minRows,
488
    popInRowHeight,
489
    visibleRowCountMode,
490
    tableState.interactiveRowsHavePopIn,
491
    adjustTableHeightOnPopIn,
492
    includeSubCompRowHeight,
493
    tableState.subComponentsHeight,
494
    tableState.bodyHeight
495
  ]);
496

497
  // scroll bar detection
498
  useEffect(() => {
35,446✔
499
    const visibleRowCount =
500
      rows.length < internalVisibleRowCount ? Math.max(rows.length, minRows) : internalVisibleRowCount;
4,036✔
501
    if (popInRowHeight !== internalRowHeight) {
4,036✔
502
      dispatch({
94✔
503
        type: 'TABLE_SCROLLING_ENABLED',
504
        payload: { isScrollable: visibleRowCount * popInRowHeight > tableBodyHeight || rows.length > visibleRowCount }
124✔
505
      });
506
    } else {
507
      dispatch({ type: 'TABLE_SCROLLING_ENABLED', payload: { isScrollable: rows.length > visibleRowCount } });
3,942✔
508
    }
509
  }, [rows.length, minRows, internalVisibleRowCount, popInRowHeight, tableBodyHeight]);
510

511
  const noDataStyles = {
35,446✔
512
    height: `${tableBodyHeight}px`,
513
    width: totalColumnsWidth ? `${totalColumnsWidth}px` : '100%'
35,446✔
514
  };
515

516
  const onGroupByChanged = useCallback(
35,446✔
517
    (e) => {
518
      const { column, isGrouped } = e.detail;
76✔
519
      let groupedColumns;
520
      if (isGrouped) {
76✔
521
        groupedColumns = [...tableState.groupBy, column.id];
65✔
522
      } else {
523
        groupedColumns = tableState.groupBy.filter((group) => group !== column.id);
11✔
524
      }
525
      setGroupBy(groupedColumns);
76✔
526
      onGroup(
76✔
527
        enrichEventWithDetails(e, {
528
          column,
529
          groupedColumns
530
        })
531
      );
532
    },
533
    [tableState.groupBy, onGroup, setGroupBy]
534
  );
535

536
  useEffect(() => {
35,446✔
537
    if (columnOrder?.length > 0) {
1,981✔
538
      setColumnOrder(columnOrder);
52✔
539
    }
540
  }, [columnOrder]);
541

542
  const inlineStyle = useMemo(() => {
35,446✔
543
    const tableStyles = {
5,673✔
544
      maxWidth: '100%',
545
      overflowX: 'auto',
546
      display: 'flex',
547
      flexDirection: 'column'
548
    };
549
    if (!!rowHeight) {
5,673✔
550
      tableStyles['--_ui5wcr-AnalyticalTableRowHeight'] = `${rowHeight}px`;
266✔
551
      tableStyles['--_ui5wcr-AnalyticalTableHeaderRowHeight'] = `${rowHeight}px`;
266✔
552
    }
553
    if (!!headerRowHeight) {
5,673✔
554
      tableStyles['--_ui5wcr-AnalyticalTableHeaderRowHeight'] = `${headerRowHeight}px`;
114✔
555
    }
556

557
    if (tableState.tableClientWidth > 0) {
5,673✔
558
      return {
3,692✔
559
        ...tableStyles,
560
        ...style
561
      } as CSSProperties;
562
    }
563
    return {
1,981✔
564
      ...tableStyles,
565
      ...style,
566
      visibility: 'hidden'
567
    } as CSSProperties;
568
  }, [tableState.tableClientWidth, style, rowHeight, headerRowHeight]);
569

570
  useEffect(() => {
35,446✔
571
    if (retainColumnWidth && tableState.columnResizing?.isResizingColumn && tableState.tableColResized == null) {
7,908!
572
      dispatch({ type: 'TABLE_COL_RESIZED', payload: true });
×
573
    }
574
    if (tableState.tableColResized && !retainColumnWidth) {
7,908!
575
      dispatch({ type: 'TABLE_COL_RESIZED', payload: undefined });
×
576
    }
577
  }, [tableState.columnResizing, retainColumnWidth, tableState.tableColResized]);
578

579
  const handleBodyScroll = (e) => {
35,446✔
580
    if (typeof onTableScroll === 'function') {
482✔
581
      onTableScroll(e);
84✔
582
    }
583
    const targetScrollTop = e.currentTarget.scrollTop;
482✔
584
    if (verticalScrollBarRef.current && verticalScrollBarRef.current.scrollTop !== targetScrollTop) {
482✔
585
      if (!e.currentTarget.isExternalVerticalScroll) {
482✔
586
        verticalScrollBarRef.current.scrollTop = targetScrollTop;
482✔
587
        verticalScrollBarRef.current.isExternalVerticalScroll = true;
482✔
588
      }
589
      e.currentTarget.isExternalVerticalScroll = false;
482✔
590
    }
591
  };
592

593
  const handleVerticalScrollBarScroll = useCallback((e) => {
35,446✔
594
    if (parentRef.current && !e.currentTarget.isExternalVerticalScroll) {
433!
595
      parentRef.current.scrollTop = e.currentTarget.scrollTop;
×
596
      parentRef.current.isExternalVerticalScroll = true;
×
597
    }
598
    e.currentTarget.isExternalVerticalScroll = false;
433✔
599
  }, []);
600

601
  useEffect(() => {
35,446✔
602
    columnVirtualizer.measure();
2,083✔
603
  }, [columnVirtualizer, tableState.columnOrder, tableState.columnResizing?.isResizingColumn]);
604

605
  const totalSize = columnVirtualizer.getTotalSize();
35,446✔
606
  const showVerticalEndBorder = tableState.tableClientWidth > totalSize;
35,446✔
607

608
  const tableClasses = clsx(
35,446✔
609
    classes.table,
610
    GlobalStyleClasses.sapScrollBar,
611
    withNavigationHighlight && classes.hasNavigationIndicator,
37,596✔
612
    showVerticalEndBorder && classes.showVerticalEndBorder
41,819✔
613
  );
614

615
  const handleOnLoadMore = (e) => {
35,446✔
616
    const rootNodes = rows.filter((row) => row.depth === 0);
16,018✔
617
    onLoadMore(
200✔
618
      enrichEventWithDetails(e, {
619
        rowCount: rootNodes.length,
620
        totalRowCount: rows.length
621
      })
622
    );
623
  };
624

625
  return (
35,446✔
626
    <>
627
      <div
628
        className={className}
629
        style={inlineStyle}
630
        //@ts-expect-error: types are compatible
631
        ref={componentRef}
632
        {...rest}
633
      >
634
        {header && (
38,403✔
635
          <TitleBar ref={titleBarRef} titleBarId={titleBarId}>
636
            {header}
637
          </TitleBar>
638
        )}
639
        {extension && <div ref={extensionRef}>{extension}</div>}
35,446!
640
        <FlexBox
641
          className={classes.tableContainerWithScrollBar}
642
          data-component-name="AnalyticalTableContainerWithScrollbar"
643
        >
644
          {showOverlay && (
35,662✔
645
            <>
646
              <span id={invalidTableTextId} className={classes.hiddenA11yText} aria-hidden>
647
                {invalidTableA11yText}
648
              </span>
649
              <div
650
                tabIndex={0}
651
                aria-labelledby={`${titleBarId} ${invalidTableTextId}`}
652
                role="region"
653
                data-component-name="AnalyticalTableOverlay"
654
                className={classes.overlay}
655
              />
656
            </>
657
          )}
658
          <div
659
            aria-labelledby={titleBarId}
660
            {...getTableProps()}
661
            tabIndex={showOverlay ? -1 : 0}
35,446✔
662
            role="grid"
663
            aria-rowcount={rows.length}
664
            aria-colcount={visibleColumns.length}
665
            data-per-page={internalVisibleRowCount}
666
            data-component-name="AnalyticalTableContainer"
667
            ref={tableRef}
668
            data-native-scrollbar={props['data-native-scrollbar']}
669
            className={tableClasses}
670
          >
671
            <div className={classes.tableHeaderBackgroundElement} />
672
            <div className={classes.tableBodyBackgroundElement} />
673
            {headerGroups.map((headerGroup) => {
674
              let headerProps: Record<string, unknown> = {};
35,182✔
675
              if (headerGroup.getHeaderGroupProps) {
35,182✔
676
                headerProps = headerGroup.getHeaderGroupProps();
35,182✔
677
              }
678
              return (
35,182✔
679
                tableRef.current && (
64,520✔
680
                  <ColumnHeaderContainer
681
                    ref={headerRef}
682
                    key={headerProps.key as string}
683
                    resizeInfo={tableState.columnResizing}
684
                    headerProps={headerProps}
685
                    headerGroup={headerGroup}
686
                    onSort={onSort}
687
                    onGroupByChanged={onGroupByChanged}
688
                    isRtl={isRtl}
689
                    portalContainer={portalContainer}
690
                    columnVirtualizer={columnVirtualizer}
691
                    uniqueId={uniqueId}
692
                    showVerticalEndBorder={showVerticalEndBorder}
693
                  />
694
                )
695
              );
696
            })}
697
            {loading && rawData?.length > 0 && <LoadingComponent style={{ width: `${totalColumnsWidth}px` }} />}
36,286✔
698
            {loading && rawData?.length === 0 && (
36,118✔
699
              <TablePlaceholder columns={visibleColumns} rows={minRows} style={noDataStyles} />
700
            )}
701
            {!loading && rawData?.length === 0 && (
70,792✔
702
              <NoDataComponent noDataText={noDataText} className={classes.noDataContainer} style={noDataStyles} />
703
            )}
704
            {rawData?.length > 0 && tableRef.current && (
99,455✔
705
              <VirtualTableBodyContainer
706
                rowCollapsedFlag={tableState.rowCollapsed}
707
                dispatch={dispatch}
708
                tableBodyHeight={tableBodyHeight}
709
                totalColumnsWidth={columnVirtualizer.getTotalSize()}
710
                parentRef={parentRef}
711
                classes={classes}
712
                infiniteScroll={infiniteScroll}
713
                infiniteScrollThreshold={infiniteScrollThreshold}
714
                onLoadMore={handleOnLoadMore}
715
                internalRowHeight={internalRowHeight}
716
                popInRowHeight={popInRowHeight}
717
                rows={rows}
718
                handleExternalScroll={handleBodyScroll}
719
                visibleRows={internalVisibleRowCount}
720
              >
721
                <VirtualTableBody
722
                  scrollContainerRef={scrollContainerRef}
723
                  classes={classes}
724
                  prepareRow={prepareRow}
725
                  rows={rows}
726
                  minRows={minRows}
727
                  scrollToRef={scrollToRef}
728
                  isTreeTable={isTreeTable}
729
                  internalRowHeight={internalRowHeight}
730
                  popInRowHeight={popInRowHeight}
731
                  visibleRows={internalVisibleRowCount}
732
                  alternateRowColor={alternateRowColor}
733
                  overscanCount={overscanCount}
734
                  parentRef={parentRef}
735
                  visibleColumns={visibleColumns}
736
                  renderRowSubComponent={renderRowSubComponent}
737
                  alwaysShowSubComponent={alwaysShowSubComponent}
738
                  markNavigatedRow={markNavigatedRow}
739
                  isRtl={isRtl}
740
                  subComponentsHeight={tableState.subComponentsHeight}
741
                  dispatch={dispatch}
742
                  columnVirtualizer={columnVirtualizer}
743
                  manualGroupBy={reactTableOptions?.manualGroupBy as boolean | undefined}
744
                  subRowsKey={subRowsKey}
745
                  subComponentsBehavior={subComponentsBehavior}
746
                  triggerScroll={tableState.triggerScroll}
747
                />
748
              </VirtualTableBodyContainer>
749
            )}
750
          </div>
77,014✔
751
          {(tableState.isScrollable === undefined || tableState.isScrollable) && (
752
            <VerticalScrollbar
753
              tableBodyHeight={tableBodyHeight}
754
              internalRowHeight={internalHeaderRowHeight}
755
              tableRef={tableRef}
756
              handleVerticalScrollBarScroll={handleVerticalScrollBarScroll}
757
              ref={verticalScrollBarRef}
758
              data-native-scrollbar={props['data-native-scrollbar']}
759
              scrollContainerRef={scrollContainerRef}
760
              parentRef={parentRef}
761
            />
762
          )}
763
        </FlexBox>
764
        {visibleRowCountMode === AnalyticalTableVisibleRowCountMode.Interactive && (
36,047✔
765
          <VerticalResizer
766
            popInRowHeight={popInRowHeight}
767
            hasPopInColumns={tableState?.popInColumns?.length > 0}
768
            analyticalTableRef={analyticalTableRef}
769
            dispatch={dispatch}
770
            extensionsHeight={extensionsHeight}
771
            internalRowHeight={internalRowHeight}
772
            portalContainer={portalContainer}
773
            rowsLength={rows.length}
774
            visibleRows={internalVisibleRowCount}
775
            handleOnLoadMore={handleOnLoadMore}
776
          />
777
        )}
778
      </div>
779
      <Text
780
        aria-hidden="true"
781
        id={`scaleModeHelper-${uniqueId}`}
782
        className={classes.hiddenSmartColMeasure}
783
        data-component-name="AnalyticalTableScaleModeHelper"
784
      >
785
        {''}
786
      </Text>
787
      <Text
788
        aria-hidden="true"
789
        id={`scaleModeHelperHeader-${uniqueId}`}
790
        className={clsx(classes.hiddenSmartColMeasure, classes.hiddenSmartColMeasureHeader)}
791
        data-component-name="AnalyticalTableScaleModeHelperHeader"
792
      >
793
        {''}
794
      </Text>
795
    </>
796
  );
797
});
798

799
AnalyticalTable.displayName = 'AnalyticalTable';
421✔
800
AnalyticalTable.defaultProps = {
421✔
801
  infiniteScrollThreshold: 20,
802
  loading: false,
803
  sortable: true,
804
  filterable: false,
805
  groupable: false,
806
  selectionMode: AnalyticalTableSelectionMode.None,
807
  selectionBehavior: AnalyticalTableSelectionBehavior.Row,
808
  scaleWidthMode: AnalyticalTableScaleWidthMode.Default,
809
  subComponentsBehavior: AnalyticalTableSubComponentsBehavior.Expandable,
810
  data: [],
811
  columns: [],
812
  minRows: 5,
813
  groupBy: [],
814
  NoDataComponent: DefaultNoDataComponent,
815
  LoadingComponent: DefaultLoadingComponent,
816
  noDataText: 'No Data',
817
  reactTableOptions: {},
818
  tableHooks: [],
819
  visibleRows: 15,
820
  subRowsKey: 'subRows',
821
  highlightField: 'status',
822
  markNavigatedRow: () => false,
172,933✔
823
  selectedRowIds: {},
824
  onGroup: () => {},
825
  onRowExpandChange: () => {},
826
  isTreeTable: false,
827
  alternateRowColor: false,
828
  overscanCountHorizontal: 5,
829
  visibleRowCountMode: AnalyticalTableVisibleRowCountMode.Fixed
830
};
831

832
export { AnalyticalTable };
833
export type {
834
  AnalyticalTableColumnDefinition,
835
  AnalyticalTableDomRef,
836
  AnalyticalTablePropTypes,
837
  DivWithCustomScrollProp
838
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc