import React, { forwardRef, useRef, useEffect, useState, useCallback, } from 'react' import { styled } from 'styled-components' import { TableCellProps } from './types' import { TableTokenType } from '../dataTable.tokens' import Block from '../../Primitives/Block/Block' import PrimitiveInput from '../../Primitives/PrimitiveInput/PrimitiveInput' import { FOUNDATION_THEME } from '../../../tokens' import { ColumnType, DropdownColumnProps, DateColumnProps, DateFormat, } from '../types' import SingleSelect from '../../SingleSelect/SingleSelect' import { SelectMenuVariant } from '../../Select' import { SelectMenuGroupType } from '../../Select/types' import { useResponsiveTokens } from '../../../hooks/useResponsiveTokens' import { useResizeObserver } from '../../../hooks/useResizeObserver' import Tooltip from '../../Tooltip/Tooltip' import { TooltipSize } from '../../Tooltip/types' import { parseDateLike, formatDateString } from '../utils' const StyledTableCell = styled.td<{ width?: React.CSSProperties $hasCustomContent?: boolean $tableToken: TableTokenType $isFirstRow?: boolean $customBackgroundColor?: string $hasCustomBackground?: boolean }>` ${(props) => props.$tableToken ? props.$tableToken.dataTable.table.body.cell : ''} background-color: ${({ $customBackgroundColor, $hasCustomBackground, $tableToken, }) => $hasCustomBackground && $customBackgroundColor ? $customBackgroundColor : $tableToken.dataTable.table.body.row.backgroundColor} !important; box-sizing: border-box; ${({ $isFirstRow }) => $isFirstRow && 'border-top: none'} ` const isEmptyValue = (value: unknown, columnType?: ColumnType): boolean => { if (value == null) return true if (typeof value === 'string' && value.trim() === '') return true if (typeof value === 'number' && value === 0) return true if (columnType === ColumnType.DROPDOWN) { const dropdownData = value as DropdownColumnProps if ( !dropdownData || !dropdownData.options || !dropdownData.selectedValue || (typeof dropdownData.selectedValue === 'string' && dropdownData.selectedValue.trim() === '') ) { return true } } if (columnType === ColumnType.DATE) { const dateValue = typeof value === 'object' && value !== null && 'date' in value ? (value as DateColumnProps).date : value if ( !dateValue || (typeof dateValue === 'string' && dateValue.trim() === '') || !parseDateLike(dateValue) ) { return true } } return false } const TruncatedTextWithTooltip = ({ text, style = {}, suffix, tableToken, }: { text: string style?: React.CSSProperties suffix?: string tableToken?: TableTokenType }) => { const textRef = useRef(null) const [isTruncated, setIsTruncated] = useState(false) const rafRef = useRef(null) const checkTruncation = useCallback(() => { if (rafRef.current !== null) { cancelAnimationFrame(rafRef.current) } rafRef.current = requestAnimationFrame(() => { const element = textRef.current if (element) { const truncated = element.scrollWidth > element.clientWidth && element.clientWidth > 0 setIsTruncated(truncated) } rafRef.current = null }) }, []) useResizeObserver(textRef as React.RefObject, () => checkTruncation() ) useEffect(() => { const timeoutId = setTimeout(checkTruncation, 0) window.addEventListener('resize', checkTruncation, { passive: true }) return () => { clearTimeout(timeoutId) window.removeEventListener('resize', checkTruncation) if (rafRef.current !== null) { cancelAnimationFrame(rafRef.current) } } }, [text, checkTruncation]) const truncatedContent = ( {text} {suffix != null && ( {suffix} )} ) if (isTruncated && text) { const isVeryLongString = text.length > 50 && !text.includes(' ') const tooltipMaxWidth = isVeryLongString ? '600px' : undefined return ( {truncatedContent} ) } return truncatedContent } const TableCell = forwardRef< HTMLTableCellElement, TableCellProps> >( ( { column, row, rowIndex, colIndex, tableTitle, isEditing, currentValue, width, frozenStyles, isFirstRow, customBackgroundColor, hasCustomBackground, onFieldChange, getDisplayValue, dateLabel, 'data-row-index': dataRowIndex, 'data-col-index': dataColIndex, tabIndex: cellTabIndex, onFocus: cellOnFocus, onClick: cellOnClick, role: cellRole, }, ref ) => { const tableToken = useResponsiveTokens('TABLE') as TableTokenType // Apply formatting if getDisplayValue is provided and we're not editing const displayValue = !isEditing && getDisplayValue ? getDisplayValue(currentValue, column) : currentValue const generateDataAttributes = () => { const attrs: Record = {} if ( tableTitle && typeof rowIndex === 'number' && typeof colIndex === 'number' ) { attrs['data-element'] = `${tableTitle}_tr${rowIndex + 1}_td${colIndex + 1}` } const valueToCheck = displayValue || currentValue if ( valueToCheck == null || React.isValidElement(valueToCheck) || (typeof valueToCheck === 'object' && valueToCheck.constructor === Object) ) { if (row.id) { attrs['data-row-id'] = String(row.id) } return attrs } if ( column.type === ColumnType.NUMBER || typeof valueToCheck === 'number' ) { attrs['data-type'] = 'numeric' attrs['data-numeric'] = String(valueToCheck || 0) } else if (column.type === ColumnType.DATE) { attrs['data-type'] = 'date' const dateValue = typeof valueToCheck === 'object' && valueToCheck !== null && 'date' in valueToCheck ? (valueToCheck as DateColumnProps).date : valueToCheck const showTime = typeof valueToCheck === 'object' && valueToCheck !== null && 'showTime' in valueToCheck ? Boolean((valueToCheck as DateColumnProps).showTime) : false if (dateValue) { const parsedDate = parseDateLike(dateValue) if (parsedDate) { attrs['data-date'] = parsedDate.toLocaleDateString( 'en-US', { year: 'numeric', month: 'short', day: '2-digit', hour: showTime ? '2-digit' : undefined, minute: showTime ? '2-digit' : undefined, } ) } } } else if ( typeof valueToCheck === 'string' && valueToCheck.trim() !== '' ) { const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ const moneyPattern = /[$€£¥₹₽₪₩₦₡₵₸₴₺₻₼₽¢]/ const numberPattern = /^\s*[$€£¥₹₽₪₩₦₡₵₸₴₺₻₼₽¢]?[\d,]+\.?\d*\s*$/ if (emailPattern.test(valueToCheck)) { attrs['data-type'] = 'email' attrs['data-id'] = valueToCheck } else if ( moneyPattern.test(valueToCheck) || (numberPattern.test(valueToCheck) && column.header?.toLowerCase().includes('amount')) ) { attrs['data-type'] = 'money' attrs['data-money-cell'] = valueToCheck } else { attrs['data-type'] = 'text' attrs['data-id'] = valueToCheck } } else if (typeof valueToCheck === 'boolean') { attrs['data-type'] = 'boolean' attrs['data-boolean'] = String(valueToCheck) } if (row.id) { attrs['data-row-id'] = String(row.id) } return attrs } const dataAttributes = generateDataAttributes() const renderContent = () => { if (isEditing && column.isEditable) { if (column.type === ColumnType.DROPDOWN) { const dropdownData = currentValue as DropdownColumnProps if (dropdownData && dropdownData.options) { const selectedOption = dropdownData.options.find( (opt) => opt.value === dropdownData.selectedValue ) const selectItems: SelectMenuGroupType[] = [ { items: dropdownData.options.map((option) => ({ label: option.label, value: String(option.value), slot1: option.icon || undefined, })), showSeparator: false, }, ] return (
{ const updatedDropdownData: DropdownColumnProps = { ...dropdownData, selectedValue: value, } if (onFieldChange) { onFieldChange(updatedDropdownData) } }} minMenuWidth={150} maxMenuWidth={250} />
) } } return ( ) => onFieldChange(e.target.value) } style={{ width: '100%', border: 'none', background: 'transparent', outline: 'none', fontSize: tableToken.dataTable.table.body.cell.fontSize, fontFamily: 'inherit', fontWeight: tableToken.dataTable.table.body.cell.fontWeight, color: 'inherit', padding: '4px 0', }} /> ) } if (column.type === ColumnType.DROPDOWN && !isEditing) { const dropdownData = displayValue as DropdownColumnProps if (isEmptyValue(dropdownData, ColumnType.DROPDOWN)) { return ( ) } const selectedOption = dropdownData.options.find( (opt) => opt.value === dropdownData.selectedValue ) const selectItems: SelectMenuGroupType[] = [ { items: dropdownData.options.map((option) => ({ label: option.label, value: String(option.value), slot1: option.icon || undefined, })), showSeparator: false, }, ] return (
{ const updatedDropdownData: DropdownColumnProps = { ...dropdownData, selectedValue: value, } if (onFieldChange) { onFieldChange(updatedDropdownData) } }} minMenuWidth={150} maxMenuWidth={250} />
) } if (column.type === ColumnType.DATE && !isEditing) { const dateData = typeof displayValue === 'object' && displayValue !== null && 'date' in displayValue ? (displayValue as DateColumnProps) : ({ date: displayValue as Date | string, showTime: false, } as DateColumnProps) if (isEmptyValue(dateData, ColumnType.DATE)) { return ( ) } const date = parseDateLike(dateData.date) const showTime = dateData.showTime || false // Format string precedence: cell-level `DateColumnProps.format` // beats column-level `dateFormat`. Falls back to a sensible // default based on whether time should be shown. const formatStr = dateData.format || (column as { dateFormat?: DateFormat }).dateFormat || (showTime ? 'DD MMM YYYY, hh:mm A' : 'DD MMM YYYY') const dateLabelStr = dateData.dateLabel || (column as { dateLabel?: string }).dateLabel || dateLabel const formatDate = (date: Date): string => { if (isNaN(date.getTime())) return '-' return formatDateString(date, formatStr) } return ( ) } if (column.renderCell) { return ( {( column.renderCell as ( value: unknown, row: unknown, index: number ) => React.ReactNode )(displayValue, row, rowIndex)} ) } if (isEmptyValue(displayValue, column.type)) { return ( ) } return ( ) } return ( {renderContent()} ) } ) TableCell.displayName = 'TableCell' export default TableCell