@patternfly/react-data-view 6.4.0-prerelease.8 → 6.4.0-prerelease.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,148 @@
1
+ import { FunctionComponent, useState } from 'react';
2
+ import { DataViewTable, DataViewTr, DataViewTh } from '@patternfly/react-data-view/dist/dynamic/DataViewTable';
3
+ import { ExpandableContent } from '@patternfly/react-data-view/dist/dynamic/DataViewTableBasic';
4
+ import { ExclamationCircleIcon } from '@patternfly/react-icons';
5
+ import { Button, Toolbar, ToolbarContent, ToolbarItem, Switch } from '@patternfly/react-core';
6
+
7
+ interface Repository {
8
+ id: number;
9
+ name: string;
10
+ branches: string | null;
11
+ prs: string | null;
12
+ workspaces: string;
13
+ lastCommit: string;
14
+ contributors: string;
15
+ stars: string;
16
+ forks: string;
17
+ }
18
+
19
+ const expandableContents: ExpandableContent[] = [
20
+ // Row 1 - Repository one
21
+ { rowId: 1, columnId: 2, content: <div><strong>Branch Details:</strong> 5 active branches, main, develop, feature/new-ui, hotfix/bug-123, release/v2.0</div> },
22
+ { rowId: 1, columnId: 3, content: <div><strong>PR Details:</strong> 3 open PRs, 45 merged this month, avg review time: 2 days</div> },
23
+ { rowId: 1, columnId: 5, content: <div><strong>Commit Info:</strong> Author: John Doe, Message: "Fix critical authentication bug", SHA: a1b2c3d</div> },
24
+
25
+ // Row 2 - Repository two
26
+ { rowId: 2, columnId: 2, content: <div><strong>Branch Details:</strong> 8 active branches, main, staging, feature/api-v2, feature/dashboard</div> },
27
+ { rowId: 2, columnId: 3, content: <div><strong>PR Details:</strong> 5 open PRs, 120 merged this month, avg review time: 1.5 days</div> },
28
+ { rowId: 2, columnId: 4, content: <div><strong>Workspace Info:</strong> Development env, 3 active deployments, last updated 30 mins ago</div> },
29
+ { rowId: 2, columnId: 5, content: <div><strong>Commit Info:</strong> Author: Jane Smith, Message: "Add new API endpoints", SHA: x9y8z7w</div> },
30
+
31
+ // Row 3 - Repository three
32
+ { rowId: 3, columnId: 2, content: <div><strong>Branch Details:</strong> 12 active branches including main, develop, multiple feature branches</div> },
33
+ { rowId: 3, columnId: 3, content: <div><strong>PR Details:</strong> 8 open PRs, 200 merged this month, avg review time: 3 days</div> },
34
+ { rowId: 3, columnId: 4, content: <div><strong>Workspace Info:</strong> Staging env, 10 active deployments, last updated 1 day ago</div> },
35
+ { rowId: 3, columnId: 5, content: <div><strong>Commit Info:</strong> Author: Bob Johnson, Message: "Refactor core modules", SHA: p0o9i8u</div> },
36
+
37
+ // Row 4 - Repository four
38
+ { rowId: 4, columnId: 2, content: <div><strong>Branch Details:</strong> 6 active branches, focusing on microservices architecture</div> },
39
+ { rowId: 4, columnId: 3, content: <div><strong>PR Details:</strong> 2 open PRs, 90 merged this month, avg review time: 2.5 days</div> },
40
+ { rowId: 4, columnId: 4, content: <div><strong>Workspace Info:</strong> QA env, 7 active deployments, automated testing enabled</div> },
41
+ { rowId: 4, columnId: 5, content: <div><strong>Commit Info:</strong> Author: Alice Williams, Message: "Update dependencies", SHA: m5n4b3v</div> },
42
+
43
+ // Row 5 - Repository five
44
+ { rowId: 5, columnId: 2, content: <div><strong>Branch Details:</strong> 4 active branches, clean branch strategy</div> },
45
+ { rowId: 5, columnId: 3, content: <div><strong>PR Details:</strong> 6 open PRs, 75 merged this month, avg review time: 1 day</div> },
46
+ { rowId: 5, columnId: 4, content: <div><strong>Workspace Info:</strong> Pre-production env, CI/CD pipeline configured</div> },
47
+ { rowId: 5, columnId: 5, content: <div><strong>Commit Info:</strong> Author: Charlie Brown, Message: "Implement dark mode", SHA: q2w3e4r</div> },
48
+
49
+ // Row 6 - Repository six
50
+ { rowId: 6, columnId: 2, content: <div><strong>Branch Details:</strong> 15 active branches, complex branching model</div> },
51
+ { rowId: 6, columnId: 3, content: <div><strong>PR Details:</strong> 10 open PRs, 250 merged this month, avg review time: 4 days</div> },
52
+ { rowId: 6, columnId: 4, content: <div><strong>Workspace Info:</strong> Multi-region deployment, high availability setup</div> },
53
+ { rowId: 6, columnId: 5, content: <div><strong>Commit Info:</strong> Author: David Lee, Message: "Security patches applied", SHA: t6y7u8i</div> },
54
+ ];
55
+
56
+ const repositories: Repository[] = [
57
+ { id: 1, name: 'Repository one', branches: 'Branch one', prs: 'Pull request one', workspaces: 'Workspace one', lastCommit: 'Timestamp one', contributors: '25 contributors', stars: '1.2k stars', forks: '340 forks' },
58
+ { id: 2, name: 'Repository two', branches: 'Branch two', prs: 'Pull request two', workspaces: 'Workspace two', lastCommit: 'Timestamp two', contributors: '45 contributors', stars: '3.5k stars', forks: '890 forks' },
59
+ { id: 3, name: 'Repository three', branches: 'Branch three', prs: 'Pull request three', workspaces: 'Workspace three', lastCommit: 'Timestamp three', contributors: '200 contributors', stars: '15k stars', forks: '2.1k forks' },
60
+ { id: 4, name: 'Repository four', branches: 'Branch four', prs: 'Pull request four', workspaces: 'Workspace four', lastCommit: 'Timestamp four', contributors: '80 contributors', stars: '5.7k stars', forks: '1.2k forks' },
61
+ { id: 5, name: 'Repository five', branches: 'Branch five', prs: 'Pull request five', workspaces: 'Workspace five', lastCommit: 'Timestamp five', contributors: '60 contributors', stars: '4.3k stars', forks: '780 forks' },
62
+ { id: 6, name: 'Repository six', branches: 'Branch six', prs: 'Pull request six', workspaces: 'Workspace six', lastCommit: 'Timestamp six', contributors: '300 contributors', stars: '22k stars', forks: '4.5k forks' },
63
+ { id: 7, name: 'Repository seven', branches: 'Branch seven', prs: 'Pull request seven', workspaces: 'Workspace seven', lastCommit: 'Timestamp seven', contributors: '12 contributors', stars: '567 stars', forks: '120 forks' },
64
+ { id: 8, name: 'Repository eight', branches: 'Branch eight', prs: 'Pull request eight', workspaces: 'Workspace eight', lastCommit: 'Timestamp eight', contributors: '98 contributors', stars: '7.8k stars', forks: '1.5k forks' },
65
+ { id: 9, name: 'Repository nine', branches: 'Branch nine', prs: 'Pull request nine', workspaces: 'Workspace nine', lastCommit: 'Timestamp nine', contributors: '33 contributors', stars: '2.1k stars', forks: '456 forks' },
66
+ { id: 10, name: 'Repository ten', branches: 'Branch ten', prs: 'Pull request ten', workspaces: 'Workspace ten', lastCommit: 'Timestamp ten', contributors: '150 contributors', stars: '11k stars', forks: '2.8k forks' },
67
+ { id: 11, name: 'Repository eleven', branches: 'Branch eleven', prs: 'Pull request eleven', workspaces: 'Workspace eleven', lastCommit: 'Timestamp eleven', contributors: '67 contributors', stars: '5.2k stars', forks: '980 forks' },
68
+ { id: 12, name: 'Repository twelve', branches: 'Branch twelve', prs: 'Pull request twelve', workspaces: 'Workspace twelve', lastCommit: 'Timestamp twelve', contributors: '41 contributors', stars: '3.1k stars', forks: '670 forks' },
69
+ { id: 13, name: 'Repository thirteen', branches: 'Branch thirteen', prs: 'Pull request thirteen', workspaces: 'Workspace thirteen', lastCommit: 'Timestamp thirteen', contributors: '89 contributors', stars: '6.4k stars', forks: '1.3k forks' },
70
+ { id: 14, name: 'Repository fourteen', branches: 'Branch fourteen', prs: 'Pull request fourteen', workspaces: 'Workspace fourteen', lastCommit: 'Timestamp fourteen', contributors: '120 contributors', stars: '9.2k stars', forks: '1.9k forks' },
71
+ { id: 15, name: 'Repository fifteen', branches: 'Branch fifteen', prs: 'Pull request fifteen', workspaces: 'Workspace fifteen', lastCommit: 'Timestamp fifteen', contributors: '78 contributors', stars: '5.9k stars', forks: '1.1k forks' }
72
+ ];
73
+
74
+ const ouiaId = 'TableInteractiveExample';
75
+
76
+ export const InteractiveExample: FunctionComponent = () => {
77
+ const [isExpandable, setIsExpandable] = useState(true);
78
+ const [isSticky, setIsSticky] = useState(true);
79
+
80
+ // Generate rows based on current settings
81
+ const rows: DataViewTr[] = repositories.map(({ id, name, branches, prs, workspaces, lastCommit, contributors, stars, forks }) => [
82
+ {
83
+ id,
84
+ cell: workspaces,
85
+ props: {
86
+ favorites: { isFavorited: true }
87
+ }
88
+ },
89
+ { cell: <Button href='#' variant='link' isInline>{name}</Button>, props: { isStickyColumn: isSticky, hasRightBorder: true, hasLeftBorder: true, modifier: "nowrap" } },
90
+ { cell: branches, props: { modifier: "nowrap" } },
91
+ { cell: prs, props: { modifier: "nowrap" } },
92
+ { cell: workspaces, props: { modifier: "nowrap" } },
93
+ { cell: lastCommit, props: { modifier: "nowrap" } },
94
+ { cell: contributors, props: { modifier: "nowrap" } },
95
+ { cell: stars, props: { modifier: "nowrap" } },
96
+ { cell: forks, props: { modifier: "nowrap" } }
97
+ ]);
98
+
99
+ const columns: DataViewTh[] = [
100
+ null,
101
+ { cell: 'Repositories', props: { isStickyColumn: isSticky, modifier: 'fitContent', hasRightBorder: true, hasLeftBorder: true } },
102
+ { cell: <>Branches<ExclamationCircleIcon className='pf-v6-u-ml-sm' color="var(--pf-t--global--color--status--danger--default)"/></>, props: { width: 20 } },
103
+ { cell: 'Pull requests', props: { width: 20 } },
104
+ { cell: 'Workspaces', props: { info: { tooltip: 'More information' }, width: 20 } },
105
+ { cell: 'Last commit', props: { sort: { sortBy: {}, columnIndex: 4 }, width: 20 } },
106
+ { cell: 'Contributors', props: { width: 20 } },
107
+ { cell: 'Stars', props: { width: 20 } },
108
+ { cell: 'Forks', props: { width: 20 } },
109
+ ];
110
+
111
+ return (
112
+ <>
113
+ <Toolbar>
114
+ <ToolbarContent>
115
+ <ToolbarItem>
116
+ <Switch
117
+ id="expandable-switch"
118
+ label="Expandable"
119
+ isChecked={isExpandable}
120
+ onChange={(_event, checked) => setIsExpandable(checked)}
121
+ aria-label="Toggle expandable rows"
122
+ />
123
+ </ToolbarItem>
124
+ <ToolbarItem>
125
+ <Switch
126
+ id="sticky-switch"
127
+ label="Sticky header & column"
128
+ isChecked={isSticky}
129
+ onChange={(_event, checked) => setIsSticky(checked)}
130
+ aria-label="Toggle sticky header and columns"
131
+ />
132
+ </ToolbarItem>
133
+ </ToolbarContent>
134
+ </Toolbar>
135
+ <div style={{ height: '400px', overflow: 'auto' }}>
136
+ <DataViewTable
137
+ aria-label='Interactive repositories table'
138
+ ouiaId={ouiaId}
139
+ columns={columns}
140
+ rows={rows}
141
+ expandedRows={isExpandable ? expandableContents : undefined}
142
+ isExpandable={isExpandable}
143
+ isSticky={isSticky}
144
+ />
145
+ </div>
146
+ </>
147
+ );
148
+ };
@@ -0,0 +1,90 @@
1
+ import { FunctionComponent } from 'react';
2
+ import { DataViewTable, DataViewTr, DataViewTh } from '@patternfly/react-data-view/dist/dynamic/DataViewTable';
3
+ import { ExclamationCircleIcon } from '@patternfly/react-icons';
4
+ import { Button } from '@patternfly/react-core';
5
+ import { ActionsColumn } from '@patternfly/react-table';
6
+
7
+ interface Repository {
8
+ id: number;
9
+ name: string;
10
+ branches: string | null;
11
+ prs: string | null;
12
+ workspaces: string;
13
+ lastCommit: string;
14
+ contributors: string;
15
+ stars: string;
16
+ forks: string;
17
+ }
18
+
19
+ const repositories: Repository[] = [
20
+ { id: 1, name: 'Repository one', branches: 'Branch one', prs: 'Pull request one', workspaces: 'Workspace one', lastCommit: 'Timestamp one', contributors: '25 contributors', stars: '1.2k stars', forks: '340 forks' },
21
+ { id: 2, name: 'Repository two', branches: 'Branch two', prs: 'Pull request two', workspaces: 'Workspace two', lastCommit: 'Timestamp two', contributors: '45 contributors', stars: '3.5k stars', forks: '890 forks' },
22
+ { id: 3, name: 'Repository three', branches: 'Branch three', prs: 'Pull request three', workspaces: 'Workspace three', lastCommit: 'Timestamp three', contributors: '200 contributors', stars: '15k stars', forks: '2.1k forks' },
23
+ { id: 4, name: 'Repository four', branches: 'Branch four', prs: 'Pull request four', workspaces: 'Workspace four', lastCommit: 'Timestamp four', contributors: '80 contributors', stars: '5.7k stars', forks: '1.2k forks' },
24
+ { id: 5, name: 'Repository five', branches: 'Branch five', prs: 'Pull request five', workspaces: 'Workspace five', lastCommit: 'Timestamp five', contributors: '60 contributors', stars: '4.3k stars', forks: '780 forks' },
25
+ { id: 6, name: 'Repository six', branches: 'Branch six', prs: 'Pull request six', workspaces: 'Workspace six', lastCommit: 'Timestamp six', contributors: '300 contributors', stars: '22k stars', forks: '4.5k forks' },
26
+ { id: 7, name: 'Repository seven', branches: 'Branch seven', prs: 'Pull request seven', workspaces: 'Workspace seven', lastCommit: 'Timestamp seven', contributors: '12 contributors', stars: '567 stars', forks: '120 forks' },
27
+ { id: 8, name: 'Repository eight', branches: 'Branch eight', prs: 'Pull request eight', workspaces: 'Workspace eight', lastCommit: 'Timestamp eight', contributors: '98 contributors', stars: '7.8k stars', forks: '1.5k forks' },
28
+ { id: 9, name: 'Repository nine', branches: 'Branch nine', prs: 'Pull request nine', workspaces: 'Workspace nine', lastCommit: 'Timestamp nine', contributors: '33 contributors', stars: '2.1k stars', forks: '456 forks' },
29
+ { id: 10, name: 'Repository ten', branches: 'Branch ten', prs: 'Pull request ten', workspaces: 'Workspace ten', lastCommit: 'Timestamp ten', contributors: '150 contributors', stars: '11k stars', forks: '2.8k forks' },
30
+ { id: 11, name: 'Repository eleven', branches: 'Branch eleven', prs: 'Pull request eleven', workspaces: 'Workspace eleven', lastCommit: 'Timestamp eleven', contributors: '67 contributors', stars: '5.2k stars', forks: '980 forks' },
31
+ { id: 12, name: 'Repository twelve', branches: 'Branch twelve', prs: 'Pull request twelve', workspaces: 'Workspace twelve', lastCommit: 'Timestamp twelve', contributors: '41 contributors', stars: '3.1k stars', forks: '670 forks' }
32
+ ];
33
+
34
+ const rowActions = [
35
+ {
36
+ title: 'Some action',
37
+ onClick: () => console.log('clicked on Some action') // eslint-disable-line no-console
38
+ },
39
+ {
40
+ title: <div>Another action</div>,
41
+ onClick: () => console.log('clicked on Another action') // eslint-disable-line no-console
42
+ },
43
+ {
44
+ isSeparator: true
45
+ },
46
+ {
47
+ title: 'Third action',
48
+ onClick: () => console.log('clicked on Third action') // eslint-disable-line no-console
49
+ }
50
+ ];
51
+
52
+ const rows: DataViewTr[] = repositories.map(({ id, name, branches, prs, workspaces, lastCommit, contributors, stars, forks }) => [
53
+ { id, cell: workspaces, props: { favorites: { isFavorited: true } } },
54
+ { cell: <Button href='#' variant='link' isInline>{name}</Button>, props: { isStickyColumn: true, hasRightBorder: true, hasLeftBorder: true, modifier: "nowrap" } },
55
+ { cell: branches, props: { modifier: "nowrap" } },
56
+ { cell: prs, props: { modifier: "nowrap" } },
57
+ { cell: workspaces, props: { modifier: "nowrap" } },
58
+ { cell: lastCommit, props: { modifier: "nowrap" } },
59
+ { cell: contributors, props: { modifier: "nowrap" } },
60
+ { cell: stars, props: { modifier: "nowrap" } },
61
+ { cell: forks, props: { modifier: "nowrap" } },
62
+ { cell: <ActionsColumn items={rowActions}/>, props: { isActionCell: true } },
63
+ ]);
64
+
65
+ const columns: DataViewTh[] = [
66
+ null,
67
+ { cell: 'Repositories', props: { isStickyColumn: true, modifier: 'fitContent', hasRightBorder: true, hasLeftBorder: true } },
68
+ { cell: <>Branches<ExclamationCircleIcon className='pf-v6-u-ml-sm' color="var(--pf-t--global--color--status--danger--default)"/></>, props: { width: 20 } },
69
+ { cell: 'Pull requests', props: { width: 20 } },
70
+ { cell: 'Workspaces', props: { info: { tooltip: 'More information' }, width: 20 } },
71
+ { cell: 'Last commit', props: { sort: { sortBy: {}, columnIndex: 4 }, width: 20 } },
72
+ { cell: 'Contributors', props: { width: 20 } },
73
+ { cell: 'Stars', props: { width: 20 } },
74
+ { cell: 'Forks', props: { width: 20 } },
75
+ null, // Actions column header
76
+ ];
77
+
78
+ const ouiaId = 'TableStickyExample';
79
+
80
+ export const StickyExample: FunctionComponent = () => (
81
+ <div style={{ height: '400px', width: '800px', overflow: 'auto' }}>
82
+ <DataViewTable
83
+ aria-label='Sticky repositories table'
84
+ ouiaId={ouiaId}
85
+ columns={columns}
86
+ rows={rows}
87
+ isSticky
88
+ />
89
+ </div>
90
+ );
@@ -58,6 +58,67 @@ If you want to have all expandable nodes open on initial load pass the `expandAl
58
58
 
59
59
  ```
