UNPKG

@ui5/webcomponents-react

Version:

React Wrapper for UI5 Web Components and additional components

358 lines (352 loc) 13.6 kB
import { useMemo } from 'react'; import { AnalyticalTableScaleWidthMode } from '../../../enums/index.js'; import { DEFAULT_COLUMN_WIDTH } from '../defaults/Column/index.js'; const ROW_SAMPLE_SIZE = 20; const MAX_WIDTH = 700; export const CELL_PADDING_PX = 18; /* padding left and right 0.5rem each (16px) + borders (1px) + buffer (1px) */ function findLongestString(str1, str2) { if (typeof str1 !== 'string' || typeof str2 !== 'string') { return str1 || str2 || undefined; } return str1.length > str2.length ? str1 : str2; } function getContentPxAvg(rowSample, columnIdOrAccessor, uniqueId) { return rowSample.reduce((acc, item) => { const dataPoint = item.values?.[columnIdOrAccessor]; let val = 0; if (dataPoint) { val = stringToPx(dataPoint, uniqueId) + CELL_PADDING_PX; } return acc + val; }, 0) / (rowSample.length || 1); } function stringToPx(dataPoint, id, isHeader = false) { const elementId = isHeader ? 'scaleModeHelperHeader' : 'scaleModeHelper'; const ruler = document.getElementById(`${elementId}-${id}`); if (ruler) { ruler.textContent = `${dataPoint}`; return ruler.scrollWidth; } return 0; } const columnsDeps = (deps, { instance: { state, webComponentsReactProperties, visibleColumns, data, rows, columns } }) => { const isLoadingPlaceholder = !data?.length && webComponentsReactProperties.loading; const hasRows = rows?.length > 0; // eslint-disable-next-line react-hooks/rules-of-hooks const colsEqual = useMemo(() => { return visibleColumns?.filter(col => col.id !== '__ui5wcr__internal_selection_column' && col.id !== '__ui5wcr__internal_highlight_column' && col.id !== '__ui5wcr__internal_navigation_column').every(visCol => { const id = visCol.id ?? visCol.accessor; return columns.some(item => { return item.accessor === id || item.id === id; }); }); }, [visibleColumns, columns]); return [...deps, hasRows, colsEqual, visibleColumns?.length, !state.tableColResized && state.tableClientWidth, state.hiddenColumns.length, webComponentsReactProperties.scaleWidthMode, isLoadingPlaceholder]; }; const smartColumns = (columns, instance, hiddenColumns) => { const { rows, state, webComponentsReactProperties } = instance; const rowSample = rows.slice(0, ROW_SAMPLE_SIZE); const { tableClientWidth: totalWidth } = state; const visibleColumns = columns.filter(column => (column.isVisible ?? true) && !hiddenColumns.includes(column.id ?? column.accessor)); const columnMeta = visibleColumns.reduce((metadata, column) => { const columnIdOrAccessor = column.id ?? column.accessor; if (column.id === '__ui5wcr__internal_selection_column' || column.id === '__ui5wcr__internal_highlight_column' || column.id === '__ui5wcr__internal_navigation_column') { metadata[columnIdOrAccessor] = { headerPx: column.width || column.minWidth || 60, contentPxAvg: 0 }; return metadata; } let headerPx, contentPxAvg; if (column.scaleWidthModeOptions?.cellString) { contentPxAvg = stringToPx(column.scaleWidthModeOptions.cellString, webComponentsReactProperties.uniqueId) + CELL_PADDING_PX; } else { contentPxAvg = getContentPxAvg(rowSample, columnIdOrAccessor, webComponentsReactProperties.uniqueId); } if (column.scaleWidthModeOptions?.headerString) { headerPx = Math.max(stringToPx(column.scaleWidthModeOptions.headerString, webComponentsReactProperties.uniqueId, true) + CELL_PADDING_PX, 60); } else { headerPx = typeof column.Header === 'string' ? Math.max(stringToPx(column.Header, webComponentsReactProperties.uniqueId, true) + CELL_PADDING_PX, 60) : 60; } metadata[columnIdOrAccessor] = { headerPx, contentPxAvg }; return metadata; }, {}); let totalContentPxAvgPrio1 = 0; let totalNumberColPrio2 = 0; // width reserved by predefined widths or columns defined by header const reservedWidth = visibleColumns.reduce((acc, column) => { const columnIdOrAccessor = column.id ?? column.accessor; const { contentPxAvg, headerPx } = columnMeta[columnIdOrAccessor]; if (contentPxAvg > headerPx) { if (!column.minWidth && !column.width) { totalContentPxAvgPrio1 += columnMeta[columnIdOrAccessor].contentPxAvg; totalNumberColPrio2++; return acc; } else { return acc + Math.max(column.minWidth || 0, column.width || 0); } } else { if (!column.minWidth && !column.width) { totalNumberColPrio2++; } const max = Math.max(column.minWidth || 0, column.width || 0, headerPx); columnMeta[columnIdOrAccessor].headerDefinesWidth = true; return acc + max; } }, 0); const availableWidthPrio1 = totalWidth - reservedWidth; let availableWidthPrio2 = availableWidthPrio1; // Step 1: Give columns defined by content more space (priority 1) const visibleColumnsAdaptedPrio1 = visibleColumns.map(column => { const columnIdOrAccessor = column.id ?? column.accessor; const meta = columnMeta[columnIdOrAccessor]; if (meta && !column.minWidth && !column.width && !meta.headerDefinesWidth) { let targetWidth; const { contentPxAvg, headerPx } = meta; if (availableWidthPrio1 > 0) { const factor = contentPxAvg / totalContentPxAvgPrio1; targetWidth = Math.max(Math.min(availableWidthPrio1 * factor, contentPxAvg), headerPx); availableWidthPrio2 -= targetWidth; } return { ...column, nextWidth: targetWidth || headerPx }; } return column; }); // Step 2: Give all columns more space (priority 2) return visibleColumnsAdaptedPrio1.map(column => { const columnIdOrAccessor = column.id ?? column.accessor; const meta = columnMeta[columnIdOrAccessor]; const { headerPx } = meta; if (meta && !column.minWidth && !column.width) { let targetWidth = column.nextWidth || headerPx; if (availableWidthPrio2 > 0) { targetWidth = targetWidth + availableWidthPrio2 * (1 / totalNumberColPrio2); } return { ...column, width: targetWidth }; } else { return { ...column, width: Math.max(column.width || 0, 60, headerPx) }; } }); }; const columns = (columns, { instance }) => { if (!instance.state || !instance.rows) { return columns; } const { rows, state } = instance; const { hiddenColumns, tableClientWidth: totalWidth } = state; const { scaleWidthMode, loading, uniqueId } = instance.webComponentsReactProperties; if (columns.length === 0 || !totalWidth || !AnalyticalTableScaleWidthMode[scaleWidthMode]) { return columns; } // map columns to visibleColumns const visibleColumns = instance.visibleColumns.map(visCol => { const column = columns.find(col => { return col.id === visCol.id || col.accessor !== undefined && visCol.id !== undefined && col.accessor === visCol.id; }); if (column) { return column; } return column ?? false; }).filter(Boolean); if (scaleWidthMode === AnalyticalTableScaleWidthMode.Smart) { return smartColumns(columns, instance, hiddenColumns); } const calculateDefaultTableWidth = () => { const columnsWithWidthProperties = visibleColumns.filter(column => column.width ?? column.minWidth ?? column.maxWidth ?? false).map(column => ({ accessor: column.id ?? column.accessor, minWidth: column.minWidth, width: column.width, maxWidth: column.maxWidth })); let availableWidth = totalWidth; let defaultColumnsCount = visibleColumns.length; const columnsWithFixedWidth = columnsWithWidthProperties.map(column => { const { width, minWidth, maxWidth, accessor } = column; if (width) { // necessary because of default minWidth const acceptedWidth = accessor !== '__ui5wcr__internal_highlight_column' && accessor !== '__ui5wcr__internal_selection_column' && accessor !== '__ui5wcr__internal_navigation_column' && width < 60 ? 60 : width; availableWidth -= acceptedWidth; defaultColumnsCount--; return acceptedWidth; } const columnsWithMaxWidth = columnsWithWidthProperties.filter(item => item.maxWidth); const aggregatedColumnsMaxWidth = columnsWithMaxWidth.reduce((acc, cur) => acc + cur.maxWidth, 0); const aggregatedColumnsMinWidth = columnsWithWidthProperties.filter(item => item.minWidth && !item.maxWidth).reduce((acc, cur) => acc + cur.minWidth, 0); if (minWidth > availableWidth / defaultColumnsCount) { // don't apply minWidth if enough space is available because of maxWidth properties if (availableWidth - aggregatedColumnsMaxWidth > aggregatedColumnsMinWidth + (visibleColumns.length - columnsWithWidthProperties.length) * 60) { // apply minWidth only if it's larger than the calculated available width if (minWidth > (availableWidth - aggregatedColumnsMaxWidth) / columnsWithMaxWidth.length) { availableWidth -= minWidth; defaultColumnsCount--; return minWidth; } return false; } availableWidth -= minWidth; defaultColumnsCount--; return minWidth; } if (maxWidth < availableWidth / defaultColumnsCount) { availableWidth -= maxWidth; defaultColumnsCount--; return maxWidth; } return false; }).filter(Boolean); const fixedWidth = columnsWithFixedWidth.reduce((acc, val) => acc + val, 0); // check if columns are visible and table has width if (visibleColumns.length > 0 && totalWidth > 0) { // set fixedWidth as defaultWidth if all visible columns have fixed value if (visibleColumns.length === columnsWithFixedWidth.length) { return fixedWidth / visibleColumns.length; } // spread default columns if (totalWidth >= fixedWidth + defaultColumnsCount * DEFAULT_COLUMN_WIDTH) { return (totalWidth - fixedWidth) / defaultColumnsCount; } } return DEFAULT_COLUMN_WIDTH; }; const hasData = instance.data.length > 0; if (scaleWidthMode === AnalyticalTableScaleWidthMode.Default || !hasData && loading) { const defaultWidth = calculateDefaultTableWidth(); return columns.map(column => ({ ...column, width: column.width ?? defaultWidth })); } // AnalyticalTableScaleWidthMode.Grow const rowSample = rows.slice(0, ROW_SAMPLE_SIZE); const columnMeta = visibleColumns.reduce((acc, column) => { const columnIdOrAccessor = column.id ?? column.accessor; if (column.id === '__ui5wcr__internal_selection_column' || column.id === '__ui5wcr__internal_highlight_column' || column.id === '__ui5wcr__internal_navigation_column') { acc[columnIdOrAccessor] = { minHeaderWidth: column.width, fullWidth: column.width }; return acc; } const smartWidth = findLongestString(column.scaleWidthModeOptions?.headerString, column.scaleWidthModeOptions?.cellString); if (smartWidth) { const width = Math.max(stringToPx(smartWidth, uniqueId) + CELL_PADDING_PX, 60); acc[columnIdOrAccessor] = { minHeaderWidth: width, fullWidth: width }; return acc; } const minHeaderWidth = typeof column.Header === 'string' ? stringToPx(column.Header, uniqueId, true) + CELL_PADDING_PX : DEFAULT_COLUMN_WIDTH; acc[columnIdOrAccessor] = { minHeaderWidth, fullWidth: Math.max(minHeaderWidth, getContentPxAvg(rowSample, columnIdOrAccessor, uniqueId)) }; return acc; }, {}); let reservedWidth = visibleColumns.reduce((acc, column) => { const { minHeaderWidth, fullWidth } = columnMeta[column.id ?? column.accessor]; return acc + Math.max(column.minWidth || 0, column.width || 0, minHeaderWidth || 0, fullWidth) || 0; }, 0); let availableWidth = totalWidth - reservedWidth; if (availableWidth > 0) { let notReservedCount = 0; reservedWidth = visibleColumns.reduce((acc, column) => { const reserved = Math.max(column.minWidth || 0, column.width || 0) || 0; if (!reserved) { notReservedCount++; } return acc + reserved; }, 0); availableWidth = totalWidth - reservedWidth; return columns.map(column => { const isColumnVisible = (column.isVisible ?? true) && !hiddenColumns.includes(column.id ?? column.accessor); const meta = columnMeta[column.id ?? column.accessor]; if (isColumnVisible && meta) { const { minHeaderWidth } = meta; const targetWidth = availableWidth / notReservedCount; return { ...column, width: column.width ?? Math.min(targetWidth, MAX_WIDTH), minWidth: column.minWidth ?? minHeaderWidth }; } return column; }); } return columns.map(column => { const isColumnVisible = (column.isVisible ?? true) && !hiddenColumns.includes(column.id ?? column.accessor); const meta = columnMeta[column.id ?? column.accessor]; if (isColumnVisible && meta) { const { fullWidth } = meta; return { ...column, width: column.width ?? fullWidth, maxWidth: column.maxWidth ?? MAX_WIDTH }; } return column; }); }; export const useDynamicColumnWidths = hooks => { hooks.columns.push(columns); hooks.columnsDeps.push(columnsDeps); };