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

SAP / ui5-webcomponents-react / 12009185407

25 Nov 2024 11:35AM CUT coverage: 87.142% (-0.02%) from 87.16%
12009185407

push

github

web-flow
fix(NavigationLayout): add root export (#6657)

2901 of 3864 branches covered (75.08%)

5056 of 5802 relevant lines covered (87.14%)

50139.75 hits per line

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

54.21
/packages/main/src/components/AnalyticalTable/hooks/useDynamicColumnWidths.ts
1
import { useMemo } from 'react';
2
import { AnalyticalTableScaleWidthMode } from '../../../enums/index.js';
3
import { DEFAULT_COLUMN_WIDTH } from '../defaults/Column/index.js';
4
import type { AnalyticalTableColumnDefinition, ReactTableHooks, TableInstance } from '../types/index.js';
5

6
const ROW_SAMPLE_SIZE = 20;
418✔
7
const MAX_WIDTH = 700;
418✔
8
export const CELL_PADDING_PX = 18; /* padding left and right 0.5rem each (16px) + borders (1px) + buffer (1px) */
418✔
9

10
function findLongestString(str1, str2) {
11
  if (typeof str1 !== 'string' || typeof str2 !== 'string') {
114!
12
    return str1 || str2 || undefined;
114✔
13
  }
14

15
  return str1.length > str2.length ? str1 : str2;
×
16
}
17

18
function getContentPxAvg(rowSample, columnIdOrAccessor, uniqueId) {
19
  return (
114✔
20
    rowSample.reduce((acc, item) => {
21
      const dataPoint = item.values?.[columnIdOrAccessor];
456✔
22

23
      let val = 0;
456✔
24
      if (dataPoint) {
456✔
25
        val = stringToPx(dataPoint, uniqueId) + CELL_PADDING_PX;
456✔
26
      }
27
      return acc + val;
456✔
28
    }, 0) / (rowSample.length || 1)
114!
29
  );
30
}
31

32
function stringToPx(dataPoint, id, isHeader = false) {
456✔
33
  const elementId = isHeader ? 'scaleModeHelperHeader' : 'scaleModeHelper';
570✔
34
  const ruler = document.getElementById(`${elementId}-${id}`);
570✔
35
  if (ruler) {
570✔
36
    ruler.textContent = `${dataPoint}`;
570✔
37
    return ruler.scrollWidth;
570✔
38
  }
39
  return 0;
×
40
}
41

42
const columnsDeps = (
418✔
43
  deps,
44
  { instance: { state, webComponentsReactProperties, visibleColumns, data, rows, columns } }
45
) => {
46
  const isLoadingPlaceholder = !data?.length && webComponentsReactProperties.loading;
48,580✔
47
  const hasRows = rows?.length > 0;
48,580✔
48
  // eslint-disable-next-line react-hooks/rules-of-hooks
49
  const colsEqual = useMemo(() => {
48,580✔
50
    return visibleColumns
17,985✔
51
      ?.filter(
52
        (col) =>
53
          col.id !== '__ui5wcr__internal_selection_column' &&
72,496✔
54
          col.id !== '__ui5wcr__internal_highlight_column' &&
55
          col.id !== '__ui5wcr__internal_navigation_column'
56
      )
57
      .every((visCol) => {
58
        const id = visCol.id ?? visCol.accessor;
67,000!
59
        return columns.some((item) => {
67,000✔
60
          return item.accessor === id || item.id === id;
1,036,416✔
61
        });
62
      });
63
  }, [visibleColumns, columns]);
64

65
  return [
48,580✔
66
    ...deps,
67
    hasRows,
68
    colsEqual,
69
    visibleColumns?.length,
70
    !state.tableColResized && state.tableClientWidth,
97,160✔
71
    state.hiddenColumns.length,
72
    webComponentsReactProperties.scaleWidthMode,
73
    isLoadingPlaceholder
74
  ];
75
};
76
interface IColumnMeta {
77
  contentPxAvg: number;
78
  headerPx: number;
79
  headerDefinesWidth?: boolean;
80
}
81

82
const smartColumns = (columns: AnalyticalTableColumnDefinition[], instance, hiddenColumns) => {
418✔
83
  const { rows, state, webComponentsReactProperties } = instance;
×
84
  const rowSample = rows.slice(0, ROW_SAMPLE_SIZE);
×
85
  const { tableClientWidth: totalWidth } = state;
×
86

87
  const visibleColumns = columns.filter(
×
88
    (column) => (column.isVisible ?? true) && !hiddenColumns.includes(column.id ?? column.accessor)
×
89
  );
90

91
  const columnMeta: Record<string, IColumnMeta> = visibleColumns.reduce(
×
92
    (metadata: Record<string, IColumnMeta>, column) => {
93
      const columnIdOrAccessor = (column.id ?? column.accessor) as string;
×
94
      if (
×
95
        column.id === '__ui5wcr__internal_selection_column' ||
×
96
        column.id === '__ui5wcr__internal_highlight_column' ||
97
        column.id === '__ui5wcr__internal_navigation_column'
98
      ) {
99
        metadata[columnIdOrAccessor] = {
×
100
          headerPx: column.width || column.minWidth || 60,
×
101
          contentPxAvg: 0
102
        };
103
        return metadata;
×
104
      }
105

106
      let headerPx, contentPxAvg;
107

108
      if (column.scaleWidthModeOptions?.cellString) {
×
109
        contentPxAvg =
×
110
          stringToPx(column.scaleWidthModeOptions.cellString, webComponentsReactProperties.uniqueId) + CELL_PADDING_PX;
111
      } else {
112
        contentPxAvg = getContentPxAvg(rowSample, columnIdOrAccessor, webComponentsReactProperties.uniqueId);
×
113
      }
114

115
      if (column.scaleWidthModeOptions?.headerString) {
×
116
        headerPx = Math.max(
×
117
          stringToPx(column.scaleWidthModeOptions.headerString, webComponentsReactProperties.uniqueId, true) +
118
            CELL_PADDING_PX,
119
          60
120
        );
121
      } else {
122
        headerPx =
×
123
          typeof column.Header === 'string'
×
124
            ? Math.max(stringToPx(column.Header, webComponentsReactProperties.uniqueId, true) + CELL_PADDING_PX, 60)
125
            : 60;
126
      }
127

128
      metadata[columnIdOrAccessor] = {
×
129
        headerPx,
130
        contentPxAvg
131
      };
132
      return metadata;
×
133
    },
134
    {}
135
  );
136

137
  let totalContentPxAvgPrio1 = 0;
×
138
  let totalNumberColPrio2 = 0;
×
139

140
  // width reserved by predefined widths or columns defined by header
141
  const reservedWidth: number = visibleColumns.reduce((acc, column) => {
×
142
    const columnIdOrAccessor = (column.id ?? column.accessor) as string;
×
143
    const { contentPxAvg, headerPx } = columnMeta[columnIdOrAccessor];
×
144

145
    if (contentPxAvg > headerPx) {
×
146
      if (!column.minWidth && !column.width) {
×
147
        totalContentPxAvgPrio1 += columnMeta[columnIdOrAccessor].contentPxAvg;
×
148
        totalNumberColPrio2++;
×
149
        return acc;
×
150
      } else {
151
        return acc + Math.max(column.minWidth || 0, column.width || 0);
×
152
      }
153
    } else {
154
      if (!column.minWidth && !column.width) {
×
155
        totalNumberColPrio2++;
×
156
      }
157
      const max = Math.max(column.minWidth || 0, column.width || 0, headerPx);
×
158
      columnMeta[columnIdOrAccessor].headerDefinesWidth = true;
×
159
      return acc + max;
×
160
    }
161
  }, 0);
162

163
  const availableWidthPrio1 = totalWidth - reservedWidth;
×
164
  let availableWidthPrio2 = availableWidthPrio1;
×
165

166
  // Step 1: Give columns defined by content more space (priority 1)
167
  const visibleColumnsAdaptedPrio1 = visibleColumns.map((column) => {
×
168
    const columnIdOrAccessor = (column.id ?? column.accessor) as string;
×
169
    const meta = columnMeta[columnIdOrAccessor];
×
170
    if (meta && !column.minWidth && !column.width && !meta.headerDefinesWidth) {
×
171
      let targetWidth;
172
      const { contentPxAvg, headerPx } = meta;
×
173

174
      if (availableWidthPrio1 > 0) {
×
175
        const factor = contentPxAvg / totalContentPxAvgPrio1;
×
176
        targetWidth = Math.max(Math.min(availableWidthPrio1 * factor, contentPxAvg), headerPx);
×
177
        availableWidthPrio2 -= targetWidth;
×
178
      }
179
      return {
×
180
        ...column,
181
        nextWidth: targetWidth || headerPx
×
182
      };
183
    }
184
    return column;
×
185
  });
186
  // Step 2: Give all columns more space (priority 2)
187
  return visibleColumnsAdaptedPrio1.map((column) => {
×
188
    const columnIdOrAccessor = (column.id ?? column.accessor) as string;
×
189
    const meta = columnMeta[columnIdOrAccessor];
×
190
    const { headerPx } = meta;
×
191
    if (meta && !column.minWidth && !column.width) {
×
192
      let targetWidth = column.nextWidth || headerPx;
×
193
      if (availableWidthPrio2 > 0) {
×
194
        targetWidth = targetWidth + availableWidthPrio2 * (1 / totalNumberColPrio2);
×
195
      }
196
      return {
×
197
        ...column,
198
        width: targetWidth
199
      };
200
    } else {
201
      return {
×
202
        ...column,
203
        width: Math.max(column.width || 0, 60, headerPx)
×
204
      };
205
    }
206
  });
207
};
208

209
const columns = (columns: TableInstance['columns'], { instance }: { instance: TableInstance }) => {
418✔
210
  if (!instance.state || !instance.rows) {
13,665✔
211
    return columns;
3,133✔
212
  }
213
  const { rows, state } = instance;
10,532✔
214
  const { hiddenColumns, tableClientWidth: totalWidth } = state;
10,532✔
215
  const { scaleWidthMode, loading, uniqueId } = instance.webComponentsReactProperties;
10,532✔
216

217
  if (columns.length === 0 || !totalWidth || !AnalyticalTableScaleWidthMode[scaleWidthMode]) {
10,532✔
218
    return columns;
3,207✔
219
  }
220

221
  // map columns to visibleColumns
222
  const visibleColumns = instance.visibleColumns
7,325✔
223
    .map((visCol) => {
224
      const column = columns.find((col) => {
32,643✔
225
        return (
373,587✔
226
          col.id === visCol.id || (col.accessor !== undefined && visCol.id !== undefined && col.accessor === visCol.id)
1,330,336✔
227
        );
228
      });
229
      if (column) {
32,643✔
230
        return column;
32,273✔
231
      }
232
      return column ?? false;
370✔
233
    })
234
    .filter(Boolean) as TableInstance['columns'];
235
  if (scaleWidthMode === AnalyticalTableScaleWidthMode.Smart) {
7,325!
236
    return smartColumns(columns, instance, hiddenColumns);
×
237
  }
238

239
  const calculateDefaultTableWidth = () => {
7,325✔
240
    const columnsWithWidthProperties = visibleColumns
7,211✔
241
      .filter((column) => column.width ?? column.minWidth ?? column.maxWidth ?? false)
32,159✔
242
      .map((column) => ({
7,733✔
243
        accessor: column.id ?? column.accessor,
12,333✔
244
        minWidth: column.minWidth,
245
        width: column.width,
246
        maxWidth: column.maxWidth
247
      }));
248
    let availableWidth = totalWidth;
7,211✔
249
    let defaultColumnsCount = visibleColumns.length;
7,211✔
250

251
    const columnsWithFixedWidth = columnsWithWidthProperties
7,211✔
252
      .map((column) => {
253
        const { width, minWidth, maxWidth, accessor } = column;
7,733✔
254
        if (width) {
7,733✔
255
          // necessary because of default minWidth
256
          const acceptedWidth =
257
            accessor !== '__ui5wcr__internal_highlight_column' &&
7,733!
258
            accessor !== '__ui5wcr__internal_selection_column' &&
259
            accessor !== '__ui5wcr__internal_navigation_column' &&
260
            width < 60
261
              ? 60
262
              : width;
263

264
          availableWidth -= acceptedWidth;
7,733✔
265
          defaultColumnsCount--;
7,733✔
266
          return acceptedWidth;
7,733✔
267
        }
268
        const columnsWithMaxWidth = columnsWithWidthProperties.filter((item) => item.maxWidth);
×
269
        const aggregatedColumnsMaxWidth = columnsWithMaxWidth.reduce((acc, cur) => acc + cur.maxWidth, 0);
×
270
        const aggregatedColumnsMinWidth = columnsWithWidthProperties
×
271
          .filter((item) => item.minWidth && !item.maxWidth)
×
272
          .reduce((acc, cur) => acc + cur.minWidth, 0);
×
273

274
        if (minWidth > availableWidth / defaultColumnsCount) {
×
275
          // don't apply minWidth if enough space is available because of maxWidth properties
276
          if (
×
277
            availableWidth - aggregatedColumnsMaxWidth >
278
            aggregatedColumnsMinWidth + (visibleColumns.length - columnsWithWidthProperties.length) * 60
279
          ) {
280
            // apply minWidth only if it's larger than the calculated available width
281
            if (minWidth > (availableWidth - aggregatedColumnsMaxWidth) / columnsWithMaxWidth.length) {
×
282
              availableWidth -= minWidth;
×
283
              defaultColumnsCount--;
×
284
              return minWidth;
×
285
            }
286
            return false;
×
287
          }
288
          availableWidth -= minWidth;
×
289
          defaultColumnsCount--;
×
290
          return minWidth;
×
291
        }
292
        if (maxWidth < availableWidth / defaultColumnsCount) {
×
293
          availableWidth -= maxWidth;
×
294
          defaultColumnsCount--;
×
295
          return maxWidth;
×
296
        }
297
        return false;
×
298
      })
299
      .filter(Boolean) as number[];
300

301
    const fixedWidth = columnsWithFixedWidth.reduce((acc, val) => acc + val, 0);
7,733✔
302
    // check if columns are visible and table has width
303
    if (visibleColumns.length > 0 && totalWidth > 0) {
7,211✔
304
      // set fixedWidth as defaultWidth if all visible columns have fixed value
305
      if (visibleColumns.length === columnsWithFixedWidth.length) {
7,137✔
306
        return fixedWidth / visibleColumns.length;
638✔
307
      }
308
      // spread default columns
309
      if (totalWidth >= fixedWidth + defaultColumnsCount * DEFAULT_COLUMN_WIDTH) {
6,499✔
310
        return (totalWidth - fixedWidth) / defaultColumnsCount;
6,297✔
311
      }
312
    }
313
    return DEFAULT_COLUMN_WIDTH;
276✔
314
  };
315

316
  const hasData = instance.data.length > 0;
7,325✔
317

318
  if (scaleWidthMode === AnalyticalTableScaleWidthMode.Default || (!hasData && loading)) {
7,325!
319
    const defaultWidth = calculateDefaultTableWidth();
7,211✔
320
    return columns.map((column) => ({ ...column, width: column.width ?? defaultWidth }));
34,102✔
321
  }
322

323
  // AnalyticalTableScaleWidthMode.Grow
324

325
  const rowSample = rows.slice(0, ROW_SAMPLE_SIZE);
114✔
326

327
  const columnMeta = visibleColumns.reduce((acc, column) => {
114✔
328
    const columnIdOrAccessor = (column.id ?? column.accessor) as string;
114✔
329
    if (
114!
330
      column.id === '__ui5wcr__internal_selection_column' ||
342✔
331
      column.id === '__ui5wcr__internal_highlight_column' ||
332
      column.id === '__ui5wcr__internal_navigation_column'
333
    ) {
334
      acc[columnIdOrAccessor] = {
×
335
        minHeaderWidth: column.width,
336
        fullWidth: column.width
337
      };
338
      return acc;
×
339
    }
340

341
    const smartWidth = findLongestString(
114✔
342
      column.scaleWidthModeOptions?.headerString,
343
      column.scaleWidthModeOptions?.cellString
344
    );
345

346
    if (smartWidth) {
114!
347
      const width = Math.max(stringToPx(smartWidth, uniqueId) + CELL_PADDING_PX, 60);
×
348
      acc[columnIdOrAccessor] = {
×
349
        minHeaderWidth: width,
350
        fullWidth: width
351
      };
352
      return acc;
×
353
    }
354

355
    const minHeaderWidth =
356
      typeof column.Header === 'string'
114!
357
        ? stringToPx(column.Header, uniqueId, true) + CELL_PADDING_PX
358
        : DEFAULT_COLUMN_WIDTH;
359

360
    acc[columnIdOrAccessor] = {
114✔
361
      minHeaderWidth,
362
      fullWidth: Math.max(minHeaderWidth, getContentPxAvg(rowSample, columnIdOrAccessor, uniqueId))
363
    };
364
    return acc;
114✔
365
  }, {});
366

367
  let reservedWidth = visibleColumns.reduce((acc, column) => {
114✔
368
    const { minHeaderWidth, fullWidth } = columnMeta[column.id ?? column.accessor];
114✔
369
    return acc + Math.max(column.minWidth || 0, column.width || 0, minHeaderWidth || 0, fullWidth) || 0;
114!
370
  }, 0);
371

372
  let availableWidth = totalWidth - reservedWidth;
114✔
373

374
  if (availableWidth > 0) {
114✔
375
    let notReservedCount = 0;
76✔
376
    reservedWidth = visibleColumns.reduce((acc, column) => {
76✔
377
      const reserved = Math.max(column.minWidth || 0, column.width || 0) || 0;
76✔
378
      if (!reserved) {
76✔
379
        notReservedCount++;
76✔
380
      }
381
      return acc + reserved;
76✔
382
    }, 0);
383
    availableWidth = totalWidth - reservedWidth;
76✔
384

385
    return columns.map((column) => {
76✔
386
      const isColumnVisible = (column.isVisible ?? true) && !hiddenColumns.includes(column.id ?? column.accessor);
76✔
387
      const meta = columnMeta[column.id ?? (column.accessor as string)];
76✔
388

389
      if (isColumnVisible && meta) {
76✔
390
        const { minHeaderWidth } = meta;
76✔
391

392
        const targetWidth = availableWidth / notReservedCount;
76✔
393

394
        return {
76✔
395
          ...column,
396
          width: column.width ?? Math.min(targetWidth, MAX_WIDTH),
152✔
397
          minWidth: column.minWidth ?? minHeaderWidth
152✔
398
        };
399
      }
400
      return column;
×
401
    });
402
  }
403

404
  return columns.map((column) => {
38✔
405
    const isColumnVisible = (column.isVisible ?? true) && !hiddenColumns.includes(column.id ?? column.accessor);
38✔
406
    const meta = columnMeta[column.id ?? (column.accessor as string)];
38✔
407
    if (isColumnVisible && meta) {
38✔
408
      const { fullWidth } = meta;
38✔
409
      return {
38✔
410
        ...column,
411
        width: column.width ?? fullWidth,
76✔
412
        maxWidth: column.maxWidth ?? MAX_WIDTH
38!
413
      };
414
    }
415
    return column;
×
416
  });
417
};
418

419
export const useDynamicColumnWidths = (hooks: ReactTableHooks) => {
418✔
420
  hooks.columns.push(columns);
48,580✔
421
  hooks.columnsDeps.push(columnsDeps);
48,580✔
422
};
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