feat: Added keyboard navigation to Files table view (#2131)
This commit is contained in:
@@ -10,4 +10,6 @@ export enum KeyboardKey {
|
|||||||
Home = 'Home',
|
Home = 'Home',
|
||||||
End = 'End',
|
End = 'End',
|
||||||
Space = ' ',
|
Space = ' ',
|
||||||
|
PageUp = 'PageUp',
|
||||||
|
PageDown = 'PageDown',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -310,7 +310,7 @@ const FilesTableView = ({
|
|||||||
columns: columnDefs,
|
columns: columnDefs,
|
||||||
enableRowSelection: true,
|
enableRowSelection: true,
|
||||||
enableMultipleRowSelection: true,
|
enableMultipleRowSelection: true,
|
||||||
onRowDoubleClick(file) {
|
onRowActivate(file) {
|
||||||
void filesController.handleFileAction({
|
void filesController.handleFileAction({
|
||||||
type: FileItemActionType.PreviewFile,
|
type: FileItemActionType.PreviewFile,
|
||||||
payload: {
|
payload: {
|
||||||
|
|||||||
@@ -35,12 +35,15 @@ export type TableHeader = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Table<Data> = {
|
export type Table<Data> = {
|
||||||
|
id: string
|
||||||
headers: TableHeader[]
|
headers: TableHeader[]
|
||||||
rows: TableRow<Data>[]
|
rows: TableRow<Data>[]
|
||||||
rowCount: number
|
rowCount: number
|
||||||
colCount: number
|
colCount: number
|
||||||
handleRowClick: (id: string) => MouseEventHandler<HTMLTableRowElement>
|
selectRow: (id: string) => void
|
||||||
handleRowDoubleClick: (id: string) => MouseEventHandler<HTMLTableRowElement>
|
multiSelectRow: (id: string) => void
|
||||||
|
rangeSelectUpToRow: (id: string) => void
|
||||||
|
handleActivateRow: (id: string) => void
|
||||||
handleRowContextMenu: (id: string) => MouseEventHandler<HTMLTableRowElement>
|
handleRowContextMenu: (id: string) => MouseEventHandler<HTMLTableRowElement>
|
||||||
canSelectRows: boolean
|
canSelectRows: boolean
|
||||||
canSelectMultipleRows: boolean
|
canSelectMultipleRows: boolean
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { classNames } from '@standardnotes/snjs'
|
import { classNames } from '@standardnotes/snjs'
|
||||||
import { useCallback, useState } from 'react'
|
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||||
|
import { useCallback, useState, useRef } from 'react'
|
||||||
|
import { useApplication } from '../ApplicationProvider'
|
||||||
import Icon from '../Icon/Icon'
|
import Icon from '../Icon/Icon'
|
||||||
import { Table, TableRow } from './CommonTypes'
|
import { Table, TableRow } from './CommonTypes'
|
||||||
|
|
||||||
@@ -9,22 +11,25 @@ function TableRow<Data>({
|
|||||||
canSelectRows,
|
canSelectRows,
|
||||||
handleRowClick,
|
handleRowClick,
|
||||||
handleRowContextMenu,
|
handleRowContextMenu,
|
||||||
handleRowDoubleClick,
|
handleActivateRow,
|
||||||
}: {
|
}: {
|
||||||
row: TableRow<Data>
|
row: TableRow<Data>
|
||||||
index: number
|
index: number
|
||||||
canSelectRows: Table<Data>['canSelectRows']
|
canSelectRows: Table<Data>['canSelectRows']
|
||||||
handleRowClick: Table<Data>['handleRowClick']
|
handleRowClick: (event: React.MouseEvent<HTMLDivElement, MouseEvent>, id: string) => void
|
||||||
handleRowContextMenu: Table<Data>['handleRowContextMenu']
|
handleRowContextMenu: Table<Data>['handleRowContextMenu']
|
||||||
handleRowDoubleClick: Table<Data>['handleRowDoubleClick']
|
handleActivateRow: Table<Data>['handleActivateRow']
|
||||||
}) {
|
}) {
|
||||||
const [isHovered, setIsHovered] = useState(false)
|
const [isHovered, setIsHovered] = useState(false)
|
||||||
|
const [isFocused, setIsFocused] = useState(false)
|
||||||
|
const isHoveredOrFocused = isHovered || isFocused
|
||||||
|
|
||||||
const visibleCells = row.cells.filter((cell) => !cell.hidden)
|
const visibleCells = row.cells.filter((cell) => !cell.hidden)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="row"
|
role="row"
|
||||||
|
id={row.id}
|
||||||
aria-rowindex={rowIndex + 2}
|
aria-rowindex={rowIndex + 2}
|
||||||
{...(canSelectRows ? { 'aria-selected': row.isSelected } : {})}
|
{...(canSelectRows ? { 'aria-selected': row.isSelected } : {})}
|
||||||
className="group relative contents"
|
className="group relative contents"
|
||||||
@@ -34,9 +39,17 @@ function TableRow<Data>({
|
|||||||
onMouseLeave={() => {
|
onMouseLeave={() => {
|
||||||
setIsHovered(false)
|
setIsHovered(false)
|
||||||
}}
|
}}
|
||||||
onClick={handleRowClick(row.id)}
|
onClick={(event) => handleRowClick(event, row.id)}
|
||||||
onDoubleClick={handleRowDoubleClick(row.id)}
|
onDoubleClick={() => handleActivateRow(row.id)}
|
||||||
onContextMenu={handleRowContextMenu(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) => {
|
{visibleCells.map((cell, index, array) => {
|
||||||
return (
|
return (
|
||||||
@@ -46,25 +59,27 @@ function TableRow<Data>({
|
|||||||
aria-colindex={cell.colIndex + 1}
|
aria-colindex={cell.colIndex + 1}
|
||||||
key={index}
|
key={index}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'relative flex items-center overflow-hidden border-b border-border py-3 px-3',
|
'relative flex items-center overflow-hidden border-b border-border py-3 px-3 focus:border-info',
|
||||||
row.isSelected && 'bg-info-backdrop',
|
row.isSelected && 'bg-info-backdrop',
|
||||||
canSelectRows && 'cursor-pointer',
|
canSelectRows && 'cursor-pointer',
|
||||||
canSelectRows && isHovered && 'bg-contrast',
|
canSelectRows && isHoveredOrFocused && 'bg-contrast',
|
||||||
)}
|
)}
|
||||||
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
{cell.render}
|
{cell.render}
|
||||||
{row.rowActions && index === array.length - 1 && (
|
{row.rowActions && index === array.length - 1 && (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'absolute right-0 top-0 flex h-full items-center p-2',
|
'absolute right-0 top-0 flex h-full items-center p-2',
|
||||||
row.isSelected ? '' : isHovered ? '' : 'invisible',
|
row.isSelected ? '' : isHoveredOrFocused ? '' : 'invisible',
|
||||||
|
isFocused && 'visible',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="z-[1]">{row.rowActions}</div>
|
<div className="z-[1]">{row.rowActions}</div>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'absolute top-0 right-0 z-0 h-full w-full backdrop-blur-[2px]',
|
'absolute top-0 right-0 z-0 h-full w-full backdrop-blur-[2px]',
|
||||||
row.isSelected ? '' : isHovered ? '' : 'invisible',
|
row.isSelected ? '' : isHoveredOrFocused ? '' : 'invisible',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -82,6 +97,8 @@ const PageSize = Math.ceil(document.documentElement.clientHeight / MinTableRowHe
|
|||||||
const PageScrollThreshold = 200
|
const PageScrollThreshold = 200
|
||||||
|
|
||||||
function Table<Data>({ table }: { table: Table<Data> }) {
|
function Table<Data>({ table }: { table: Table<Data> }) {
|
||||||
|
const application = useApplication()
|
||||||
|
|
||||||
const [rowsToDisplay, setRowsToDisplay] = useState<number>(PageSize)
|
const [rowsToDisplay, setRowsToDisplay] = useState<number>(PageSize)
|
||||||
const paginate = useCallback(() => {
|
const paginate = useCallback(() => {
|
||||||
setRowsToDisplay((cellsToDisplay) => cellsToDisplay + PageSize)
|
setRowsToDisplay((cellsToDisplay) => cellsToDisplay + PageSize)
|
||||||
@@ -98,13 +115,16 @@ function Table<Data>({ table }: { table: Table<Data> }) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
id,
|
||||||
headers,
|
headers,
|
||||||
rows,
|
rows,
|
||||||
colCount,
|
colCount,
|
||||||
rowCount,
|
rowCount,
|
||||||
handleRowClick,
|
selectRow,
|
||||||
|
multiSelectRow,
|
||||||
|
rangeSelectUpToRow,
|
||||||
handleRowContextMenu,
|
handleRowContextMenu,
|
||||||
handleRowDoubleClick,
|
handleActivateRow,
|
||||||
selectedRows,
|
selectedRows,
|
||||||
selectionActions,
|
selectionActions,
|
||||||
canSelectRows,
|
canSelectRows,
|
||||||
@@ -112,6 +132,209 @@ function Table<Data>({ table }: { table: Table<Data> }) {
|
|||||||
showSelectionActions,
|
showSelectionActions,
|
||||||
} = table
|
} = table
|
||||||
|
|
||||||
|
const focusedRowIndex = useRef<number>(0)
|
||||||
|
const focusedCellIndex = useRef<number>(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<HTMLElement>('[role="row"]')
|
||||||
|
const currentRow = Array.from(allRenderedRows).find(
|
||||||
|
(row) => row.getAttribute('aria-rowindex') === focusedRowIndex.current.toString(),
|
||||||
|
)
|
||||||
|
const allFocusableCells = currentRow?.querySelectorAll<HTMLElement>('[tabindex]')
|
||||||
|
|
||||||
|
const focusCell = (rowIndex: number, colIndex: number) => {
|
||||||
|
const row = gridElement.querySelector(`[role="row"][aria-rowindex="${rowIndex}"]`)
|
||||||
|
if (!row) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const cell = row.querySelector<HTMLElement>(`[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 (focusedCellIndex.current > 1) {
|
||||||
|
const previousCell = focusedCellIndex.current - 1
|
||||||
|
focusCell(focusedRowIndex.current, previousCell)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case KeyboardKey.Right:
|
||||||
|
event.preventDefault()
|
||||||
|
if (focusedCellIndex.current < colCount) {
|
||||||
|
const nextCell = focusedCellIndex.current + 1
|
||||||
|
focusCell(focusedRowIndex.current, nextCell)
|
||||||
|
}
|
||||||
|
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, headers.length || 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<HTMLElement>('[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 (
|
return (
|
||||||
<div className="block min-h-0 overflow-auto" onScroll={onScroll}>
|
<div className="block min-h-0 overflow-auto" onScroll={onScroll}>
|
||||||
{showSelectionActions && selectedRows.length >= 2 && (
|
{showSelectionActions && selectedRows.length >= 2 && (
|
||||||
@@ -126,6 +349,10 @@ function Table<Data>({ table }: { table: Table<Data> }) {
|
|||||||
aria-colcount={colCount}
|
aria-colcount={colCount}
|
||||||
aria-rowcount={rowCount}
|
aria-rowcount={rowCount}
|
||||||
aria-multiselectable={canSelectMultipleRows}
|
aria-multiselectable={canSelectMultipleRows}
|
||||||
|
onFocus={onFocus}
|
||||||
|
onBlur={onBlur}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
id={`table-${id}`}
|
||||||
>
|
>
|
||||||
<div role="row" aria-rowindex={1} className="contents">
|
<div role="row" aria-rowindex={1} className="contents">
|
||||||
{headers
|
{headers
|
||||||
@@ -139,13 +366,16 @@ function Table<Data>({ table }: { table: Table<Data> }) {
|
|||||||
aria-sort={header.isSorting ? (header.sortReversed ? 'descending' : 'ascending') : 'none'}
|
aria-sort={header.isSorting ? (header.sortReversed ? 'descending' : 'ascending') : 'none'}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'border-b border-border px-3 pt-3 pb-2 text-left text-sm font-medium text-passive-0',
|
'border-b border-border px-3 pt-3 pb-2 text-left text-sm font-medium text-passive-0',
|
||||||
header.sortBy && 'cursor-pointer hover:bg-info-backdrop hover:underline',
|
header.sortBy &&
|
||||||
|
'cursor-pointer hover:bg-info-backdrop hover:underline focus:border-info focus:bg-info-backdrop',
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
gridColumn: index + 1,
|
gridColumn: index + 1,
|
||||||
}}
|
}}
|
||||||
onClick={header.onSortChange}
|
onClick={header.onSortChange}
|
||||||
key={index.toString()}
|
key={index.toString()}
|
||||||
|
data-can-sort={header.sortBy ? true : undefined}
|
||||||
|
{...(header.sortBy && { tabIndex: index === 0 ? 0 : -1 })}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{header.name}
|
{header.name}
|
||||||
@@ -170,7 +400,7 @@ function Table<Data>({ table }: { table: Table<Data> }) {
|
|||||||
canSelectRows={canSelectRows}
|
canSelectRows={canSelectRows}
|
||||||
handleRowClick={handleRowClick}
|
handleRowClick={handleRowClick}
|
||||||
handleRowContextMenu={handleRowContextMenu}
|
handleRowContextMenu={handleRowContextMenu}
|
||||||
handleRowDoubleClick={handleRowDoubleClick}
|
handleActivateRow={handleActivateRow}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { MouseEventHandler, ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
|
import { UuidGenerator } from '@standardnotes/snjs'
|
||||||
import { useApplication } from '../ApplicationProvider'
|
import { MouseEventHandler, ReactNode, useCallback, useEffect, useMemo, useState, useRef } from 'react'
|
||||||
import { Table, TableColumn, TableHeader, TableRow, TableSortBy } from './CommonTypes'
|
import { Table, TableColumn, TableHeader, TableRow, TableSortBy } from './CommonTypes'
|
||||||
|
|
||||||
type TableSortOptions =
|
type TableSortOptions =
|
||||||
@@ -34,7 +34,7 @@ type TableSelectionOptions =
|
|||||||
|
|
||||||
type TableRowOptions<Data> = {
|
type TableRowOptions<Data> = {
|
||||||
getRowId?: (data: Data) => string
|
getRowId?: (data: Data) => string
|
||||||
onRowDoubleClick?: (data: Data) => void
|
onRowActivate?: (data: Data) => void
|
||||||
onRowContextMenu?: (x: number, y: number, data: Data) => void
|
onRowContextMenu?: (x: number, y: number, data: Data) => void
|
||||||
rowActions?: (data: Data) => ReactNode
|
rowActions?: (data: Data) => ReactNode
|
||||||
}
|
}
|
||||||
@@ -57,14 +57,14 @@ export function useTable<Data>({
|
|||||||
enableMultipleRowSelection,
|
enableMultipleRowSelection,
|
||||||
selectedRowIds,
|
selectedRowIds,
|
||||||
onRowSelectionChange,
|
onRowSelectionChange,
|
||||||
onRowDoubleClick,
|
onRowActivate,
|
||||||
onRowContextMenu,
|
onRowContextMenu,
|
||||||
rowActions,
|
rowActions,
|
||||||
selectionActions,
|
selectionActions,
|
||||||
showSelectionActions,
|
showSelectionActions,
|
||||||
}: UseTableOptions<Data>): Table<Data> {
|
}: UseTableOptions<Data>): Table<Data> {
|
||||||
const application = useApplication()
|
|
||||||
const [selectedRows, setSelectedRows] = useState<string[]>(selectedRowIds || [])
|
const [selectedRows, setSelectedRows] = useState<string[]>(selectedRowIds || [])
|
||||||
|
const id = useRef(UuidGenerator.GenerateUuid())
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedRowIds) {
|
if (selectedRowIds) {
|
||||||
@@ -122,45 +122,55 @@ export function useTable<Data>({
|
|||||||
[columns, data, enableRowSelection, getRowId, rowActions, selectedRows],
|
[columns, data, enableRowSelection, getRowId, rowActions, selectedRows],
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleRowClick = useCallback(
|
const selectRow = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
const handler: MouseEventHandler<HTMLTableRowElement> = (event) => {
|
if (!enableRowSelection) {
|
||||||
if (!enableRowSelection) {
|
return
|
||||||
return
|
|
||||||
}
|
|
||||||
const isCmdOrCtrlPressed = application.keyboardService.isMac ? event.metaKey : event.ctrlKey
|
|
||||||
if (isCmdOrCtrlPressed && enableMultipleRowSelection) {
|
|
||||||
setSelectedRows((prev) => (prev.includes(id) ? prev.filter((rowId) => rowId !== id) : [...prev, id]))
|
|
||||||
} else if (event.shiftKey && enableMultipleRowSelection) {
|
|
||||||
const lastSelectedIndex = rows.findIndex((row) => row.id === selectedRows[selectedRows.length - 1])
|
|
||||||
const currentIndex = rows.findIndex((row) => row.id === id)
|
|
||||||
const start = Math.min(lastSelectedIndex, currentIndex)
|
|
||||||
const end = Math.max(lastSelectedIndex, currentIndex)
|
|
||||||
const newSelectedRows = rows.slice(start, end + 1).map((row) => row.id)
|
|
||||||
setSelectedRows(newSelectedRows)
|
|
||||||
} else {
|
|
||||||
setSelectedRows([id])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return handler
|
|
||||||
|
setSelectedRows([id])
|
||||||
},
|
},
|
||||||
[application.keyboardService.isMac, enableMultipleRowSelection, enableRowSelection, rows, selectedRows],
|
[enableRowSelection],
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleRowDoubleClick = useCallback(
|
const multiSelectRow = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
const handler: MouseEventHandler<HTMLTableRowElement> = () => {
|
if (!enableRowSelection || !enableMultipleRowSelection) {
|
||||||
if (!onRowDoubleClick) {
|
return
|
||||||
return
|
|
||||||
}
|
|
||||||
const rowData = rows.find((row) => row.id === id)?.rowData
|
|
||||||
if (rowData) {
|
|
||||||
onRowDoubleClick(rowData)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return handler
|
|
||||||
|
setSelectedRows((prev) => (prev.includes(id) ? prev.filter((rowId) => rowId !== id) : [...prev, id]))
|
||||||
},
|
},
|
||||||
[onRowDoubleClick, rows],
|
[enableMultipleRowSelection, enableRowSelection],
|
||||||
|
)
|
||||||
|
|
||||||
|
const rangeSelectUpToRow = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
if (!enableRowSelection || !enableMultipleRowSelection) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastSelectedIndex = rows.findIndex((row) => row.id === selectedRows[selectedRows.length - 1])
|
||||||
|
const currentIndex = rows.findIndex((row) => row.id === id)
|
||||||
|
const start = Math.min(lastSelectedIndex, currentIndex)
|
||||||
|
const end = Math.max(lastSelectedIndex, currentIndex)
|
||||||
|
const newSelectedRows = rows.slice(start, end + 1).map((row) => row.id)
|
||||||
|
setSelectedRows(newSelectedRows)
|
||||||
|
},
|
||||||
|
[enableMultipleRowSelection, enableRowSelection, rows, selectedRows],
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleActivateRow = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
if (!onRowActivate) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const rowData = rows.find((row) => row.id === id)?.rowData
|
||||||
|
if (rowData) {
|
||||||
|
onRowActivate(rowData)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onRowActivate, rows],
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleRowContextMenu = useCallback(
|
const handleRowContextMenu = useCallback(
|
||||||
@@ -186,12 +196,15 @@ export function useTable<Data>({
|
|||||||
|
|
||||||
const table: Table<Data> = useMemo(
|
const table: Table<Data> = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
id: id.current,
|
||||||
headers,
|
headers,
|
||||||
rows,
|
rows,
|
||||||
colCount,
|
colCount,
|
||||||
rowCount,
|
rowCount,
|
||||||
handleRowClick,
|
selectRow,
|
||||||
handleRowDoubleClick,
|
multiSelectRow,
|
||||||
|
rangeSelectUpToRow,
|
||||||
|
handleActivateRow,
|
||||||
handleRowContextMenu,
|
handleRowContextMenu,
|
||||||
selectedRows,
|
selectedRows,
|
||||||
canSelectRows: enableRowSelection || false,
|
canSelectRows: enableRowSelection || false,
|
||||||
@@ -200,16 +213,18 @@ export function useTable<Data>({
|
|||||||
showSelectionActions: showSelectionActions || false,
|
showSelectionActions: showSelectionActions || false,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
colCount,
|
|
||||||
enableMultipleRowSelection,
|
|
||||||
enableRowSelection,
|
|
||||||
handleRowClick,
|
|
||||||
handleRowContextMenu,
|
|
||||||
handleRowDoubleClick,
|
|
||||||
headers,
|
headers,
|
||||||
rowCount,
|
|
||||||
rows,
|
rows,
|
||||||
|
colCount,
|
||||||
|
rowCount,
|
||||||
|
selectRow,
|
||||||
|
multiSelectRow,
|
||||||
|
rangeSelectUpToRow,
|
||||||
|
handleActivateRow,
|
||||||
|
handleRowContextMenu,
|
||||||
selectedRows,
|
selectedRows,
|
||||||
|
enableRowSelection,
|
||||||
|
enableMultipleRowSelection,
|
||||||
selectionActions,
|
selectionActions,
|
||||||
showSelectionActions,
|
showSelectionActions,
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user