Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { FunctionComponent } from 'react';
import { DataViewTable, DataViewTr, DataViewTh } from '@patternfly/react-data-view/dist/dynamic/DataViewTable';
import { DataView } from '@patternfly/react-data-view/dist/dynamic/DataView';
import { useDataViewSelection } from '@patternfly/react-data-view/dist/dynamic/Hooks';
import { ExclamationCircleIcon } from '@patternfly/react-icons';
import { Button } from '@patternfly/react-core';
import { ActionsColumn } from '@patternfly/react-table';

interface Repository {
id: number;
name: string;
branches: string | null;
prs: string | null;
workspaces: string;
lastCommit: string;
contributors: string;
stars: string;
forks: string;
}

const repositories: Repository[] = [
{ 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' },
{ 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' },
{ 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' },
{ 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' },
{ 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' },
{ 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' },
{ 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' },
{ 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' },
{ 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' },
{ 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' },
{ 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' },
{ 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' }
];

const rowActions = [
{
title: 'Some action',
onClick: () => console.log('clicked on Some action') // eslint-disable-line no-console
},
{
title: <div>Another action</div>,
onClick: () => console.log('clicked on Another action') // eslint-disable-line no-console
},
{
isSeparator: true
},
{
title: 'Third action',
onClick: () => console.log('clicked on Third action') // eslint-disable-line no-console
}
];

const rows: DataViewTr[] = repositories.map(({ id, name, branches, prs, workspaces, lastCommit, contributors, stars, forks }) => [
{ id, cell: <Button href='#' variant='link' isInline>{name}</Button>, props: { isStickyColumn: true, hasRightBorder: true, modifier: "nowrap" } },
{ cell: branches, props: { modifier: "nowrap" } },
{ cell: prs, props: { modifier: "nowrap" } },
{ cell: workspaces, props: { modifier: "nowrap" } },
{ cell: lastCommit, props: { modifier: "nowrap" } },
{ cell: contributors, props: { modifier: "nowrap" } },
{ cell: stars, props: { modifier: "nowrap" } },
{ cell: forks, props: { modifier: "nowrap" } },
{ cell: <ActionsColumn items={rowActions}/>, props: { isActionCell: true } },
]);

const columns: DataViewTh[] = [
{ cell: 'Repositories', props: { isStickyColumn: true, modifier: 'fitContent', hasRightBorder: true } },
{ cell: <>Branches<ExclamationCircleIcon className='pf-v6-u-ml-sm' color="var(--pf-t--global--color--status--danger--default)"/></>, props: { width: 20 } },
{ cell: 'Pull requests', props: { width: 20 } },
{ cell: 'Workspaces', props: { info: { tooltip: 'More information' }, width: 20 } },
{ cell: 'Last commit', props: { sort: { sortBy: {}, columnIndex: 4 }, width: 20 } },
{ cell: 'Contributors', props: { width: 20 } },
{ cell: 'Stars', props: { width: 20 } },
{ cell: 'Forks', props: { width: 20 } },
null, // Actions column header
];

const ouiaId = 'TableStickySelectionExample';

export const StickySelectionExample: FunctionComponent = () => {
const selection = useDataViewSelection({ matchOption: (a, b) => a[0].id === b[0].id });

return (
<DataView selection={selection}>
<div style={{ height: '400px', width: '800px', overflow: 'auto' }}>
<DataViewTable
aria-label='Sticky selection repositories table'
ouiaId={ouiaId}
columns={columns}
rows={rows}
isSticky
/>
</div>
</DataView>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,22 @@ When sticky headers and columns are enabled:
- Columns marked with `isStickyColumn: true` remain visible when scrolling horizontally
- The table is wrapped in `OuterScrollContainer` and `InnerScrollContainer` components to enable sticky behavior
- Sticky columns can have additional styling like borders using `hasRightBorder` or `hasLeftBorder` props
- 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.

### Sticky header and columns example

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

```

### Sticky selection column example

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.

```js file="./DataViewTableStickySelectionExample.tsx"

```

### Interactive example
- Interactive example show how the different composable options work together.
- By toggling the toggles you can switch between them and observe the behaviour
Expand Down
4 changes: 2 additions & 2 deletions packages/module/patternfly-docs/generated/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ module.exports = {
'/extensions/data-view/table/react': {
id: "Table",
title: "Data view table",
toc: [{"text":"Configuring rows and columns"},[{"text":"Table example"}],{"text":"Expandable rows"},[{"text":"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"}]],
examples: ["Table example","Expandable rows example","Sticky header and columns example","Interactive example","Resizable columns","Tree table example","Sorting example","Empty","Error","Loading"],
toc: [{"text":"Configuring rows and columns"},[{"text":"Table example"}],{"text":"Expandable rows"},[{"text":"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"}]],
examples: ["Table example","Expandable rows example","Sticky header and columns example","Sticky selection column example","Interactive example","Resizable columns","Tree table example","Sorting example","Empty","Error","Loading"],
section: "extensions",
subsection: "Data view",
source: "react",
Expand Down
21 changes: 18 additions & 3 deletions packages/module/src/DataViewTableBasic/DataViewTableBasic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,18 @@ export const DataViewTableBasic: FC<DataViewTableBasicProps> = ({
(content) => content.rowId === rowId
) : [];

// Check if the first cell in this row has isStickyColumn
const firstCellProps = rowData[0] && isDataViewTdObject(rowData[0]) ? (rowData[0]?.props ?? {}) : {};
const firstCellIsSticky = firstCellProps.isStickyColumn;

const rowContent = (
<Tr key={needsSeparateTbody ? undefined : rowIndex} ouiaId={`${ouiaId}-tr-${rowIndex}`} {...(rowIsObject && row?.props)} isContentExpanded={isRowExpanded} isControlRow>
{isSelectable && (
<Td
key={`select-${rowIndex}`}
isStickyColumn={firstCellIsSticky}
stickyMinWidth="45px"
stickyLeftOffset="0px"
select={{
rowIndex,
onSelect: (_event, isSelecting) => {
Expand All @@ -102,10 +109,18 @@ export const DataViewTableBasic: FC<DataViewTableBasicProps> = ({
const cellExpandableContent = isExpandable ? expandedRows?.find(
(content) => content.rowId === rowId && content.columnId === colIndex
) : undefined;

// Get the cell props
const cellProps = cellIsObject ? (cell?.props ?? {}) : {};
// If the first column is sticky and selection is enabled, offset it by the selection column width
const enhancedCellProps = colIndex === 0 && cellProps.isStickyColumn && isSelectable
? { ...cellProps, stickyLeftOffset: '45px' }
: cellProps;

return (
<Td
key={colIndex}
{...(cellIsObject && (cell?.props ?? {}))}
{...enhancedCellProps}
{...(cellExpandableContent != null && {
compoundExpand: {
isExpanded: isRowExpanded && expandedColIndex === colIndex,
Expand Down Expand Up @@ -158,7 +173,7 @@ export const DataViewTableBasic: FC<DataViewTableBasicProps> = ({
<OuterScrollContainer>
<InnerScrollContainer>
<Table ref={tableRef} aria-label="Data table" ouiaId={ouiaId} isExpandable={isExpandable} hasAnimations {...props} isStickyHeader >
{ activeHeadState || <DataViewTableHead columns={columns} ouiaId={ouiaId} hasResizableColumns={hasResizableColumns} /> }
{ activeHeadState || <DataViewTableHead columns={columns} ouiaId={ouiaId} hasResizableColumns={hasResizableColumns} isSticky={isSticky} /> }
{ bodyContent }
</Table>
</InnerScrollContainer>
Expand All @@ -167,7 +182,7 @@ export const DataViewTableBasic: FC<DataViewTableBasicProps> = ({
} else {
return (
<Table ref={tableRef} aria-label="Data table" ouiaId={ouiaId} isExpandable={isExpandable} hasAnimations {...props}>
{ activeHeadState || <DataViewTableHead columns={columns} ouiaId={ouiaId} hasResizableColumns={hasResizableColumns} /> }
{ activeHeadState || <DataViewTableHead columns={columns} ouiaId={ouiaId} hasResizableColumns={hasResizableColumns} isSticky={isSticky} /> }
{ bodyContent }
</Table>
);
Expand Down
55 changes: 39 additions & 16 deletions packages/module/src/DataViewTableHead/DataViewTableHead.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,35 +14,58 @@ export interface DataViewTableHeadProps extends TheadProps {
ouiaId?: string;
/** @hide Indicates whether table is resizable */
hasResizableColumns?: boolean;
/** Toggles sticky columns and header */
isSticky?: boolean;
}

export const DataViewTableHead: FC<DataViewTableHeadProps> = ({
isTreeTable = false,
columns,
ouiaId = 'DataViewTableHead',
hasResizableColumns,
isSticky = false,
...props
}: DataViewTableHeadProps) => {
const { selection } = useInternalContext();
const { onSelect, isSelected } = selection ?? {};

const cells = useMemo(
() => [
onSelect && isSelected && !isTreeTable ? (
<Th key="row-select" screenReaderText="Data selection table head cell" />
) : null,
...columns.map((column, index) => (
<DataViewThElement
key={index}
content={isDataViewThObject(column) ? column.cell : column}
resizableProps={isDataViewThObject(column) ? column.resizableProps : undefined}
data-ouia-component-id={`${ouiaId}-th-${index}`}
thProps={isDataViewThObject(column) ? (column?.props ?? {}) : {}}
hasResizableColumns={hasResizableColumns}
/>
))
],
[ columns, ouiaId, onSelect, isSelected, isTreeTable, hasResizableColumns ]
() => {
// Check if the first column has isStickyColumn
const firstColumnProps = isDataViewThObject(columns[0]) ? (columns[0]?.props ?? {}) : {};
const firstColumnIsSticky = firstColumnProps.isStickyColumn;

return [
onSelect && isSelected && !isTreeTable ? (
<Th
key="row-select"
screenReaderText="Data selection table head cell"
isStickyColumn={firstColumnIsSticky}
stickyMinWidth="45px"
stickyLeftOffset="0px"
/>
) : null,
...columns.map((column, index) => {
const thProps = isDataViewThObject(column) ? (column?.props ?? {}) : {};
// If the first column is sticky and selection is enabled, offset it by the selection column width
const enhancedThProps = index === 0 && thProps.isStickyColumn && onSelect && isSelected
? { ...thProps, stickyLeftOffset: '45px' }
: thProps;

return (
<DataViewThElement
key={index}
content={isDataViewThObject(column) ? column.cell : column}
resizableProps={isDataViewThObject(column) ? column.resizableProps : undefined}
data-ouia-component-id={`${ouiaId}-th-${index}`}
thProps={enhancedThProps}
hasResizableColumns={hasResizableColumns}
/>
);
})
];
},
[ columns, ouiaId, onSelect, isSelected, isTreeTable, hasResizableColumns, isSticky ]
);

return (
Expand Down