Skip to content

Commit bdbf3f3

Browse files
committed
refactor(table): shift selection functionality for table rows
1 parent 9439b58 commit bdbf3f3

5 files changed

Lines changed: 144 additions & 83 deletions

File tree

apps/desktop/src/components/table/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ export * from './header'
1010
export * from './provider'
1111
export * from './table'
1212
export * from './table-context'
13+
export {
14+
useShiftSelectionClick,
15+
type UseShiftSelectionClickOptions,
16+
} from './use-shift-selection-click'
1317
export {
1418
type SelectionState,
1519
useShiftSelectionKeyDown,
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import type { KeyboardEvent, MouseEvent } from 'react'
2+
import type { SelectionState } from './use-shift-selection-key-down'
3+
import { useRef } from 'react'
4+
5+
export interface UseShiftSelectionClickOptions {
6+
// Example: { id: string, type: string }
7+
rowKey: Record<string, string>
8+
rowIndex: number
9+
currentSelected: Record<string, string>[]
10+
lastClickedIndex: number | null
11+
getRangeKeys: (startIndex: number, endIndex: number) => Record<string, string>[]
12+
onSelectionChange: (
13+
selected: Record<string, string>[],
14+
selectionState: SelectionState,
15+
lastClickedIndex: number,
16+
) => void
17+
}
18+
19+
export function useShiftSelectionClick({
20+
rowKey,
21+
rowIndex,
22+
currentSelected,
23+
lastClickedIndex,
24+
getRangeKeys,
25+
onSelectionChange,
26+
}: UseShiftSelectionClickOptions) {
27+
const shiftKeyRef = useRef(false)
28+
29+
const isSelected = currentSelected.some(row =>
30+
Object.keys(rowKey).every(key => row[key] === rowKey[key]),
31+
)
32+
33+
const handleMouseDown = (event: MouseEvent<HTMLInputElement>) => {
34+
shiftKeyRef.current = event.shiftKey
35+
}
36+
37+
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
38+
if (event.key === ' ' || event.key === 'Enter') {
39+
shiftKeyRef.current = event.shiftKey
40+
}
41+
}
42+
43+
const handleChange = () => {
44+
const isShiftHeld = shiftKeyRef.current
45+
46+
if (isShiftHeld && lastClickedIndex !== null && lastClickedIndex !== rowIndex) {
47+
const start = Math.min(lastClickedIndex, rowIndex)
48+
const end = Math.max(lastClickedIndex, rowIndex)
49+
const rangeKeys = getRangeKeys(start, end)
50+
51+
onSelectionChange(rangeKeys, {
52+
anchorIndex: lastClickedIndex,
53+
focusIndex: rowIndex,
54+
lastExpandDirection: rowIndex > lastClickedIndex ? 'down' : 'up',
55+
}, rowIndex)
56+
}
57+
else {
58+
if (isSelected) {
59+
const newSelected = currentSelected.filter(row =>
60+
!Object.keys(rowKey).every(key => row[key] === rowKey[key]),
61+
)
62+
onSelectionChange(
63+
newSelected,
64+
{ anchorIndex: null, focusIndex: null, lastExpandDirection: null },
65+
rowIndex,
66+
)
67+
}
68+
else {
69+
onSelectionChange(
70+
[...currentSelected, rowKey],
71+
{ anchorIndex: rowIndex, focusIndex: rowIndex, lastExpandDirection: null },
72+
rowIndex,
73+
)
74+
}
75+
}
76+
77+
shiftKeyRef.current = false
78+
}
79+
80+
return {
81+
handleMouseDown,
82+
handleKeyDown,
83+
handleChange,
84+
}
85+
}

apps/desktop/src/components/table/use-shift-selection-key-down.ts

Lines changed: 13 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,22 @@ export interface SelectionState {
88
}
99

1010
export interface UseShiftSelectionKeyDownOptions {
11-
rows: Record<string, unknown>[]
12-
rowKeyColumns: string[]
11+
rowCount: number
12+
getRowKey: (index: number) => Record<string, string>
13+
getRangeKeys: (startIndex: number, endIndex: number) => Record<string, string>[]
1314
getSelectionState: () => SelectionState
1415
onSelectionChange: (selected: Record<string, string>[], selectionState: SelectionState) => void
1516
}
1617