60
60
 
61
+ ## Expandable rows
62
+
63
+ To add expandable content to table cells, pass an array of `ExpandableContent` objects to the `expandedRows` prop of the `<DataViewTable>` component. Each expandable content object defines which cell can be expanded and what content to display when expanded.
64
+
65
+ The `ExpandableContent` interface is defined as:
66
+
67
+ ```typescript
68
+ interface ExpandableContent {
69
+ /** The ID of the row containing the expandable cell (must match the id property in the row data) */
70
+ rowId: number;
71
+ /** The column index (0-based) that should be expandable */
72
+ columnId: number;
73
+ /** The content to display when the cell is expanded */
74
+ content: ReactNode;
75
+ }
76
+ ```
77
+
78
+ When a cell has expandable content:
79
+ - A compound expand toggle button appears in the cell
80
+ - Clicking the toggle expands the row to show the additional content below
81
+ - Only one expanded cell is shown per row at a time
82
+ - Clicking another expandable cell in the same row switches the expanded content
83
+
84
+ ### Expandable rows example
85
+
86
+ ```js file="./DataViewTableExpandableExample.tsx"
87
+
88
+ ```
89
+
90
+ ## Sticky header and columns
91
+
92
+ To enable sticky headers and columns, set the `isSticky` prop to `true` on the `<DataViewTable>` component. This keeps the table header and designated columns visible when scrolling.
93
+
94
+ To make specific columns sticky, add the `isStickyColumn` property to the column's `props` in the column definition:
95
+
96
+ ```typescript
97
+ const columns: DataViewTh[] = [
98
+ { cell: 'Column Name', props: { isStickyColumn: true } }
99
+ ];
100
+ ```
101
+
102
+ When sticky headers and columns are enabled:
103
+ - The table header remains visible when scrolling vertically
104
+ - Columns marked with `isStickyColumn: true` remain visible when scrolling horizontally
105
+ - The table is wrapped in `OuterScrollContainer` and `InnerScrollContainer` components to enable sticky behavior
106
+ - Sticky columns can have additional styling like borders using `hasRightBorder` or `hasLeftBorder` props
107
+
108
+ ### Sticky header and columns example
109
+
110
+ ```js file="./DataViewTableStickyExample.tsx"
111
+
112
+ ```
113
+
114
+ ### Interactive example
115
+ - Interactive example show how the different composable options work together.
116
+ - By toggling the toggles you can switch between them and observe the behaviour
117
+
118
+ ```js file="./DataViewTableInteractiveExample.tsx"
119
+
120
+ ```
121
+
61
122
  ### Resizable columns
