import { classNames } from '@standardnotes/snjs' import { KeyboardKey } from '@standardnotes/ui-services' import { useCallback, useState, useRef } from 'react' import { useApplication } from '../ApplicationProvider' import Icon from '../Icon/Icon' import { Table as TableType, TableRow as TableRowType } from './CommonTypes' function TableRow({ row, index: rowIndex, canSelectRows, handleRowClick, handleRowContextMenu, handleActivateRow, }: { row: TableRowType index: number canSelectRows: TableType['canSelectRows'] handleRowClick: (event: React.MouseEvent, id: string) => void handleRowContextMenu: TableType['handleRowContextMenu'] handleActivateRow: TableType['handleActivateRow'] }) { const [isHovered, setIsHovered] = useState(false) const [isFocused, setIsFocused] = useState(false) const isHoveredOrFocused = isHovered || isFocused const visibleCells = row.cells.filter((cell) => !cell.hidden) return (
{ setIsHovered(true) }} onMouseLeave={() => { setIsHovered(false) }} onClick={(event) => handleRowClick(event, row.id)} onDoubleClick={() => handleActivateRow(row.id)} onContextMenu={handleRowContextMenu(row.id)} onFocus={() => { setIsFocused(true) }} onBlur={(event) => { if (!event.relatedTarget?.closest(`[id="${row.id}"]`)) { setIsFocused(false) } }} > {visibleCells.map((cell, index, array) => { return (
{cell.render} {row.rowActions && index === array.length - 1 && (
{row.rowActions}
)}
) })}
) } const MinTableRowHeight = 50 const MinRowsToDisplay = 20 const PageSize = Math.ceil(document.documentElement.clientHeight / MinTableRowHeight) || MinRowsToDisplay const PageScrollThreshold = 200 function Table({ table }: { table: TableType }) { const application = useApplication() const [rowsToDisplay, setRowsToDisplay] = useState(PageSize) const paginate = useCallback(() => { setRowsToDisplay((cellsToDisplay) => cellsToDisplay + PageSize) }, []) const onScroll = useCallback( (event: React.UIEvent) => { const offset = PageScrollThreshold const element = event.target as HTMLElement if (element.scrollTop + element.offsetHeight >= element.scrollHeight - offset) { paginate() } }, [paginate], ) const { id, headers, rows, colCount, rowCount, selectRow, multiSelectRow, rangeSelectUpToRow, handleRowContextMenu, handleActivateRow, selectedRows, selectionActions, canSelectRows, canSelectMultipleRows, showSelectionActions, } = table const focusedRowIndex = useRef(0) const focusedCellIndex = useRef(0) const onFocus: React.FocusEventHandler = useCallback((event) => { const target = event.target as HTMLElement const row = target.closest('[role="row"]') as HTMLElement const cell = target.closest('[role="gridcell"],[role="columnheader"]') as HTMLElement if (row) { focusedRowIndex.current = parseInt(row.getAttribute('aria-rowindex') || '0') } if (cell) { focusedCellIndex.current = parseInt(cell.getAttribute('aria-colindex') || '0') } }, []) const onBlur: React.FocusEventHandler = useCallback((event) => { const activeElement = document.activeElement as HTMLElement if (activeElement.closest('[role="grid"]') !== event.target) { focusedRowIndex.current = 0 focusedCellIndex.current = 0 } }, []) const onKeyDown: React.KeyboardEventHandler = useCallback( (event) => { const gridElement = event.currentTarget const allRenderedRows = gridElement.querySelectorAll('[role="row"]') const currentRow = Array.from(allRenderedRows).find( (row) => row.getAttribute('aria-rowindex') === focusedRowIndex.current.toString(), ) const allFocusableCells = Array.from(currentRow ? currentRow.querySelectorAll('[tabindex]') : []) const allRenderedColumnsLength = headers.length const focusCell = (rowIndex: number, colIndex: number) => { const row = gridElement.querySelector(`[role="row"][aria-rowindex="${rowIndex}"]`) if (!row) { return } const cell = row.querySelector(`[aria-colindex="${colIndex}"]`) if (cell) { cell.focus() } } switch (event.key) { case KeyboardKey.Up: event.preventDefault() if (focusedRowIndex.current > 1) { const previousRow = focusedRowIndex.current - 1 focusCell(previousRow, focusedCellIndex.current) } break case KeyboardKey.Down: event.preventDefault() if (focusedRowIndex.current <= rowCount) { const nextRow = focusedRowIndex.current + 1 focusCell(nextRow, focusedCellIndex.current) } break case KeyboardKey.Left: { event.preventDefault() if (!allFocusableCells) { return } const currentCellIndex = allFocusableCells.findIndex( (cell) => parseInt(cell.getAttribute('aria-colindex') || '0') === focusedCellIndex.current, ) if (currentCellIndex === 0) { return } const previousCell = allFocusableCells[currentCellIndex - 1] if (!previousCell) { return } previousCell.focus() break } case KeyboardKey.Right: { event.preventDefault() if (!allFocusableCells) { return } const currentCellIndex = allFocusableCells.findIndex( (cell) => parseInt(cell.getAttribute('aria-colindex') || '0') === focusedCellIndex.current, ) if (currentCellIndex === allFocusableCells.length - 1) { return } const nextCell = allFocusableCells[currentCellIndex + 1] if (!nextCell) { return } nextCell.focus() break } case KeyboardKey.Home: event.preventDefault() if (event.ctrlKey) { focusCell(1, 1) } else { if (!allFocusableCells) { return } const firstFocusableCell = allFocusableCells[0] if (!firstFocusableCell) { return } const firstCellIndex = parseInt(firstFocusableCell.getAttribute('aria-colindex') || '0') if (firstCellIndex > 0) { focusCell(focusedRowIndex.current, firstCellIndex) } } break case KeyboardKey.End: { event.preventDefault() if (event.ctrlKey) { focusCell(allRenderedRows.length, allRenderedColumnsLength || colCount) return } if (!allFocusableCells) { return } const lastFocusableCell = allFocusableCells[allFocusableCells.length - 1] if (!lastFocusableCell) { return } const lastCellIndex = parseInt(lastFocusableCell.getAttribute('aria-colindex') || '0') if (lastCellIndex > 0) { focusCell(focusedRowIndex.current, lastCellIndex) } break } case KeyboardKey.PageUp: { event.preventDefault() const previousRow = focusedRowIndex.current - 5 if (previousRow > 0) { focusCell(previousRow, focusedCellIndex.current) } else { focusCell(1, focusedCellIndex.current) } break } case KeyboardKey.PageDown: { event.preventDefault() const nextRow = focusedRowIndex.current + 5 if (nextRow <= allRenderedRows.length) { focusCell(nextRow, focusedCellIndex.current) } else { focusCell(allRenderedRows.length, focusedCellIndex.current) } break } case KeyboardKey.Enter: { const target = event.target as HTMLElement const closestColumnHeader = target.closest('[role="columnheader"]') if (closestColumnHeader && closestColumnHeader.getAttribute('data-can-sort')) { event.preventDefault() closestColumnHeader.click() return } const currentRowId = currentRow?.id if (currentRowId) { event.preventDefault() handleActivateRow(currentRowId) } break } case KeyboardKey.Space: { const target = event.target as HTMLElement const currentRowId = currentRow?.id if (!currentRowId) { return } if (target.getAttribute('role') !== 'gridcell') { return } event.preventDefault() const isCmdOrCtrlPressed = application.keyboardService.isMac ? event.metaKey : event.ctrlKey if (isCmdOrCtrlPressed && canSelectMultipleRows) { multiSelectRow(currentRowId) } else if (event.shiftKey && canSelectMultipleRows) { rangeSelectUpToRow(currentRowId) } else { selectRow(currentRowId) } break } } }, [ application.keyboardService.isMac, canSelectMultipleRows, colCount, handleActivateRow, headers.length, multiSelectRow, rangeSelectUpToRow, rowCount, selectRow, ], ) const handleRowClick = useCallback( (event: React.MouseEvent, rowId: string) => { if (!canSelectRows) { return } const isCmdOrCtrlPressed = application.keyboardService.isMac ? event.metaKey : event.ctrlKey if (isCmdOrCtrlPressed && canSelectMultipleRows) { multiSelectRow(rowId) } else if (event.shiftKey && canSelectMultipleRows) { rangeSelectUpToRow(rowId) } else { selectRow(rowId) } }, [ application.keyboardService.isMac, canSelectMultipleRows, canSelectRows, multiSelectRow, rangeSelectUpToRow, selectRow, ], ) return (
{showSelectionActions && selectedRows.length >= 2 && (
{selectedRows.length} selected {selectedRows.length > 0 && selectionActions}
)}
{headers .filter((header) => !header.hidden) .map((header, index) => { return (
{header.name} {header.isSorting && ( )}
) })}
{rows.slice(0, rowsToDisplay).map((row, index) => ( ))}
) } export default Table