import React, { cloneElement, type HTMLAttributes, type ReactElement } from 'react' import classnames from 'classnames' import { Checkbox, type CheckedStatus } from '~components/Checkbox' import { Heading } from '~components/Heading' import { Icon } from '~components/Icon' import { Tooltip } from '~components/TooltipV1' import { type OverrideClassName } from '~components/types/OverrideClassName' import styles from './Table.module.scss' export type TableContainerProps = { children?: React.ReactNode /** @default "compact" */ variant?: 'compact' | 'default' | 'data' } /** * {@link https://cultureamp.atlassian.net/wiki/spaces/DesignSystem/pages/3081929256/Table Guidance} | * {@link https://cultureamp.design/?path=/docs/components-table--docs Storybook} */ export const TableContainer = ({ variant = 'compact', children, ...otherProps }: TableContainerProps): JSX.Element => (
{children}
) export type TableHeaderProps = { children?: React.ReactNode } export const TableHeader = ({ children, ...otherProps }: TableHeaderProps): JSX.Element => (
{children}
) export type TableHeaderRowProps = { children?: React.ReactNode } const ratioToPercent = (width?: number): string | number | undefined => width != null ? `${width * 100}%` : width export type TableHeaderRowCellCheckboxProps = { checkable?: boolean checkedStatus?: CheckedStatus /** This will be passed into the aria-label for the checkbox to provide context to the user */ checkboxLabel?: string onCheck?: (event: React.ChangeEvent) => any } /** * @param width value between 1 and 0, to be calculated as a percentage * @param flex CSS flex shorthand as a string. Be sure to specify the flex grow, * shrink, and basis, due to IE11 compatibility. eg. use "1 1 auto" * instead of just "1". */ export type TableHeaderRowCellProps = { labelText: string onClick?: | ((e: React.MouseEvent) => any) | ((e: React.MouseEvent) => any) width?: number flex?: string href?: string icon?: ReactElement reversed?: boolean /** * Shows an up or down arrow, to show that the column is sorted. */ sorting?: 'ascending' | 'descending' wrapping?: 'nowrap' | 'wrap' align?: 'start' | 'center' | 'end' tooltipInfo?: string /** If set, this will hide the tooltip exclamation icon. Useful in situations where the table header does not have enough space. This should be done with caution as tooltips should have a visual indicator to users */ isTooltipIconHidden?: boolean /** * Specify where the tooltip should be rendered. */ tooltipPortalSelector?: string | undefined /** If set, this will show the arrow in the direction provided when the header cell is hovered over. */ sortingArrowsOnHover?: 'ascending' | 'descending' | undefined } & TableHeaderRowCellCheckboxProps & OverrideClassName> export const TableHeaderRowCell = ({ labelText, onClick, href, width, flex, icon, checkable, checkedStatus, checkboxLabel, onCheck, reversed, sorting: sortingRaw, // I can't say for certain why "nowrap" was the default value. Normally you wouldn't // want to clip off information because it doesn't fit on one line. // My assumption is that because since the cell width rows are decoupled, a heading // cell with a word longer than the column width would push the columns out of // alignment? I'm not sure. // Anyway, we can override this default behaviour by setting wrapping to "wrap". wrapping = 'nowrap', align = 'start', tooltipInfo, isTooltipIconHidden = false, tooltipPortalSelector, sortingArrowsOnHover, classNameOverride, // There aren't any other props in the type definition, so I'm unsure why we // have this spread. ...otherProps }: TableHeaderRowCellProps): JSX.Element => { const sorting = sortingRaw const [isHovered, setIsHovered] = React.useState(false) const updateHoverState = (hoverState: boolean): void => { if (sortingArrowsOnHover && hoverState != isHovered) setIsHovered(hoverState) } const headerColor = reversed ? 'white-reduced-opacity' : 'dark-reduced-opacity' const hoveredHeaderColor = reversed ? 'white' : 'dark' // For this "cellContents" variable, we start at the inner most child, and // wrap it elements, depending on what the props dictate. let cellContents = (
{icon && ( {cloneElement(icon, { title: labelText, ['aria-label']: labelText, // title is unreliable so this is a sensible fallback for tables with icons as headers without aria-labels role: 'img', })} )} {checkable && (
)} {tooltipInfo != null && !isTooltipIconHidden ? (
) : null} {/* If an "icon" is supplied, the label is displayed inside the icon aria title instead */} {!icon ? ( {labelText} ) : null} {(sorting ?? (isHovered && sortingArrowsOnHover)) && (
{sorting === 'ascending' || sortingArrowsOnHover === 'ascending' ? ( ) : ( )}
)}
) cellContents = href ? ( ) => any | undefined} onMouseEnter={(): void => updateHoverState(true)} onFocus={(): void => updateHoverState(true)} onMouseLeave={(): void => updateHoverState(false)} onBlur={(): void => updateHoverState(false)} > {cellContents} ) : onClick ? ( ) : ( // This div wrapper probably isn't needed, but it's a bit easier // for this flex positioning, to have the dom tree depth match for // each permutation.
{cellContents}
) cellContents = tooltipInfo != null ? ( {cellContents} ) : ( // Again, this wrapper is just to make the dom tree consistent between // different permutations.
{cellContents}
) return (
{cellContents}
) } type ButtonClickEvent = (e: React.MouseEvent) => void type AnchorClickEvent = (e: React.MouseEvent) => void /** * As the Card examples in Storybook take a TableRow, * I opted to give the child the role="row" tag. That being * said, while TableHeader has a role="rowgroup" as given * in an example on the accessibility docs, I couldn't justify * adding the same here as all rows look to be wrapped in the * TableCard. * * It may mean that the consumer needs to add their own container * around with the role="row". We could also just add it as a * very simple component similar to TableHeader. * * @see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/Table_Role */ export type TableCardProps = OverrideClassName> & { onClick?: ButtonClickEvent | AnchorClickEvent expanded?: boolean expandedStyle?: 'well' | 'popout' href?: string // Despite there being no onClick or href, still show a hover state on the // rows. An example use case is when you might want to handle click events // at a cell level, instead of the full row level. forceHoverState?: boolean children?: React.ReactNode } export const TableCard = ({ children, expanded, expandedStyle = 'well', onClick, href, forceHoverState = false, classNameOverride, ...otherProps }: TableCardProps): JSX.Element => { const className = classnames( styles.card, expanded && styles.expanded, expanded && styles[expandedStyle], (forceHoverState || onClick != null || href != null) && styles.hasHoverState, classNameOverride, ) return href != null ? ( {children} ) : onClick ? ( ) : (
{children}
) } /** * Aria roles like aria-rowindex can be added from * the component consumer. * * @param {*} { children, classNameOverride, ...otherProps } */ export type TableRowProps = OverrideClassName> & { children?: React.ReactNode } export const TableRow = ({ children, classNameOverride, ...otherProps }: TableRowProps): JSX.Element => (
{children}
) /** * @param width value between 1 and 0, to be calculated as a percentage * @param flex CSS flex shorthand as a string. Be sure to specify the flex grow, * shrink, and basis, due to IE11 compatibility. eg. use "1 1 auto" * instead of just "1". */ export type TableRowCellProps = OverrideClassName> & { children?: React.ReactNode width?: number flex?: string href?: string } export const TableRowCell = ({ children, width, flex, href, classNameOverride, ...otherProps }: TableRowCellProps): JSX.Element => href != null ? ( {children} ) : (
{children}
)