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 4 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
470 changes: 466 additions & 4 deletions src/core/Table/Table.test.tsx

Large diffs are not rendered by default.

125 changes: 119 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,6 +35,7 @@ import {
useSelectionCell,
useSubRowFiltering,
useSubRowSelection,
useResizeColumns,
} from './hooks';
import {
onExpandHandler,
Expand All @@ -44,6 +45,8 @@ import {
import { onSingleSelectHandler } from './actionHandlers/selectHandler';

const singleRowSelectedAction = 'singleRowSelected';
const tableResizingAction = 'tableResizing';
const tableResizedAction = 'tableResized';

export type TablePaginatorRendererProps = {
/**
Expand Down Expand Up @@ -198,6 +201,12 @@ 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.
* @default false
*/
isResizable?: boolean;
} & Omit<CommonProps, 'title'>;

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

Expand Down Expand Up @@ -336,6 +346,26 @@ export const Table = <
onSelectHandler(newState, instance, onSelect, isRowDisabled);
break;
}
case tableResizingAction: {
newState = {
...newState,
isTableResizing: true,
};
break;
}
case tableResizedAction: {
newState = {
...newState,
isTableResizing: false,
columnResizing: {
...newState.columnResizing,
columnWidths: {
...action.columnWidths,
},
},
};
break;
}
default:
break;
}
Expand Down Expand Up @@ -373,6 +403,7 @@ export const Table = <
initialState: { pageSize, ...props.initialState },
},
useFlexLayout,
useResizeColumns(ownerDocument),
useFilters,
useSubRowFiltering(hasAnySubRows),
useSortBy,
Expand All @@ -397,6 +428,7 @@ export const Table = <
page,
gotoPage,
setPageSize,
flatHeaders,
} = instance;

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

const columnRefs = React.useRef<Record<string, HTMLDivElement>>({});
const isTableResizing = React.useRef(false);
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].offsetWidth;
}
});

// Leave resize handling to the flex
if (Object.keys(state.columnResizing.columnWidths).length === 0) {
veekeys marked this conversation as resolved.
Show resolved Hide resolved
return;
}

isTableResizing.current = true;
dispatch({ type: tableResizingAction });
},
[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 (isTableResizing.current) {
isTableResizing.current = false;
const newColumnWidths: Record<string, number> = {};
flatHeaders.forEach((column) => {
if (columnRefs.current[column.id]) {
newColumnWidths[column.id] =
columnRefs.current[column.id].offsetWidth;
}
});
dispatch({ type: tableResizedAction, 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 +559,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) {
columnRefs.current[column.id] = el;
veekeys marked this conversation as resolved.
Show resolved Hide resolved
column.resizeWidth = el.offsetWidth;
}
}}
onMouseDown={onSortClick}
bentleyvk marked this conversation as resolved.
Show resolved Hide resolved
>
{column.render('Header')}
{!isLoading && (data.length != 0 || areFiltersSet) && (
Expand All @@ -517,14 +615,29 @@ export const Table = <
)}
</div>
)}
{isResizable &&
column.isResizerVisible &&
index !== headerGroup.headers.length - 1 && (
<div
{...column.getResizerProps()}
className='iui-resizer'
onClick={(e) => e.stopPropagation()}
>
<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;
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