import { createRef, forwardRef, useEffect, useState, useContext } from 'react'; import { css } from '@patternfly/react-styles'; import styles from '@patternfly/react-styles/css/components/Table/table'; import scrollStyles from '@patternfly/react-styles/css/components/Table/table-scrollable'; import treeViewStyles from '@patternfly/react-styles/css/components/Table/table-tree-view'; import { BaseCellProps } from './Table'; import { cellActions, selectable, collapsible, compoundExpand, cellWidth, Visibility, classNames, favoritable } from './utils'; import { IRowData, IExtraData } from './TableTypes'; import { draggable } from './utils/decorators/draggable'; import { treeRow } from './utils'; import { mergeProps } from './base/merge-props'; import { IVisibility } from './utils/decorators/classNames'; import { Tooltip } from '@patternfly/react-core/dist/esm/components/Tooltip/Tooltip'; import { IFormatterValueType, IExtra } from './TableTypes'; import { TdActionsType, TdCompoundExpandType, TdDraggableType, TdExpandType, TdFavoritesType, TdSelectType, TdTreeRowType } from './base/types'; import { TableContext } from './Table'; import cssStickyCellMinWidth from '@patternfly/react-tokens/dist/esm/c_table__sticky_cell_MinWidth'; import cssStickyCellInlineStart from '@patternfly/react-tokens/dist/esm/c_table__sticky_cell_InsetInlineStart'; import cssStickyCellInlineEnd from '@patternfly/react-tokens/dist/esm/c_table__sticky_cell_InsetInlineEnd'; export interface TdProps extends BaseCellProps, Omit, 'onSelect' | 'width'> { /** * The column header the cell corresponds to. * This attribute replaces table header in mobile viewport. It is rendered by ::before pseudo element. */ dataLabel?: string; /** Renders a checkbox or radio select */ select?: TdSelectType; /** Turns the cell into an actions cell. Recommended to use an ActionsColumn component as a child of the Td rather than this prop. */ actions?: TdActionsType; /** Indicates the cell contains an interactive element and prevents that element's padding from increasing row height. Recommended when other cells in the same row contains text. */ hasAction?: boolean; /** Turns the cell into an expansion toggle and determines if the corresponding expansion row is open */ expand?: TdExpandType; /** Turns the cell into a compound expansion toggle */ compoundExpand?: TdCompoundExpandType; /** Turns the cell into a favorites cell with a star button */ favorites?: TdFavoritesType; /** Turns the cell into the first cell in a tree table row */ treeRow?: TdTreeRowType; /** Turns the cell into the first cell in a draggable row*/ draggableRow?: TdDraggableType; /** True to remove padding */ noPadding?: boolean; /** Applies pf-v6-c-table__action to td */ isActionCell?: boolean; /** * Tooltip to show on the body cell. * Note: If the body cell is truncated and has simple string content, it will already attempt to display the cell text. * If you want to show a tooltip that differs from the cell text, you can set it here. * To disable it completely you can set it to null. */ tooltip?: React.ReactNode; /** Callback on mouse enter */ onMouseEnter?: (event: any) => void; /** Indicates the column should be sticky */ isStickyColumn?: boolean; /** Adds a border to the right side of the cell */ hasRightBorder?: boolean; /** Adds a border to the left side of the cell */ hasLeftBorder?: boolean; /** Minimum width for a sticky column */ stickyMinWidth?: string; /** Left offset of a sticky column. This will typically be equal to the combined value set by stickyMinWidth of any sticky columns that precede the current sticky column. */ stickyLeftOffset?: string; /** Right offset of a sticky column. This will typically be equal to the combined value set by stickyMinWidth of any sticky columns that come after the current sticky column. */ stickyRightOffset?: string; } const TdBase: React.FunctionComponent = ({ children, className, isActionCell = false, hasAction = false, component = 'td', dataLabel, textCenter = false, modifier, select = null, actions = null, expand = null, treeRow: treeRowProp = null, compoundExpand: compoundExpandProp = null, noPadding, width, visibility, innerRef, favorites = null, draggableRow: draggableRowProp = null, tooltip = '', onMouseEnter: onMouseEnterProp = () => {}, isStickyColumn = false, hasRightBorder = false, hasLeftBorder = false, stickyMinWidth = '120px', stickyLeftOffset, stickyRightOffset, ...props }: TdProps) => { const [showTooltip, setShowTooltip] = useState(false); const [truncated, setTruncated] = useState(false); const cellRef = innerRef ? innerRef : createRef(); const onMouseEnter = (event: any) => { if (event.target.offsetWidth < event.target.scrollWidth) { !showTooltip && setShowTooltip(true); } else { showTooltip && setShowTooltip(false); } onMouseEnterProp(event); }; const selectParams = select ? selectable(children as IFormatterValueType, { rowIndex: select.rowIndex, rowData: { selected: select.isSelected, disableSelection: select?.isDisabled, props: select?.props }, column: { extraParams: { onSelect: select?.onSelect, selectVariant: select.variant || 'checkbox' } } }) : null; const favoriteParams = favorites ? favoritable(null, { rowIndex: favorites?.rowIndex, rowData: { favorited: favorites.isFavorited, favoritesProps: favorites?.props }, column: { extraParams: { onFavorite: favorites?.onFavorite } } }) : null; const draggableParams = draggableRowProp !== null ? draggable(null, { rowData: { id: draggableRowProp.id } }) : null; const actionParamsFunc = actions ? cellActions(actions.items, null, null) : null; const actionParams = actionParamsFunc ? actionParamsFunc(null, { rowIndex: actions?.rowIndex, rowData: { disableActions: actions?.isDisabled }, column: { extraParams: { dropdownPosition: actions?.dropdownPosition, dropdownDirection: actions?.dropdownDirection, menuAppendTo: actions?.menuAppendTo, actionsToggle: actions?.actionsToggle } } }) : null; const { hasAnimations, variant } = useContext(TableContext); const expandableParams = expand !== null ? collapsible(null, { rowIndex: expand.rowIndex, columnIndex: expand?.columnIndex, rowData: { isOpen: expand.isExpanded }, column: { extraParams: { onCollapse: expand?.onToggle, expandId: expand?.expandId, variant } } }) : null; const updateAnimationClass = () => { const ancestorControlRow = (cellRef as React.RefObject)?.current?.closest( `.${styles.tableTr}.${styles.tableControlRow}` ); const isControlRowExpanded = ancestorControlRow.classList.contains(styles.modifiers.expanded); if (!isControlRowExpanded) { return; } const isCurrentCellExpanded = (cellRef as React.RefObject)?.current?.classList.contains( styles.modifiers.expanded ); if (isCurrentCellExpanded) { ancestorControlRow.classList.remove(styles.modifiers.noAnimateExpand); } else { ancestorControlRow.classList.add(styles.modifiers.noAnimateExpand); } }; const internalCompoundOnToggle = ( event: React.MouseEvent, rowIndex: number, colIndex: number, isOpen: boolean, rowData: IRowData, extraData: IExtraData ) => { hasAnimations && updateAnimationClass(); compoundExpandProp?.onToggle(event, rowIndex, colIndex, isOpen, rowData, extraData); }; const compoundParams = compoundExpandProp !== null ? compoundExpand( { title: children, props: { isOpen: compoundExpandProp.isExpanded } } as IFormatterValueType, { rowIndex: compoundExpandProp?.rowIndex, columnIndex: compoundExpandProp?.columnIndex, column: { extraParams: { onExpand: internalCompoundOnToggle, expandId: compoundExpandProp?.expandId } } } ) : null; const widthParams = width ? cellWidth(width)() : null; const visibilityParams = visibility ? classNames(...visibility.map((vis: keyof IVisibility) => Visibility[vis]))() : null; const treeRowParams = treeRowProp !== null ? treeRow( treeRowProp.onCollapse, treeRowProp.onCheckChange, treeRowProp.onToggleRowDetails )( { title: children } as IFormatterValueType, { rowIndex: treeRowProp.rowIndex, rowData: { props: treeRowProp.props } } as IExtra ) : null; const merged = mergeProps( selectParams, actionParams, expandableParams, compoundParams, widthParams, visibilityParams, favoriteParams, treeRowParams, draggableParams ); const { // selectable adds this but we don't want it // eslint-disable-next-line @typescript-eslint/no-unused-vars isVisible = null, children: mergedChildren = null, className: mergedClassName = '', component: MergedComponent = component, ...mergedProps } = merged; const treeTableTitleCell = (className && className.includes(treeViewStyles.tableTreeViewTitleCell)) || (mergedClassName && mergedClassName.includes(treeViewStyles.tableTreeViewTitleCell)); useEffect(() => { setTruncated( (cellRef as React.RefObject).current.offsetWidth < (cellRef as React.RefObject).current.scrollWidth ); }, [cellRef]); const cell = ( setShowTooltip(false)} onMouseEnter={tooltip !== null ? onMouseEnter : onMouseEnterProp} className={css( styles.tableTd, className, isActionCell && styles.tableAction, hasAction && styles.modifiers.action, textCenter && styles.modifiers.center, noPadding && styles.modifiers.noPadding, isStickyColumn && scrollStyles.tableStickyCell, hasRightBorder && scrollStyles.modifiers.borderRight, hasLeftBorder && scrollStyles.modifiers.borderLeft, styles.modifiers[modifier as 'breakWord' | 'fitContent' | 'nowrap' | 'truncate' | 'wrap' | undefined], draggableParams && styles.tableDraggable, mergedClassName )} ref={cellRef} {...mergedProps} {...props} {...(isStickyColumn && { style: { [cssStickyCellMinWidth.name]: stickyMinWidth ? stickyMinWidth : undefined, [cssStickyCellInlineStart.name]: stickyLeftOffset ? stickyLeftOffset : 0, [cssStickyCellInlineEnd.name]: stickyRightOffset ? stickyRightOffset : 0, ...props.style } as React.CSSProperties })} > {mergedChildren || children} ); const canMakeDefaultTooltip = tooltip === '' ? typeof children === 'string' : true; return tooltip !== null && canMakeDefaultTooltip && showTooltip ? ( <> {cell} } content={tooltip || (tooltip === '' && children)} isVisible /> ) : ( cell ); }; export const Td = forwardRef((props: TdProps, ref: React.Ref) => ( )); Td.displayName = 'Td';