62
123
 
63
124
  To allow a column to resize, add `isResizable` to the `DataViewTable` element, and pass `resizableProps` to each applicable header cell. The `resizableProps` object consists of the following fields:
@@ -1,9 +1,11 @@
1
1
  import { FC, ReactNode } from 'react';
2
2
  import { TdProps, ThProps, TrProps, InnerScrollContainer } from '@patternfly/react-table';
3
3
  import { DataViewTableTree, DataViewTableTreeProps } from '../DataViewTableTree';
4
- import { DataViewTableBasic, DataViewTableBasicProps } from '../DataViewTableBasic';
4
+ import { DataViewTableBasic, DataViewTableBasicProps, ExpandableContent } from '../DataViewTableBasic';
5
5
  import { DataViewThResizableProps } from '../DataViewTh/DataViewTh';
6
6
 
7
+ export type { ExpandableContent };
8
+
7
9
  // Table head typings
8
10
  export type DataViewTh =
9
11
  | ReactNode
@@ -4,7 +4,7 @@ exports[`DataViewTable component should render a basic table correctly 1`] = `
4
4
  <div>
5
5
  <table
6
6
  aria-label="Repositories table"
7
- class="pf-v6-c-table pf-m-grid-md"
7
+ class="pf-v6-c-table pf-m-grid-md pf-m-animate-expand"
8
8
  data-ouia-component-id="TableExample"
9
9
  data-ouia-component-type="PF6/Table"
10
10
  data-ouia-safe="true"
@@ -67,7 +67,7 @@ exports[`DataViewTable component should render a basic table correctly 1`] = `
67
67
  role="rowgroup"
