From 4cbde4319dcf62fa3c67907180aa6649fde0bbb2 Mon Sep 17 00:00:00 2001 From: Danail Hadjiatanasov Date: Wed, 4 Nov 2020 15:40:31 +0100 Subject: [PATCH] [DataGrid] Add fluid columns width support (#480) * [DataGrid] Add fluid columns width support * Fix doc formatting * Fix documentation so that it is complient to the MUI documentation standarts * Add unit tests * Update packages/grid/_modules_/grid/hooks/root/useColumns.ts Co-authored-by: Olivier Tassinari * Update packages/grid/_modules_/grid/models/colDef/colDef.ts Co-authored-by: Olivier Tassinari * Fix PR comments * Fix unit tests * Resolve TS issues * Add // @ts-expect-error need to migrate helpers to TypeScript before toHaveInlineStyle * Add storybook examples * Update docs/src/pages/components/data-grid/columns/columns.md Co-authored-by: Matt * Update docs/src/pages/components/data-grid/columns/columns.md Co-authored-by: Matt * Update docs/src/pages/components/data-grid/columns/columns.md Co-authored-by: Matt * Update docs/src/pages/components/data-grid/columns/columns.md Co-authored-by: Matt * Update docs/src/pages/components/data-grid/columns/columns.md Co-authored-by: Matt * Update packages/grid/data-grid/src/DataGrid.test.tsx Co-authored-by: Matt * Fix storybook flex col width examples * rerun ci * Fix formatting * Update docs/src/pages/components/data-grid/columns/columns.md * Trigger CI Co-authored-by: Olivier Tassinari Co-authored-by: Matt --- .../data-grid/columns/ColumnFluidWidthGrid.js | 34 +++++ .../columns/ColumnFluidWidthGrid.tsx | 34 +++++ .../components/data-grid/columns/columns.md | 16 +++ .../styled-wrappers/GridRootStyles.ts | 4 + .../grid/hooks/features/columns/useColumns.ts | 42 ++++++- .../_modules_/grid/models/colDef/colDef.ts | 4 + packages/grid/data-grid/src/DataGrid.test.tsx | 118 ++++++++++++++++++ .../src/stories/grid-columns.stories.tsx | 51 ++++++++ 8 files changed, 300 insertions(+), 3 deletions(-) create mode 100644 docs/src/pages/components/data-grid/columns/ColumnFluidWidthGrid.js create mode 100644 docs/src/pages/components/data-grid/columns/ColumnFluidWidthGrid.tsx diff --git a/docs/src/pages/components/data-grid/columns/ColumnFluidWidthGrid.js b/docs/src/pages/components/data-grid/columns/ColumnFluidWidthGrid.js new file mode 100644 index 000000000000..767f6bca655c --- /dev/null +++ b/docs/src/pages/components/data-grid/columns/ColumnFluidWidthGrid.js @@ -0,0 +1,34 @@ +import * as React from 'react'; +import { DataGrid } from '@material-ui/data-grid'; + +const rows = [ + { + id: 1, + username: 'defunkt', + age: 38, + }, +]; + +export default function ColumnFluidWidthGrid() { + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/columns/ColumnFluidWidthGrid.tsx b/docs/src/pages/components/data-grid/columns/ColumnFluidWidthGrid.tsx new file mode 100644 index 000000000000..767f6bca655c --- /dev/null +++ b/docs/src/pages/components/data-grid/columns/ColumnFluidWidthGrid.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import { DataGrid } from '@material-ui/data-grid'; + +const rows = [ + { + id: 1, + username: 'defunkt', + age: 38, + }, +]; + +export default function ColumnFluidWidthGrid() { + return ( +
+ +
+ ); +} diff --git a/docs/src/pages/components/data-grid/columns/columns.md b/docs/src/pages/components/data-grid/columns/columns.md index d0ed067d9b3c..f8c112880071 100644 --- a/docs/src/pages/components/data-grid/columns/columns.md +++ b/docs/src/pages/components/data-grid/columns/columns.md @@ -49,6 +49,22 @@ To change the width of a column, use the `width` property available in `ColDef`. {{"demo": "pages/components/data-grid/columns/ColumnWidthGrid.js", "bg": "inline"}} +### Fluid width + +Each column has a fixed width of 100 pixels by default, but column fluidity (responsiveness) can be by achieved by setting the `flex` property in `ColDef`. + +The `flex` property accepts a value between 0 and ∞. + +The `flex` property works by dividing the remaining space in the grid among all flex columns in proportion to their `flex` value. +For example, consider a grid with a total width of 500px that has three columns: the first with `width: 200`; the second with `flex: 1`; and third with `flex: 0.5`. +The first column will be 200px wide, leaving 300px remaining. The column with `flex: 1` is twice the size of `flex: 0.5`, which means that final sizes will be: 200px, 200px, 100px. + +Note that `flex` doesn't work together with `width`. If you set both `flex` and `width` in `ColDef`, `flex` will override `width`. + +In addition, `flex` does not work if the combined width of the columns that have `width` is more than the width of the grid itself. If that is the case a scroll bar will be visible, and the columns that have `flex` will default back to their base value of 100px. + +{{"demo": "pages/components/data-grid/columns/ColumnFluidWidthGrid.js", "bg": "inline"}} + ## Column resizing [⚡️](https://material-ui.com/store/items/material-ui-x/) By default, `XGrid` allows all columns to be resized by dragging the right portion of the column separator. diff --git a/packages/grid/_modules_/grid/components/styled-wrappers/GridRootStyles.ts b/packages/grid/_modules_/grid/components/styled-wrappers/GridRootStyles.ts index 336d6e659841..1050631096c2 100644 --- a/packages/grid/_modules_/grid/components/styled-wrappers/GridRootStyles.ts +++ b/packages/grid/_modules_/grid/components/styled-wrappers/GridRootStyles.ts @@ -126,6 +126,10 @@ export const useStyles = makeStyles( cursor: 'col-resize', '&:hover, &.Mui-resizing': { color: theme.palette.text.primary, + // Reset on touch devices, it doesn't add specificity + '@media (hover: none)': { + color: borderColor, + }, }, }, '& .MuiDataGrid-iconSeparator': { diff --git a/packages/grid/_modules_/grid/hooks/features/columns/useColumns.ts b/packages/grid/_modules_/grid/hooks/features/columns/useColumns.ts index 8f1224824cf5..3442c0a125da 100644 --- a/packages/grid/_modules_/grid/hooks/features/columns/useColumns.ts +++ b/packages/grid/_modules_/grid/hooks/features/columns/useColumns.ts @@ -16,14 +16,47 @@ import { useApiMethod } from '../../root/useApiMethod'; import { Logger, useLogger } from '../../utils/useLogger'; import { useGridState } from '../core/useGridState'; +function mapColumns( + columns: Columns, + columnTypes: ColumnTypesRecord, + containerWidth: number, +): Columns { + let extendedColumns = columns.map((c) => ({ ...getColDef(columnTypes, c.type), ...c })); + const numberOfFluidColumns = columns.filter((column) => !!column.flex).length; + let flexDivider = 0; + + if (numberOfFluidColumns && containerWidth) { + extendedColumns.forEach((column) => { + if (!column.flex) { + containerWidth -= column.width!; + } else { + flexDivider += column.flex; + } + }); + } + + if (containerWidth > 0 && numberOfFluidColumns) { + const flexMultiplier = containerWidth / flexDivider; + extendedColumns = extendedColumns.map((column) => { + return { + ...column, + width: column.flex! ? Math.floor(flexMultiplier * column.flex!) : column.width, + }; + }); + } + + return extendedColumns; +} + function hydrateColumns( columns: Columns, columnTypes: ColumnTypesRecord, + containerWidth: number, withCheckboxSelection: boolean, logger: Logger, ): Columns { logger.debug('Hydrating Columns with default definitions'); - let mappedCols = columns.map((c) => ({ ...getColDef(columnTypes, c.type), ...c })); + let mappedCols = mapColumns(columns, columnTypes, containerWidth); if (withCheckboxSelection) { mappedCols = [checkboxSelectionColDef, ...mappedCols]; } @@ -58,6 +91,7 @@ function toMeta(logger: Logger, visibleColumns: Columns): ColumnsMeta { const resetState = ( columns: Columns, columnTypes: ColumnTypesRecord, + containerWidth: number, withCheckboxSelection: boolean, logger: Logger, ): InternalColumns => { @@ -65,7 +99,7 @@ const resetState = ( return getInitialColumnsState(); } - const all = hydrateColumns(columns, columnTypes, withCheckboxSelection, logger); + const all = hydrateColumns(columns, columnTypes, containerWidth, withCheckboxSelection, logger); const visible = filterVisible(logger, all); const meta = toMeta(logger, visible); const lookup = toLookup(logger, all); @@ -115,7 +149,7 @@ const getUpdatedColumnState = ( export function useColumns(columns: Columns, apiRef: ApiRef): InternalColumns { const logger = useLogger('useColumns'); const [gridState, setGridState, forceUpdate] = useGridState(apiRef); - + const viewportWidth = gridState.containerSizes ? gridState.containerSizes.viewportSize.width : 0; const updateState = React.useCallback( (newState: InternalColumns, emit = true) => { logger.debug('Updating columns state.'); @@ -134,6 +168,7 @@ export function useColumns(columns: Columns, apiRef: ApiRef): InternalColumns { const newState = resetState( columns, gridState.options.columnTypes, + viewportWidth, !!gridState.options.checkboxSelection, logger, ); @@ -142,6 +177,7 @@ export function useColumns(columns: Columns, apiRef: ApiRef): InternalColumns { columns, gridState.options.columnTypes, gridState.options.checkboxSelection, + viewportWidth, logger, updateState, ]); diff --git a/packages/grid/_modules_/grid/models/colDef/colDef.ts b/packages/grid/_modules_/grid/models/colDef/colDef.ts index 089f4cd4363e..5c2a32b55f2f 100644 --- a/packages/grid/_modules_/grid/models/colDef/colDef.ts +++ b/packages/grid/_modules_/grid/models/colDef/colDef.ts @@ -32,6 +32,10 @@ export interface ColDef { * @default 100 */ width?: number; + /** + * If set, it indicates that a column has fluid width. Range [0, ∞]. + */ + flex?: number; /** * If `true`, hide the column. * @default false; diff --git a/packages/grid/data-grid/src/DataGrid.test.tsx b/packages/grid/data-grid/src/DataGrid.test.tsx index 64edc84b33e7..6fd967c0236f 100644 --- a/packages/grid/data-grid/src/DataGrid.test.tsx +++ b/packages/grid/data-grid/src/DataGrid.test.tsx @@ -181,4 +181,122 @@ describe('', () => { ); }); }); + + describe('column width', () => { + before(function beforeHook() { + if (/jsdom/.test(window.navigator.userAgent)) { + // Need layouting + this.skip(); + } + }); + + it('should set the columns width to 100px by default', () => { + const rows = [ + { + id: 1, + username: 'John Doe', + age: 30, + }, + ]; + + const columns = [ + { + field: 'id', + }, + { + field: 'name', + }, + { + field: 'age', + }, + ]; + + const { getAllByRole } = render( +
+ +
, + ); + + const DOMColumns = getAllByRole('columnheader'); + DOMColumns.forEach((col) => { + // @ts-expect-error need to migrate helpers to TypeScript + expect(col).toHaveInlineStyle({ width: '100px' }); + }); + }); + + it('should set the columns width value to what is provided', () => { + const rows = [ + { + id: 1, + username: 'John Doe', + age: 30, + }, + ]; + + const colWidthValues = [50, 50, 200]; + const columns = [ + { + field: 'id', + width: colWidthValues[0], + }, + { + field: 'name', + width: colWidthValues[1], + }, + { + field: 'age', + width: colWidthValues[2], + }, + ]; + + const { getAllByRole } = render( +
+ +
, + ); + + const DOMColumns = getAllByRole('columnheader'); + DOMColumns.forEach((col, index) => { + // @ts-expect-error need to migrate helpers to TypeScript + expect(col).toHaveInlineStyle({ width: `${colWidthValues[index]}px` }); + }); + }); + + it('should set the first column to be twice as wide as the second one', () => { + const rows = [ + { + id: 1, + username: 'John Doe', + age: 30, + }, + ]; + + const columns = [ + { + field: 'id', + flex: 1, + }, + { + field: 'name', + flex: 0.5, + }, + ]; + + render( +
+ +
, + ); + + const firstColumn = document.querySelector('[role="columnheader"][aria-colindex="1"]'); + const secondColumn: HTMLElement | null = document.querySelector( + '[role="columnheader"][aria-colindex="2"]', + ); + const secondColumnWidthVal = secondColumn!.style.width.split('px')[0]; + // @ts-expect-error need to migrate helpers to TypeScript + expect(firstColumn).toHaveInlineStyle({ + width: `${2 * parseInt(secondColumnWidthVal, 10)}px`, + }); + }); + }); }); diff --git a/packages/storybook/src/stories/grid-columns.stories.tsx b/packages/storybook/src/stories/grid-columns.stories.tsx index 04499fee39c8..462d60b3100d 100644 --- a/packages/storybook/src/stories/grid-columns.stories.tsx +++ b/packages/storybook/src/stories/grid-columns.stories.tsx @@ -221,3 +221,54 @@ export function NewColumnTypes() { ); } + +export const FewFlexColumns = () => { + const data = useData(20, 3); + const transformColSizes = React.useCallback( + (columns: ColDef[]) => + columns.map((col, index) => + index % 2 === 0 ? { ...col, flex: index + 1 } : { ...col, width: 200 }, + ), + [], + ); + + return ( +
+ +
+ ); +}; + +export const SeveralFlexColumn = () => { + const data = useData(20, 7); + const transformColSizes = React.useCallback( + (columns: ColDef[]) => + columns.map((col, index) => + index % 3 !== 0 ? { ...col, flex: index } : { ...col, flex: 1 }, + ), + [], + ); + + return ( +
+ +
+ ); +}; + +export const FlexColumnWidth2000 = () => { + const data = useData(20, 3); + const transformColSizes = React.useCallback( + (columns: ColDef[]) => + columns.map((col, index) => + index % 2 !== 0 ? { ...col, width: 2000 } : { ...col, flex: index + 1 }, + ), + [], + ); + + return ( +
+ +
+ ); +};