1718
export function useShiftSelectionKeyDown({
18-
rows,
19-
rowKeyColumns,
19+
rowCount,
20+
getRowKey,
21+
getRangeKeys,
2022
getSelectionState,
2123
onSelectionChange,
2224
}: UseShiftSelectionKeyDownOptions) {
2325
return useCallback((event: KeyboardEvent<HTMLDivElement>) => {
24-
if (!event.shiftKey || rows.length === 0 || rowKeyColumns.length === 0)
26+
if (!event.shiftKey || rowCount === 0)
2527
return
2628

2729
const isArrowDown = event.key === 'ArrowDown'
@@ -36,11 +38,8 @@ export function useShiftSelectionKeyDown({
3638
const currentDirection = isArrowDown ? 'down' : 'up'
3739

3840
if (anchorIndex === null || focusIndex === null) {
39-
const startIndex = isArrowDown ? 0 : rows.length - 1
40-
const rowKeys = rowKeyColumns.reduce<Record<string, string>>(
41-
(acc, key) => ({ ...acc, [key]: rows[startIndex]![key] as string }),
42-
{},
43-
)
41+
const startIndex = isArrowDown ? 0 : rowCount - 1
42+
const rowKeys = getRowKey(startIndex)
4443

4544
onSelectionChange([rowKeys], {
4645
anchorIndex: startIndex,
@@ -51,7 +50,7 @@ export function useShiftSelectionKeyDown({
5150
}
5251

5352
const newFocusIndex = isArrowDown
54-
? Math.min(focusIndex + 1, rows.length - 1)
53+
? Math.min(focusIndex + 1, rowCount - 1)
5554
: Math.max(focusIndex - 1, 0)
5655

5756
const atBoundary = newFocusIndex === focusIndex
@@ -62,13 +61,7 @@ export function useShiftSelectionKeyDown({
6261

6362
const start = Math.min(anchorIndex, newFocusIndex)
6463
const end = Math.max(anchorIndex, newFocusIndex)
65-
const rangeRows = rows.slice(start, end + 1)
66-
const rangeKeys = rangeRows.map(row =>
67-
rowKeyColumns.reduce<Record<string, string>>(
68-
(acc, key) => ({ ...acc, [key]: row[key] as string }),
69-
{},
70-
),
71-
)
64+
const rangeKeys = getRangeKeys(start, end)
7265

7366
onSelectionChange(rangeKeys, {
7467
anchorIndex,
@@ -94,15 +87,8 @@ export function useShiftSelectionKeyDown({
9487

9588
const start = Math.min(anchorIndex, newFocusIndex)
9689
const end = Math.max(anchorIndex, newFocusIndex)
97-
98-
const rangeRows = rows.slice(start, end + 1)
99-
const rangeKeys = rangeRows.map(row =>
100-
rowKeyColumns.reduce<Record<string, string>>(
101-
(acc, key) => ({ ...acc, [key]: row[key] as string }),
102-
{},
103-
),
104-
)
90+
const rangeKeys = getRangeKeys(start, end)
10591

10692
onSelectionChange(rangeKeys, updatedSelectionState)
107-
}, [rows, rowKeyColumns, getSelectionState, onSelectionChange])
93+
}, [rowCount, getRowKey, getRangeKeys, getSelectionState, onSelectionChange])
10894
}

apps/desktop/src/routes/_protected/database/$id/table/-components/table/table-selection.tsx

Lines changed: 28 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import type { ComponentProps, KeyboardEvent, MouseEvent } from 'react'
1+
import type { ComponentProps } from 'react'
22
import type { TableCellProps, TableHeaderCellProps } from '~/components/table'
33
import { cn } from '@conar/ui/lib/utils'
44
import { RiCheckLine, RiSubtractLine } from '@remixicon/react'
55
import { useStore } from '@tanstack/react-store'
6-
import { useRef } from 'react'
7-
import { useTableContext } from '~/components/table'
6+
import { useShiftSelectionClick, useTableContext } from '~/components/table'
87
import { usePageStoreContext } from '../../-store'
98

109
function IndeterminateCheckbox({
@@ -97,65 +96,40 @@ export function SelectionCell({ rowIndex, columnIndex, className, style, keys }:
9796
}) {
9897
const store = usePageStoreContext()
9998
const rows = useTableContext(state => state.rows)
100-
const shiftKeyRef = useRef(false)
10199
const isSelected = useStore(store, state => state.selected.some(row => keys.every(key => row[key] === rows[rowIndex]![key])))
100+
const [currentSelected, lastClickedIndex] = useStore(store, state => [
101+
state.selected,
102+
state.lastClickedIndex,
103+
])
102104

103-
const handleMouseDown = (event: MouseEvent<HTMLInputElement>) => {
104-
shiftKeyRef.current = event.shiftKey
105-
}
106-
107-
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
108-
if (event.key === ' ' || event.key === 'Enter') {
109-
shiftKeyRef.current = event.shiftKey
110-
}
111-
}
112-
113-
const handleChange = () => {
114-
const lastIndex = store.state.lastClickedIndex
115-
const isShiftHeld = shiftKeyRef.current
116-
117-
if (isShiftHeld && lastIndex !== null && lastIndex !== rowIndex) {
118-
const start = Math.min(lastIndex, rowIndex)
119-
const end = Math.max(lastIndex, rowIndex)
105+
const rowKey = keys.reduce<Record<string, string>>(
106+
(acc, key) => ({ ...acc, [key]: rows[rowIndex]![key] as string }),
107+
{},
108+
)
120109

110+
const { handleMouseDown, handleKeyDown, handleChange } = useShiftSelectionClick({
111+
rowKey,
112+
rowIndex,
113+
currentSelected,
114+
lastClickedIndex,
115+
getRangeKeys: (start, end) => {
121116
const rangeRows = rows.slice(start, end + 1)
122-
const rangeKeys = rangeRows.map(row =>
123-
keys.reduce<Record<string, string>>((acc, key) => ({ ...acc, [key]: row[key] as string }), {}),
117+
return rangeRows.map(row =>
118+
keys.reduce<Record<string, string>>(
119+
(acc, key) => ({ ...acc, [key]: row[key] as string }),
120+
{},
121+
),
124122
)
125-
123+
},
124+
onSelectionChange: (selected, selectionState, newLastClickedIndex) => {
126125
store.setState(state => ({
127126
...state,
128-
selected: rangeKeys,
129-
selectionState: {
130-
anchorIndex: lastIndex,
131-
focusIndex: rowIndex,
132-
lastExpandDirection: rowIndex > lastIndex ? 'down' : 'up',
133-
},
127+
selected,
128+
selectionState,
129+
lastClickedIndex: newLastClickedIndex,
134130
} satisfies typeof state))
135-
}
136-
else {
137-
if (isSelected) {
138-
store.setState(state => ({
139-
...state,
140-
selected: store.state.selected.filter(row => !keys.every(key => row[key] === rows[rowIndex]![key])),
141-
selectionState: { anchorIndex: null, focusIndex: null, lastExpandDirection: null },
142-
} satisfies typeof state))
143-
}
144-
else {
145-
store.setState(state => ({
146-
...state,
147-
selected: [...state.selected, keys.reduce((acc, key) => ({ ...acc, [key]: rows[rowIndex]![key] }), {})],
148-
selectionState: { anchorIndex: rowIndex, focusIndex: rowIndex, lastExpandDirection: null },
149-
} satisfies typeof state))
150-
}
151-
}
152-
153-
store.setState(state => ({
154-
...state,
155-
lastClickedIndex: rowIndex,
156-
} satisfies typeof state))
157-
shiftKeyRef.current = false
158-
}
131+
},
132+
})
159133

160134
return (
161135
<div

apps/desktop/src/routes/_protected/database/$id/table/-components/table/table.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,8 +260,20 @@ function TableComponent({ table, schema }: { table: string, schema: string }) {
260260
}, [connection, table, schema, columns, hiddenColumns, primaryColumns, saveValue, toggleOrder, enums])
261261

262262
const handleShiftSelectionKeyDown = useShiftSelectionKeyDown({
263-
rows,
264-
rowKeyColumns: primaryColumns,
263+
rowCount: rows.length,
264+
getRowKey: index => primaryColumns.reduce<Record<string, string>>(
265+
(acc, key) => ({ ...acc, [key]: rows[index]![key] as string }),
266+
{},
267+
),
268+
getRangeKeys: (start, end) => {
269+
const rangeRows = rows.slice(start, end + 1)
270+
return rangeRows.map(row =>
271+
primaryColumns.reduce<Record<string, string>>(
272+
(acc, key) => ({ ...acc, [key]: row[key] as string }),
273+
{},
274+
),
275+
)
276+
},
265277
getSelectionState: () => store.state.selectionState,
266278
onSelectionChange: (selected, selectionState) => {
267279
store.setState(state => ({

0 commit comments

Comments
 (0)