mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-25 13:27:35 +02:00
fix table keyboard navigation (#1469)
This commit is contained in:
@@ -57,8 +57,8 @@ const hasRequiredItemProperties = (item: unknown): item is { id: string; serverI
|
|||||||
item !== null &&
|
item !== null &&
|
||||||
'id' in item &&
|
'id' in item &&
|
||||||
typeof (item as any).id === 'string' &&
|
typeof (item as any).id === 'string' &&
|
||||||
'serverId' in item &&
|
'_serverId' in item &&
|
||||||
typeof (item as any).serverId === 'string'
|
typeof (item as any)._serverId === 'string'
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -76,9 +76,7 @@ const hasRequiredStateItemProperties = (
|
|||||||
'_serverId' in item &&
|
'_serverId' in item &&
|
||||||
typeof (item as any)._serverId === 'string' &&
|
typeof (item as any)._serverId === 'string' &&
|
||||||
'_itemType' in item &&
|
'_itemType' in item &&
|
||||||
typeof (item as any)._itemType === 'string' &&
|
typeof (item as any)._itemType === 'string'
|
||||||
'rowId' in item &&
|
|
||||||
typeof (item as any).rowId === 'string'
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1722,7 +1720,9 @@ const BaseItemTableList = ({
|
|||||||
// Helper function to get ItemListStateItemWithRequiredProperties (rowId is separate, not part of item)
|
// Helper function to get ItemListStateItemWithRequiredProperties (rowId is separate, not part of item)
|
||||||
const getStateItem = useCallback(
|
const getStateItem = useCallback(
|
||||||
(item: any): ItemListStateItemWithRequiredProperties | null => {
|
(item: any): ItemListStateItemWithRequiredProperties | null => {
|
||||||
if (!hasRequiredItemProperties(item)) return null;
|
if (!hasRequiredItemProperties(item)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
typeof item === 'object' &&
|
typeof item === 'object' &&
|
||||||
item !== null &&
|
item !== null &&
|
||||||
@@ -1750,7 +1750,7 @@ const BaseItemTableList = ({
|
|||||||
if (validSelected.length > 0) {
|
if (validSelected.length > 0) {
|
||||||
const lastSelected = validSelected[validSelected.length - 1];
|
const lastSelected = validSelected[validSelected.length - 1];
|
||||||
currentIndex = data.findIndex(
|
currentIndex = data.findIndex(
|
||||||
(d) => hasRequiredItemProperties(d) && d.id === lastSelected.id,
|
(d) => extractRowId(d) === extractRowId(lastSelected),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1765,93 +1765,111 @@ const BaseItemTableList = ({
|
|||||||
const newItem: any = data[newIndex];
|
const newItem: any = data[newIndex];
|
||||||
if (!newItem) return;
|
if (!newItem) return;
|
||||||
|
|
||||||
// Handle Shift + Arrow for incremental range selection (matches shift+click behavior)
|
const newItemListItem = getStateItem(newItem);
|
||||||
if (e.shiftKey) {
|
if (newItemListItem && extractRowId(newItemListItem)) {
|
||||||
const selectedItems = internalState.getSelected();
|
internalState.setSelected([newItemListItem]);
|
||||||
const validSelectedItems = selectedItems.filter(hasRequiredStateItemProperties);
|
|
||||||
const lastSelectedItem = validSelectedItems[validSelectedItems.length - 1];
|
|
||||||
|
|
||||||
if (lastSelectedItem) {
|
|
||||||
// Find the indices of the last selected item and new item
|
|
||||||
const lastRowId = lastSelectedItem.rowId;
|
|
||||||
const lastIndex = data.findIndex((d) => {
|
|
||||||
const rowId = extractRowId(d);
|
|
||||||
return rowId === lastRowId;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (lastIndex !== -1 && newIndex !== -1) {
|
|
||||||
// Create range selection from last selected to new position
|
|
||||||
const startIndex = Math.min(lastIndex, newIndex);
|
|
||||||
const stopIndex = Math.max(lastIndex, newIndex);
|
|
||||||
|
|
||||||
const rangeItems: ItemListStateItemWithRequiredProperties[] = [];
|
|
||||||
for (let i = startIndex; i <= stopIndex; i++) {
|
|
||||||
const rangeItem = data[i];
|
|
||||||
const stateItem = getStateItem(rangeItem);
|
|
||||||
if (stateItem && extractRowId(stateItem)) {
|
|
||||||
rangeItems.push(stateItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add range items to selection (matching shift+click behavior)
|
|
||||||
const currentSelected = internalState.getSelected();
|
|
||||||
const validSelected = currentSelected.filter(
|
|
||||||
hasRequiredStateItemProperties,
|
|
||||||
);
|
|
||||||
const newSelected: ItemListStateItemWithRequiredProperties[] = [
|
|
||||||
...validSelected,
|
|
||||||
];
|
|
||||||
rangeItems.forEach((rangeItem) => {
|
|
||||||
const rangeRowId = extractRowId(rangeItem);
|
|
||||||
if (
|
|
||||||
rangeRowId &&
|
|
||||||
!newSelected.some(
|
|
||||||
(selected) => extractRowId(selected) === rangeRowId,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
newSelected.push(rangeItem);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ensure the last item in selection is the item at newIndex for incremental extension
|
|
||||||
const newItemListItem = getStateItem(newItem);
|
|
||||||
if (newItemListItem && extractRowId(newItemListItem)) {
|
|
||||||
const newItemRowId = extractRowId(newItemListItem);
|
|
||||||
// Remove the new item from its current position if it exists
|
|
||||||
const filteredSelected = newSelected.filter(
|
|
||||||
(item) => extractRowId(item) !== newItemRowId,
|
|
||||||
);
|
|
||||||
// Add it at the end so it becomes the last selected item
|
|
||||||
filteredSelected.push(newItemListItem);
|
|
||||||
internalState.setSelected(filteredSelected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No previous selection, just select the new item
|
|
||||||
const newItemListItem = getStateItem(newItem);
|
|
||||||
if (newItemListItem && extractRowId(newItemListItem)) {
|
|
||||||
internalState.setSelected([newItemListItem]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Without Shift: select only the new item
|
|
||||||
const newItemListItem = getStateItem(newItem);
|
|
||||||
if (newItemListItem && extractRowId(newItemListItem)) {
|
|
||||||
internalState.setSelected([newItemListItem]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const offset = calculateScrollTopForIndex(newIndex);
|
// Check if we need to scroll by determining if the item is at the edge of the viewport
|
||||||
scrollToTableOffset(offset);
|
const gridIndex = enableHeader ? newIndex + 1 : newIndex;
|
||||||
|
|
||||||
|
const mainContainer = rowRef.current?.childNodes[0] as HTMLDivElement | undefined;
|
||||||
|
const pinnedRightContainer = pinnedRightColumnRef.current?.childNodes[0] as
|
||||||
|
| HTMLDivElement
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
// Use right pinned column scroll position if right-pinned columns exist
|
||||||
|
const scrollContainer =
|
||||||
|
pinnedRightColumnCount > 0 && pinnedRightContainer
|
||||||
|
? pinnedRightContainer
|
||||||
|
: mainContainer;
|
||||||
|
|
||||||
|
if (scrollContainer) {
|
||||||
|
const viewportTop = scrollContainer.scrollTop;
|
||||||
|
const viewportHeight = scrollContainer.clientHeight;
|
||||||
|
const viewportBottom = viewportTop + viewportHeight;
|
||||||
|
|
||||||
|
const rowTop = calculateScrollTopForIndex(gridIndex);
|
||||||
|
const adjustedIndex = enableHeader ? Math.max(0, newIndex - 1) : newIndex;
|
||||||
|
const mockCellProps: TableItemProps = {
|
||||||
|
cellPadding,
|
||||||
|
columns: parsedColumns,
|
||||||
|
controls: {} as ItemControls,
|
||||||
|
data: enableHeader ? [null, ...data] : data,
|
||||||
|
enableAlternateRowColors,
|
||||||
|
enableExpansion,
|
||||||
|
enableHeader,
|
||||||
|
enableHorizontalBorders,
|
||||||
|
enableRowHoverHighlight,
|
||||||
|
enableSelection,
|
||||||
|
enableVerticalBorders,
|
||||||
|
getRowHeight: () => DEFAULT_ROW_HEIGHT,
|
||||||
|
internalState: {} as ItemListStateActions,
|
||||||
|
itemType,
|
||||||
|
playerContext,
|
||||||
|
size,
|
||||||
|
tableId,
|
||||||
|
};
|
||||||
|
|
||||||
|
let calculatedRowHeight: number;
|
||||||
|
if (typeof rowHeight === 'number') {
|
||||||
|
calculatedRowHeight = rowHeight;
|
||||||
|
} else if (typeof rowHeight === 'function') {
|
||||||
|
calculatedRowHeight = rowHeight(adjustedIndex, mockCellProps);
|
||||||
|
} else {
|
||||||
|
calculatedRowHeight = DEFAULT_ROW_HEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowBottom = rowTop + calculatedRowHeight;
|
||||||
|
|
||||||
|
// Check if row is fully visible within viewport
|
||||||
|
const isFullyVisible = rowTop >= viewportTop && rowBottom <= viewportBottom;
|
||||||
|
|
||||||
|
// Check if row is at the edge (top or bottom of viewport)
|
||||||
|
const isAtTopEdge = rowTop < viewportTop;
|
||||||
|
const isAtBottomEdge = rowBottom >= viewportBottom;
|
||||||
|
|
||||||
|
// Only scroll if the item is not fully visible or at the edge
|
||||||
|
if (!isFullyVisible || isAtTopEdge || isAtBottomEdge) {
|
||||||
|
// Determine alignment based on direction
|
||||||
|
const align: 'bottom' | 'top' =
|
||||||
|
e.key === 'ArrowDown' && isAtBottomEdge
|
||||||
|
? 'bottom'
|
||||||
|
: e.key === 'ArrowUp' && isAtTopEdge
|
||||||
|
? 'top'
|
||||||
|
: isAtBottomEdge
|
||||||
|
? 'bottom'
|
||||||
|
: isAtTopEdge
|
||||||
|
? 'top'
|
||||||
|
: 'top';
|
||||||
|
|
||||||
|
scrollToTableIndex(gridIndex, { align });
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
data,
|
data,
|
||||||
enableSelection,
|
enableSelection,
|
||||||
internalState,
|
internalState,
|
||||||
calculateScrollTopForIndex,
|
calculateScrollTopForIndex,
|
||||||
scrollToTableOffset,
|
scrollToTableIndex,
|
||||||
extractRowId,
|
extractRowId,
|
||||||
getStateItem,
|
getStateItem,
|
||||||
|
pinnedRightColumnCount,
|
||||||
|
enableHeader,
|
||||||
|
cellPadding,
|
||||||
|
parsedColumns,
|
||||||
|
enableAlternateRowColors,
|
||||||
|
enableExpansion,
|
||||||
|
enableHorizontalBorders,
|
||||||
|
enableRowHoverHighlight,
|
||||||
|
enableVerticalBorders,
|
||||||
|
itemType,
|
||||||
|
playerContext,
|
||||||
|
size,
|
||||||
|
tableId,
|
||||||
|
DEFAULT_ROW_HEIGHT,
|
||||||
|
rowHeight,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user