import { forwardRef, useState, useRef, useEffect, useCallback } from 'react'
import { Filter, Search, ListFilter, X } from 'lucide-react'
import { DataTableHeaderProps } from './types'
import { Button } from '../../Button'
import { TextInput } from '../../Inputs/TextInput'
import { ButtonSize, ButtonType } from '../../Button/types'
import Block from '../../Primitives/Block/Block'
import PrimitiveText from '../../Primitives/PrimitiveText/PrimitiveText'
import PrimitiveButton from '../../Primitives/PrimitiveButton/PrimitiveButton'
import { FOUNDATION_THEME } from '../../../tokens'
import { TableTokenType } from '../dataTable.tokens'
import { Popover } from '../../Popover'
import { PopoverSize } from '../../Popover/types'
import { useMobileDataTable } from '../hooks/useMobileDataTable'
import {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerContent,
DrawerBody,
} from '../../Drawer'
import { useResponsiveTokens } from '../../../hooks/useResponsiveTokens'
import Tooltip from '../../Tooltip/Tooltip'
import { TooltipSize, TooltipSide, TooltipAlign } from '../../Tooltip/types'
const SearchCloseButton = ({
onClose,
tableToken,
}: {
onClose: () => void
tableToken: TableTokenType
}) => (
)
const MobileSearchInput = ({
searchPlaceholder,
searchConfig,
onSearch,
onClose,
tableToken,
}: {
searchPlaceholder: string
searchConfig: { query: string }
onSearch: (query: string) => void
onClose: () => void
tableToken: TableTokenType
}) => (
) =>
onSearch(event.target.value)
}
onBlur={() => {
if (!searchConfig.query.trim()) {
onClose()
}
}}
onKeyDown={(event: React.KeyboardEvent) => {
if (event.key === 'Escape') {
onClose()
}
}}
rightSlot={
}
autoFocus
/>
)
const TruncatedTextWithTooltip = ({
children,
tooltipProps,
...textProps
}: {
// This component is only used for the `description` prop in this file.
// `description` is a string, so we keep the typing narrow to avoid ReactNode/bigint mismatches.
children: string
tooltipProps?: {
side?: TooltipSide
align?: TooltipAlign
size?: TooltipSize
showArrow?: boolean
delayDuration?: number
offset?: number
}
[key: string]: unknown
}) => {
const wrapperRef = useRef(null)
const [isTruncated, setIsTruncated] = useState(false)
const checkTruncation = useCallback(() => {
const wrapper = wrapperRef.current
if (!wrapper) return
const textElement =
(wrapper.querySelector('p') as HTMLElement) ||
(wrapper.firstElementChild as HTMLElement)
if (!textElement) {
setIsTruncated(false)
return
}
if (textElement.offsetWidth === 0 || textElement.offsetHeight === 0) {
setIsTruncated(false)
return
}
const truncated = textElement.scrollWidth > textElement.clientWidth
setIsTruncated(truncated)
}, [])
useEffect(() => {
const wrapper = wrapperRef.current
if (!wrapper) return
const timeoutId = setTimeout(checkTruncation, 0)
const resizeObserver = new ResizeObserver(() => {
requestAnimationFrame(checkTruncation)
})
resizeObserver.observe(wrapper)
const observeTextElement = () => {
const textElement = wrapper.querySelector('p') as HTMLElement
if (textElement) {
resizeObserver.observe(textElement)
}
}
const observeTimeoutId = setTimeout(observeTextElement, 100)
window.addEventListener('resize', checkTruncation, { passive: true })
return () => {
clearTimeout(timeoutId)
clearTimeout(observeTimeoutId)
resizeObserver.disconnect()
window.removeEventListener('resize', checkTruncation)
}
}, [children, checkTruncation])
const handleMouseEnter = useCallback(() => {
if (!isTruncated) {
checkTruncation()
}
}, [isTruncated, checkTruncation])
const content = (
{children}
)
if (isTruncated && children) {
return (
{content}
)
}
return content
}
const DataTableHeader = forwardRef<
HTMLDivElement,
DataTableHeaderProps>
>(
(
{
title,
description,
showHeader = true,
showToolbar = true,
enableSearch = false,
searchPlaceholder = 'Search...',
searchConfig,
enableAdvancedFilter = false,
advancedFilterComponent: AdvancedFilterComponent,
advancedFilters = [],
onSearch,
onAdvancedFiltersChange,
onClearAllFilters,
headerSlot1,
headerSlot2,
headerSlot3,
mobileToolbarSlot,
descriptionTooltipProps,
},
ref
) => {
const tableToken = useResponsiveTokens('TABLE')
const mobileConfig = useMobileDataTable()
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
const [isSearchOpen, setIsSearchOpen] = useState(false)
// Don't render anything if showHeader is false
if (!showHeader) {
return null
}
const hasToolbarContent =
showToolbar &&
(enableSearch ||
enableAdvancedFilter ||
headerSlot1 ||
headerSlot2 ||
headerSlot3 ||
mobileToolbarSlot)
if (!title && !description && !hasToolbarContent) {
return null
}
if (mobileConfig.isMobile) {
return (
{title && (
{title}
)}
{showToolbar && (
{enableSearch && isSearchOpen && (
setIsSearchOpen(false)}
tableToken={tableToken}
/>
)}
{enableSearch && !isSearchOpen && (
}
aria-label="Open search"
size={ButtonSize.SMALL}
onClick={() => setIsSearchOpen(true)}
/>
)}
{mobileToolbarSlot}
{(enableAdvancedFilter ||
headerSlot1 ||
headerSlot2 ||
headerSlot3) && (
)}
{description && (
{description}
)}
{enableAdvancedFilter &&
AdvancedFilterComponent && (
Advanced Filters
{})
}
onClearFilters={
onClearAllFilters
}
/>
)}
{(headerSlot1 ||
headerSlot2 ||
headerSlot3) && (
Actions
{headerSlot1 && (
{headerSlot1}
)}
{headerSlot2 && (
{headerSlot2}
)}
{headerSlot3 && (
{headerSlot3}
)}
)}
{enableAdvancedFilter &&
advancedFilters.length > 0 && (
)}
)
}
const hasTitleOrDescription = Boolean(title || description)
return (
{hasTitleOrDescription && (
{title && (
{title}
)}
{description && (
{description}
)}
)}
{showToolbar && (
{(enableSearch || enableAdvancedFilter) && (
{enableSearch && (
) => onSearch(event.target.value)}
/>
)}
{enableAdvancedFilter &&
AdvancedFilterComponent && (
0
? ButtonType.PRIMARY
: ButtonType.SECONDARY
}
leadingIcon={
}
aria-label={`Advanced filters${advancedFilters.length > 0 ? ` (${advancedFilters.length} active)` : ''}`}
size={ButtonSize.SMALL}
>
Advanced Filters{' '}
{advancedFilters.length >
0 &&
`(${advancedFilters.length})`}
}
size={PopoverSize.MEDIUM}
align="end"
alignOffset={-20}
secondaryAction={{
onClick: onClearAllFilters,
disabled:
!searchConfig.query.trim() &&
advancedFilters.length ===
0,
}}
>
{})
}
onClearFilters={
onClearAllFilters
}
/>
)}
)}
{headerSlot1 && (
{headerSlot1}
)}
{headerSlot2 && (
{headerSlot2}
)}
{headerSlot3 && (
{headerSlot3}
)}
)}
)
}
)
DataTableHeader.displayName = 'DataTableHeader'
export default DataTableHeader