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
65 changes: 35 additions & 30 deletions packages/main/src/components/AnalyticalTable/demo/demo.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ 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 = [
{
Header: 'Name',
accessor: 'name' // String-based value accessors!
},
{
Header: 'Long Header Name and long Content',
accessor: 'longColumn'
},
{
Header: 'Age',
accessor: 'age',
Expand All @@ -25,7 +30,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: 'Friend Age',
accessor: 'friend.age',
hAlign: TextAlign.End,
filter: (rows, accessor, filterValue) => {
Expand Down Expand Up @@ -56,39 +61,39 @@ const columns = [
const data = generateData(200);
const dataTree = generateData(20, true);

export const defaultTable = () => {
const renderTable = () => {
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 please export this story again and remove the new export you have creating? having these nested component demos is breaking our storybook:
image

// const innerData = generateData(200);
Copy link
Contributor

Choose a reason for hiding this comment

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

please remove

Suggested change
// const innerData = generateData(200);

return (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<AnalyticalTable
title="Table Title"
data={data}
columns={columns}
loading={boolean('loading', false)}
busyIndicatorEnabled={boolean('busyIndicatorEnabled', true)}
alternateRowColor={boolean('alternateRowColor', false)}
sortable={boolean('sortable', true)}
filterable={boolean('filterable', true)}
visibleRows={number('visibleRows', 5)}
minRows={number('minRows', 5)}
groupable={boolean('groupable', true)}
selectionMode={select<TableSelectionMode>(
'selectionMode',
TableSelectionMode,
TableSelectionMode.SINGLE_SELECT
)}
onRowSelected={action('onRowSelected')}
onSort={action('onSort')}
onGroup={action('onGroup')}
onRowExpandChange={action('onRowExpandChange')}
groupBy={array('groupBy', [])}
rowHeight={number('rowHeight', 60)}
selectedRowIds={object('selectedRowIds', { 3: true })}
onColumnsReordered={action('onColumnsReordered')}
/>
</div>
<AnalyticalTable
title="Table Title"
data={data}
columns={columns}
loading={boolean('loading', false)}
busyIndicatorEnabled={boolean('busyIndicatorEnabled', true)}
alternateRowColor={boolean('alternateRowColor', false)}
sortable={boolean('sortable', true)}
filterable={boolean('filterable', true)}
visibleRows={number('visibleRows', 5)}
minRows={number('minRows', 5)}
groupable={boolean('groupable', true)}
selectionMode={select<TableSelectionMode>('selectionMode', 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', 44)}
selectedRowIds={object('selectedRowIds', { 3: true })}
onColumnsReordered={action('onColumnsReordered')}
/>
);
};

export const defaultTable = () => {
return <div style={{ display: 'flex', flexDirection: 'column' }}>{renderTable()}</div>;
};

defaultTable.story = {
name: 'Default'
};
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,155 @@
import { useCallback, useEffect, useMemo } from 'react';
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 = (scaleWidthMode, columns, rows, totalWidth, hiddenColumns, setColumnWidth) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

can we run this as a tableHook? just the our useXYZStyle hooks? then we would have access to all table data like columns, row, width, hidden columns, etc...

const updateTableSizes = useCallback(() => {
const visibleColumns = columns.filter(Boolean).filter((item) => {
return (item.isVisible ?? true) && !hiddenColumns.includes(item.accessor);
});
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) {
setColumnWidth(fixedWidth / visibleColumns.length);
return;
}
//spread default columns
if (totalWidth >= fixedWidth + defaultColumnsCount * DEFAULT_COLUMN_WIDTH) {
setColumnWidth((totalWidth - fixedWidth) / defaultColumnsCount);
} else {
//set defaultWidth for default columns if table is overflowing
setColumnWidth(DEFAULT_COLUMN_WIDTH);
}
} else {
setColumnWidth(DEFAULT_COLUMN_WIDTH);
}
}, [totalWidth, columns, hiddenColumns.length]);

useEffect(() => {
if (scaleWidthMode === TableScaleWidthMode.Default) {
updateTableSizes();
}
}, [scaleWidthMode, updateTableSizes]);

return useMemo(() => {
if (columns.length === 0 || !totalWidth || scaleWidthMode === TableScaleWidthMode.Default) return columns;

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

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;
});
}, [scaleWidthMode, columns, rows.length, totalWidth, hiddenColumns.length]); // too expensive to check for reference equality on rows
};
Loading