import React, { useState, useEffect, useRef } from 'react' import { ArrowUp, ArrowDown, ListFilter, ChevronRight, Search, } from 'lucide-react' import { TrashSimpleIcon } from '@phosphor-icons/react' import Block from '../../Primitives/Block/Block' import PrimitiveText from '../../Primitives/PrimitiveText/PrimitiveText' import { TextInput } from '../../Inputs/TextInput' import { Checkbox } from '../../Checkbox' import { CheckboxSize } from '../../Checkbox/types' import Slider from '../../Slider/Slider' import { SliderSize, SliderValueType } from '../../Slider/types' import { ColumnDefinition, FilterType, SortDirection } from '../types' import { TableTokenType } from '../dataTable.tokens' import { SortHandlers, FilterHandlers, FilterState, ColumnFilterHandler, SortState, } from './handlers' import { getSelectMenuItems, getMultiSelectMenuItems, filterItemsBySearch, getColumnTypeConfigForColumn, } from './utils' import { FOUNDATION_THEME } from '../../../tokens' import { useTheme } from '../../../context/ThemeContext' import { Drawer, DrawerPortal, DrawerOverlay, DrawerContent, DrawerBody, } from '../../Drawer' import DateRangePicker from '../../DateRangePicker/DateRangePicker' import { DateRange } from '../../DateRangePicker/types' import { parseDateLike, toLocalDateString } from '../utils' type MobileFilterDrawerProps = { column: ColumnDefinition> data?: Record[] tableToken: TableTokenType sortHandlers: SortHandlers filterHandlers: FilterHandlers filterState: FilterState sortState: SortState /** When false, no filter UI is offered — a filter here could not affect rows. */ enableFiltering?: boolean onColumnFilter?: ColumnFilterHandler onPopoverClose?: () => void } export const MobileFilterDrawer: React.FC = ({ column, data, tableToken, sortHandlers, filterHandlers, filterState, sortState, enableFiltering = true, onColumnFilter, onPopoverClose, }) => { const [filterDrawerOpen, setFilterDrawerOpen] = useState(false) const { theme } = useTheme() const destructiveColor = theme === 'dark' ? FOUNDATION_THEME.colors.red[400] : FOUNDATION_THEME.colors.red[600] const filterToken = tableToken.dataTable.table.header.filter const columnConfig = getColumnTypeConfigForColumn(column) const fieldKey = String(column.field) const hasFiltering = columnConfig.supportsFiltering && enableFiltering // Mirrors the desktop clear affordance: an applied value can be absent from // the current option list, so this is driven by filter state rather than by // what the option list happens to render. The date filter has its own clear // inside the nested drawer. const selectedFilterValue = filterState.columnSelectedValues[fieldKey] const hasActiveSelection = Array.isArray(selectedFilterValue) ? selectedFilterValue.length > 0 : selectedFilterValue !== undefined && selectedFilterValue !== '' const isSortingEnabled = columnConfig.supportsSorting && column.isSortable !== false const isCurrentField = sortState.currentSortField === fieldKey const currentDirection = isCurrentField ? sortState.currentSortDirection : SortDirection.NONE const isAscendingActive = currentDirection === SortDirection.ASCENDING const isDescendingActive = currentDirection === SortDirection.DESCENDING const handleSortAndClose = (sortFn: () => void) => { sortFn() onPopoverClose?.() } const handleOpenFilterDrawer = () => { setFilterDrawerOpen(true) } const handleCloseFilterDrawer = () => { setFilterDrawerOpen(false) } const renderSortItem = ( icon: React.ReactNode, label: string, onClick: () => void, isActive: boolean = false ) => ( {icon} {label} ) const renderFilterItem = ( label: string, isSelected: boolean, onClick: () => void ) => ( {label} ) // SliderFilter component to handle hooks properly const SliderFilter: React.FC = () => { const sliderColumn = column as ColumnDefinition< Record > & { sliderConfig?: { min: number max: number step?: number valueType?: string prefix?: string suffix?: string decimalPlaces?: number } } const sliderConfig = sliderColumn.sliderConfig const dataValues = data ?.map((row) => { const value = row[column.field] return typeof value === 'number' ? value : parseFloat(String(value)) || 0 }) .filter((val) => !isNaN(val)) || [] const dataMin = dataValues.length > 0 ? Math.min(...dataValues) : 0 const dataMax = dataValues.length > 0 ? Math.max(...dataValues) : 100 const min = sliderConfig?.min ?? dataMin const max = sliderConfig?.max ?? dataMax const step = sliderConfig?.step ?? 1 const currentFilter = filterState.columnSelectedValues[fieldKey] const isRangeFilter = currentFilter && typeof currentFilter === 'object' && 'min' in currentFilter && 'max' in currentFilter const currentMin = isRangeFilter && typeof currentFilter.min === 'number' ? currentFilter.min : min const currentMax = isRangeFilter && typeof currentFilter.max === 'number' ? currentFilter.max : max const validCurrentMin = Math.max(min, Math.min(max, currentMin)) const validCurrentMax = Math.max(min, Math.min(max, currentMax)) const [localValue, setLocalValue] = useState<[number, number]>([ validCurrentMin, validCurrentMax, ]) const debounceRef = useRef(null) useEffect(() => { setLocalValue([validCurrentMin, validCurrentMax]) }, [validCurrentMin, validCurrentMax]) useEffect(() => { return () => { if (debounceRef.current) { clearTimeout(debounceRef.current) } } }, []) if (!sliderConfig) { return ( Slider configuration missing ) } const handleSliderChange = (values: number[]) => { const [newMin, newMax] = values const newValue: [number, number] = [newMin, newMax] setLocalValue(newValue) if (debounceRef.current) { clearTimeout(debounceRef.current) } debounceRef.current = setTimeout(() => { const filterValue = { min: newValue[0], max: newValue[1] } if (onColumnFilter) { onColumnFilter( fieldKey, FilterType.SLIDER, filterValue, 'range' ) } }, 300) } const getValueType = (): SliderValueType => { switch (sliderConfig.valueType) { case 'percentage': return SliderValueType.PERCENTAGE case 'decimal': return SliderValueType.DECIMAL default: return SliderValueType.NUMBER } } return ( Filter by Range {sliderConfig.prefix || ''} {localValue[0]} {sliderConfig.suffix || ''} {sliderConfig.prefix || ''} {localValue[1]} {sliderConfig.suffix || ''} {sliderConfig.prefix || ''} {min} {sliderConfig.suffix || ''} {sliderConfig.prefix || ''} {max} {sliderConfig.suffix || ''} ) } return ( <> {isSortingEnabled && ( <> {renderSortItem( , 'Sort Ascending', () => handleSortAndClose(() => sortHandlers.handleSortAscending(fieldKey) ), isAscendingActive )} {renderSortItem( , 'Sort Descending', () => handleSortAndClose(() => sortHandlers.handleSortDescending(fieldKey) ), isDescendingActive )} )} {isSortingEnabled && hasFiltering && ( )} {hasFiltering && ( Filter )} {hasFiltering && columnConfig.filterComponent !== 'dateRange' && hasActiveSelection && ( { onColumnFilter?.( fieldKey, columnConfig.filterType, [], 'equals' ) onPopoverClose?.() }} > Clear Filter )} {(columnConfig.filterComponent === 'multiselect' || columnConfig.filterComponent === 'select') && ( } onChange={( e: React.ChangeEvent ) => { filterHandlers.handleSearchChange( fieldKey, e.target.value ) }} /> )} {columnConfig.filterComponent === 'select' && ( <> {getSelectMenuItems(column, data).map( (group, groupIndex) => filterItemsBySearch( group.items, filterState .columnSearchValues[ fieldKey ] || '' ).map((item, itemIndex) => { const selectedValues = filterState .columnSelectedValues[ fieldKey ] const currentSelected = Array.isArray( selectedValues ) ? selectedValues[0] : typeof selectedValues === 'string' ? selectedValues : '' const isSelected = currentSelected === item.value return (
{renderFilterItem( item.label, isSelected, () => { filterHandlers.handleSelectFilter( column, fieldKey, item.value, onColumnFilter ) handleCloseFilterDrawer() } )}
) }) )} )} {columnConfig.filterComponent === 'multiselect' && ( <> {getMultiSelectMenuItems( column, data ).map((group, groupIndex) => filterItemsBySearch( group.items, filterState.columnSearchValues[ fieldKey ] || '' ).map((item, itemIndex) => { const selectedValues = filterState .columnSelectedValues[ fieldKey ] const isSelected = Array.isArray( selectedValues ) && selectedValues.includes( item.value ) return (
{renderFilterItem( item.label, isSelected, () => { filterHandlers.handleMultiSelectFilter( column, fieldKey, item.value, onColumnFilter ) } )}
) }) )} )} {columnConfig.filterComponent === 'slider' && ( )} {columnConfig.filterComponent === 'dateRange' && ( {(() => { const selectedValue = filterState .columnSelectedValues[ fieldKey ] const selectedRange = Array.isArray( selectedValue ) ? selectedValue : typeof selectedValue === 'string' && selectedValue ? [ selectedValue, selectedValue, ] : [] const hasActiveDateRange = selectedRange.length > 0 && Boolean(selectedRange[0]) return hasActiveDateRange ? ( ) : null })()} { const selectedValue = filterState .columnSelectedValues[ fieldKey ] const selectedRange = Array.isArray(selectedValue) ? selectedValue : typeof selectedValue === 'string' && selectedValue ? [ selectedValue, selectedValue, ] : [] if ( selectedRange.length === 0 || !selectedRange[0] ) { return undefined } const start = parseDateLike( selectedRange[0] ) if (!start) return undefined const end = parseDateLike( selectedRange[1] || selectedRange[0] ) || start return { startDate: start, endDate: end, } })()} onChange={(range) => { const start = toLocalDateString( range.startDate ) const end = toLocalDateString( range.endDate || range.startDate ) onColumnFilter?.( fieldKey, FilterType.DATE, [start, end], 'range' ) }} showDateTimePicker={false} showPresets={false} allowSingleDateSelection={false} useDrawerOnMobile={false} /> )} {!columnConfig.filterComponent && ( No filter component configured for this column type )}
) } export default MobileFilterDrawer