import { createContext, forwardRef, useEffect, useRef, useState } from 'react'; import styles from '@patternfly/react-styles/css/components/Table/table'; import stylesGrid from '@patternfly/react-styles/css/components/Table/table-grid'; import stylesTreeView from '@patternfly/react-styles/css/components/Table/table-tree-view'; import { css } from '@patternfly/react-styles'; import { toCamel } from './utils'; import { IVisibility } from './utils/decorators/classNames'; import { handleArrows, setTabIndex } from '@patternfly/react-core/dist/esm/helpers/KeyboardHandler'; import { KeyTypes } from '@patternfly/react-core/dist/esm/helpers/constants'; import { useOUIAProps, OUIAProps } from '@patternfly/react-core/dist/esm/helpers/OUIA/ouia'; import { useHasAnimations } from '@patternfly/react-core/dist/esm/helpers'; import { TableGridBreakpoint, TableVariant } from './TableTypes'; export interface BaseCellProps { /** Content rendered inside the cell */ children?: React.ReactNode; /** Additional classes added to the cell */ className?: string; /** Element to render */ component?: React.ReactNode; /** Modifies cell to center its contents. */ textCenter?: boolean; /** Style modifier to apply */ modifier?: 'breakWord' | 'fitContent' | 'nowrap' | 'truncate' | 'wrap'; /** Width percentage modifier */ width?: 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 60 | 70 | 80 | 90 | 100; /** Visibility breakpoint modifiers */ visibility?: (keyof IVisibility)[]; /** @hide Forwarded ref */ innerRef?: React.Ref; } export interface TableProps extends React.HTMLProps, OUIAProps { /** Adds an accessible name for the Table */ 'aria-label'?: string; /** Content rendered inside the Table */ children?: React.ReactNode; /** Additional classes added to the Table */ className?: string; /** * Style variant for the Table * compact: Reduces spacing and makes the table more compact */ variant?: TableVariant | 'compact'; /** Render borders */ borders?: boolean; /** Specifies the grid breakpoints */ gridBreakPoint?: '' | 'grid' | 'grid-md' | 'grid-lg' | 'grid-xl' | 'grid-2xl'; /** A valid WAI-ARIA role to be applied to the table element */ role?: string; /** @beta Flag indicating if the table should have plain styling with a transparent background */ isPlain?: boolean; /** @beta Flag indicating if the table should not have plain styling when in the glass theme */ isNoPlainOnGlass?: boolean; /** If set to true, the table header sticks to the top of its container. This property applies both the sticky position and styling. */ isStickyHeader?: boolean; /** @beta Flag indicating the table header should have sticky positioning to the top of the parent InnerScrollContainer. */ isStickyHeaderBase?: boolean; /** @beta Flag indicating the table header should have stuck styling, when the header is not at the top of the scroll container. */ isStickyHeaderStuck?: boolean; /** @hide Forwarded ref */ innerRef?: React.RefObject; /** Flag indicating table is a tree table */ isTreeTable?: boolean; /** Flag indicating this table is nested within another table */ isNested?: boolean; /** Flag indicating this table should be striped. This property works best for a single table. Striping may also be done manually by applying this property to Tbody and Tr components. */ isStriped?: boolean; /** Flag indicating this table contains expandable rows. */ isExpandable?: boolean; /** @beta Flag indicating whether expandable rows within the table have animations. Expandable rows cannot be dynamically rendered. This prop * will be removed in the next breaking change, with the default behavior becoming animations always being enabled. */ hasAnimations?: boolean; /** Flag indicating this table's rows will not have the inset typically reserved for expanding/collapsing rows in a tree table. Intended for use on tree tables with no visible rows with children. */ hasNoInset?: boolean; /** Collection of column spans for nested headers. Deprecated: see https://github.com/patternfly/patternfly/issues/4584 */ nestedHeaderColumnSpans?: number[]; /** Visible text to add alongside the hidden a11y caption for tables with selectable rows. */ selectableRowCaptionText?: string; /** Value to overwrite the randomly generated data-ouia-component-id.*/ ouiaId?: number | string; /** Set the value of data-ouia-safe. Only set to true when the component is in a static state, i.e. no animations are occurring. At all other times, this value must be false. */ ouiaSafe?: boolean; } interface TableContextProps { registerSelectableRow?: () => void; hasAnimations?: boolean; variant?: TableVariant | 'compact'; } export const TableContext = createContext({ registerSelectableRow: () => {}, hasAnimations: false, variant: undefined }); const TableBase: React.FunctionComponent = ({ children, className, variant, borders = true, isStickyHeader = false, isStickyHeaderBase = false, isStickyHeaderStuck = false, isPlain = false, isNoPlainOnGlass = false, gridBreakPoint = TableGridBreakpoint.gridMd, 'aria-label': ariaLabel, role = 'grid', innerRef, ouiaId, ouiaSafe = true, isTreeTable = false, isNested = false, isStriped = false, isExpandable = false, hasAnimations: hasAnimationsProp, hasNoInset = false, // eslint-disable-next-line @typescript-eslint/no-unused-vars nestedHeaderColumnSpans, selectableRowCaptionText, ...props }: TableProps) => { const hasAnimations = useHasAnimations(hasAnimationsProp); const ref = useRef(null); const tableRef = innerRef || ref; const [hasSelectableRows, setHasSelectableRows] = useState(false); const [tableCaption, setTableCaption] = useState(); useEffect(() => { document.addEventListener('keydown', handleKeys); // sets up roving tab-index to tree tables only if (tableRef && tableRef.current && tableRef.current.classList.contains('pf-m-tree-view')) { const tbody = tableRef.current.querySelector('tbody'); tbody && setTabIndex(Array.from(tbody.querySelectorAll('button, a, input'))); } return function cleanup() { document.removeEventListener('keydown', handleKeys); }; }, [tableRef, tableRef.current]); useEffect(() => { if (selectableRowCaptionText) { setTableCaption( {selectableRowCaptionText}
This table has selectable rows. It can be navigated by row using tab, and each row can be selected using space or enter.
); } else { setTableCaption( This table has selectable rows. It can be navigated by row using tab, and each row can be selected using space or enter. ); } }, [selectableRowCaptionText]); const ouiaProps = useOUIAProps('Table', ouiaId, ouiaSafe); const grid = stylesGrid.modifiers?.[ toCamel(gridBreakPoint || '').replace(/-?2xl/, '_2xl') as 'grid' | 'gridMd' | 'gridLg' | 'gridXl' | 'grid_2xl' ]; const breakPointPrefix = `treeView${gridBreakPoint.charAt(0).toUpperCase() + gridBreakPoint.slice(1)}`; const treeGrid = stylesTreeView.modifiers?.[ toCamel(breakPointPrefix || '').replace(/-?2xl/, '_2xl') as | 'treeViewGrid' | 'treeViewGridMd' | 'treeViewGridLg' | 'treeViewGridXl' | 'treeViewGrid_2xl' ]; const handleKeys = (event: KeyboardEvent) => { if ( isNested || !(tableRef && tableRef.current && tableRef.current.classList.contains(stylesTreeView.modifiers.treeView)) || // implements roving tab-index to tree tables only (tableRef && tableRef.current !== (event.target as HTMLElement).closest(`.${styles.table}:not(.pf-m-nested)`)) ) { return; } const activeElement = document.activeElement; const key = event.key; const rows = (Array.from(tableRef.current.querySelectorAll('tbody tr')) as Element[]).filter( (el) => !el.classList.contains('pf-m-disabled') && !(el as HTMLElement).hidden ); if (key === KeyTypes.Space || key === KeyTypes.Enter) { (activeElement as HTMLElement).click(); event.preventDefault(); } const getFocusableElement = (element: Element) => element.querySelectorAll('button:not(:disabled), input:not(:disabled), a:not(:disabled)')[0]; handleArrows( event, rows, (element: Element) => element === activeElement.closest('tr'), getFocusableElement, ['button', 'input', 'a'], undefined, false, true, false ); }; const registerSelectableRow = () => { !hasSelectableRows && setHasSelectableRows(true); }; return ( {hasSelectableRows && tableCaption} {children}
); }; export const Table = forwardRef((props: TableProps, ref: React.Ref) => ( } /> )); Table.displayName = 'Table';