Skip to content
Draft
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
Expand Up @@ -5,7 +5,7 @@ propComponents: ['Toolbar', 'ToolbarContent', 'ToolbarGroup', 'ToolbarItem', 'To
section: components
---

import { Fragment, useState } from 'react';
import { Fragment, useState, useRef, useLayoutEffect } from 'react';

import EditIcon from '@patternfly/react-icons/dist/esm/icons/edit-icon';
import CloneIcon from '@patternfly/react-icons/dist/esm/icons/clone-icon';
Expand Down Expand Up @@ -114,11 +114,13 @@ When all of a toolbar's required elements cannot fit in a single line, you can s
```

## Examples with spacers and wrapping

You may adjust the space between toolbar items to arrange them into groups. Read our spacers documentation to learn more about using spacers.

Items are spaced “16px” apart by default and can be modified by changing their or their parents' `gap`, `columnGap`, and `rowGap` properties. You can set the property values at multiple breakpoints, including "default", "md", "lg", "xl", and "2xl".

### Toolbar content wrapping

The toolbar content section will wrap by default, but you can set the `rowRap` property to `noWrap` to make it not wrap.

```ts file="./ToolbarContentWrap.tsx"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,57 @@
import { Fragment, useState } from 'react';
import { Fragment, useLayoutEffect, useRef, useState } from 'react';
import { Toolbar, ToolbarItem, ToolbarContent, SearchInput, Checkbox } from '@patternfly/react-core';

const useTheadPinnedFromScrollParent = ({ track, scrollRootRef, theadRef }): { isPinned } => {
const [isPinned, setIsPinned] = useState(false);

useLayoutEffect(() => {
if (!track) {
setIsPinned(false);
return;
}

const scrollRoot = scrollRootRef.current;
if (!scrollRoot) {
setIsPinned(false);
return;
}

const syncFromScroll = () => {
setIsPinned(scrollRoot.scrollTop > 0);
};
syncFromScroll();
scrollRoot.addEventListener('scroll', syncFromScroll, { passive: true });
return () => scrollRoot.removeEventListener('scroll', syncFromScroll);
}, [track, scrollRootRef, theadRef]);

return { isPinned };
};

export const ToolbarSticky = () => {
const [isSticky, setIsSticky] = useState(true);
const [showEvenOnly, setShowEvenOnly] = useState(true);
const [searchValue, setSearchValue] = useState('');
const array = Array.from(Array(30), (_, x) => x); // create array of numbers from 1-30 for demo purposes
const numbers = showEvenOnly ? array.filter((number) => number % 2 === 0) : array;

const innerScrollRef = useRef<HTMLDivElement>(null);
const toolbarRef = useRef<HTMLDivElement>(null);
const { isPinned } = useTheadPinnedFromScrollParent({
track: true,
scrollRootRef: innerScrollRef,
theadRef: toolbarRef
});

return (
<Fragment>
<div style={{ overflowY: 'scroll', height: '200px' }}>
<Toolbar id="toolbar-sticky" inset={{ default: 'insetNone' }} isSticky={isSticky}>
<div style={{ overflowY: 'scroll', height: '200px' }} ref={innerScrollRef}>
<Toolbar
className={isPinned ? 'PINNED' : ''}
id="toolbar-sticky"
inset={{ default: 'insetNone' }}
isSticky={isSticky}
ref={toolbarRef}
>
<ToolbarContent>
<ToolbarItem>
<SearchInput
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { css } from '@patternfly/react-styles';
import styles from '@patternfly/react-styles/css/components/Table/table-scrollable';
import { forwardRef } from 'react';

export interface InnerScrollContainerProps extends React.HTMLProps<HTMLDivElement> {
/** Content rendered inside the inner scroll container */
Expand All @@ -8,14 +9,14 @@ export interface InnerScrollContainerProps extends React.HTMLProps<HTMLDivElemen
className?: string;
}

export const InnerScrollContainer: React.FunctionComponent<InnerScrollContainerProps> = ({
children,
className,
...props
}: InnerScrollContainerProps) => (
<div className={css(className, styles.scrollInnerWrapper)} {...props}>
const InnerScrollContainerBase = (
{ children, className, ...props }: InnerScrollContainerProps,
ref: React.ForwardedRef<HTMLDivElement>
) => (
<div ref={ref} className={css(className, styles.scrollInnerWrapper)} {...props}>
{children}
</div>
);

export const InnerScrollContainer = forwardRef(InnerScrollContainerBase);
InnerScrollContainer.displayName = 'InnerScrollContainer';
6 changes: 4 additions & 2 deletions packages/react-table/src/components/Table/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,14 @@ interface TableContextProps {
registerSelectableRow?: () => void;
hasAnimations?: boolean;
variant?: TableVariant | 'compact';
isStickyHeader?: boolean;
}

export const TableContext = createContext<TableContextProps>({
registerSelectableRow: () => {},
hasAnimations: false,
variant: undefined
variant: undefined,
isStickyHeader: false
});

const TableBase: React.FunctionComponent<TableProps> = ({
Expand Down Expand Up @@ -214,7 +216,7 @@ const TableBase: React.FunctionComponent<TableProps> = ({
};

return (
<TableContext.Provider value={{ registerSelectableRow, hasAnimations, variant }}>
<TableContext.Provider value={{ registerSelectableRow, hasAnimations, variant, isStickyHeader }}>
<table
aria-label={ariaLabel}
role={role}
Expand Down
9 changes: 8 additions & 1 deletion packages/react-table/src/components/Table/Thead.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ export interface TheadProps extends React.HTMLProps<HTMLTableSectionElement> {
innerRef?: React.Ref<any>;
/** Indicates the <thead> contains a nested header */
hasNestedHeader?: boolean;
/**
* When true, applies the placeholder `PINNED` class for styling while the sticky header is scrolled
* within its scroll container. Drive this from app logic or a hook (see table examples).
*/
isPinned?: boolean;
}

const TheadBase: React.FunctionComponent<TheadProps> = ({
Expand All @@ -21,14 +26,16 @@ const TheadBase: React.FunctionComponent<TheadProps> = ({
noWrap = false,
innerRef,
hasNestedHeader,
isPinned,
...props
}: TheadProps) => (
<thead
className={css(
styles.tableThead,
className,
noWrap && styles.modifiers.nowrap,
hasNestedHeader && styles.modifiers.nestedColumnHeader
hasNestedHeader && styles.modifiers.nestedColumnHeader,
isPinned && 'PINNED'
)}
ref={innerRef}
{...props}
Expand Down
11 changes: 9 additions & 2 deletions packages/react-table/src/components/Table/examples/Table.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ The `Table` component takes an explicit and declarative approach, and its implem

The documentation for the deprecated table implementation can be found under the [React deprecated](/components/table/react-deprecated) tab. It is configuration based and takes a less declarative and more implicit approach to laying out the table structure, such as the rows and cells within it.

import { Fragment, isValidElement, useCallback, useEffect, useRef, useState } from 'react';
import { Fragment, isValidElement, useLayoutEffect, useCallback, useEffect, useRef, useState } from 'react';
import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon';
import CodeBranchIcon from '@patternfly/react-icons/dist/esm/icons/code-branch-icon';
import CodeIcon from '@patternfly/react-icons/dist/esm/icons/code-icon';
Expand Down Expand Up @@ -327,7 +327,6 @@ To enable a tree table:
- `checkAriaLabel` - (optional) accessible label for the checkbox
- `showDetailsAriaLabel` - (optional) accessible label for the show row details button in the responsive view
4. The first `Td` in each row will pass the following to the `treeRow` prop:

- `onCollapse` - Callback when user expands/collapses a row to reveal/hide the row's children.
- `onCheckChange` - (optional) Callback when user changes the checkbox on a row.
- `onToggleRowDetails` - (optional) Callback when user shows/hides the row details in responsive view.
Expand Down Expand Up @@ -427,6 +426,14 @@ To maintain proper sticky behavior across sticky columns and header, `Table` mus

```

### Sticky columns and header (scroll-pinned class)

This example matches [Sticky columns and header](#sticky-columns-and-header) but uses the `useTheadPinnedFromScrollParent` hook with refs on `InnerScrollContainer` and `Thead` to toggle `isPinned` and apply the placeholder `PINNED` class when the inner scroll container has been scrolled. If the scroll-root ref is not set, the hook falls back to the exported `getOverflowScrollParent` helper using the thead ref.

```ts file="TableStickyColumnsAndHeaderScrollPinned.tsx"

```

### Nested column headers

To make a nested column header:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { useLayoutEffect, useRef, useState } from 'react';
import {
Table,
Thead,
Tr,
Th,
Tbody,
Td,
InnerScrollContainer,
OuterScrollContainer,
ThProps,
ISortBy
} from '@patternfly/react-table';
import BlueprintIcon from '@patternfly/react-icons/dist/esm/icons/blueprint-icon';

const useTheadPinnedFromScrollParent = ({ track, scrollRootRef, theadRef }): { isPinned } => {
const [isPinned, setIsPinned] = useState(false);

useLayoutEffect(() => {
if (!track) {
setIsPinned(false);
return;
}

const scrollRoot = scrollRootRef.current;
if (!scrollRoot) {
setIsPinned(false);
return;
}

const syncFromScroll = () => {
setIsPinned(scrollRoot.scrollTop > 0);
};
syncFromScroll();
scrollRoot.addEventListener('scroll', syncFromScroll, { passive: true });
return () => scrollRoot.removeEventListener('scroll', syncFromScroll);
}, [track, scrollRootRef, theadRef]);

return { isPinned };
};

interface Fact {
name: string;
state: string;
detail1: string;
detail2: string;
detail3: string;
detail4: string;
detail5: string;
detail6: string;
detail7: string;
}

export const TableStickyColumnsAndHeaderScrollPinned: React.FunctionComponent = () => {
const facts: Fact[] = Array.from({ length: 9 }, (_, index) => ({
name: `Fact ${index + 1}`,
state: `State ${index + 1}`,
detail1: `Test cell ${index + 1}-3`,
detail2: `Test cell ${index + 1}-4`,
detail3: `Test cell ${index + 1}-5`,
detail4: `Test cell ${index + 1}-6`,
detail5: `Test cell ${index + 1}-7`,
detail6: `Test cell ${index + 1}-8`,
detail7: `Test cell ${index + 1}-9`
}));

const columnNames = {
name: 'Fact',
state: 'State',
header3: 'Header 3',
header4: 'Header 4',
header5: 'Header 5',
header6: 'Header 6',
header7: 'Header 7',
header8: 'Header 8',
header9: 'Header 9'
};

const [activeSortIndex, setActiveSortIndex] = useState(-1);
const [activeSortDirection, setActiveSortDirection] = useState<ISortBy['direction']>();

const innerScrollRef = useRef<HTMLDivElement>(null);
const theadRef = useRef<HTMLTableSectionElement>(null);

const { isPinned } = useTheadPinnedFromScrollParent({
track: true,
scrollRootRef: innerScrollRef,
theadRef
});

const getSortableRowValues = (fact: Fact): (string | number)[] => {
const { name, state, detail1, detail2, detail3, detail4, detail5, detail6, detail7 } = fact;
return [name, state, detail1, detail2, detail3, detail4, detail5, detail6, detail7];
};

let sortedFacts = facts;
if (activeSortIndex > -1) {
sortedFacts = facts.sort((a, b) => {
const aValue = getSortableRowValues(a)[activeSortIndex];
const bValue = getSortableRowValues(b)[activeSortIndex];
if (aValue === bValue) {
return 0;
}
if (activeSortDirection === 'asc') {
return aValue > bValue ? 1 : -1;
} else {
return bValue > aValue ? 1 : -1;
}
});
}

const getSortParams = (columnIndex: number): ThProps['sort'] => ({
sortBy: {
index: activeSortIndex,
direction: activeSortDirection
},
onSort: (_event, index, direction) => {
setActiveSortIndex(index);
setActiveSortDirection(direction);
},
columnIndex
});

return (
<div style={{ height: '400px' }}>
<OuterScrollContainer>
<InnerScrollContainer ref={innerScrollRef}>
<Table aria-label="Sticky columns and header with scroll-pinned class" gridBreakPoint="" isStickyHeader>
<Thead ref={theadRef} isPinned={isPinned}>
<Tr>
<Th isStickyColumn modifier="truncate" sort={getSortParams(0)}>
{columnNames.name}
</Th>
<Th
isStickyColumn
stickyMinWidth="120px"
stickyLeftOffset="120px"
hasRightBorder
modifier="truncate"
sort={getSortParams(1)}
>
{columnNames.state}
</Th>
<Th modifier="truncate">{columnNames.header3}</Th>
<Th modifier="truncate">{columnNames.header4}</Th>
<Th modifier="truncate">{columnNames.header5}</Th>
<Th modifier="truncate">{columnNames.header6}</Th>
<Th modifier="truncate">{columnNames.header7}</Th>
<Th modifier="truncate">{columnNames.header8}</Th>
<Th modifier="truncate">{columnNames.header9}</Th>
</Tr>
</Thead>
<Tbody>
{sortedFacts.map((fact) => (
<Tr key={fact.name}>
<Th isStickyColumn modifier="truncate">
{fact.name}
</Th>
<Th isStickyColumn stickyMinWidth="120px" stickyLeftOffset="120px" modifier="truncate" hasRightBorder>
<BlueprintIcon />
{` ${fact.state}`}
</Th>
<Td modifier="nowrap" dataLabel={columnNames.header3}>
{fact.detail1}
</Td>
<Td modifier="nowrap" dataLabel={columnNames.header4}>
{fact.detail2}
</Td>
<Td modifier="nowrap" dataLabel={columnNames.header5}>
{fact.detail3}
</Td>
<Td modifier="nowrap" dataLabel={columnNames.header6}>
{fact.detail4}
</Td>
<Td modifier="nowrap" dataLabel={columnNames.header7}>
{fact.detail5}
</Td>
<Td modifier="nowrap" dataLabel={columnNames.header8}>
{fact.detail6}
</Td>
<Td modifier="nowrap" dataLabel={columnNames.header9}>
{fact.detail7}
</Td>
</Tr>
))}
</Tbody>
</Table>
</InnerScrollContainer>
</OuterScrollContainer>
</div>
);
};
Loading