68
68
  >
69
69
  <tr
70
- class="pf-v6-c-table__tr"
70
+ class="pf-v6-c-table__tr pf-v6-c-table__control-row"
71
71
  data-ouia-component-id="TableExample-tr-0"
72
72
  data-ouia-component-type="PF6/TableRow"
73
73
  data-ouia-safe="true"
@@ -109,7 +109,7 @@ exports[`DataViewTable component should render a basic table correctly 1`] = `
109
109
  </td>
110
110
  </tr>
111
111
  <tr
112
- class="pf-v6-c-table__tr"
112
+ class="pf-v6-c-table__tr pf-v6-c-table__control-row"
113
113
  data-ouia-component-id="TableExample-tr-1"
114
114
  data-ouia-component-type="PF6/TableRow"
115
115
  data-ouia-safe="true"
@@ -151,7 +151,7 @@ exports[`DataViewTable component should render a basic table correctly 1`] = `
151
151
  </td>
152
152
  </tr>
153
153
  <tr
154
- class="pf-v6-c-table__tr"
154
+ class="pf-v6-c-table__tr pf-v6-c-table__control-row"
155
155
  data-ouia-component-id="TableExample-tr-2"
156
156
  data-ouia-component-type="PF6/TableRow"
157
157
  data-ouia-safe="true"
