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

SAP / ui5-webcomponents-react / 14902210870

08 May 2025 08:32AM CUT coverage: 88.206% (-0.4%) from 88.625%
14902210870

Pull #7308

github

web-flow
Merge 7ea0c9eb2 into 5b75245a7
Pull Request #7308: feat: update to UI5 Web Components 2.10.0

3016 of 3985 branches covered (75.68%)

1 of 1 new or added line in 1 file covered. (100.0%)

28 existing lines in 3 files now uncovered.

5295 of 6003 relevant lines covered (88.21%)

104231.16 hits per line

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

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

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

95
// 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`.
96
const sortTypesFallback = {
444✔
UNCOV
97
  undefined: () => undefined
×
98
};
99

100
const measureElement = (el: HTMLElement) => {
444✔
101
  return el.offsetHeight;
153,354✔
102
};
103

104
/**
105
 * 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.
106
 * It also provides several possibilities for working with the data, including sorting, filtering, grouping and aggregation.
107
 */
108
const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTypes>((props, ref) => {
444✔
109
  const {
110
    alternateRowColor,
111
    adjustTableHeightOnPopIn,
112
    className,
113
    columnOrder,
114
    columns,
115
    data: rawData,
116
    extension,
117
    filterable,
118
    globalFilterValue,
119
    groupBy,
120
    groupable,
121
    header,
122
    headerRowHeight,
123
    highlightField = 'status',
98,151✔
124
    infiniteScroll,
125
    infiniteScrollThreshold = 20,
89,800✔
126
    isTreeTable,
127
    loading,
128
    loadingDelay,
129
    markNavigatedRow,
130
    minRows = 5,
90,815✔
131
    noDataText,
132
    overscanCount,
133
    overscanCountHorizontal = 5,
98,903✔
134
    retainColumnWidth,
135
    reactTableOptions,
136
    renderRowSubComponent,
137
    rowHeight,
138
    scaleWidthMode = AnalyticalTableScaleWidthMode.Default,
97,880✔
139
    scaleXFactor,
140
    selectedRowIds,
141
    selectionBehavior = AnalyticalTableSelectionBehavior.Row,
96,235✔
142
    selectionMode = AnalyticalTableSelectionMode.None,
65,283✔
143
    showOverlay,
144
    sortable,
145
    style,
146
    subComponentsBehavior = AnalyticalTableSubComponentsBehavior.Expandable,
94,507✔
147
    subRowsKey = 'subRows',
98,415✔
148
    tableHooks = [],
89,857✔
149
    tableInstance,
150
    visibleRowCountMode = AnalyticalTableVisibleRowCountMode.Fixed,
90,949✔
151
    visibleRows = 15,
89,065✔
152
    withNavigationHighlight,
153
    withRowHighlight,
154
    onColumnsReorder,
155
    onGroup,
156
    onLoadMore,
157
    onRowClick,
158
    onRowExpandChange,
159
    onRowSelect,
160
    onSort,
161
    onTableScroll,
162
    onAutoResize,
163
    NoDataComponent = DefaultNoDataComponent,
98,903✔
164
    additionalEmptyRowsCount = 0,
97,195✔
165
    ...rest
166
  } = props;
98,903✔
167

168
  useStylesheet(styleData, AnalyticalTable.displayName);
98,903✔
169
  const isInitialized = useRef(false);
98,903✔
170

171
  const alwaysShowSubComponent =
172
    subComponentsBehavior === AnalyticalTableSubComponentsBehavior.Visible ||
98,903✔
173
    subComponentsBehavior === AnalyticalTableSubComponentsBehavior.IncludeHeight;
174

175
  const uniqueId = useId();
98,903✔
176
  const i18nBundle = useI18nBundle('@ui5/webcomponents-react');
98,903✔
177
  const titleBarId = useRef(`titlebar-${uniqueId}`).current;
98,903✔
178
  const invalidTableTextId = useRef(`invalidTableText-${uniqueId}`).current;
98,903✔
179

180
  const tableRef = useRef<DivWithCustomScrollProp>(null);
98,903✔
181
  const parentRef = useRef<DivWithCustomScrollProp>(null);
98,903✔
182
  const verticalScrollBarRef = useRef<DivWithCustomScrollProp>(null);
98,903✔
183

184
  const getSubRows = useCallback((row) => getSubRowsByString(subRowsKey, row) || [], [subRowsKey]);
1,411,444✔
185

186
  const invalidTableA11yText = i18nBundle.getText(INVALID_TABLE);
98,903✔
187
  const tableInstanceRef = useRef<TableInstance>(null);
98,903✔
188
  const scrollContainerRef = useRef<HTMLDivElement>(null);
98,903✔
189

190
  tableInstanceRef.current = useTable(
98,903✔
191
    {
192
      columns,
193
      data: rawData,
194
      defaultColumn: DefaultColumn,
195
      getSubRows,
196
      stateReducer,
197
      disableFilters: !filterable,
198
      disableSortBy: !sortable,
199
      disableGroupBy: isTreeTable || (!alwaysShowSubComponent && renderRowSubComponent) ? true : !groupable,
349,904✔
200
      selectSubRows: false,
201
      sortTypes: sortTypesFallback,
202
      webComponentsReactProperties: {
203
        translatableTexts: {
204
          selectAllText: i18nBundle.getText(SELECT_ALL),
205
          deselectAllText: i18nBundle.getText(DESELECT_ALL),
206
          expandA11yText: i18nBundle.getText(EXPAND_PRESS_SPACE),
207
          collapseA11yText: i18nBundle.getText(COLLAPSE_PRESS_SPACE),
208
          selectA11yText: i18nBundle.getText(SELECT_PRESS_SPACE),
209
          unselectA11yText: i18nBundle.getText(UNSELECT_PRESS_SPACE),
210
          expandNodeA11yText: i18nBundle.getText(EXPAND_NODE),
211
          collapseNodeA11yText: i18nBundle.getText(COLLAPSE_NODE),
212
          filteredA11yText: i18nBundle.getText(FILTERED),
213
          groupedA11yText: i18nBundle.getText(GROUPED),
214
          selectAllA11yText: i18nBundle.getText(SELECT_ALL_PRESS_SPACE),
215
          deselectAllA11yText: i18nBundle.getText(UNSELECT_ALL_PRESS_SPACE),
216
          rowExpandedAnnouncementText: i18nBundle.getText(ROW_EXPANDED),
217
          rowCollapsedAnnouncementText: i18nBundle.getText(ROW_COLLAPSED)
218
        },
219
        alternateRowColor,
220
        alwaysShowSubComponent,
221
        classes: classNames,
222
        highlightField,
223
        isTreeTable,
224
        loading,
225
        markNavigatedRow,
226
        renderRowSubComponent,
227
        scaleWidthMode,
228
        selectionBehavior,
229
        selectionMode,
230
        showOverlay,
231
        subRowsKey,
232
        tableRef,
233
        tagNamesWhichShouldNotSelectARow,
234
        uniqueId,
235
        withNavigationHighlight,
236
        withRowHighlight,
237
        onAutoResize,
238
        onColumnsReorder,
239
        onGroup,
240
        onRowClick,
241
        onRowExpandChange,
242
        onRowSelect: onRowSelect,
243
        onSort
244
      },
245
      ...reactTableOptions
246
    },
247
    useFilters,
248
    useGlobalFilter,
249
    useColumnOrder,
250
    useGroupBy,
251
    useSortBy,
252
    useExpanded,
253
    useRowSelect,
254
    useResizeColumns,
255
    useResizeColumnsConfig,
256
    useRowSelectionColumn,
257
    useAutoResize,
258
    useSingleRowStateSelection,
259
    useSelectionChangeCallback,
260
    useRowHighlight,
261
    useRowNavigationIndicators,
262
    useDynamicColumnWidths,
263
    useStyling,
264
    useToggleRowExpand,
265
    useA11y,
266
    usePopIn,
267
    useVisibleColumnsWidth,
268
    useKeyboardNavigation,
269
    useColumnDragAndDrop,
270
    ...tableHooks
271
  );
272

273
  const {
274
    getTableProps,
275
    headerGroups,
276
    rows,
277
    prepareRow,
278
    setColumnOrder,
279
    dispatch,
280
    totalColumnsWidth,
281
    visibleColumns,
282
    visibleColumnsWidth,
283
    setGroupBy,
284
    setGlobalFilter
285
  } = tableInstanceRef.current;
98,903✔
286

287
  const tableState: AnalyticalTableState = tableInstanceRef.current.state;
98,903✔
288
  const { popInColumns, triggerScroll } = tableState;
98,903✔
289
  const isGrouped = !!tableState.groupBy.length;
98,903✔
290

291
  const noDataTextI18n = i18nBundle.getText(LIST_NO_DATA);
98,903✔
292
  const noDataTextFiltered = i18nBundle.getText(NO_DATA_FILTERED);
98,903✔
293
  const noDataTextLocal =
294
    noDataText ?? (tableState.filters?.length > 0 || tableState.globalFilter ? noDataTextFiltered : noDataTextI18n);
98,903✔
295

296
  const [componentRef, updatedRef] = useSyncRef<AnalyticalTableDomRef>(ref);
98,903✔
297
  //@ts-expect-error: types are compatible
298
  const isRtl = useIsRTL(updatedRef);
98,903✔
299

300
  const columnVirtualizer = useVirtualizer({
98,903✔
301
    count: visibleColumnsWidth.length,
302
    getScrollElement: () => tableRef.current,
44,702✔
303
    estimateSize: useCallback((index) => visibleColumnsWidth[index], [visibleColumnsWidth]),
128,269✔
304
    horizontal: true,
305
    overscan: isRtl ? Infinity : overscanCountHorizontal,
98,903✔
306
    indexAttribute: 'data-column-index',
307
    // necessary as otherwise values are rounded which leads to wrong total width calculation leading to unnecessary scrollbar
308
    measureElement: !scaleXFactor || scaleXFactor === 1 ? (el) => el.getBoundingClientRect().width : undefined
63,927!
309
  });
310
  // force re-measure if `visibleColumns` change
311
  useEffect(() => {
98,903✔
312
    if (isInitialized.current && visibleColumns.length) {
8,208✔
313
      columnVirtualizer.measure();
4,523✔
314
    } else {
315
      isInitialized.current = true;
3,685✔
316
    }
317
  }, [visibleColumns.length]);
318
  // force re-measure if `state.groupBy` or `state.columnOrder` changes
319
  useEffect(() => {
98,903✔
320
    if (isInitialized.current && (tableState.groupBy || tableState.columnOrder)) {
11,587!
321
      setTimeout(() => {
11,587✔
322
        columnVirtualizer.measure();
11,526✔
323
      }, 100);
324
    } else {
UNCOV
325
      isInitialized.current = true;
×
326
    }
327
  }, [tableState.groupBy, tableState.columnOrder]);
328

329
  const [analyticalTableRef, scrollToRef] = useTableScrollHandles(updatedRef, dispatch);
98,903✔
330

331
  if (parentRef.current) {
98,903✔
332
    scrollToRef.current = {
71,774✔
333
      ...scrollToRef.current,
334
      horizontalScrollToOffset: columnVirtualizer.scrollToOffset,
335
      horizontalScrollToIndex: columnVirtualizer.scrollToIndex
336
    };
337
  }
338
  useEffect(() => {
98,903✔
339
    if (triggerScroll && triggerScroll.direction === 'horizontal') {
7,330✔
340
      if (triggerScroll.type === 'offset') {
20✔
341
        columnVirtualizer.scrollToOffset(...triggerScroll.args);
10✔
342
      } else {
343
        columnVirtualizer.scrollToIndex(...triggerScroll.args);
10✔
344
      }
345
    }
346
  }, [triggerScroll]);
347

348
  const includeSubCompRowHeight =
349
    !!renderRowSubComponent &&
98,903✔
350
    (subComponentsBehavior === AnalyticalTableSubComponentsBehavior.IncludeHeight ||
351
      subComponentsBehavior === AnalyticalTableSubComponentsBehavior.IncludeHeightExpandable) &&
352
    !!tableState.subComponentsHeight &&
353
    !!Object.keys(tableState.subComponentsHeight);
354

355
  if (tableInstance && {}.hasOwnProperty.call(tableInstance, 'current')) {
98,903✔
356
    (tableInstance as MutableRefObject<Record<string, any>>).current = tableInstanceRef.current;
4,641✔
357
  }
358
  if (typeof tableInstance === 'function') {
98,903!
UNCOV
359
    tableInstance(tableInstanceRef.current);
×
360
  }
361

362
  const titleBarRef = useRef(null);
98,903✔
363
  const extensionRef = useRef(null);
98,903✔
364
  const headerRef = useRef(null);
98,903✔
365

366
  const extensionsHeight =
98,903✔
367
    (titleBarRef.current?.offsetHeight ?? 0) +
193,284✔
368
    (extensionRef.current?.offsetHeight ?? 0) +
197,806✔
369
    (headerRef.current?.offsetHeight ?? 0);
125,017✔
370

371
  const internalRowHeight = getRowHeight(rowHeight, tableRef);
98,903✔
372
  const internalHeaderRowHeight = headerRowHeight ?? internalRowHeight;
98,903✔
373
  const popInRowHeight = (() => {
98,903✔
374
    if (popInColumns?.length) {
98,903✔
375
      return popInColumns.reduce(
3,038✔
376
        (acc, cur) =>
377
          cur.popinDisplay === AnalyticalTablePopinDisplay.Block
15,190✔
378
            ? acc + internalRowHeight + 16 // 16px for Header
379
            : acc + internalRowHeight,
380
        internalRowHeight
381
      );
382
    } else {
383
      return internalRowHeight;
95,865✔
384
    }
385
  })();
386

387
  const internalVisibleRowCount = tableState.visibleRows ?? visibleRows;
98,903✔
388

389
  const updateTableClientWidth = useCallback(() => {
98,903✔
390
    if (tableRef.current) {
14,568✔
391
      dispatch({
14,568✔
392
        type: 'TABLE_RESIZE',
393
        payload: {
394
          tableClientWidth:
395
            !scaleXFactor || scaleXFactor === 1
29,136!
396
              ? tableRef.current.getBoundingClientRect().width
397
              : tableRef.current.clientWidth
398
        }
399
      });
400
    }
401
  }, [tableRef.current, scaleXFactor]);
402

403
  const updateRowsCount = useCallback(() => {
98,903✔
404
    if (
22,382✔
405
      (visibleRowCountMode === AnalyticalTableVisibleRowCountMode.Auto ||
406
        visibleRowCountMode === AnalyticalTableVisibleRowCountMode.AutoWithEmptyRows) &&
407
      analyticalTableRef.current?.parentElement
408
    ) {
409
      const parentElement = analyticalTableRef.current?.parentElement;
1,664✔
410
      const tableYPosition =
411
        parentElement &&
1,664!
412
        getComputedStyle(parentElement).position === 'relative' &&
413
        analyticalTableRef.current?.offsetTop
414
          ? analyticalTableRef.current?.offsetTop
415
          : 0;
416
      const parentHeight = parentElement?.getBoundingClientRect().height;
1,664✔
417
      const tableHeight = parentHeight ? parentHeight - tableYPosition : 0;
1,664!
418
      const bodyHeight = tableHeight - extensionsHeight;
1,664✔
419
      let subCompsRowCount = 0;
1,664✔
420
      if (includeSubCompRowHeight) {
1,664!
421
        let localBodyHeight = 0;
×
422
        let i = 0;
×
423
        while (localBodyHeight < bodyHeight) {
×
424
          if (tableState.subComponentsHeight[i]) {
×
425
            localBodyHeight += tableState.subComponentsHeight[i].subComponentHeight + popInRowHeight;
×
426
          } else if (rows[i]) {
×
UNCOV
427
            localBodyHeight += popInRowHeight;
×
428
          } else {
UNCOV
429
            break;
×
430
          }
431
          if (localBodyHeight >= bodyHeight) {
×
UNCOV
432
            break;
×
433
          }
434
          subCompsRowCount++;
×
UNCOV
435
          i++;
×
436
        }
UNCOV
437
        dispatch({
×
438
          type: 'VISIBLE_ROWS',
439
          payload: { visibleRows: Math.max(1, subCompsRowCount) }
440
        });
441
      } else {
442
        const rowCount = Math.max(1, Math.floor(bodyHeight / popInRowHeight));
1,664✔
443
        dispatch({
1,664✔
444
          type: 'VISIBLE_ROWS',
445
          payload: { visibleRows: rowCount }
446
        });
447
      }
448
    }
449
  }, [
450
    analyticalTableRef.current?.parentElement?.getBoundingClientRect().height,
451
    analyticalTableRef.current?.getBoundingClientRect().y,
452
    extensionsHeight,
453
    popInRowHeight,
454
    visibleRowCountMode,
455
    includeSubCompRowHeight,
456
    tableState.subComponentsHeight
457
  ]);
458

459
  useEffect(() => {
98,903✔
460
    setGlobalFilter(globalFilterValue);
7,994✔
461
  }, [globalFilterValue, setGlobalFilter]);
462

463
  useEffect(() => {
98,903✔
464
    const debouncedWidthObserverFn = debounce(updateTableClientWidth, 60);
18,722✔
465
    const tableWidthObserver = new ResizeObserver(debouncedWidthObserverFn);
18,722✔
466
    tableWidthObserver.observe(tableRef.current);
18,722✔
467

468
    const debouncedHeightObserverFn = debounce(updateRowsCount, 60);
18,722✔
469
    const parentHeightObserver = new ResizeObserver(debouncedHeightObserverFn);
18,722✔
470
    if (analyticalTableRef.current?.parentElement) {
18,722✔
471
      parentHeightObserver.observe(analyticalTableRef.current?.parentElement);
18,722✔
472
    }
473
    return () => {
18,722✔
474
      debouncedHeightObserverFn.cancel();
18,669✔
475
      debouncedWidthObserverFn.cancel();
18,669✔
476
      tableWidthObserver.disconnect();
18,669✔
477
      parentHeightObserver.disconnect();
18,669✔
478
    };
479
  }, [updateTableClientWidth, updateRowsCount]);
480

481
  useIsomorphicLayoutEffect(() => {
98,903✔
482
    dispatch({ type: 'IS_RTL', payload: { isRtl } });
7,473✔
483
  }, [isRtl]);
484

485
  useIsomorphicLayoutEffect(() => {
98,903✔
486
    updateTableClientWidth();
10,935✔
487
  }, [updateTableClientWidth]);
488

489
  useIsomorphicLayoutEffect(() => {
98,903✔
490
    updateRowsCount();
18,722✔
491
  }, [updateRowsCount]);
492

493
  useEffect(() => {
98,903✔
494
    if (tableState.visibleRows !== undefined && visibleRowCountMode === AnalyticalTableVisibleRowCountMode.Fixed) {
8,226!
UNCOV
495
      dispatch({
×
496
        type: 'VISIBLE_ROWS',
497
        payload: { visibleRows: undefined }
498
      });
499
    }
500
  }, [visibleRowCountMode, tableState.visibleRows]);
501

502
  useEffect(() => {
98,903✔
503
    if (groupBy) {
7,290!
UNCOV
504
      setGroupBy(groupBy);
×
505
    }
506
  }, [groupBy, setGroupBy]);
507

508
  useEffect(() => {
98,903✔
509
    if (selectedRowIds) {
7,336✔
510
      dispatch({ type: 'SET_SELECTED_ROW_IDS', payload: { selectedRowIds } });
196✔
511
    }
512
  }, [selectedRowIds]);
513

514
  useEffect(() => {
98,903✔
515
    if (tableState?.interactiveRowsHavePopIn && (!tableState?.popInColumns || tableState?.popInColumns?.length === 0)) {
7,586!
UNCOV
516
      dispatch({ type: 'WITH_POPIN', payload: false });
×
517
    }
518
  }, [tableState?.interactiveRowsHavePopIn, tableState?.popInColumns?.length]);
519

520
  const tableBodyHeight = useMemo(() => {
98,903✔
521
    if (typeof tableState.bodyHeight === 'number') {
14,986✔
522
      return tableState.bodyHeight;
22✔
523
    }
524
    let rowNum;
525
    if (visibleRowCountMode === AnalyticalTableVisibleRowCountMode.AutoWithEmptyRows) {
14,964✔
526
      rowNum = internalVisibleRowCount;
1,248✔
527
    } else {
528
      rowNum = rows.length < internalVisibleRowCount ? Math.max(rows.length, minRows) : internalVisibleRowCount;
13,716✔
529
    }
530

531
    const rowHeight =
532
      visibleRowCountMode === AnalyticalTableVisibleRowCountMode.Auto ||
14,964✔
533
      visibleRowCountMode === AnalyticalTableVisibleRowCountMode.AutoWithEmptyRows ||
534
      tableState.interactiveRowsHavePopIn ||
535
      adjustTableHeightOnPopIn
536
        ? popInRowHeight
537
        : internalRowHeight;
538
    if (includeSubCompRowHeight) {
14,964✔
539
      let initialBodyHeightWithSubComps = 0;
448✔
540
      for (let i = 0; i < rowNum; i++) {
448✔
541
        if (tableState.subComponentsHeight[i]) {
2,000✔
542
          initialBodyHeightWithSubComps += tableState.subComponentsHeight[i].subComponentHeight + rowHeight;
960✔
543
        } else if (rows[i]) {
1,040✔
544
          initialBodyHeightWithSubComps += rowHeight;
1,040✔
545
        }
546
      }
547
      return initialBodyHeightWithSubComps;
448✔
548
    }
549
    return rowHeight * rowNum;
14,516✔
550
  }, [
551
    internalRowHeight,
552
    rows.length,
553
    internalVisibleRowCount,
554
    minRows,
555
    popInRowHeight,
556
    visibleRowCountMode,
557
    tableState.interactiveRowsHavePopIn,
558
    adjustTableHeightOnPopIn,
559
    includeSubCompRowHeight,
560
    tableState.subComponentsHeight,
561
    tableState.bodyHeight
562
  ]);
563

564
  // scroll bar detection
565
  useEffect(() => {
98,903✔
566
    const visibleRowCount =
567
      rows.length < internalVisibleRowCount ? Math.max(rows.length, minRows) : internalVisibleRowCount;
10,724✔
568
    if (popInRowHeight !== internalRowHeight) {
10,724✔
569
      dispatch({
250✔
570
        type: 'TABLE_SCROLLING_ENABLED',
571
        payload: { isScrollable: visibleRowCount * popInRowHeight > tableBodyHeight || rows.length > visibleRowCount }
316✔
572
      });
573
    } else {
574
      dispatch({ type: 'TABLE_SCROLLING_ENABLED', payload: { isScrollable: rows.length > visibleRowCount } });
10,474✔
575
    }
576
  }, [rows.length, minRows, internalVisibleRowCount, popInRowHeight, tableBodyHeight]);
577

578
  const noDataStyles = {
98,903✔
579
    height: `${tableBodyHeight}px`,
580
    width: totalColumnsWidth ? `${totalColumnsWidth}px` : '100%'
98,903✔
581
  };
582

583
  useEffect(() => {
98,903✔
584
    if (columnOrder?.length > 0) {
7,290✔
585
      setColumnOrder(columnOrder);
132✔
586
    }
587
  }, [columnOrder]);
588

589
  const inlineStyle = useMemo(() => {
98,903✔
590
    const tableStyles = {
21,660✔
591
      maxWidth: '100%',
592
      overflowX: 'auto',
593
      display: 'flex',
594
      flexDirection: 'column'
595
    };
596
    if (rowHeight) {
21,660✔
597
      tableStyles['--_ui5wcr-AnalyticalTableRowHeight'] = `${rowHeight}px`;
630✔
598
      tableStyles['--_ui5wcr-AnalyticalTableHeaderRowHeight'] = `${rowHeight}px`;
630✔
599
    }
600
    if (headerRowHeight) {
21,660✔
601
      tableStyles['--_ui5wcr-AnalyticalTableHeaderRowHeight'] = `${headerRowHeight}px`;
270✔
602
    }
603

604
    if (tableState.tableClientWidth > 0) {
21,660✔
605
      return {
14,370✔
606
        ...tableStyles,
607
        ...style
608
      } as CSSProperties;
609
    }
610
    return {
7,290✔
611
      ...tableStyles,
612
      ...style,
613
      visibility: 'hidden'
614
    } as CSSProperties;
615
  }, [tableState.tableClientWidth, style, rowHeight, headerRowHeight]);
616

617
  useEffect(() => {
98,903✔
618
    if (retainColumnWidth && tableState.columnResizing?.isResizingColumn && tableState.tableColResized == null) {
19,081!
UNCOV
619
      dispatch({ type: 'TABLE_COL_RESIZED', payload: true });
×
620
    }
621
    if (tableState.tableColResized && !retainColumnWidth) {
19,081!
UNCOV
622
      dispatch({ type: 'TABLE_COL_RESIZED', payload: undefined });
×
623
    }
624
  }, [tableState.columnResizing, retainColumnWidth, tableState.tableColResized]);
625

626
  const handleBodyScroll = (e) => {
98,903✔
627
    if (typeof onTableScroll === 'function') {
747✔
628
      onTableScroll(e);
100✔
629
    }
630
    const targetScrollTop = e.currentTarget.scrollTop;
747✔
631

632
    if (verticalScrollBarRef.current) {
747✔
633
      const vertScrollbarScrollElement = verticalScrollBarRef.current.firstElementChild as HTMLDivElement;
747✔
634
      if (vertScrollbarScrollElement.offsetHeight !== scrollContainerRef.current?.offsetHeight) {
747✔
635
        vertScrollbarScrollElement.style.height = `${scrollContainerRef.current.offsetHeight}px`;
245✔
636
      }
637
      if (verticalScrollBarRef.current.scrollTop !== targetScrollTop) {
747✔
638
        if (!e.currentTarget.isExternalVerticalScroll) {
747✔
639
          verticalScrollBarRef.current.scrollTop = targetScrollTop;
672✔
640
          verticalScrollBarRef.current.isExternalVerticalScroll = true;
672✔
641
        }
642
        e.currentTarget.isExternalVerticalScroll = false;
747✔
643
      }
644
    }
645
  };
646

647
  const handleVerticalScrollBarScroll = useCallback((e) => {
98,903✔
648
    if (parentRef.current && !e.currentTarget.isExternalVerticalScroll) {
860✔
649
      parentRef.current.scrollTop = e.currentTarget.scrollTop;
188✔
650
      parentRef.current.isExternalVerticalScroll = true;
188✔
651
    }
652
    e.currentTarget.isExternalVerticalScroll = false;
860✔
653
  }, []);
654

655
  useEffect(() => {
98,903✔
656
    columnVirtualizer.measure();
12,534✔
657
  }, [
658
    columnVirtualizer,
659
    tableState.columnOrder,
660
    tableState.columnResizing?.isResizingColumn,
661
    columns,
662
    tableState.groupBy
663
  ]);
664

665
  const totalSize = columnVirtualizer.getTotalSize();
98,903✔
666
  const showVerticalEndBorder = tableState.tableClientWidth > totalSize;
98,903✔
667

668
  const tableClasses = clsx(
98,903✔
669
    classNames.table,
670
    withNavigationHighlight && classNames.hasNavigationIndicator,
102,563✔
671
    showVerticalEndBorder && classNames.showVerticalEndBorder,
106,922✔
672
    className?.includes('ui5-content-native-scrollbars') && 'ui5-content-native-scrollbars'
98,903!
673
  );
674

675
  const handleOnLoadMore = (e) => {
98,903✔
676
    const rootNodes = rows.filter((row) => row.depth === 0);
20,087✔
677
    onLoadMore(
287✔
678
      enrichEventWithDetails(e, {
679
        rowCount: rootNodes.length,
680
        totalRowCount: rows.length
681
      })
682
    );
683
  };
684

685
  const overscan = overscanCount ? overscanCount : Math.floor(visibleRows / 2);
98,903✔
686
  const rHeight = popInRowHeight !== internalRowHeight ? popInRowHeight : internalRowHeight;
98,903✔
687

688
  const itemCount =
689
    Math.max(
98,903✔
690
      minRows,
691
      rows.length,
692
      visibleRowCountMode === AnalyticalTableVisibleRowCountMode.AutoWithEmptyRows ? internalVisibleRowCount : 0
98,903✔
693
    ) + (!tableState.isScrollable ? additionalEmptyRowsCount : 0);
98,903✔
694

695
  const rowVirtualizer = useVirtualizer({
98,903✔
696
    count: itemCount,
697
    getScrollElement: () => parentRef.current,
44,702✔
698
    estimateSize: useCallback(
699
      (index) => {
700
        if (
184,453✔
701
          renderRowSubComponent &&
213,261✔
702
          (rows[index]?.isExpanded || alwaysShowSubComponent) &&
703
          tableState.subComponentsHeight?.[index]?.rowId === rows[index]?.id
704
        ) {
705
          return rHeight + (tableState.subComponentsHeight?.[index]?.subComponentHeight ?? 0);
1,601✔
706
        }
707
        return rHeight;
182,852✔
708
      },
709
      [rHeight, rows, renderRowSubComponent, alwaysShowSubComponent, tableState.subComponentsHeight]
710
    ),
711
    overscan,
712
    measureElement,
713
    indexAttribute: 'data-virtual-row-index',
714
    useAnimationFrameWithResizeObserver: true
715
  });
716
  // add range to instance for `useAutoResize` plugin hook
717
  tableInstanceRef.current.virtualRowsRange = rowVirtualizer.range;
98,903✔
718

719
  return (
98,903✔
720
    <>
721
      <div
722
        className={className}
723
        style={inlineStyle}
724
        //@ts-expect-error: types are compatible
725
        ref={componentRef}
726
        {...rest}
727
      >
728
        {header && (
104,773✔
729
          <TitleBar ref={titleBarRef} titleBarId={titleBarId}>
730
            {header}
731
          </TitleBar>
732
        )}
733
        {extension && <div ref={extensionRef}>{extension}</div>}
98,903!
734
        <FlexBox
735
          className={classNames.tableContainerWithScrollBar}
736
          data-component-name="AnalyticalTableContainerWithScrollbar"
737
        >
738
          {loading && !!rows.length && (
101,423✔
739
            <BusyIndicator
740
              className={classNames.busyIndicator}
741
              active={true}
742
              delay={loadingDelay}
743
              data-component-name="AnalyticalTableBusyIndicator"
744
            >
745
              {/*todo: This is necessary; otherwise, the overlay bg color will not be applied. https://github.com/SAP/ui5-webcomponents/issues/9723 */}
746
              <span />
747
            </BusyIndicator>
748
          )}
749
          {/*todo: use global CSS once --sapBlockLayer_Opacity is available*/}
750
          {showOverlay && (
99,353✔
751
            <>
752
              <span id={invalidTableTextId} className={classNames.hiddenA11yText} aria-hidden="true">
753
                {invalidTableA11yText}
754
              </span>
755
              <div
756
                tabIndex={0}
757
                aria-labelledby={`${titleBarId} ${invalidTableTextId}`}
758
                role="region"
759
                data-component-name="AnalyticalTableOverlay"
760
                className={classNames.overlay}
761
              />
762
            </>
763
          )}
764
          <div
765
            aria-labelledby={titleBarId}
766
            {...getTableProps()}
767
            tabIndex={loading || showOverlay ? -1 : 0}
295,239✔
768
            role={isTreeTable ? 'treegrid' : 'grid'}
98,903✔
769
            aria-rowcount={rows.length}
770
            aria-colcount={visibleColumns.length}
771
            data-per-page={internalVisibleRowCount}
772
            aria-multiselectable={selectionMode === AnalyticalTableSelectionMode.Multiple}
773
            data-component-name="AnalyticalTableContainer"
774
            ref={tableRef}
775
            className={tableClasses}
776
          >
777
            <div className={classNames.tableHeaderBackgroundElement} aria-hidden="true" />
778
            <div className={classNames.tableBodyBackgroundElement} aria-hidden="true" />
779
            {headerGroups.map((headerGroup) => {
780
              let headerProps: Record<string, unknown> = {};
98,423✔
781
              if (headerGroup.getHeaderGroupProps) {
98,423✔
782
                headerProps = headerGroup.getHeaderGroupProps();
98,423✔
783
              }
784
              return (
98,423✔
785
                tableRef.current && (
182,426✔
786
                  <ColumnHeaderContainer
787
                    ref={headerRef}
788
                    key={headerProps.key as string}
789
                    resizeInfo={tableState.columnResizing}
790
                    headerProps={headerProps}
791
                    headerGroup={headerGroup}
792
                    isRtl={isRtl}
793
                    columnVirtualizer={columnVirtualizer}
794
                    uniqueId={uniqueId}
795
                    showVerticalEndBorder={showVerticalEndBorder}
796
                    classNames={classNames}
797
                  />
798
                )
799
              );
800
            })}
801
            {rows?.length === 0 && (
101,133✔
802
              <div
803
                style={noDataStyles}
804
                data-component-name="AnalyticalTableNoDataContainer"
805
                role="row"
806
                tabIndex={0}
807
                className={classNames.noDataContainer}
808
              >
809
                {loading ? (
2,230✔
810
                  <TablePlaceholder
811
                    columns={visibleColumns}
812
                    rows={minRows}
813
                    style={noDataStyles}
814
                    pleaseWaitText={i18nBundle.getText(PLEASE_WAIT)}
815
                  />
816
                ) : (
817
                  <NoDataComponent noDataText={noDataTextLocal} className={classNames.noData} />
818
                )}
819
              </div>
820
            )}
821
            {rows?.length > 0 && tableRef.current && (
278,109✔
822
              <VirtualTableBodyContainer
823
                rowCollapsedFlag={tableState.rowCollapsed}
824
                dispatch={dispatch}
825
                tableBodyHeight={tableBodyHeight}
826
                totalColumnsWidth={columnVirtualizer.getTotalSize()}
827
                parentRef={parentRef}
828
                classes={classNames}
829
                infiniteScroll={infiniteScroll}
830
                infiniteScrollThreshold={infiniteScrollThreshold}
831
                onLoadMore={handleOnLoadMore}
832
                internalRowHeight={internalRowHeight}
833
                popInRowHeight={popInRowHeight}
834
                rows={rows}
835
                handleExternalScroll={handleBodyScroll}
836
                visibleRows={internalVisibleRowCount}
837
                isGrouped={isGrouped}
838
              >
839
                <VirtualTableBody
840
                  scrollContainerRef={scrollContainerRef}
841
                  classes={classNames}
842
                  prepareRow={prepareRow}
843
                  rows={rows}
844
                  scrollToRef={scrollToRef}
845
                  isTreeTable={isTreeTable}
846
                  internalRowHeight={internalRowHeight}
847
                  popInRowHeight={popInRowHeight}
848
                  alternateRowColor={alternateRowColor}
849
                  visibleColumns={visibleColumns}
850
                  renderRowSubComponent={renderRowSubComponent}
851
                  alwaysShowSubComponent={alwaysShowSubComponent}
852
                  markNavigatedRow={markNavigatedRow}
853
                  isRtl={isRtl}
854
                  subComponentsHeight={tableState.subComponentsHeight}
855
                  dispatch={dispatch}
856
                  columnVirtualizer={columnVirtualizer}
857
                  manualGroupBy={reactTableOptions?.manualGroupBy as boolean | undefined}
858
                  subRowsKey={subRowsKey}
859
                  subComponentsBehavior={subComponentsBehavior}
860
                  triggerScroll={tableState.triggerScroll}
861
                  rowVirtualizer={rowVirtualizer}
862
                />
863
              </VirtualTableBodyContainer>
864
            )}
865
          </div>
318,394✔
866
          {(additionalEmptyRowsCount || tableState.isScrollable === undefined || tableState.isScrollable) && (
867
            <VerticalScrollbar
868
              tableBodyHeight={tableBodyHeight}
869
              internalRowHeight={internalHeaderRowHeight}
870
              tableRef={tableRef}
871
              handleVerticalScrollBarScroll={handleVerticalScrollBarScroll}
872
              ref={verticalScrollBarRef}
873
              scrollContainerRef={scrollContainerRef}
874
              parentRef={parentRef}
875
              nativeScrollbar={className?.includes('ui5-content-native-scrollbars')}
876
              classNames={classNames}
877
            />
878
          )}
879
        </FlexBox>
880
        {visibleRowCountMode === AnalyticalTableVisibleRowCountMode.Interactive && (
100,305✔
881
          <VerticalResizer
882
            popInRowHeight={popInRowHeight}
883
            hasPopInColumns={tableState?.popInColumns?.length > 0}
884
            analyticalTableRef={analyticalTableRef}
885
            dispatch={dispatch}
886
            extensionsHeight={extensionsHeight}
887
            internalRowHeight={internalRowHeight}
888
            rowsLength={rows.length}
889
            visibleRows={internalVisibleRowCount}
890
            handleOnLoadMore={handleOnLoadMore}
891
            classNames={classNames}
892
          />
893
        )}
894
      </div>
895
      <Text
896
        aria-hidden="true"
897
        id={`scaleModeHelper-${uniqueId}`}
898
        className={classNames.hiddenSmartColMeasure}
899
        data-component-name="AnalyticalTableScaleModeHelper"
900
      >
901
        {''}
902
      </Text>
903
      <Text
904
        aria-hidden="true"
905
        id={`scaleModeHelperHeader-${uniqueId}`}
906
        className={clsx(classNames.hiddenSmartColMeasure, classNames.hiddenSmartColMeasureHeader)}
907
        data-component-name="AnalyticalTableScaleModeHelperHeader"
908
      >
909
        {''}
910
      </Text>
911
    </>
912
  );
913
});
914

915
AnalyticalTable.displayName = 'AnalyticalTable';
444✔
916

917
export { AnalyticalTable };
918
export type {
919
  AnalyticalTableColumnDefinition,
920
  AnalyticalTableDomRef,
921
  AnalyticalTablePropTypes,
922
  DivWithCustomScrollProp
923
};
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