import React from 'react' import { useGridComponents } from '../context/grid-components-context/hook' import { useColumnResize } from '../hooks/use-column-resize' import { useGridContext, useGridSelector } from '../context/grid-context/hook' import { SELECTION_COLUMN_ID } from '../constants' import { useColumnIndex, useFocusHandler } from '../hooks' import { useSubFocusHandler } from '../context/grid-sub-navigation-context' import { size, useIsFocusVisible } from '@planview/pv-utilities' import { useDraggable } from '@dnd-kit/core' import { DRAG_TYPE_HEADER_CELL } from './grid-drag-controller' import { useColumnBorders } from '../hooks/use-column-borders' export type GridHeaderCellProps = { columnId: string tabIndex?: number placeholder?: boolean lastPlaceholder?: boolean } const stopPropagation: React.MouseEventHandler = (e): void => { e.stopPropagation() } export const GridHeaderCell = ({ columnId, placeholder = false, lastPlaceholder = false, }: GridHeaderCellProps) => { const { GridHeaderCellLayout, GridCellFocusLayout, GridHeaderCellExpand, GridColumnMoveHandle, GridColumnResizeHandle, } = useGridComponents() const { selectors, api, getState } = useGridContext() const sort = useGridSelector((state) => selectors.selectColumnSort(state, columnId) ) const { actualWidth, data: column, parentId, } = useGridSelector((state) => selectors.selectColumn(state, columnId)) const isTreeColumn = !!column.tree const focusProps = useIsFocusVisible() const { levels, groups } = useGridSelector((state) => selectors.selectHeaderHierarchy(state) ) const rowId = useGridSelector((state) => { const ids = selectors.selectHeaderRowIds(state) return ids[ids.length - 1] }) const { onKeyDown, rendererWantsToDisplayFocusStyles, registeredIndexes, subFocus, } = useSubFocusHandler(columnId, 'header', rowId, isTreeColumn ? 1 : 0) const CellHeaderComponent = column.header?.Renderer const [hovering, setHovering] = React.useState(false) const columnIndex = useColumnIndex(columnId) const [hasFocus, handleFocus] = useFocusHandler(columnId, 'header', rowId) const isCellEditing = useGridSelector((state) => selectors.selectIsEditingCellInColumn(state, columnId) ) const { showLeftBorder, showRightBorder } = useColumnBorders( columnId, 'header' ) const parent = groups.find((g) => g.id === parentId) const onEnter = React.useCallback(() => setHovering(true), []) const onLeave = React.useCallback(() => setHovering(false), []) const [resizing, resizeHandlers] = useColumnResize(column.id, actualWidth) const onSort = React.useCallback( (replaceSort: boolean) => { api.column.toggleSort(columnId, replaceSort) }, [api.column, columnId] ) const handleClick = React.useCallback( (e) => { const modifierPressed = e.ctrlKey || e.metaKey || e.shiftKey onSort(!modifierPressed) }, [onSort] ) const handleKeyDown = React.useCallback< React.KeyboardEventHandler >( (e) => { onKeyDown(e) if (e.key === ' ' || e.key === 'Enter') { e.preventDefault() if ( e.key === 'Enter' && e.shiftKey && selectors.selectColumnMenuEnabled(getState()) ) { const el = e.currentTarget const { top, left } = el.getBoundingClientRect() api.column.showMenu({ top: top + size.xsmall, left: left + size.xsmall, }) } else { const modifierPressed = e.ctrlKey || e.metaKey || e.shiftKey onSort(!modifierPressed) } } }, [onKeyDown, getState, selectors, api.column, onSort] ) const parentOffsetHeight = parent ? (parent.level - 1) * size.small : levels * size.small const height = size.small - 1 + (levels ? parentOffsetHeight : 0) const { attributes, listeners, setNodeRef, setActivatorNodeRef, isDragging, } = useDraggable({ id: columnId, data: { type: DRAG_TYPE_HEADER_CELL, columnsDragged: [column.id], column, height, offsetY: parentOffsetHeight, }, disabled: !column.movable, }) React.useEffect(() => { if (isDragging) { setHovering(false) } }, [isDragging]) const onPointerDown = listeners?.onPointerDown const handlePointerDown = React.useCallback< React.PointerEventHandler >( (e) => { handleFocus() onPointerDown?.(e) }, [handleFocus, onPointerDown] ) const functionalTabIndex = isTreeColumn && hasFocus && (subFocus === 'first' || subFocus === 0) ? 0 : -1 /** This means the renderer OR child of renderer has focus */ const rendererHasFocus = hasFocus && functionalTabIndex === -1 /** This is the tabIndex passed to the renderer, we dot use it if sub-navigation is enabled */ const rendererTabIndex = rendererHasFocus && !registeredIndexes ? 0 : -1 const sticky = column.sticky === 'left' || column.sticky === 'right' ? column.sticky : null const stickyOffset = useGridSelector((state) => selectors.selectColumnStickyOffset(state, columnId) ) const extraStyle: Partial | null = sticky ? { [sticky]: stickyOffset, } : null return ( ) => { if (column.sortable) { handleKeyDown(ev) } listeners?.onKeyDown?.(ev) }} onMouseEnter={onEnter} onMouseLeave={onLeave} onClick={column.sortable ? handleClick : undefined} onPointerDown={ column.id !== SELECTION_COLUMN_ID ? handlePointerDown : handleFocus } $sortable={column.sortable} $movable={column.movable} $hovering={!placeholder && hovering} $placeholder={placeholder && !lastPlaceholder} $resizing={!!resizing} $highlight={isCellEditing} data-current-cell={hasFocus ? 'true' : undefined} $sticky={!!sticky} $leftBorder={showLeftBorder} $rightBorder={showRightBorder} onFocus={focusProps.onFocus} onBlur={focusProps.onBlur} > {isTreeColumn && !placeholder && ( )} {!placeholder ? ( CellHeaderComponent ? ( ) : ( column.label ) ) : null} {column.movable && hovering && !resizing && !placeholder ? ( ) : null} {(column.resizable && hovering && !placeholder) || resizing ? ( ) : null} ) } export const GridHeaderCellMemo = React.memo( GridHeaderCell ) as typeof GridHeaderCell