@@ -193,7 +193,7 @@ exports[`DataViewTable component should render a basic table correctly 1`] = `
193
193
  </td>
194
194
  </tr>
195
195
  <tr
196
- class="pf-v6-c-table__tr"
196
+ class="pf-v6-c-table__tr pf-v6-c-table__control-row"
197
197
  data-ouia-component-id="TableExample-tr-3"
198
198
  data-ouia-component-type="PF6/TableRow"
199
199
  data-ouia-safe="true"
@@ -235,7 +235,7 @@ exports[`DataViewTable component should render a basic table correctly 1`] = `
235
235
  </td>
236
236
  </tr>
237
237
  <tr
238
- class="pf-v6-c-table__tr"
238
+ class="pf-v6-c-table__tr pf-v6-c-table__control-row"
239
239
  data-ouia-component-id="TableExample-tr-4"
240
240
  data-ouia-component-type="PF6/TableRow"
241
241
  data-ouia-safe="true"
@@ -277,7 +277,7 @@ exports[`DataViewTable component should render a basic table correctly 1`] = `
277
277
  </td>
278
278
  </tr>
279
279
  <tr
280
- class="pf-v6-c-table__tr"
280
+ class="pf-v6-c-table__tr pf-v6-c-table__control-row"
281
281
  data-ouia-component-id="TableExample-tr-5"
282
282
  data-ouia-component-type="PF6/TableRow"
283
283
  data-ouia-safe="true"
@@ -1,8 +1,10 @@
1
- import { render } from '@testing-library/react';
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
2
3
  import { DataView } from '../DataView';
3
- import { DataViewTableBasic } from './DataViewTableBasic';
4
+ import { DataViewTableBasic, ExpandableContent } from './DataViewTableBasic';
4
5
 
5
6
  interface Repository {
7
+ id: number;
6
8
  name: string;
7
9
  branches: string | null;
8
10
  prs: string | null;
@@ -11,20 +13,28 @@ interface Repository {
11
13
  }
12
14
 
13
15
  const repositories: Repository[] = [
14
- { name: 'Repository one', branches: 'Branch one', prs: 'Pull request one', workspaces: 'Workspace one', lastCommit: 'Timestamp one' },
15
- { name: 'Repository two', branches: 'Branch two', prs: 'Pull request two', workspaces: 'Workspace two', lastCommit: 'Timestamp two' },
16
- { name: 'Repository three', branches: 'Branch three', prs: 'Pull request three', workspaces: 'Workspace three', lastCommit: 'Timestamp three' },
17
- { name: 'Repository four', branches: 'Branch four', prs: 'Pull request four', workspaces: 'Workspace four', lastCommit: 'Timestamp four' },
18
- { name: 'Repository five', branches: 'Branch five', prs: 'Pull request five', workspaces: 'Workspace five', lastCommit: 'Timestamp five' },
19
- { name: 'Repository six', branches: 'Branch six', prs: 'Pull request six', workspaces: 'Workspace six', lastCommit: 'Timestamp six' }
16
+ { id: 1, name: 'Repository one', branches: 'Branch one', prs: 'Pull request one', workspaces: 'Workspace one', lastCommit: 'Timestamp one' },
17
+ { id: 2, name: 'Repository two', branches: 'Branch two', prs: 'Pull request two', workspaces: 'Workspace two', lastCommit: 'Timestamp two' },
18
+ { id: 3, name: 'Repository three', branches: 'Branch three', prs: 'Pull request three', workspaces: 'Workspace three', lastCommit: 'Timestamp three' },
19
+ { id: 4, name: 'Repository four', branches: 'Branch four', prs: 'Pull request four', workspaces: 'Workspace four', lastCommit: 'Timestamp four' },
20
+ { id: 5, name: 'Repository five', branches: 'Branch five', prs: 'Pull request five', workspaces: 'Workspace five', lastCommit: 'Timestamp five' },
21
+ { id: 6, name: 'Repository six', branches: 'Branch six', prs: 'Pull request six', workspaces: 'Workspace six', lastCommit: 'Timestamp six' }
20
22
  ];
21
23
 
22
- const rows = repositories.map(repo => ({
23
- row: Object.values(repo),
24
- }));
24
+ const rows = repositories.map(({ id, name, branches, prs, workspaces, lastCommit }) => [
25
+ { id, cell: name },
26
+ branches,
27
+ prs,
28
+ workspaces,
29
+ lastCommit
30
+ ]);
25
31
 
26
32
  const columns = [ 'Repositories', 'Branches', 'Pull requests', 'Workspaces', 'Last commit' ];
27
33
 
34
+ const expandableContents: ExpandableContent[] = [
35
+ { rowId: 1, columnId: 1, content: <div>Branch details for Repository one</div> },
36
+ ];
37
+
28
38
  const ouiaId = 'TableExample';
29
39
 
30
40
  describe('DataViewTable component', () => {
@@ -57,8 +67,40 @@ describe('DataViewTable component', () => {
57
67
  const { container } = render(
58
68
  <DataView activeState="loading">
59
69
  <DataViewTableBasic aria-label='Repositories table' ouiaId={ouiaId} columns={columns} bodyStates={{ loading: "Data is loading" }} rows={[]} />
60
- </DataView>
70
+ </DataView>
61
71
  );
62
72
  expect(container).toMatchSnapshot();
63
73
  });
74
+
75
+ test('when isExpandable cell should be clickable and expandable', async () => {
76
+ const user = userEvent.setup();
77
+
78
+ render(
79
+ <DataViewTableBasic
80
+ aria-label='Repositories table'
81
+ ouiaId={ouiaId}
82
+ columns={columns}
83
+ rows={rows}
84
+ isExpandable={true}
85
+ expandedRows={expandableContents}
86
+ />
87
+ );
88
+
89
+ // Initially, expandable content is rendered but should be hidden (not visible)
90
+ const initialBranchContent = screen.getByText('Branch details for Repository one');
91
+ expect(initialBranchContent.closest('tr')?.classList.contains('pf-m-expanded')).toBeFalsy();
92
+
93
+ // Find the first expandable button by ID
94
+ const branchExpandButton = document.getElementById('expandable-0-0-1');
95
+ expect(branchExpandButton).toBeTruthy();
96
+ // Verify the button is in the cell with "Branch one" text
97
+ expect(branchExpandButton?.closest('td')?.textContent).toContain('Branch one');
98
+
99
+ // Click the expand button for Branches column
100
+ await user.click(branchExpandButton!);
101
+
102
+ // After clicking, the expandable content should be visible
103
+ const branchContent = screen.getByText('Branch details for Repository one');
104
+ expect(branchContent.closest('tr')?.classList.contains('pf-m-expanded')).toBeTruthy();
105
+ });
64
106
  });
@@ -1,5 +1,8 @@
1
- import { FC, useMemo } from 'react';
1
+ import { FC, useMemo, useState, useRef } from 'react';
2
2
  import {
3
+ ExpandableRowContent,
4
+ InnerScrollContainer,
5
+ OuterScrollContainer,
3
6
  Table,
4
7
  TableProps,
5
8
  Tbody,
@@ -11,12 +14,20 @@ import { DataViewTableHead } from '../DataViewTableHead';
11
14
  import { DataViewTh, DataViewTr, isDataViewTdObject, isDataViewTrObject } from '../DataViewTable';
12
15
  import { DataViewState } from '../DataView/DataView';
13
16
 
17
+ export interface ExpandableContent {
18
+ rowId: number;
19
+ columnId: number;
20
+ content: React.ReactNode;
21
+ }
22
+
14
23
  /** extends TableProps */
15
24
  export interface DataViewTableBasicProps extends Omit<TableProps, 'onSelect' | 'rows'> {
16
25
  /** Columns definition */
17
26
  columns: DataViewTh[];
18
27
  /** Current page rows */
19
28
  rows: DataViewTr[];
29
+ /** Expanded rows content */
30
+ expandedRows?: ExpandableContent[];
20
31
  /** Table head states to be displayed when active */
21
32
  headStates?: Partial<Record<DataViewState | string, React.ReactNode>>
22
33
  /** Table body states to be displayed when active */
@@ -25,15 +36,22 @@ export interface DataViewTableBasicProps extends Omit<TableProps, 'onSelect' | '
25
36
  ouiaId?: string;
26
37
  /** @hide Indicates if the table is resizable */
27
38
  hasResizableColumns?: boolean;
39
+ /** Toggles expandable */
40
+ isExpandable?: boolean;
41
+ /** Toggles sticky columns and header */
42
+ isSticky?: boolean;
28
43
  }
29
44
 
30
45
  export const DataViewTableBasic: FC<DataViewTableBasicProps> = ({
31
46
  columns,
32
47
  rows,
48
+ expandedRows,
33
49
  ouiaId = 'DataViewTableBasic',
34
50
  headStates,
35
51
  bodyStates,
36
52
  hasResizableColumns,
53
+ isExpandable = false,
54
+ isSticky = false,
37
55
  ...props
38
56
  }: DataViewTableBasicProps) => {
39
57
  const { selection, activeState, isSelectable } = useInternalContext();
@@ -42,10 +60,30 @@ export const DataViewTableBasic: FC<DataViewTableBasicProps> = ({
42
60
  const activeHeadState = useMemo(() => activeState ? headStates?.[activeState] : undefined, [ activeState, headStates ]);
43
61
  const activeBodyState = useMemo(() => activeState ? bodyStates?.[activeState] : undefined, [ activeState, bodyStates ]);
44
62
 
63
+ const [expandedRowsState, setExpandedRowsState] = useState<Record<number, boolean>>({})
64
+ const [expandedColumnIndex, setExpandedColumnIndex] = useState<Record<number, number>>({})
65
+
66
+ const tableRef = useRef<HTMLTableElement>(null);
67
+
68
+ const needsSeparateTbody = isExpandable;
69
+
45
70
  const renderedRows = useMemo(() => rows.map((row, rowIndex) => {
46
71
  const rowIsObject = isDataViewTrObject(row);
47
- return (
48
- <Tr key={rowIndex} ouiaId={`${ouiaId}-tr-${rowIndex}`} {...(rowIsObject && row?.props)}>
72
+ const isRowExpanded = expandedRowsState[rowIndex] || false;
73
+ const expandedColIndex = expandedColumnIndex[rowIndex];
74
+
75
+ // Get the first cell to extract the row ID
76
+ const rowData = rowIsObject ? row.row : row;
77
+ const firstCell = rowData[0];
78
+ const rowId = isDataViewTdObject(firstCell) ? (firstCell as { id?: number }).id : undefined;
79
+
80
+ // Find all expandable contents for this row
81
+ const rowExpandableContents = isExpandable ? expandedRows?.filter(
82
+ (content) => content.rowId === rowId
83
+ ) : [];
84
+
85
+ const rowContent = (
86
+ <Tr key={needsSeparateTbody ? undefined : rowIndex} ouiaId={`${ouiaId}-tr-${rowIndex}`} {...(rowIsObject && row?.props)} isContentExpanded={isRowExpanded} isControlRow>
49
87
  {isSelectable && (
50
88
  <Td
51
89
  key={`select-${rowIndex}`}
@@ -61,10 +99,29 @@ export const DataViewTableBasic: FC<DataViewTableBasicProps> = ({
61
99
  )}
62
100
  {(rowIsObject ? row.row : row).map((cell, colIndex) => {
63
101
  const cellIsObject = isDataViewTdObject(cell);
102
+ const cellExpandableContent = isExpandable ? expandedRows?.find(
103
+ (content) => content.rowId === rowId && content.columnId === colIndex
104
+ ) : undefined;
64
105
  return (
65
106
  <Td
66
107
  key={colIndex}
67
108
  {...(cellIsObject && (cell?.props ?? {}))}
109
+ {...(cellExpandableContent != null && {
110
+ compoundExpand: {
111
+ isExpanded: isRowExpanded && expandedColIndex === colIndex,
112
+ expandId: `expandable-${rowIndex}`,
113
+ onToggle: () => {
114
+ setExpandedRowsState(prev => {
115
+ const isSameColumn = expandedColIndex === colIndex;
116
+ const wasExpanded = prev[rowIndex];
117
+ return { ...prev, [rowIndex]: isSameColumn ? !wasExpanded : true };
118
+ });
119
+ setExpandedColumnIndex(prev => ({ ...prev, [rowIndex]: colIndex }));
120
+ },
121
+ rowIndex,
122
+ columnIndex: colIndex
123
+ }
124
+ })}
68
125
  data-ouia-component-id={`${ouiaId}-td-${rowIndex}-${colIndex}`}
69
126
  >
70
127
  {cellIsObject ? cell.cell : cell}
@@ -73,14 +130,48 @@ export const DataViewTableBasic: FC<DataViewTableBasicProps> = ({
73
130
  })}
74
131
  </Tr>
75
132
  );
76
- }), [ rows, isSelectable, isSelected, isSelectDisabled, onSelect, ouiaId ]);
77
133
 
78
- return (
79
- <Table aria-label="Data table" ouiaId={ouiaId} {...props}>
80
- { activeHeadState || <DataViewTableHead columns={columns} ouiaId={ouiaId} hasResizableColumns={hasResizableColumns} /> }
81
- { activeBodyState || <Tbody>{renderedRows}</Tbody> }
82
- </Table>
83
- );
134
+ if (needsSeparateTbody) {
135
+ return (
136
+ <Tbody key={rowIndex} isExpanded={isRowExpanded}>
137
+ {rowContent}
138
+ {rowExpandableContents?.map((expandableContent) => (
139
+ <Tr key={`expand-${rowIndex}-${expandableContent.columnId}`} isExpanded={isRowExpanded && expandedColIndex === expandableContent.columnId}>
140
+ <Td colSpan={rowData.length + (isSelectable ? 1 : 0)} data-expanded-column-index={expandableContent.columnId}>
141
+ <ExpandableRowContent>
142
+ {expandableContent.content}
143
+ </ExpandableRowContent>
144
+ </Td>
145
+ </Tr>
146
+ ))}
147
+ </Tbody>
148
+ );
149
+ } else {
150
+ return rowContent;
151
+ }
152
+ }), [ rows, isSelectable, isSelected, isSelectDisabled, onSelect, ouiaId, expandedRowsState, expandedColumnIndex, expandedRows, isExpandable, needsSeparateTbody ]);
153
+
154
+ const bodyContent = activeBodyState || (needsSeparateTbody ? renderedRows : <Tbody>{renderedRows}</Tbody>);
155
+
156
+ if (isSticky) {
157
+ return (
158
+ <OuterScrollContainer>
159
+ <InnerScrollContainer>
160
+ <Table ref={tableRef} aria-label="Data table" ouiaId={ouiaId} isExpandable={isExpandable} hasAnimations {...props} isStickyHeader >
161
+ { activeHeadState || <DataViewTableHead columns={columns} ouiaId={ouiaId} hasResizableColumns={hasResizableColumns} /> }
162
+ { bodyContent }
163
+ </Table>
164
+ </InnerScrollContainer>
165
+ </OuterScrollContainer>
166
+ );
167
+ } else {
168
+ return (
169
+ <Table ref={tableRef} aria-label="Data table" ouiaId={ouiaId} isExpandable={isExpandable} hasAnimations {...props}>
170
+ { activeHeadState || <DataViewTableHead columns={columns} ouiaId={ouiaId} hasResizableColumns={hasResizableColumns} /> }
171
+ { bodyContent }
172
+ </Table>
173
+ );
174
+ }
84
175
  };
85
176
 
86
177
  export default DataViewTableBasic;