Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(AnalyticalTable): add experimental feature to determine column widths based on content #295

Merged
merged 12 commits into from
Feb 4, 2020
Merged

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { TextAlign } from '@ui5/webcomponents-react/lib/TextAlign';
import { Title } from '@ui5/webcomponents-react/lib/Title';
import React from 'react';
import generateData from './generateData';
import { TableScaleWidthMode } from '../../../enums/TableScaleWidthMode';

const columns = [
{
Expand All @@ -25,7 +26,7 @@ const columns = [
accessor: 'friend.name'
},
{
Header: () => <span>Friend Age</span>, // Custom header components!
MarcusNotheis marked this conversation as resolved.
Show resolved Hide resolved
Header: () => <span>Friend Age</span>,
accessor: 'friend.age',
hAlign: TextAlign.End,
filter: (rows, accessor, filterValue) => {
Expand Down Expand Up @@ -76,12 +77,13 @@ export const defaultTable = () => {
TableSelectionMode,
TableSelectionMode.SINGLE_SELECT
)}
scaleWidthMode={select<TableScaleWidthMode>('scaleWidthMode', TableScaleWidthMode, TableScaleWidthMode.Default)}
onRowSelected={action('onRowSelected')}
onSort={action('onSort')}
onGroup={action('onGroup')}
onRowExpandChange={action('onRowExpandChange')}
groupBy={array('groupBy', [])}
rowHeight={number('rowHeight', 60)}
rowHeight={number('rowHeight', 44)}
selectedRowIds={object('selectedRowIds', { 3: true })}
onColumnsReordered={action('onColumnsReordered')}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ const makeTreeEntry = () => ({

const makeEntry = () => ({
name: getRandomName(),
longColumn: 'Really really long column content... don´t crop please',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add the long column to the treeTableData as well? otherwise this column is always empty. Or change the columns for the treeTable back to the old config.

age: getRandomNumber(18, 65),
friend: {
name: getRandomName(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const useColumnsDependencies = (hooks) => {
hooks.columnsDeps.push((deps, { instance: { state, webComponentsReactProperties } }) => {
return [state.tableClientWidth, state.hiddenColumns, webComponentsReactProperties.scaleWidthMode];
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { DEFAULT_COLUMN_WIDTH } from '../defaults/Column';
import { TableScaleWidthMode } from '@ui5/webcomponents-react/lib/TableScaleWidthMode';

const ROW_SAMPLE_SIZE = 20;
const DEFAULT_HEADER_NUM_CHAR = 10;
const MAX_WIDTH = 700;

// a function, which approximates header px sizes given a character length
const approximateHeaderPxFromCharLength = (charLength) =>
charLength < 15 ? Math.sqrt(charLength * 1500) : 8 * charLength;
const approximateContentPxFromCharLength = (charLength) => 8 * charLength;

export const useDynamicColumnWidths = (hooks) => {
hooks.columns.push((columns, { instance }) => {
if(!instance.state || !instance.rows) {
return columns;
}

const { rows, state } = instance;

const { hiddenColumns, tableClientWidth: totalWidth } = state;
const { scaleWidthMode } = instance.webComponentsReactProperties;

const visibleColumns = columns.filter(Boolean).filter((item) => {
return (item.isVisible ?? true) && !hiddenColumns.includes(item.accessor);
});

const calculateDefaultTableWidth = () => {
const columnsWithFixedWidth = visibleColumns.filter(({ width }) => width ?? false).map(({ width }) => width);
const fixedWidth = columnsWithFixedWidth.reduce((acc, val) => acc + val, 0);

const defaultColumnsCount = visibleColumns.length - columnsWithFixedWidth.length;

//check if columns are visible and table has width
if (visibleColumns.length > 0 && totalWidth > 0) {
//set fixedWidth as defaultWidth if 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;
} else {
//set defaultWidth for default columns if table is overflowing
return DEFAULT_COLUMN_WIDTH;
}
} else {
return DEFAULT_COLUMN_WIDTH;
}
};

if (columns.length === 0 || !totalWidth) return columns;

if (scaleWidthMode === TableScaleWidthMode.Default) {
const defaultWidth = calculateDefaultTableWidth();
return columns.map((column) => ({ ...column, width: column.width ?? defaultWidth }));
}

const rowSample = rows.slice(0, ROW_SAMPLE_SIZE);

const columnMeta = visibleColumns.reduce((acc, column) => {
const headerLength = typeof column.Header === 'string' ? column.Header.length : DEFAULT_HEADER_NUM_CHAR;

// max character length
const contentMaxCharLength = Math.max(
headerLength,
...rowSample.map((row) => {
const dataPoint = row.values?.[column.accessor];
if (dataPoint) {
if (typeof dataPoint === 'string') return dataPoint.length;
if (typeof dataPoint === 'number') return (dataPoint + '').length;
}
return 0;
})
);

// avg character length
const contentCharAvg =
rowSample.reduce((acc, item) => {
const dataPoint = item.values?.[column.accessor];
let val = 0;
if (dataPoint) {
if (typeof dataPoint === 'string') val = dataPoint.length;
if (typeof dataPoint === 'number') val = (dataPoint + '').length;
}
return acc + val;
}, 0) / rowSample.length;

const minHeaderWidth = approximateHeaderPxFromCharLength(headerLength);

acc[column.accessor] = {
minHeaderWidth,
fullWidth: Math.max(minHeaderWidth, approximateContentPxFromCharLength(contentMaxCharLength)),
contentCharAvg
};
return acc;
}, {});

const totalCharNum = Object.values(columnMeta).reduce(
(acc: number, item: any) => acc + item.contentCharAvg,
0
) as number;

let reservedWidth = visibleColumns.reduce((acc, column) => {
const { minHeaderWidth, fullWidth } = columnMeta[column.accessor];
return (
acc +
Math.max(
column.minWidth || 0,
column.width || 0,
minHeaderWidth || 0,
scaleWidthMode === TableScaleWidthMode.Grow ? fullWidth : 0
) || 0
);
}, 0);

let availableWidth = totalWidth - reservedWidth;

if (scaleWidthMode === TableScaleWidthMode.Smart || availableWidth > 0) {
if (scaleWidthMode === TableScaleWidthMode.Grow) {
reservedWidth = visibleColumns.reduce((acc, column) => {
const { minHeaderWidth } = columnMeta[column.accessor];
return acc + Math.max(column.minWidth || 0, column.width || 0, minHeaderWidth || 0) || 0;
}, 0);
availableWidth = totalWidth - reservedWidth;
}

return columns.map((column) => {
const isColumnVisible = (column.isVisible ?? true) && !hiddenColumns.includes(column.accessor);
if (totalCharNum > 0 && isColumnVisible) {
const { minHeaderWidth, contentCharAvg } = columnMeta[column.accessor];
const targetWidth = (contentCharAvg / totalCharNum) * availableWidth + minHeaderWidth;

return {
...column,
width: column.width ?? targetWidth,
minWidth: column.minWidth ?? minHeaderWidth
};
}

return column;
});
}

// TableScaleWidthMode Grow
return columns.map((column) => {
const isColumnVisible = (column.isVisible ?? true) && !hiddenColumns.includes(column.accessor);
if (isColumnVisible) {
const { fullWidth } = columnMeta[column.accessor];
return {
...column,
width: column.width ?? fullWidth,
maxWidth: MAX_WIDTH
};
}
return column;
});
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { TableSelectionMode } from '@ui5/webcomponents-react/lib/TableSelectionM
const ROW_SELECTION_ATTRIBUTE = 'data-is-selected';

export const useTableRowStyling = (hooks) => {

hooks.getRowProps.push((passedRowProps, { instance, row }) => {
const { classes, selectionMode, onRowSelected } = instance.webComponentsReactProperties;
const isEmptyRow = row.original?.emptyRow;
Expand Down
82 changes: 31 additions & 51 deletions packages/main/src/components/AnalyticalTable/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Device } from '@ui5/webcomponents-react-base/lib/Device';
import { Event } from '@ui5/webcomponents-react-base/lib/Event';
import { StyleClassHelper } from '@ui5/webcomponents-react-base/lib/StyleClassHelper';
import { usePassThroughHtmlProps } from '@ui5/webcomponents-react-base/lib/usePassThroughHtmlProps';
Expand All @@ -16,8 +15,7 @@ import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState
useRef
} from 'react';
import { createUseStyles } from 'react-jss';
import {
Expand All @@ -37,7 +35,7 @@ import { CommonProps } from '../../interfaces/CommonProps';
import { JSSTheme } from '../../interfaces/JSSTheme';
import styles from './AnayticalTable.jss';
import { ColumnHeader } from './ColumnHeader';
import { DEFAULT_COLUMN_WIDTH, DefaultColumn } from './defaults/Column';
import { DefaultColumn } from './defaults/Column';
import { DefaultLoadingComponent } from './defaults/LoadingComponent';
import { TablePlaceholder } from './defaults/LoadingComponent/TablePlaceholder';
import { DefaultNoDataComponent } from './defaults/NoDataComponent';
Expand All @@ -52,6 +50,9 @@ import { useToggleRowExpand } from './hooks/useToggleRowExpand';
import { stateReducer } from './tableReducer/stateReducer';
import { TitleBar } from './TitleBar';
import { VirtualTableBody } from './virtualization/VirtualTableBody';
import { useDynamicColumnWidths } from './hooks/useDynamicColumnWidths';
import { TableScaleWidthMode } from '../../enums/TableScaleWidthMode';
import {useColumnsDependencies} from "./hooks/useColumnsDependencies";

export interface ColumnConfiguration extends Column {
accessor?: string;
Expand Down Expand Up @@ -100,6 +101,7 @@ export interface TableProps extends CommonProps {
groupable?: boolean;
groupBy?: string[];
selectionMode?: TableSelectionMode;
scaleWidthMode?: TableScaleWidthMode;
columnOrder?: object[];

// events
Expand Down Expand Up @@ -157,7 +159,8 @@ const AnalyticalTable: FC<TableProps> = forwardRef((props: TableProps, ref: Ref<
minRows,
isTreeTable,
alternateRowColor,
overscanCount
overscanCount,
scaleWidthMode
} = props;

const classes = useStyles({ rowHeight: props.rowHeight });
Expand All @@ -167,18 +170,6 @@ const AnalyticalTable: FC<TableProps> = forwardRef((props: TableProps, ref: Ref<

const getSubRows = useCallback((row) => row[subRowsKey] || [], [subRowsKey]);

const [columnWidth, setColumnWidth] = useState(null);

const defaultColumn = useMemo(() => {
if (columnWidth) {
return {
width: columnWidth,
...DefaultColumn
};
}
return DefaultColumn;
}, [columnWidth]);

const data = useMemo(() => {
if (minRows > props.data.length) {
const missingRows = minRows - props.data.length;
Expand All @@ -190,6 +181,7 @@ const AnalyticalTable: FC<TableProps> = forwardRef((props: TableProps, ref: Ref<
return props.data;
}, [props.data, minRows]);


const {
getTableProps,
headerGroups,
Expand All @@ -204,15 +196,17 @@ const AnalyticalTable: FC<TableProps> = forwardRef((props: TableProps, ref: Ref<
{
columns,
data,
defaultColumn,
defaultColumn: DefaultColumn,
getSubRows,
stateReducer,
webComponentsReactProperties: {
selectionMode,
classes,
onRowSelected,
onRowExpandChange,
isTreeTable
isTreeTable,
// tableClientWidth,
scaleWidthMode
},
...reactTableOptions
},
Expand All @@ -228,45 +222,30 @@ const AnalyticalTable: FC<TableProps> = forwardRef((props: TableProps, ref: Ref<
useTableHeaderGroupStyling,
useTableHeaderStyling,
useTableRowStyling,
useDynamicColumnWidths,
useColumnsDependencies,
useTableCellStyling,
useToggleRowExpand,
...tableHooks
);

const updateTableSizes = useCallback(() => {
const visibleColumns = columns.filter(Boolean).filter((item) => {
return (item.isVisible ?? true) && !tableState.hiddenColumns.includes(item.accessor);
});
const columnsWithFixedWidth = visibleColumns.filter(({ width }) => width ?? false).map(({ width }) => width);
const fixedWidth = columnsWithFixedWidth.reduce((acc, val) => acc + val, 0);

const tableClientWidth = tableRef.current.clientWidth;
const defaultColumnsCount = visibleColumns.length - columnsWithFixedWidth.length;

//check if columns are visible and table has width
if (visibleColumns.length > 0 && tableClientWidth > 0) {
//set fixedWidth as defaultWidth if visible columns have fixed value
if (visibleColumns.length === columnsWithFixedWidth.length) {
setColumnWidth(fixedWidth / visibleColumns.length);
return;
const updateTableClientWidth = useCallback(() => {
requestAnimationFrame(() => {
if (tableRef.current) {
dispatch({ type: 'TABLE_RESIZE', payload: { tableClientWidth: tableRef.current.clientWidth }});
}
//spread default columns
if (tableClientWidth >= fixedWidth + defaultColumnsCount * DEFAULT_COLUMN_WIDTH) {
setColumnWidth((tableClientWidth - fixedWidth) / defaultColumnsCount);
} else {
//set defaultWidth for default columns if table is overflowing
setColumnWidth(DEFAULT_COLUMN_WIDTH);
}
} else {
setColumnWidth(DEFAULT_COLUMN_WIDTH);
}
}, [tableRef.current, columns, tableState.hiddenColumns, DEFAULT_COLUMN_WIDTH]);
});
}, []);

// @ts-ignore
const tableWidthObserver = useRef(new ResizeObserver(updateTableClientWidth));

useEffect(() => {
updateTableSizes();
Device.resize.attachHandler(updateTableSizes, null);
return () => Device.resize.detachHandler(updateTableSizes, null);
}, [updateTableSizes]);
tableWidthObserver.current.observe(tableRef.current);
return () => {
tableWidthObserver.current.disconnect();
};
}, [tableWidthObserver.current, tableRef.current]);

useEffect(() => {
dispatch({ type: 'SET_GROUP_BY', payload: groupBy });
Expand Down Expand Up @@ -347,7 +326,7 @@ const AnalyticalTable: FC<TableProps> = forwardRef((props: TableProps, ref: Ref<
{title && <TitleBar>{title}</TitleBar>}
{typeof renderExtension === 'function' && <div>{renderExtension()}</div>}
<div className={tableContainerClasses.valueOf()} ref={tableRef}>
{columnWidth && (
{(
<div {...getTableProps()} role="table" aria-rowcount={rows.length}>
{headerGroups.map((headerGroup) => {
let headerProps = {};
Expand Down Expand Up @@ -430,6 +409,7 @@ AnalyticalTable.defaultProps = {
filterable: false,
groupable: false,
selectionMode: TableSelectionMode.NONE,
scaleWidthMode: TableScaleWidthMode.Default,
data: [],
columns: [],
title: null,
Expand Down
Loading