Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 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
5 changes: 5 additions & 0 deletions src-docs/src/views/datagrid/_snippets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ inMemory={{ level: 'sorting' }}`,
)}`,
renderFooterCellValue:
'renderFooterCellValue={({ rowIndex, columnId }) => {}}',
renderCustomGridBody: `// Optional; advanced usage only. This render function is an escape hatch for consumers who need to opt out of virtualization or otherwise need total custom control over how data grid cells are rendered.

renderCustomDataGridBody={({ visibleColumns, visibleRowData, Cell }) => (
<Cell colIndex={mappedFromVisibleColumns} visibleRowIndex={mappedFromVisibleRowData} />
)}`,
pagination: `pagination={{
pageIndex: 1,
pageSize: 100,
Expand Down
285 changes: 285 additions & 0 deletions src-docs/src/views/datagrid/advanced/custom_renderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
import React, { useEffect, useCallback, useState } from 'react';
import { css } from '@emotion/react';
import { faker } from '@faker-js/faker';

import {
EuiDataGrid,
EuiDataGridProps,
EuiDataGridCustomBodyProps,
EuiDataGridColumnCellActionProps,
EuiScreenReaderOnly,
EuiCheckbox,
EuiButtonIcon,
EuiIcon,
EuiFlexGroup,
EuiSwitch,
EuiSpacer,
useEuiTheme,
logicalCSS,
} from '../../../../../src';

const raw_data: Array<{ [key: string]: string }> = [];
for (let i = 1; i < 100; i++) {
raw_data.push({
name: `${faker.name.lastName()}, ${faker.name.firstName()}`,
email: faker.internet.email(),
location: `${faker.address.city()}, ${faker.address.country()}`,
date: `${faker.date.past()}`,
amount: faker.commerce.price(1, 1000, 2, '$'),
});
}

const columns = [
{
id: 'name',
displayAsText: 'Name',
cellActions: [
({ Component }: EuiDataGridColumnCellActionProps) => (
<Component
onClick={() => alert('action')}
iconType="faceHappy"
aria-label="Some action"
>
Some action
</Component>
),
],
},
{
id: 'email',
displayAsText: 'Email address',
initialWidth: 130,
},
{
id: 'location',
displayAsText: 'Location',
},
{
id: 'date',
displayAsText: 'Date',
},
{
id: 'amount',
displayAsText: 'Amount',
},
];

const leadingControlColumns: EuiDataGridProps['leadingControlColumns'] = [
{
id: 'selection',
width: 32,
headerCellRender: () => (
<EuiCheckbox
id="select-all-rows"
aria-label="Select all rows"
onChange={() => {}}
/>
),
rowCellRender: ({ rowIndex }) => (
<EuiCheckbox
id={`select-row-${rowIndex}`}
aria-label="Select row"
onChange={() => {}}
/>
),
},
];

const trailingControlColumns: EuiDataGridProps['trailingControlColumns'] = [
{
id: 'actions',
width: 40,
headerCellRender: () => (
<EuiScreenReaderOnly>
<span>Actions</span>
</EuiScreenReaderOnly>
),
rowCellRender: () => (
<EuiButtonIcon iconType="boxesHorizontal" aria-label="See row actions" />
),
},
];

// The custom row details is actually a trailing control column cell with
// a hidden header. This is important for accessibility and markup reasons
// @see https://fuschia-stretch.glitch.me/ for more
Comment on lines +103 to +105
Copy link
Member

Choose a reason for hiding this comment

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

TIL, thank you 👍

const rowDetails: EuiDataGridProps['trailingControlColumns'] = [
{
id: 'row-details',

// The header cell should be visually hidden, but available to screen readers
width: 0,
headerCellRender: () => <>Row details</>,
headerCellProps: { className: 'euiScreenReaderOnly' },

// The footer cell can be hidden to both visual & SR users, as it does not contain meaningful information
footerCellProps: { style: { display: 'none' } },

// When rendering this custom cell, we'll want to override
// the automatic width/heights calculated by EuiDataGrid
rowCellRender: ({ setCellProps, rowIndex }) => {
setCellProps({ style: { width: '100%', height: 'auto' } });

const firstName = raw_data[rowIndex].name.split(', ')[1];
const isGood = faker.datatype.boolean();
return (
<>
{firstName}&apos;s account has {isGood ? 'no' : ''} outstanding fees.{' '}
<EuiIcon
type={isGood ? 'checkInCircleFilled' : 'error'}
color={isGood ? 'success' : 'danger'}
/>
</>
);
},
},
];

const footerCellValues: { [key: string]: string } = {
amount: `Total: ${raw_data
.reduce((acc, { amount }) => acc + Number(amount.split('$')[1]), 0)
.toLocaleString('en-US', { style: 'currency', currency: 'USD' })}`,
};

const RenderFooterCellValue: EuiDataGridProps['renderFooterCellValue'] = ({
columnId,
setCellProps,
}) => {
const value = footerCellValues[columnId];

useEffect(() => {
// Turn off the cell expansion button if the footer cell is empty
if (!value) setCellProps({ isExpandable: false });
}, [value, setCellProps, columnId]);

return value || null;
};

export default () => {
const [autoHeight, setAutoHeight] = useState(true);
const [showRowDetails, setShowRowDetails] = useState(false);

// Column visibility
const [visibleColumns, setVisibleColumns] = useState(() =>
columns.map(({ id }) => id)
);

// Pagination
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 });
const onChangePage = useCallback((pageIndex) => {
setPagination((pagination) => ({ ...pagination, pageIndex }));
}, []);
const onChangePageSize = useCallback((pageSize) => {
setPagination((pagination) => ({ ...pagination, pageSize }));
}, []);

// Sorting
const [sortingColumns, setSortingColumns] = useState([]);
const onSort = useCallback((sortingColumns) => {
setSortingColumns(sortingColumns);
}, []);

const { euiTheme } = useEuiTheme();

// Custom grid body renderer
const RenderCustomGridBody = useCallback(
({ Cell, visibleColumns, visibleRowData }: EuiDataGridCustomBodyProps) => {
const visibleRows = raw_data.slice(
visibleRowData.startRow,
visibleRowData.endRow
);

const styles = {
row: css`
${logicalCSS('width', 'fit-content')};
${logicalCSS('border-bottom', euiTheme.border.thin)};
background-color: ${euiTheme.colors.emptyShade};
`,
rowCellsWrapper: css`
display: flex;
`,
rowDetailsWrapper: css`
text-align: center;
background-color: ${euiTheme.colors.body};
`,
};

return (
<>
{visibleRows.map((row, rowIndex) => (
<div role="row" css={styles.row} key={rowIndex}>
<div css={styles.rowCellsWrapper}>
{visibleColumns.map((column, colIndex) => {
// Skip the row details cell - we'll render it manually outside of the flex wrapper
if (column.id !== 'row-details') {
return (
<Cell
colIndex={colIndex}
visibleRowIndex={rowIndex}
key={`${rowIndex},${colIndex}`}
/>
);
}
})}
</div>
{showRowDetails && (
<div css={styles.rowDetailsWrapper}>
<Cell
colIndex={visibleColumns.length - 1} // If the row is being shown, it should always be the last index
visibleRowIndex={rowIndex}
/>
</div>
)}
</div>
))}
</>
);
},
[showRowDetails, euiTheme]
);

return (
<>
<EuiFlexGroup alignItems="center">
<EuiSwitch
label="Set static grid height"
checked={!autoHeight}
onChange={() => setAutoHeight(!autoHeight)}
/>
<EuiSwitch
label="Toggle custom row details"
checked={showRowDetails}
onChange={() => setShowRowDetails(!showRowDetails)}
/>
</EuiFlexGroup>
<EuiSpacer />
<EuiDataGrid
aria-label="Data grid custom body renderer demo"
columns={columns}
leadingControlColumns={leadingControlColumns}
trailingControlColumns={
showRowDetails
? [...trailingControlColumns, ...rowDetails]
: trailingControlColumns
}
columnVisibility={{ visibleColumns, setVisibleColumns }}
sorting={{ columns: sortingColumns, onSort }}
inMemory={{ level: 'sorting' }}
pagination={{
...pagination,
pageSizeOptions: [10, 25, 50],
onChangePage: onChangePage,
onChangeItemsPerPage: onChangePageSize,
}}
rowCount={raw_data.length}
renderCellValue={({ rowIndex, columnId }) =>
raw_data[rowIndex][columnId]
}
renderFooterCellValue={RenderFooterCellValue}
renderCustomGridBody={RenderCustomGridBody}
height={autoHeight ? undefined : 400}
gridStyle={{ border: 'none', header: 'underline' }}
/>
</>
);
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { Link } from 'react-router-dom';

import { GuideSectionTypes } from '../../../components';
import {
Expand Down Expand Up @@ -31,6 +32,28 @@ dataGridRef.current.openCellPopover({ rowIndex, colIndex });
dataGridRef.current.closeCellPopover();
`;

