Skip to content
This repository has been archived by the owner on Jan 5, 2023. It is now read-only.

feat(Table): Added column resizing #448

Merged
merged 17 commits into from
Dec 10, 2021
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
526 changes: 522 additions & 4 deletions src/core/Table/Table.test.tsx

Large diffs are not rendered by default.

118 changes: 112 additions & 6 deletions src/core/Table/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
usePagination,
} from 'react-table';
import { ProgressRadial } from '../ProgressIndicators';
import { useTheme, CommonProps } from '../utils';
import { useTheme, CommonProps, useResizeObserver } from '../utils';
import '@itwin/itwinui-css/css/table.css';
import SvgSortDown from '@itwin/itwinui-icons-react/cjs/icons/SortDown';
import SvgSortUp from '@itwin/itwinui-icons-react/cjs/icons/SortUp';
Expand All @@ -35,15 +35,22 @@ import {
useSelectionCell,
useSubRowFiltering,
useSubRowSelection,
useResizeColumns,
} from './hooks';
import {
onExpandHandler,
onFilterHandler,
onSelectHandler,
} from './actionHandlers';
import { onSingleSelectHandler } from './actionHandlers/selectHandler';
import {
onTableResizeEnd,
onTableResizeStart,
} from './actionHandlers/resizeHandler';

const singleRowSelectedAction = 'singleRowSelected';
const tableResizeStartAction = 'tableResizeStart';
const tableResizeEndAction = 'tableResizeEnd';

export type TablePaginatorRendererProps = {
/**
Expand Down Expand Up @@ -198,6 +205,14 @@ export type TableProps<
* @default 25
*/
pageSize?: number;
/**
* Flag whether columns are resizable.
* In order to disable resizing for specific column, set `disableResizing: true` for that column.
*
* If you want to use it in older browsers e.g. IE, then you need to have `ResizeObserver` polyfill.
* @default false
*/
isResizable?: boolean;
} & Omit<CommonProps, 'title'>;

