Skip to content

Commit da16538

Browse files
committed
feat(DataViewTable): Make select column sticky
When attempting to make the first column in a table row sticky, it doesn't make the selection column sticky and the selection column will scroll with the rest of the table. This PR makes the selection column sticky along with the "first" column.
1 parent 6e33d36 commit da16538

5 files changed

Lines changed: 165 additions & 22 deletions

File tree

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { FunctionComponent } from 'react';
2+
import { DataViewTable, DataViewTr, DataViewTh } from '@patternfly/react-data-view/dist/dynamic/DataViewTable';
3+
import { DataView } from '@patternfly/react-data-view/dist/dynamic/DataView';
4+
import { useDataViewSelection } from '@patternfly/react-data-view/dist/dynamic/Hooks';
5+
import { ExclamationCircleIcon } from '@patternfly/react-icons';
6+
import { Button } from '@patternfly/react-core';
7+
import { ActionsColumn } from '@patternfly/react-table';
8+
9+
interface Repository {
10+
id: number;
11+
name: string;
12+
branches: string | null;
13+
prs: string | null;
14+
workspaces: string;
15+
lastCommit: string;
16+
contributors: string;
17+
stars: string;
18+
forks: string;
19+
}
20+
21+
const repositories: Repository[] = [
22+
{ 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' },
23+
{ 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' },
24+
{ 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' },
25+
{ 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' },
26+
{ 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' },
27+
{ 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' },
28+
{ 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' },
29+
{ 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' },
30+
{ 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' },
31+
{ 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' },
32+
{ 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' },
33+
{ 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' }
34+
];
35+
36+
const rowActions = [
37+
{
38+
title: 'Some action',
39+
onClick: () => console.log('clicked on Some action') // eslint-disable-line no-console
40+
},
41+
{
42+
title: <div>Another action</div>,
43+
onClick: () => console.log('clicked on Another action') // eslint-disable-line no-console
44+
},
45+
{
46+
isSeparator: true
47+
},
48+
{
49+
title: 'Third action',
50+
onClick: () => console.log('clicked on Third action') // eslint-disable-line no-console
51+
}
52+
];
53+
54+
const rows: DataViewTr[] = repositories.map(({ id, name, branches, prs, workspaces, lastCommit, contributors, stars, forks }) => [
55+
{ id, cell: <Button href='#' variant='link' isInline>{name}</Button>, props: { isStickyColumn: true, hasRightBorder: true, modifier: "nowrap" } },
56+
{ cell: branches, props: { modifier: "nowrap" } },
57+
{ cell: prs, props: { modifier: "nowrap" } },
58+
{ cell: workspaces, props: { modifier: "nowrap" } },
59+
{ cell: lastCommit, props: { modifier: "nowrap" } },
60+
{ cell: contributors, props: { modifier: "nowrap" } },
61+
{ cell: stars, props: { modifier: "nowrap" } },
62+
{ cell: forks, props: { modifier: "nowrap" } },
63+
{ cell: <ActionsColumn items={rowActions}/>, props: { isActionCell: true } },
64+
]);
65+
66+
const columns: DataViewTh[] = [
67+
{ cell: 'Repositories', props: { isStickyColumn: true, modifier: 'fitContent', hasRightBorder: 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 = 'TableStickySelectionExample';
79+
80+
export const StickySelectionExample: FunctionComponent = () => {
81+
const selection = useDataViewSelection({ matchOption: (a, b) => a[0].id === b[0].id });
82+
83+
return (
84+
<DataView selection={selection}>
85+
<div style={{ height: '400px', width: '800px', overflow: 'auto' }}>
86+
<DataViewTable
87+
aria-label='Sticky selection repositories table'
88+
ouiaId={ouiaId}
89+
columns={columns}
90+
rows={rows}
91+
isSticky
92+
/>
93+
</div>
94+
</DataView>
95+
);
96+
};

‎packages/module/patternfly-docs/content/extensions/data-view/examples/Table/Table.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,22 @@ When sticky headers and columns are enabled:
114114
- Columns marked with `isStickyColumn: true` remain visible when scrolling horizontally
115115
- The table is wrapped in `OuterScrollContainer` and `InnerScrollContainer` components to enable sticky behavior
116116
- Sticky columns can have additional styling like borders using `hasRightBorder` or `hasLeftBorder` props
117+
- When selection is enabled (via `<DataView selection={...}>`), the selection checkbox column automatically becomes sticky when the first data column has `isStickyColumn: true`. The selection column is properly offset to appear to the left of the sticky data column.
117118

118119
### Sticky header and columns example
119120

120121
```js file="./DataViewTableStickyExample.tsx"
121122

122123
```
123124

125+
### Sticky selection column example
126+
127+
This example demonstrates how the selection checkbox column automatically becomes sticky when the first data column has `isStickyColumn: true`. The selection column is positioned at the left edge with proper offset handling.
128+
129+
```js file="./DataViewTableStickySelectionExample.tsx"
130+
131+
```
132+
124133
### Interactive example
125134
- This interactive example shows how the different composable options work together
126135
- By toggling the toggles, you can switch between them and observe the behaviour

‎packages/module/patternfly-docs/generated/index.js‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ module.exports = {
1414
'/extensions/data-view/table/react': {
1515
id: "Table",
1616
title: "Data view table",
17-
toc: [{"text":"Configuring rows and columns"},[{"text":"Table example"}],{"text":"Adding expandable content to rows"},{"text":"Expandable rows"},[{"text":"Expandable rows example"}],{"text":"Compound expandable rows"},[{"text":"Compound expandable rows example"}],{"text":"Sticky header and columns"},[{"text":"Sticky header and columns example"},{"text":"Interactive example"},{"text":"Resizable columns"}],{"text":"Tree table"},[{"text":"Tree table example"}],{"text":"Sorting"},[{"text":"Sorting example"},{"text":"Sorting state"}],{"text":"States"},[{"text":"Empty"},{"text":"Error"},{"text":"Loading"}]],
18-
examples: ["Table example","Expandable rows example","Compound expandable rows example","Sticky header and columns example","Interactive example","Resizable columns","Tree table example","Sorting example","Empty","Error","Loading"],
17+
toc: [{"text":"Configuring rows and columns"},[{"text":"Table example"}],{"text":"Adding expandable content to rows"},{"text":"Expandable rows"},[{"text":"Expandable rows example"}],{"text":"Compound expandable rows"},[{"text":"Compound expandable rows example"}],{"text":"Sticky header and columns"},[{"text":"Sticky header and columns example"},{"text":"Sticky selection column example"},{"text":"Interactive example"},{"text":"Resizable columns"}],{"text":"Tree table"},[{"text":"Tree table example"}],{"text":"Sorting"},[{"text":"Sorting example"},{"text":"Sorting state"}],{"text":"States"},[{"text":"Empty"},{"text":"Error"},{"text":"Loading"}]],
18+
examples: ["Table example","Expandable rows example","Compound expandable rows example","Sticky header and columns example","Sticky selection column example","Interactive example","Resizable columns","Tree table example","Sorting example","Empty","Error","Loading"],
1919
section: "extensions",
2020
subsection: "Data view",
2121
source: "react",
@@ -35,4 +35,4 @@ module.exports = {
3535
sortValue: 1,
3636
Component: () => import(/* webpackChunkName: "extensions/data-view/overview/extensions/index" */ './extensions/data-view/overview/extensions')
3737
}
38-
};
38+
};

‎packages/module/src/DataViewTableBasic/DataViewTableBasic.tsx‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,18 @@ export const DataViewTableBasic: FC<DataViewTableBasicProps> = ({
9393
// Find all expandable contents for this row
9494
const rowExpandableContents = isExpandable ? expandedRows?.filter(matchesRow) : [];
9595

96+
// Check if the first cell in this row has isStickyColumn
97+
const firstCellProps = rowData[0] && isDataViewTdObject(rowData[0]) ? (rowData[0]?.props ?? {}) : {};
98+
const firstCellIsSticky = firstCellProps.isStickyColumn;
99+
96100
const rowContent = (
97101
<Tr key={needsSeparateTbody ? undefined : rowIndex} ouiaId={`${ouiaId}-tr-${rowIndex}`} {...(rowIsObject && row?.props)} isContentExpanded={isRowExpanded} isControlRow>
98102
{isSelectable && (
99103
<Td
100104
key={`select-${rowIndex}`}
105+
isStickyColumn={firstCellIsSticky}
106+
stickyMinWidth="45px"
107+
stickyLeftOffset="0px"
101108
select={{
102109
rowIndex,
103110
onSelect: (_event, isSelecting) => {
@@ -125,10 +132,18 @@ export const DataViewTableBasic: FC<DataViewTableBasicProps> = ({
125132
const cellExpandableContent = isCompoundExpandable
126133
? expandedRows?.find((entry) => matchesRow(entry) && entry.columnId === colIndex)
127134
: undefined;
135+
136+
// Get the cell props
137+
const cellProps = cellIsObject ? (cell?.props ?? {}) : {};
138+
// If the first column is sticky and selection is enabled, offset it by the selection column width
139+
const enhancedCellProps = colIndex === 0 && cellProps.isStickyColumn && isSelectable
140+
? { ...cellProps, stickyLeftOffset: '45px' }
141+
: cellProps;
142+
128143
return (
129144
<Td
130145
key={colIndex}
131-
{...(cellIsObject && (cell?.props ?? {}))}
146+
{...enhancedCellProps}
132147
{...(cellExpandableContent != null && {
133148
compoundExpand: {
134149
isExpanded: isRowExpanded && expandedColIndex === colIndex,
@@ -181,7 +196,7 @@ export const DataViewTableBasic: FC<DataViewTableBasicProps> = ({
181196
<OuterScrollContainer>
182197
<InnerScrollContainer>
183198
<Table ref={tableRef} aria-label="Data table" ouiaId={ouiaId} isExpandable={isExpandable} hasAnimations {...props} isStickyHeader >
184-
{ activeHeadState || <DataViewTableHead columns={columns} ouiaId={ouiaId} hasResizableColumns={hasResizableColumns} /> }
199+
{ activeHeadState || <DataViewTableHead columns={columns} ouiaId={ouiaId} hasResizableColumns={hasResizableColumns} isSticky={isSticky} /> }
185200
{ bodyContent }
186201
</Table>
187202
</InnerScrollContainer>
@@ -190,7 +205,7 @@ export const DataViewTableBasic: FC<DataViewTableBasicProps> = ({
190205
} else {
191206
return (
192207
<Table ref={tableRef} aria-label="Data table" ouiaId={ouiaId} isExpandable={isExpandable} hasAnimations {...props}>
193-
{ activeHeadState || <DataViewTableHead columns={columns} ouiaId={ouiaId} hasResizableColumns={hasResizableColumns} /> }
208+
{ activeHeadState || <DataViewTableHead columns={columns} ouiaId={ouiaId} hasResizableColumns={hasResizableColumns} isSticky={isSticky} /> }
194209
{ bodyContent }
195210
</Table>
196211
);

‎packages/module/src/DataViewTableHead/DataViewTableHead.tsx‎

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,35 +14,58 @@ export interface DataViewTableHeadProps extends TheadProps {
1414
ouiaId?: string;
1515
/** @hide Indicates whether table is resizable */
1616
hasResizableColumns?: boolean;
17+
/** Toggles sticky columns and header */
18+
isSticky?: boolean;
1719
}
1820

1921
export const DataViewTableHead: FC<DataViewTableHeadProps> = ({
2022
isTreeTable = false,
2123
columns,
2224
ouiaId = 'DataViewTableHead',
2325
hasResizableColumns,
26+
isSticky = false,
2427
...props
2528
}: DataViewTableHeadProps) => {
2629
const { selection } = useInternalContext();
2730
const { onSelect, isSelected } = selection ?? {};
2831

2932
const cells = useMemo(
30-
() => [
31-
onSelect && isSelected && !isTreeTable ? (
32-
<Th key="row-select" screenReaderText="Data selection table head cell" />
33-
) : null,
34-
...columns.map((column, index) => (
35-
<DataViewThElement
36-
key={index}
37-
content={isDataViewThObject(column) ? column.cell : column}
38-
resizableProps={isDataViewThObject(column) ? column.resizableProps : undefined}
39-
data-ouia-component-id={`${ouiaId}-th-${index}`}
40-
thProps={isDataViewThObject(column) ? (column?.props ?? {}) : {}}
41-
hasResizableColumns={hasResizableColumns}
42-
/>
43-
))
44-
],
45-
[ columns, ouiaId, onSelect, isSelected, isTreeTable, hasResizableColumns ]
33+
() => {
34+
// Check if the first column has isStickyColumn
35+
const firstColumnProps = isDataViewThObject(columns[0]) ? (columns[0]?.props ?? {}) : {};
36+
const firstColumnIsSticky = firstColumnProps.isStickyColumn;
37+
38+
return [
39+
onSelect && isSelected && !isTreeTable ? (
40+
<Th
41+
key="row-select"
42+
screenReaderText="Data selection table head cell"
43+
isStickyColumn={firstColumnIsSticky}
44+
stickyMinWidth="45px"
45+
stickyLeftOffset="0px"
46+
/>
47+
) : null,
48+
...columns.map((column, index) => {
49+
const thProps = isDataViewThObject(column) ? (column?.props ?? {}) : {};
50+
// If the first column is sticky and selection is enabled, offset it by the selection column width
51+
const enhancedThProps = index === 0 && thProps.isStickyColumn && onSelect && isSelected
52+
? { ...thProps, stickyLeftOffset: '45px' }
53+
: thProps;
54+
55+
return (
56+
<DataViewThElement
57+
key={index}
58+
content={isDataViewThObject(column) ? column.cell : column}
59+
resizableProps={isDataViewThObject(column) ? column.resizableProps : undefined}
60+
data-ouia-component-id={`${ouiaId}-th-${index}`}
61+
thProps={enhancedThProps}
62+
hasResizableColumns={hasResizableColumns}
63+
/>
64+
);
65+
})
66+
];
67+
},
68+
[ columns, ouiaId, onSelect, isSelected, isTreeTable, hasResizableColumns, isSticky ]
4669
);
4770

4871
return (

0 commit comments

Comments
 (0)