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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,8 @@ When adding/making changes to a component, always make sure your code is tested:

## Testing and Linting
- run `npm run test` to run the unit tests
- run `cypress:run:ci:cp` to run component tests
- run `cypress:run:ci:e2e` to run E2E tests
- run `npm run cypress:run:ci:cp` to run component tests
- run `npm run cypress:run:ci:e2e` to run E2E tests
- run `npm run lint` to run the linter

## A11y testing
Expand Down
43 changes: 43 additions & 0 deletions cypress/component/DataViewTableBasic.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,30 @@ const rows = repositories.map(item => Object.values(item));

const columns = [ 'Repositories', 'Branches', 'Pull requests', 'Workspaces', 'Last commit' ];

const stickyColumns = [
{ cell: 'Repositories', props: { isStickyColumn: true, hasRightBorder: true } },
'Branches',
'Pull requests',
'Workspaces',
'Last commit',
];

const stickyRows = [
{ name: 'Repository one', branches: 'Branch one', prs: 'Pull request one', workspaces: 'Workspace one', lastCommit: 'Timestamp one' },
].map(item => [
{ cell: item.name, props: { isStickyColumn: true, hasRightBorder: true } },
item.branches,
item.prs,
item.workspaces,
item.lastCommit,
]);

const selection = {
onSelect: () => undefined,
isSelected: () => false,
isSelectDisabled: () => false,
};

