Skip to content

Commit

Permalink
Added the ability to use DataTable rowDetails in a controlled way. Ex…
Browse files Browse the repository at this point in the history
…tended `rowDetails` property to be oneOf function or a shape that may contain four functions, `render`, `isExpanded`, `isExpandable` and `expandClick`.

`render` is the equivalent of the current function `rowDetails` rendering the row details.
`isExpanded` is the function which determines if the row details should be expanded
`isExpandable` is a function which allows certain rows to be expandable or not (i.e. not expandable means no expand button or expand/collapse events)
`expandClick` is a function which is fired when the expand/collapse button is clicked.

`render` and `isExpanded` are mandatory. `expandClick` can be handled using `onClickRow` or similar and the absence of `isExpandable` the default is that the row _is_ expandable.

The change is fully backwards compatible (all previous tests for rowDetails are untouched as well as storybook).

Storybook example is provided.
  • Loading branch information
aheintz committed Jan 10, 2023
1 parent 35ddfd6 commit 93a67a9
Show file tree
Hide file tree
Showing 6 changed files with 1,538 additions and 112 deletions.
259 changes: 150 additions & 109 deletions src/js/components/DataTable/Body.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,120 +39,153 @@ const Row = memo(
primaryProperty,
data,
verticalAlign,
}) => (
<>
<StyledDataTableRow
ref={rowRef}
size={size}
active={active}
aria-disabled={(onClickRow && isDisabled) || undefined}
onClick={
onClickRow
? (event) => {
if (onClickRow && !isDisabled) {
if (typeof onClickRow === 'function') {
// extract from React's synthetic event pool
event.persist();
const adjustedEvent = event;
adjustedEvent.datum = datum;
adjustedEvent.index = index;
onClickRow(adjustedEvent);
} else if (onClickRow === 'select') {
if (isSelected) {
onSelect(selected.filter((s) => s !== primaryValue));
} else onSelect([...selected, primaryValue]);
}) => {
// If there is a controlled function, use that, otherwise the internal.
const isExpanded =
(rowDetails &&
rowDetails.isExpanded &&
rowDetails.isExpanded(data[index])) ??
isRowExpanded;

// If there's a controlled function, don't do anything, otherwise use
// internal setRowExpand.
const setExpanded =
rowDetails && rowDetails.isExpanded
? (rowDetails.expandClick &&
rowDetails.expandClick.bind(this, data[index])) ||
(() => {})
: setRowExpand;

// Determine the rowDetails render function.
const rowDetailsRender =
rowDetails && typeof rowDetails.render === 'function'
? rowDetails.render
: rowDetails;

// rowExpandable contains true if the row should be expandable.
const rowExpandable =
rowDetails && rowDetails.isExpandable
? rowDetails.isExpandable(data[index])
: true;

return (
<>
<StyledDataTableRow
ref={rowRef}
size={size}
active={active}
aria-disabled={(onClickRow && isDisabled) || undefined}
onClick={
onClickRow
? (event) => {
if (onClickRow && !isDisabled) {
if (typeof onClickRow === 'function') {
// extract from React's synthetic event pool
event.persist();
const adjustedEvent = event;
adjustedEvent.datum = datum;
adjustedEvent.index = index;
onClickRow(adjustedEvent);
} else if (onClickRow === 'select') {
if (isSelected) {
onSelect(selected.filter((s) => s !== primaryValue));
} else onSelect([...selected, primaryValue]);
}
}
}
: undefined
}
onMouseEnter={
onClickRow && !isDisabled ? () => setActive(index) : undefined
}
onMouseLeave={onClickRow ? () => setActive(undefined) : undefined}
>
{(selected || onSelect) && (
<Cell
background={
(pinnedOffset?._grommetDataTableSelect &&
cellProps.pinned.background) ||
cellProps.background
}
: undefined
}
onMouseEnter={
onClickRow && !isDisabled ? () => setActive(index) : undefined
}
onMouseLeave={onClickRow ? () => setActive(undefined) : undefined}
>
{(selected || onSelect) && (
<Cell
background={
(pinnedOffset?._grommetDataTableSelect &&
cellProps.pinned.background) ||
cellProps.background
}
pinnedOffset={pinnedOffset?._grommetDataTableSelect}
aria-disabled={isDisabled || !onSelect || undefined}
column={{
pin: Boolean(pinnedOffset?._grommetDataTableSelect),
plain: 'noPad',
size: 'auto',
render: () => (
<CheckBox
tabIndex={onClickRow === 'select' ? -1 : undefined}
a11yTitle={`${
isSelected ? 'unselect' : 'select'
} ${primaryValue}`}
checked={isSelected}
disabled={isDisabled || !onSelect}
onChange={() => {
if (isSelected) {
onSelect(selected.filter((s) => s !== primaryValue));
} else onSelect([...selected, primaryValue]);
}}
pad={cellProps.pad}
/>
),
}}
verticalAlign={verticalAlign}
/>
)}
pinnedOffset={pinnedOffset?._grommetDataTableSelect}
aria-disabled={isDisabled || !onSelect || undefined}
column={{
pin: Boolean(pinnedOffset?._grommetDataTableSelect),
plain: 'noPad',
size: 'auto',
render: () => (
<CheckBox
tabIndex={onClickRow === 'select' ? -1 : undefined}
a11yTitle={`${
isSelected ? 'unselect' : 'select'
} ${primaryValue}`}
checked={isSelected}
disabled={isDisabled || !onSelect}
onChange={() => {
if (isSelected) {
onSelect(selected.filter((s) => s !== primaryValue));
} else onSelect([...selected, primaryValue]);
}}
pad={cellProps.pad}
/>
),
}}
verticalAlign={verticalAlign}
/>
)}

{rowDetails && (
<ExpanderCell
context={isRowExpanded ? 'groupHeader' : 'body'}
expanded={isRowExpanded}
onToggle={() => {
if (isRowExpanded) {
setRowExpand(rowExpand.filter((s) => s !== index));
} else {
setRowExpand([...rowExpand, index]);
{rowDetailsRender && (
<ExpanderCell
context={isExpanded ? 'groupHeader' : 'body'}
expanded={isExpanded}
disabled={!rowExpandable}
onToggle={() => {
if (isExpanded) {
setExpanded(rowExpand.filter((s) => s !== index));
} else {
setExpanded([...rowExpand, index]);
}
}}
pad={cellProps.pad}
verticalAlign={verticalAlign}
/>
)}
{columns.map((column) => (
<Cell
key={column.property}
background={
(column.pin && cellProps.pinned.background) ||
cellProps.background
}
}}
pad={cellProps.pad}
verticalAlign={verticalAlign}
/>
)}
{columns.map((column) => (
<Cell
key={column.property}
background={
(column.pin && cellProps.pinned.background) ||
cellProps.background
}
border={(column.pin && cellProps.pinned.border) || cellProps.border}
context="body"
column={column}
datum={datum}
pad={(column.pin && cellProps.pinned.pad) || cellProps.pad}
pinnedOffset={pinnedOffset && pinnedOffset[column.property]}
primaryProperty={primaryProperty}
scope={
column.primary || column.property === primaryProperty
? 'row'
: undefined
}
verticalAlign={verticalAlign}
/>
))}
</StyledDataTableRow>
{rowDetails && isRowExpanded && (
<StyledDataTableRow key={`${index.toString()}_expand`}>
{(selected || onSelect) && <TableCell />}
<TableCell colSpan={columns.length + 1}>
{rowDetails(data[index])}
</TableCell>
border={
(column.pin && cellProps.pinned.border) || cellProps.border
}
context="body"
column={column}
datum={datum}
pad={(column.pin && cellProps.pinned.pad) || cellProps.pad}
pinnedOffset={pinnedOffset && pinnedOffset[column.property]}
primaryProperty={primaryProperty}
scope={
column.primary || column.property === primaryProperty
? 'row'
: undefined
}
verticalAlign={verticalAlign}
/>
))}
</StyledDataTableRow>
)}
</>
),
{rowDetailsRender && rowExpandable && isExpanded && (
<StyledDataTableRow key={`${index.toString()}_expand`}>
{(selected || onSelect) && <TableCell />}
<TableCell colSpan={columns.length + 1}>
{rowDetailsRender(data[index])}
</TableCell>
</StyledDataTableRow>
)}
</>
);
},
);

const Body = forwardRef(
Expand Down Expand Up @@ -266,7 +299,15 @@ const Body = forwardRef(
: undefined;
const isSelected = selected && selected.includes(primaryValue);
const isDisabled = disabled && disabled.includes(primaryValue);
const isRowExpanded = rowExpand && rowExpand.includes(index);
const rowDetailsIsExpanded =
rowDetails &&
rowDetails.isExpanded &&
typeof rowDetails.isExpanded === 'function' &&
rowDetails.isExpanded(data[index]);

const isRowExpanded =
rowDetailsIsExpanded ||
(rowExpand && rowExpand.includes(index));
const cellProps = normalizeRowCellProps(
rowProps,
cellPropsProp,
Expand Down
4 changes: 2 additions & 2 deletions src/js/components/DataTable/ExpanderCell.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,15 @@ const ExpanderControl = ({ context, expanded, onToggle, pad, ...rest }) => {
return content;
};

const ExpanderCell = ({ background, border, context, ...rest }) => (
const ExpanderCell = ({ background, border, context, disabled, ...rest }) => (
<TableCell
background={background}
border={border}
size="xxsmall"
plain="noPad"
verticalAlign={context === 'groupEnd' ? 'bottom' : 'top'}
>
<ExpanderControl context={context} {...rest} />
{!disabled && <ExpanderControl context={context} {...rest} />}
</TableCell>
);

Expand Down
61 changes: 61 additions & 0 deletions src/js/components/DataTable/__tests__/DataTable-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,66 @@ describe('DataTable', () => {
fireEvent.click(expandButtons[1], {});
expect(container.firstChild).toMatchSnapshot();
});

test('rowDetails controlled', () => {
function TestComponent() {
const [openRow, setOpenRow] = React.useState();
return (
<Grommet>
<DataTable
columns={[
{ property: 'a', header: 'A' },
{ property: 'b', header: 'B' },
]}
data={[
{ a: 'one', b: 1.1 },
{ a: 'two', b: 1.2 },
{ a: 'three', b: 2.1 },
{ a: 'four', b: 2.2 },
]}
rowDetails={{
render: (row) => <Box>{`Open ${row.b}`}</Box>,
isExpandable: (row) => row.b !== 1.2,
isExpanded: (row) => row.b === openRow,
expandClick: (row) => {
if (openRow === row.b) {
setOpenRow(null);
} else {
setOpenRow(row.b);
}
},
}}
primaryKey="b"
/>
</Grommet>
);
}

// Second row is not expandable, therefore no expand button. That means
// that expandButton with index 1 is the expand button for row 3;
const { container, getAllByLabelText } = render(<TestComponent />);
const expandButton = getAllByLabelText('expand');
expect(container.querySelectorAll('td').item(4).textContent).toBe('two');
expect(container.querySelectorAll('td').item(6).textContent).toBe('three');
expect(container.querySelectorAll('td').item(7).textContent).toBe('');
expect(container.querySelectorAll('td').item(8).textContent).toBe('four');
expect(container.firstChild).toMatchSnapshot();
fireEvent.click(expandButton[1], {});
expect(container.querySelectorAll('td').item(4).textContent).toBe('two');
expect(container.querySelectorAll('td').item(6).textContent).toBe('three');
expect(container.querySelectorAll('td').item(7).textContent).toBe(
'Open 2.1',
);
expect(container.querySelectorAll('td').item(9).textContent).toBe('four');
expect(container.firstChild).toMatchSnapshot();
fireEvent.click(expandButton[1], {});
expect(container.querySelectorAll('td').item(4).textContent).toBe('two');
expect(container.querySelectorAll('td').item(6).textContent).toBe('three');
expect(container.querySelectorAll('td').item(7).textContent).toBe('');
expect(container.querySelectorAll('td').item(8).textContent).toBe('four');
expect(container.firstChild).toMatchSnapshot();
});

test('groupBy', () => {
const { container, getByText } = render(
<Grommet>
Expand Down Expand Up @@ -603,6 +663,7 @@ describe('DataTable', () => {
</Grommet>
);
}

const { container, getByText } = render(<TestComponent />);
expect(container.firstChild).toMatchSnapshot();

Expand Down

0 comments on commit 93a67a9

Please sign in to comment.