import CustomRenderer from './custom_renderer';
const customRendererSource = require('!!raw-loader!./custom_renderer');
const customRendererSnippet = `const CustomGridBody = ({ visibleColumns, visibleRowData, Cell }) => {
const visibleRows = raw_data.slice(
visibleRowData.startRow,
visibleRowData.endRow
);
return (
<>
{visibleRows.map((row, rowIndex) => (
<div role="row" style={{ display: 'flex' }} key={rowIndex}>
{visibleColumns.map((column, colIndex) => (
<Cell colIndex={colIndex} visibleRowIndex={rowIndex} key={\`\${rowIndex},\${colIndex}\`} />
))}
</div>
))}
</>
);
};

<EuiDataGridBody renderCustomGridBody={CustomGridBody} {...props} />`;

export const DataGridAdvancedExample = {
title: 'Data grid advanced',
sections: [
Expand Down Expand Up @@ -185,5 +208,46 @@ export const DataGridAdvancedExample = {
props: { EuiDataGridRefProps },
},
...DataGridMemoryExample.sections,
{
title: 'Custom body renderer',
source: [
{
type: GuideSectionTypes.TSX,
code: customRendererSource,
},
],
text: (
<>
<p>
For <strong>extremely</strong> advanced use cases, the{' '}
<EuiCode>renderCustomGridBody</EuiCode> prop may be used to take
complete control over rendering the grid body. This may be useful
for scenarios where the default{' '}
<Link to="/tabular-content/data-grid#virtualization">
virtualized
</Link>{' '}
rendering is not desired, or where custom row layouts (e.g., the
conditional row details cell below) are required.
</p>
<p>
Please note that this prop is meant to be an{' '}
<strong>escape hatch</strong>, and should only be used if you know
exactly what you are doing. Once a custom renderer is used, you are
in charge of ensuring the grid has all the correct semantic and aria
labels required by the{' '}
<EuiLink
href="https://www.w3.org/WAI/ARIA/apg/patterns/grid"
target="_blank"
>
data grid spec
</EuiLink>
, and that keyboard focus and navigation still work in an accessible
manner.
</p>
</>
),
demo: <CustomRenderer />,
snippet: customRendererSnippet,
},
],
};
3 changes: 3 additions & 0 deletions src-docs/src/views/datagrid/basics/_props.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const gridLinks = {
schemaDetectors: '/tabular-content/data-grid-schema-columns#schemas',
toolbarVisibility: '/tabular-content/data-grid-toolbar#toolbar-visibility',
ref: '/tabular-content/data-grid-advanced#ref-methods',
renderCustomGridBody:
'/tabular-content/data-grid-advanced#custom-body-renderer',
};

export const DataGridTopProps = () => {
Expand All @@ -27,6 +29,7 @@ export const DataGridTopProps = () => {
component={EuiDataGrid}
exclude={[
'className',
'css',
'data-test-subj',
'aria-label',
'width',
Expand Down
Loading