describe('DataViewTableBasic', () => {

it('renders a basic data view table', () => {
Expand Down Expand Up @@ -102,4 +126,23 @@ describe('DataViewTableBasic', () => {
cy.get('[data-ouia-component-id="data-tr-loading"]').contains('Data is loading');
});

it('applies sticky column styling to the selection and first data column when isSticky and the first column is sticky', () => {
const ouiaId = 'data-sticky-select';

cy.mount(
<DataView selection={selection}>
<DataViewTableBasic
aria-label="Sticky selectable table"
ouiaId={ouiaId}
columns={stickyColumns}
rows={stickyRows}
isSticky
/>
</DataView>
);

cy.get('thead tr th.pf-v6-c-table__sticky-cell').should('have.length', 2);
cy.get('tbody tr').first().find('td.pf-v6-c-table__sticky-cell').should('have.length', 2);
});

});
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ propComponents:
'DataViewTrTree',
'DataViewTrObject',
'DataViewTh',
'DataViewThResizableProps'
'DataViewThResizableProps',
'DataViewTableHead'
]
sourceLink: https://github.com/patternfly/react-data-view/blob/main/packages/module/patternfly-docs/content/extensions/data-view/examples/Table/Table.md
---
Expand Down Expand Up @@ -105,6 +106,8 @@ When sticky headers and columns are enabled:
- 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 **row selection** is enabled (via the `DataView` `selection` prop) and the first column in the `columns` array is a sticky column (`isStickyColumn: true` in that column’s `props`), the row-selection checkbox column is included in the same sticky group as that first data column. The first data column’s `stickyLeftOffset` is aligned so it sits to the right of the selection column. This requires the first **defined** column in `columns` to be the sticky one; a leading `null` placeholder in `columns` is not treated as a sticky first column for this behavior.

### Sticky header and columns example

```js file="./DataViewTableStickyExample.tsx"
Expand Down
1 change: 1 addition & 0 deletions packages/module/src/DataViewTable/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { default } from './DataViewTable';
export * from './DataViewTable';
export * from './stickySelectionColumn';
112 changes: 112 additions & 0 deletions packages/module/src/DataViewTable/stickySelectionColumn.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import {
mergeFirstStickyDataColumnProps,
shouldIncludeStickySelectionColumn,
STICKY_SELECTION_COLUMN_WIDTH,
stickySelectionCellProps,
} from './stickySelectionColumn';

describe('stickySelectionColumn', () => {
describe('stickySelectionCellProps', () => {
it('matches row-selection sticky grouping props', () => {
expect(stickySelectionCellProps).toEqual({
isStickyColumn: true,
hasRightBorder: false,
stickyMinWidth: STICKY_SELECTION_COLUMN_WIDTH,
});
});
});

describe('shouldIncludeStickySelectionColumn', () => {
it('is true when table is sticky, selectable, and first column is sticky', () => {
expect(
shouldIncludeStickySelectionColumn(
[ { cell: 'Name', props: { isStickyColumn: true } } ],
true,
true
)
).toBe(true);
});

it('is false when table is not sticky', () => {
expect(
shouldIncludeStickySelectionColumn(
[ { cell: 'Name', props: { isStickyColumn: true } } ],
true,
false
)
).toBe(false);
});

it('is false when not selectable', () => {
expect(
shouldIncludeStickySelectionColumn(
[ { cell: 'Name', props: { isStickyColumn: true } } ],
false,
true
)
).toBe(false);
});

it('is false when first column is not sticky', () => {
expect(
shouldIncludeStickySelectionColumn(
[ { cell: 'Name', props: { isStickyColumn: false } } ],
true,
true
)
).toBe(false);
});

it('is false when columns is empty', () => {
expect(shouldIncludeStickySelectionColumn([], true, true)).toBe(false);
});

it('is false when first column entry is null', () => {
expect(
shouldIncludeStickySelectionColumn([ null, { cell: 'Name', props: { isStickyColumn: true } } ], true, true)
).toBe(false);
});
});

describe('mergeFirstStickyDataColumnProps', () => {
it('adds stickyLeftOffset when including selection sticky', () => {
expect(
mergeFirstStickyDataColumnProps(
{ isStickyColumn: true, hasRightBorder: true },
true
)
).toEqual({
isStickyColumn: true,
hasRightBorder: true,
stickyLeftOffset: STICKY_SELECTION_COLUMN_WIDTH,
});
});

it('preserves existing stickyLeftOffset', () => {
expect(
mergeFirstStickyDataColumnProps(
{ isStickyColumn: true, stickyLeftOffset: '80px' },
true
)
).toEqual({
isStickyColumn: true,
stickyLeftOffset: '80px',
});
});

it('does not merge when first column is not sticky', () => {
expect(
mergeFirstStickyDataColumnProps({ isStickyColumn: false }, true)
).toEqual({ isStickyColumn: false });
});

it('returns column props unchanged when not including sticky selection', () => {
const props = { isStickyColumn: true, hasRightBorder: true };
expect(mergeFirstStickyDataColumnProps(props, false)).toBe(props);
});

it('returns undefined when column props are undefined', () => {
expect(mergeFirstStickyDataColumnProps(undefined, true)).toBeUndefined();
});
});
});
56 changes: 56 additions & 0 deletions packages/module/src/DataViewTable/stickySelectionColumn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { TdProps, ThProps } from '@patternfly/react-table';

/**
* Min width / left offset for the row-selection column when it is grouped with a sticky first data column.
* Matches PatternFly’s typical checkbox column width so the Name column’s sticky inset aligns.
*/
export const STICKY_SELECTION_COLUMN_WIDTH = '4rem';

/** Props applied to the injected checkbox Th/Td when they participate in a sticky first-column group */
export const stickySelectionCellProps: Pick<
ThProps,
'isStickyColumn' | 'hasRightBorder' | 'stickyMinWidth'
> = {
isStickyColumn: true,
hasRightBorder: false,
stickyMinWidth: STICKY_SELECTION_COLUMN_WIDTH,
};

/** `columns[0]` object shape from {@link DataViewTh} (avoids importing DataViewTable and a circular dependency). */
function isStickyFirstColumnDefinition(column: unknown): boolean {
return (
column != null &&
typeof column === 'object' &&
'props' in column &&
(column as { props?: { isStickyColumn?: boolean } }).props?.isStickyColumn === true
);
}

export function shouldIncludeStickySelectionColumn(
columns: unknown[],
isSelectable: boolean,
isStickyTable: boolean
): boolean {
if (!isStickyTable || !isSelectable || columns.length === 0) {
return false;
}
const first = columns[0];
if (first == null) {
return false;
}
return isStickyFirstColumnDefinition(first);
}

/** Adds horizontal inset so the first sticky data column sits after the sticky selection column */
export function mergeFirstStickyDataColumnProps<P extends ThProps | TdProps>(
columnProps: P | undefined,
includeStickySelection: boolean
): P | undefined {
if (!columnProps || !includeStickySelection || !columnProps.isStickyColumn) {
return columnProps;
}
return {
...columnProps,
stickyLeftOffset: columnProps.stickyLeftOffset ?? STICKY_SELECTION_COLUMN_WIDTH,
};
}
43 changes: 43 additions & 0 deletions packages/module/src/DataViewTableBasic/DataViewTableBasic.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { DataView } from '../DataView';
import { DataViewSelection } from '../InternalContext';
import { DataViewTableBasic, ExpandableContent } from './DataViewTableBasic';
import { DataViewTh } from '../DataViewTable';

interface Repository {
id: number;
Expand Down Expand Up @@ -31,6 +33,20 @@ const rows = repositories.map(({ id, name, branches, prs, workspaces, lastCommit

const columns = [ 'Repositories', 'Branches', 'Pull requests', 'Workspaces', 'Last commit' ];

const stickyColumns: DataViewTh[] = [
{ cell: 'Repositories', props: { isStickyColumn: true, hasRightBorder: true } },
'Branches',
'Pull requests',
'Workspaces',
'Last commit',
];

const mockSelection: DataViewSelection = {
onSelect: jest.fn(),
isSelected: jest.fn(() => false),
isSelectDisabled: jest.fn(() => false),
};

const expandableContents: ExpandableContent[] = [
{ rowId: 1, columnId: 1, content: <div>Branch details for Repository one</div> },
];
Expand Down Expand Up @@ -72,6 +88,33 @@ describe('DataViewTable component', () => {
expect(container).toMatchSnapshot();
});

test('applies sticky classes to selection and first data cells when isSticky and first column is sticky', () => {
const stickyRows = repositories.map(({ id, name, branches, prs, workspaces, lastCommit }) => [
{ id, cell: name, props: { isStickyColumn: true, hasRightBorder: true } },
branches,
prs,
workspaces,
lastCommit,
]);

const { container } = render(
<DataView selection={mockSelection}>
<DataViewTableBasic
aria-label='Repositories table'
ouiaId={ouiaId}
columns={stickyColumns}
rows={stickyRows}
isSticky
/>
</DataView>
);

const firstBodyRow = container.querySelector('tbody tr');
const bodyCells = firstBodyRow?.querySelectorAll('td');
expect(bodyCells?.[0]?.classList.contains('pf-v6-c-table__sticky-cell')).toBe(true);
expect(bodyCells?.[1]?.classList.contains('pf-v6-c-table__sticky-cell')).toBe(true);
});

test('when isExpandable cell should be clickable and expandable', async () => {
const user = userEvent.setup();

Expand Down
Loading