/**
Expand Down Expand Up @@ -278,6 +293,7 @@ export const Table = <
selectRowOnClick = true,
paginatorRenderer,
pageSize = 25,
isResizable = false,
...rest
} = props;

Expand Down Expand Up @@ -336,6 +352,14 @@ export const Table = <
onSelectHandler(newState, instance, onSelect, isRowDisabled);
break;
}
case tableResizeStartAction: {
newState = onTableResizeStart(newState);
break;
}
case tableResizeEndAction: {
newState = onTableResizeEnd(newState, action);
break;
}
default:
break;
}
Expand Down Expand Up @@ -373,6 +397,7 @@ export const Table = <
initialState: { pageSize, ...props.initialState },
},
useFlexLayout,
useResizeColumns(ownerDocument),
useFilters,
useSubRowFiltering(hasAnySubRows),
useSortBy,
Expand All @@ -397,6 +422,7 @@ export const Table = <
page,
gotoPage,
setPageSize,
flatHeaders,
} = instance;

const ariaDataAttributes = Object.entries(rest).reduce(
Expand Down Expand Up @@ -456,10 +482,59 @@ export const Table = <
],
);

const columnRefs = React.useRef<Record<string, HTMLDivElement>>({});
const previousTableWidth = React.useRef(0);
const onTableResize = React.useCallback(
({ width }: DOMRectReadOnly) => {
if (width === previousTableWidth.current) {
return;
}
previousTableWidth.current = width;

// Update column widths when table was resized
flatHeaders.forEach((header) => {
if (columnRefs.current[header.id]) {
header.resizeWidth = columnRefs.current[
header.id
].getBoundingClientRect().width;
}
});

// If no column was resized then leave table resize handling to the flexbox
if (Object.keys(state.columnResizing.columnWidths).length === 0) {
veekeys marked this conversation as resolved.
Show resolved Hide resolved
return;
}

dispatch({ type: tableResizeStartAction });
},
[dispatch, state.columnResizing.columnWidths, flatHeaders],
);
const [resizeRef] = useResizeObserver(onTableResize);

// Flexbox handles columns resize so we take new column widths before browser repaints.
React.useLayoutEffect(() => {
if (state.isTableResizing) {
const newColumnWidths: Record<string, number> = {};
flatHeaders.forEach((column) => {
if (columnRefs.current[column.id]) {
newColumnWidths[column.id] = columnRefs.current[
column.id
].getBoundingClientRect().width;
}
});
dispatch({ type: tableResizeEndAction, columnWidths: newColumnWidths });
}
});

return (
<>
<div
ref={(element) => setOwnerDocument(element?.ownerDocument)}
ref={(element) => {
setOwnerDocument(element?.ownerDocument);
if (isResizable) {
resizeRef(element);
}
}}
id={id}
{...getTableProps({
className: cx(
Expand All @@ -478,22 +553,39 @@ export const Table = <
});
return (
<div {...headerGroupProps} key={headerGroupProps.key}>
{headerGroup.headers.map((column) => {
{headerGroup.headers.map((column, index) => {
const {
onClick: onSortClick,
...sortByProps
} = column.getSortByToggleProps() as {
onClick:
| React.MouseEventHandler<HTMLDivElement>
| undefined;
style: React.CSSProperties;
title: string;
};
const columnProps = column.getHeaderProps({
...column.getSortByToggleProps(),
...sortByProps,
className: cx(
'iui-cell',
{ 'iui-actionable': column.canSort },
{ 'iui-sorted': column.isSorted },
column.columnClassName,
),
style: { ...getCellStyle(column) },
style: { ...getCellStyle(column, !!state.isTableResizing) },
});
return (
<div
{...columnProps}
key={columnProps.key}
title={undefined}
ref={(el) => {
if (el && isResizable) {
columnRefs.current[column.id] = el;
veekeys marked this conversation as resolved.
Show resolved Hide resolved
column.resizeWidth = el.getBoundingClientRect().width;
}
}}
onMouseDown={onSortClick}
bentleyvk marked this conversation as resolved.
Show resolved Hide resolved
>
{column.render('Header')}
{!isLoading && (data.length != 0 || areFiltersSet) && (
Expand All @@ -517,14 +609,28 @@ export const Table = <
)}
</div>
)}
{isResizable &&
column.isResizerVisible &&
index !== headerGroup.headers.length - 1 && (
<div
{...column.getResizerProps()}
className='iui-resizer'
>
<div className='iui-resizer-bar' />
</div>
)}
</div>
);
})}
</div>
);
})}
</div>
<div {...getTableBodyProps({ className: 'iui-table-body' })}>
<div
{...getTableBodyProps({
className: 'iui-table-body',
})}
>
{data.length !== 0 &&
page.map((row: Row<T>) => {
prepareRow(row);
Expand Down
2 changes: 1 addition & 1 deletion src/core/Table/TableCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export const TableCell = <T extends Record<string, unknown>>(
const cellElementProps = cell.getCellProps({
className: cx('iui-cell', cell.column.cellClassName),
style: {
...getCellStyle(cell.column),
...getCellStyle(cell.column, !!tableInstance.state.isTableResizing),
...getSubRowStyle(),
},
});
Expand Down
6 changes: 4 additions & 2 deletions src/core/Table/TableRowMemoized.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { TableCell } from './TableCell';
* Although state is not used it is needed for `React.memo` to check state that changes row state e.g. selection.
* When adding new features check whether it changes state that affects row. If it does then add equality check to `React.memo`.
*/
const TableRow = <T extends Record<string, unknown>>(props: {
export const TableRow = <T extends Record<string, unknown>>(props: {
veekeys marked this conversation as resolved.
Show resolved Hide resolved
row: Row<T>;
rowProps?: (row: Row<T>) => React.ComponentPropsWithRef<'div'>;
isLast: boolean;
Expand Down Expand Up @@ -145,5 +145,7 @@ export const TableRowMemoized = React.memo(
prevProp.isDisabled === nextProp.isDisabled &&
prevProp.rowProps === nextProp.rowProps &&
prevProp.expanderCell === nextProp.expanderCell &&
prevProp.tableHasSubRows === nextProp.tableHasSubRows,
prevProp.tableHasSubRows === nextProp.tableHasSubRows &&
!nextProp.state.columnResizing.isResizingColumn &&
!nextProp.state.isTableResizing,
) as typeof TableRow;
26 changes: 26 additions & 0 deletions src/core/Table/actionHandlers/resizeHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import { ActionType, TableState } from 'react-table';

export const onTableResizeStart = <T extends Record<string, unknown>>(
state: TableState<T>,
) => {
return { ...state, isTableResizing: true };
};
export const onTableResizeEnd = <T extends Record<string, unknown>>(
state: TableState<T>,
action: ActionType,
) => {
return {
...state,
isTableResizing: false,
columnResizing: {
...state.columnResizing,
columnWidths: {
...action.columnWidths,
},
},
};
};
1 change: 1 addition & 0 deletions src/core/Table/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export { EXPANDER_CELL_ID, useExpanderCell } from './useExpanderCell';
export { SELECTION_CELL_ID, useSelectionCell } from './useSelectionCell';
export { useSubRowFiltering } from './useSubRowFiltering';
export { useSubRowSelection } from './useSubRowSelection';
export { useResizeColumns } from './useResizeColumns';
Loading