Skip to content

Commit 9adc88b

Browse files
feat(Table): added optin animations for expansion (#11865)
* feat(Table): added optin animations for expansion * Added tests, updated compound expand example * Fixed typo * Updated Tabs context setAccentStyles to optional * Tested handling animation control * Bumped core for animation fix
1 parent d4e6de5 commit 9adc88b

25 files changed

+444
-137
lines changed

packages/react-core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
"tslib": "^2.8.1"
5555
},
5656
"devDependencies": {
57-
"@patternfly/patternfly": "6.3.0-prerelease.33",
57+
"@patternfly/patternfly": "6.3.0-prerelease.35",
5858
"case-anything": "^3.1.2",
5959
"css": "^3.0.0",
6060
"fs-extra": "^11.3.0"

packages/react-core/src/components/Tabs/TabsContext.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export interface TabsContextProps {
66
unmountOnExit: boolean;
77
localActiveKey: string | number;
88
uniqueId: string;
9-
setAccentStyles: (shouldInitializeStyles?: boolean) => void;
9+
setAccentStyles?: (shouldInitializeStyles?: boolean) => void;
1010
handleTabClick: (
1111
event: React.MouseEvent<HTMLElement, MouseEvent>,
1212
eventKey: number | string,

packages/react-docs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"test:a11y": "patternfly-a11y --config patternfly-a11y.config"
2424
},
2525
"dependencies": {
26-
"@patternfly/patternfly": "6.3.0-prerelease.33",
26+
"@patternfly/patternfly": "6.3.0-prerelease.35",
2727
"@patternfly/react-charts": "workspace:^",
2828
"@patternfly/react-code-editor": "workspace:^",
2929
"@patternfly/react-core": "workspace:^",

packages/react-icons/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"@fortawesome/free-brands-svg-icons": "^5.15.4",
3434
"@fortawesome/free-regular-svg-icons": "^5.15.4",
3535
"@fortawesome/free-solid-svg-icons": "^5.15.4",
36-
"@patternfly/patternfly": "6.3.0-prerelease.33",
36+
"@patternfly/patternfly": "6.3.0-prerelease.35",
3737
"fs-extra": "^11.3.0",
3838
"tslib": "^2.8.1"
3939
},

packages/react-styles/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"clean": "rimraf dist css"
2020
},
2121
"devDependencies": {
22-
"@patternfly/patternfly": "6.3.0-prerelease.33",
22+
"@patternfly/patternfly": "6.3.0-prerelease.35",
2323
"change-case": "^5.4.4",
2424
"fs-extra": "^11.3.0"
2525
},

packages/react-table/src/components/Table/ExpandableRowContent.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,20 @@ import { css } from '@patternfly/react-styles';
22
import styles from '@patternfly/react-styles/css/components/Table/table';
33

44
interface ExpandableRowContentProps {
5+
/** Content rendered inside the expandable row. */
56
children?: React.ReactNode;
7+
/** Flag indicating whether the expandable row content has no background. This should be passed in when
8+
* the parent Td is passed the noPadding prop.
9+
*/
10+
hasNoBackground?: boolean;
611
}
712

813
export const ExpandableRowContent: React.FunctionComponent<ExpandableRowContentProps> = ({
914
children = null as React.ReactNode,
15+
hasNoBackground,
1016
...props
1117
}: ExpandableRowContentProps) => (
12-
<div {...props} className={css(styles.tableExpandableRowContent)}>
18+
<div {...props} className={css(styles.tableExpandableRowContent, hasNoBackground && styles.modifiers.noBackground)}>
1319
{children}
1420
</div>
1521
);

packages/react-table/src/components/Table/Table.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,12 @@ export interface TableProps extends React.HTMLProps<HTMLTableElement>, OUIAProps
5757
isNested?: boolean;
5858
/** Flag indicating this table should be striped. This property works best for a single <tbody> table. Striping may also be done manually by applying this property to Tbody and Tr components. */
5959
isStriped?: boolean;
60-
/** Flag indicating this table contains expandable rows to maintain proper striping */
60+
/** Flag indicating this table contains expandable rows. */
6161
isExpandable?: boolean;
62+
/** Flag indicating whether expandable rows within the table have animations. Expandable rows cannot be dynamically rendered. This prop
63+
* will be removed in the next breaking change, with the default behavior becoming animations always being enabled.
64+
*/
65+
hasAnimations?: boolean;
6266
/** Flag indicating this table's rows will not have the inset typically reserved for expanding/collapsing rows in a tree table. Intended for use on tree tables with no visible rows with children. */
6367
hasNoInset?: boolean;
6468
/** Collection of column spans for nested headers. Deprecated: see https://github.com/patternfly/patternfly/issues/4584 */
@@ -73,10 +77,12 @@ export interface TableProps extends React.HTMLProps<HTMLTableElement>, OUIAProps
7377

7478
interface TableContextProps {
7579
registerSelectableRow?: () => void;
80+
hasAnimations?: boolean;
7681
}
7782

7883
export const TableContext = createContext<TableContextProps>({
79-
registerSelectableRow: () => {}
84+
registerSelectableRow: () => {},
85+
hasAnimations: false
8086
});
8187

8288
const TableBase: React.FunctionComponent<TableProps> = ({
@@ -95,6 +101,7 @@ const TableBase: React.FunctionComponent<TableProps> = ({
95101
isNested = false,
96102
isStriped = false,
97103
isExpandable = false,
104+
hasAnimations = false,
98105
hasNoInset = false,
99106
// eslint-disable-next-line @typescript-eslint/no-unused-vars
100107
nestedHeaderColumnSpans,
@@ -197,7 +204,7 @@ const TableBase: React.FunctionComponent<TableProps> = ({
197204
};
198205

199206
return (
200-
<TableContext.Provider value={{ registerSelectableRow }}>
207+
<TableContext.Provider value={{ registerSelectableRow, hasAnimations }}>
201208
<table
202209
aria-label={ariaLabel}
203210
role={role}
@@ -212,7 +219,8 @@ const TableBase: React.FunctionComponent<TableProps> = ({
212219
isStriped && styles.modifiers.striped,
213220
isExpandable && styles.modifiers.expandable,
214221
hasNoInset && stylesTreeView.modifiers.noInset,
215-
isNested && 'pf-m-nested'
222+
isNested && 'pf-m-nested',
223+
hasAnimations && styles.modifiers.animateExpand
216224
)}
217225
ref={tableRef}
218226
{...(isTreeTable && { role: 'treegrid' })}

packages/react-table/src/components/Table/Td.tsx

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createRef, forwardRef, useEffect, useState } from 'react';
1+
import { createRef, forwardRef, useEffect, useState, useContext } from 'react';
22
import { css } from '@patternfly/react-styles';
33
import styles from '@patternfly/react-styles/css/components/Table/table';
44
import scrollStyles from '@patternfly/react-styles/css/components/Table/table-scrollable';
@@ -14,6 +14,7 @@ import {
1414
classNames,
1515
favoritable
1616
} from './utils';
17+
import { IRowData, IExtraData } from './TableTypes';
1718
import { draggable } from './utils/decorators/draggable';
1819
import { treeRow } from './utils';
1920
import { mergeProps } from './base/merge-props';
@@ -29,6 +30,7 @@ import {
2930
TdSelectType,
3031
TdTreeRowType
3132
} from './base/types';
33+
import { TableContext } from './Table';
3234
import cssStickyCellMinWidth from '@patternfly/react-tokens/dist/esm/c_table__sticky_cell_MinWidth';
3335
import cssStickyCellInlineStart from '@patternfly/react-tokens/dist/esm/c_table__sticky_cell_InsetInlineStart';
3436
import cssStickyCellInlineEnd from '@patternfly/react-tokens/dist/esm/c_table__sticky_cell_InsetInlineEnd';
@@ -193,6 +195,39 @@ const TdBase: React.FunctionComponent<TdProps> = ({
193195
}
194196
})
195197
: null;
198+
199+
const { hasAnimations } = useContext(TableContext);
200+
const updateAnimationClass = () => {
201+
const ancestorControlRow = (cellRef as React.RefObject<HTMLElement | null>)?.current?.closest(
202+
`.${styles.tableTr}.${styles.tableControlRow}`
203+
);
204+
const isControlRowExpanded = ancestorControlRow.classList.contains(styles.modifiers.expanded);
205+
if (!isControlRowExpanded) {
206+
return;
207+
}
208+
209+
const isCurrentCellExpanded = (cellRef as React.RefObject<HTMLElement | null>)?.current?.classList.contains(
210+
styles.modifiers.expanded
211+
);
212+
if (isCurrentCellExpanded) {
213+
ancestorControlRow.classList.remove(styles.modifiers.noAnimateExpand);
214+
} else {
215+
ancestorControlRow.classList.add(styles.modifiers.noAnimateExpand);
216+
}
217+
};
218+
219+
const internalCompoundOnToggle = (
220+
event: React.MouseEvent,
221+
rowIndex: number,
222+
colIndex: number,
223+
isOpen: boolean,
224+
rowData: IRowData,
225+
extraData: IExtraData
226+
) => {
227+
hasAnimations && updateAnimationClass();
228+
compoundExpandProp?.onToggle(event, rowIndex, colIndex, isOpen, rowData, extraData);
229+
};
230+
196231
const compoundParams =
197232
compoundExpandProp !== null
198233
? compoundExpand(
@@ -207,7 +242,7 @@ const TdBase: React.FunctionComponent<TdProps> = ({
207242
columnIndex: compoundExpandProp?.columnIndex,
208243
column: {
209244
extraParams: {
210-
onExpand: compoundExpandProp?.onToggle,
245+
onExpand: internalCompoundOnToggle,
211246
expandId: compoundExpandProp?.expandId
212247
}
213248
}

packages/react-table/src/components/Table/Tr.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ const TrBase: React.FunctionComponent<TrProps> = ({
8080

8181
const rowIsHidden = isHidden || (isExpanded !== undefined && !isExpanded && isExpandable);
8282

83-
const { registerSelectableRow } = useContext(TableContext);
83+
const { registerSelectableRow, hasAnimations } = useContext(TableContext);
8484

8585
useEffect(() => {
8686
if (isSelectable && !rowIsHidden) {
@@ -113,6 +113,7 @@ const TrBase: React.FunctionComponent<TrProps> = ({
113113
{...(isClickable && { tabIndex: 0 })}
114114
aria-label={ariaLabel}
115115
ref={innerRef}
116+
{...(hasAnimations && rowIsHidden && { inert: '' })}
116117
{...(onRowClick && { onClick: onRowClick, onKeyDown })}
117118
{...ouiaProps}
118119
{...props}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { render, screen } from '@testing-library/react';
2+
import { ExpandableRowContent } from '../ExpandableRowContent';
3+
import styles from '@patternfly/react-styles/css/components/Table/table';
4+
5+
test('Renders without children', () => {
6+
render(<ExpandableRowContent data-testid="row-id" />);
7+
8+
expect(screen.getByTestId('row-id')).toBeInTheDocument();
9+
});
10+
11+
test('Renders with children', () => {
12+
render(<ExpandableRowContent>Row content</ExpandableRowContent>);
13+
14+
expect(screen.getByText('Row content')).toBeVisible();
15+
});
16+
17+
test(`Renders only with class ${styles.tableExpandableRowContent} by default`, () => {
18+
render(<ExpandableRowContent>Row content</ExpandableRowContent>);
19+
20+
expect(screen.getByText('Row content')).toHaveClass(styles.tableExpandableRowContent, { exact: true });
21+
});
22+
23+
test(`Renders with class ${styles.modifiers.noBackground} when hasNoBackground is true`, () => {
24+
render(<ExpandableRowContent hasNoBackground>Row content</ExpandableRowContent>);
25+
26+
expect(screen.getByText('Row content')).toHaveClass(styles.modifiers.noBackground);
27+
});
28+
29+
test(`Spreads additional props`, () => {
30+
render(<ExpandableRowContent data-custom="true">Row content</ExpandableRowContent>);
31+
32+
expect(screen.getByText('Row content')).toHaveAttribute('data-custom', 'true');
33+
});
34+
35+
test('Matches snapshot', () => {
36+
const { asFragment } = render(<ExpandableRowContent>Row content</ExpandableRowContent>);
37+
38+
expect(asFragment()).toMatchSnapshot();
39+
});
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { render, screen } from '@testing-library/react';
2+
import { Table } from '../Table';
3+
import styles from '@patternfly/react-styles/css/components/Table/table';
4+
5+
test('Renders without children', () => {
6+
render(<Table aria-label="Test table" />);
7+
8+
expect(screen.getByRole('grid', { name: 'Test table' })).toBeInTheDocument();
9+
});
10+
11+
test('Renders with children', () => {
12+
render(
13+
<Table aria-label="Test table">
14+
<caption>Table caption</caption>
15+
</Table>
16+
);
17+
18+
expect(screen.getByRole('grid', { name: 'Test table' })).toHaveTextContent('Table caption');
19+
});
20+
21+
test('Renders with role="treegrid" when isTreeTable is true', () => {
22+
render(<Table isTreeTable aria-label="Test table" />);
23+
24+
expect(screen.getByRole('treegrid', { name: 'Test table' })).toBeInTheDocument();
25+
});
26+
27+
test(`Renders with class ${styles.table} by default`, () => {
28+
render(<Table aria-label="Test table" />);
29+
30+
expect(screen.getByRole('grid', { name: 'Test table' })).toHaveClass(styles.table);
31+
});
32+
33+
test(`Renders with a pf-m-grid class by default`, () => {
34+
render(<Table aria-label="Test table" />);
35+
36+
expect(screen.getByRole('grid', { name: 'Test table' })).toHaveClass(/^pf-m-grid/);
37+
});
38+
39+
test(`Renders with a pf-m-tree-view-grid class when isTreeTable is true`, () => {
40+
render(<Table isTreeTable aria-label="Test table" />);
41+
42+
expect(screen.getByRole('treegrid', { name: 'Test table' })).not.toHaveClass(/^pf-m-grid/);
43+
expect(screen.getByRole('treegrid', { name: 'Test table' })).toHaveClass(/^pf-m-tree-view-grid/);
44+
});
45+
46+
test(`Does not render with class ${styles.modifiers.animateExpand} by default`, () => {
47+
render(<Table aria-label="Test table" />);
48+
49+
expect(screen.getByRole('grid', { name: 'Test table' })).not.toHaveClass(styles.modifiers.animateExpand);
50+
});
51+
52+
test(`Renders with class ${styles.modifiers.animateExpand} hasAnimations is true`, () => {
53+
render(<Table hasAnimations aria-label="Test table" />);
54+
55+
expect(screen.getByRole('grid', { name: 'Test table' })).toHaveClass(styles.modifiers.animateExpand);
56+
});
57+
58+
test('Matches snapshot without children', () => {
59+
const { asFragment } = render(<Table />);
60+
61+
expect(asFragment()).toMatchSnapshot();
62+
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { render, screen } from '@testing-library/react';
2+
import { Tr } from '../Tr';
3+
import styles from '@patternfly/react-styles/css/components/Table/table';
4+
5+
test('Renders without children', () => {
6+
render(
7+
<table>
8+
<tbody>
9+
<Tr />
10+
</tbody>
11+
</table>
12+
);
13+
14+
expect(screen.getByRole('row')).toBeInTheDocument();
15+
});
16+
17+
test('Renders with children', () => {
18+
render(
19+
<table>
20+
<tbody>
21+
<Tr>
22+
<td>Row content</td>
23+
</Tr>
24+
</tbody>
25+
</table>
26+
);
27+
28+
expect(screen.getByRole('row')).toHaveTextContent('Row content');
29+
});
30+
31+
test(`Does not render as hidden by default`, () => {
32+
render(
33+
<table>
34+
<tbody>
35+
<Tr />
36+
</tbody>
37+
</table>
38+
);
39+
40+
expect(screen.getByRole('row')).not.toHaveAttribute('hidden');
41+
});
42+
43+
test(`Renders as hidden when isHidden is true`, () => {
44+
render(
45+
<table>
46+
<tbody>
47+
<Tr isHidden />
48+
</tbody>
49+
</table>
50+
);
51+
52+
expect(screen.queryByRole('row')).not.toBeInTheDocument();
53+
});
54+
55+
test('Matches snapshot without children', () => {
56+
const { asFragment } = render(
57+
<table>
58+
<tbody>
59+
<Tr />
60+
</tbody>
61+
</table>
62+
);
63+
64+
expect(asFragment()).toMatchSnapshot();
65+
});
66+
67+
test('Matches snapshot with children', () => {
68+
const { asFragment } = render(
69+
<table>
70+
<tbody>
71+
<Tr>
72+
<td>Row content</td>
73+
</Tr>
74+
</tbody>
75+
</table>
76+
);
77+
78+
expect(asFragment()).toMatchSnapshot();
79+
});

0 commit comments

Comments
 (0)