From 733d9577efe2d28b80bd6af885c582b587a91671 Mon Sep 17 00:00:00 2001 From: Michael Johnson Date: Thu, 2 Jul 2026 15:51:04 -0400 Subject: [PATCH] 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. --- .../DataViewTableStickySelectionExample.tsx | 96 +++++++++++++++++++ .../data-view/examples/Table/Table.md | 9 ++ .../module/patternfly-docs/generated/index.js | 4 +- .../DataViewTableBasic/DataViewTableBasic.tsx | 21 +++- .../DataViewTableHead/DataViewTableHead.tsx | 55 +++++++---- 5 files changed, 164 insertions(+), 21 deletions(-) create mode 100644 packages/module/patternfly-docs/content/extensions/data-view/examples/Table/DataViewTableStickySelectionExample.tsx diff --git a/packages/module/patternfly-docs/content/extensions/data-view/examples/Table/DataViewTableStickySelectionExample.tsx b/packages/module/patternfly-docs/content/extensions/data-view/examples/Table/DataViewTableStickySelectionExample.tsx new file mode 100644 index 00000000..01fce174 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/data-view/examples/Table/DataViewTableStickySelectionExample.tsx @@ -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:
Another action
, + 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: , 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: , props: { isActionCell: true } }, +]); + +const columns: DataViewTh[] = [ + { cell: 'Repositories', props: { isStickyColumn: true, modifier: 'fitContent', hasRightBorder: true } }, + { cell: <>Branches, 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 ( + +
+ +
+
+ ); +}; diff --git a/packages/module/patternfly-docs/content/extensions/data-view/examples/Table/Table.md b/packages/module/patternfly-docs/content/extensions/data-view/examples/Table/Table.md index 2aa382ad..2c3320e5 100644 --- a/packages/module/patternfly-docs/content/extensions/data-view/examples/Table/Table.md +++ b/packages/module/patternfly-docs/content/extensions/data-view/examples/Table/Table.md @@ -104,6 +104,7 @@ 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 ``), 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 @@ -111,6 +112,14 @@ When sticky headers and columns are enabled: ``` +### 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 diff --git a/packages/module/patternfly-docs/generated/index.js b/packages/module/patternfly-docs/generated/index.js index 610e983e..b9d46728 100644 --- a/packages/module/patternfly-docs/generated/index.js +++ b/packages/module/patternfly-docs/generated/index.js @@ -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", diff --git a/packages/module/src/DataViewTableBasic/DataViewTableBasic.tsx b/packages/module/src/DataViewTableBasic/DataViewTableBasic.tsx index 7d3fe3ac..4fa257b9 100644 --- a/packages/module/src/DataViewTableBasic/DataViewTableBasic.tsx +++ b/packages/module/src/DataViewTableBasic/DataViewTableBasic.tsx @@ -82,11 +82,18 @@ export const DataViewTableBasic: FC = ({ (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 = ( {isSelectable && ( { @@ -102,10 +109,18 @@ export const DataViewTableBasic: FC = ({ 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 ( = ({ - { activeHeadState || } + { activeHeadState || } { bodyContent }
@@ -167,7 +182,7 @@ export const DataViewTableBasic: FC = ({ } else { return ( - { activeHeadState || } + { activeHeadState || } { bodyContent }
); diff --git a/packages/module/src/DataViewTableHead/DataViewTableHead.tsx b/packages/module/src/DataViewTableHead/DataViewTableHead.tsx index 81fc1e19..78bacf5d 100644 --- a/packages/module/src/DataViewTableHead/DataViewTableHead.tsx +++ b/packages/module/src/DataViewTableHead/DataViewTableHead.tsx @@ -14,6 +14,8 @@ 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 = ({ @@ -21,28 +23,49 @@ export const DataViewTableHead: FC = ({ columns, ouiaId = 'DataViewTableHead', hasResizableColumns, + isSticky = false, ...props }: DataViewTableHeadProps) => { const { selection } = useInternalContext(); const { onSelect, isSelected } = selection ?? {}; const cells = useMemo( - () => [ - onSelect && isSelected && !isTreeTable ? ( - - ) : null, - ...columns.map((column, index) => ( - - )) - ], - [ 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 ? ( + + ) : 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 ( + + ); + }) + ]; + }, + [ columns, ouiaId, onSelect, isSelected, isTreeTable, hasResizableColumns, isSticky ] ); return (