dbeaver/pro#5998 Fix: can't copy the whole table selected with the corner cell (#3550)

* dbeaver/pro#5998 fix: selectAll index header cell

* dbeaver/pro#5998 fix: removed OrderButton from tabbing order, add sorting on space/enter when header column cell is in focus

* dbeaver/pro#5998 feat: add onHeaderKeyDown handler to DataGrid and HeaderCellContentRenderer

we lost ability to copy data grid cells' data when one of the header cells is focused, because the header cell renderer was not handling keydown events. This commit utilizing gridSelectedCellCopy.onKeydownHandler inside HeaderCellContentRenderer keydown event handler

* dbeaver/pro#5998 style: change onKeyDown to a named function

* dbeaver/pro#5998 refactor: simplify OrderButton and update sorting logic in HeaderCellContentRenderer

* dbeaver/pro#5998 refactor: rename container class to clickAreaOverlay in TableIndexColumnHeader, remove z-index

---------

Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
This commit is contained in:
Sychev Andrey
2025-06-27 18:23:37 +08:00
committed by GitHub
co-authored by Daria Marutkina
parent 69330aa0fe
commit cb84409701
7 changed files with 50 additions and 34 deletions
@@ -70,6 +70,7 @@ export const DataGrid = forwardRef<DataGridRef, DataGridProps>(function DataGrid
onFocus,
onCellChange,
onColumnSort,
onHeaderKeyDown,
children,
className,
onCellKeyDown,
@@ -151,7 +152,7 @@ export const DataGrid = forwardRef<DataGridRef, DataGridProps>(function DataGrid
<HeaderDnDContext value={dndHeaderContext}>
<DataGridRowContext value={{ rowCount, onScrollToBottom }}>
<DataGridCellContext value={{ cell, cellText, cellElement, cellTooltip, onCellChange }}>
<DataGridCellHeaderContext value={{ headerElement, headerText, getHeaderDnD, columnSortable, onColumnSort, columnSortingState }}>
<DataGridCellHeaderContext value={{ headerElement, headerText, getHeaderDnD, columnSortable, onColumnSort, columnSortingState, onHeaderKeyDown }}>
<DataGridBase
ref={innerGridRef}
columns={dndHeaderContext.columns}
@@ -14,6 +14,7 @@ export interface IDataGridHeaderCellContext {
columnSortingState?: IGridReactiveValue<'asc' | 'desc' | undefined | null, [colIdx: number]>;
columnSortable?: IGridReactiveValue<boolean, [colIdx: number]>;
onColumnSort?: (colIdx: number, order: 'asc' | 'desc' | null, isMultiple: boolean) => void;
onHeaderKeyDown?: (event: React.KeyboardEvent) => void;
}
export const DataGridCellHeaderContext = createContext<IDataGridHeaderCellContext | null>(null);
@@ -1,4 +1,4 @@
import { memo, use, useRef } from 'react';
import { memo, use } from 'react';
import { useDrag, useDrop, type DnDStoreProvider } from '@dbeaver/react-dnd';
import { DataGridCellHeaderContext } from '../DataGridHeaderCellContext.js';
import { useGridReactiveValue } from '../useGridReactiveValue.js';
@@ -9,6 +9,7 @@ interface Props {
colIdx: number;
tabIndex?: number;
}
export const HeaderCellContentRenderer = memo(function HeaderCellContentRenderer({ colIdx, tabIndex }: Props) {
const dndHeaderContext = use(HeaderDnDContext);
const cellHeaderContext = use(DataGridCellHeaderContext);
@@ -16,11 +17,9 @@ export const HeaderCellContentRenderer = memo(function HeaderCellContentRenderer
const getHeaderText = useGridReactiveValue(headerElement ? undefined : cellHeaderContext?.headerText, colIdx);
const isColumnSortable = useGridReactiveValue(cellHeaderContext?.columnSortable, colIdx);
const onColumnSort = cellHeaderContext?.onColumnSort;
const onHeaderKeyDown = cellHeaderContext?.onHeaderKeyDown;
const sortingState = useGridReactiveValue(cellHeaderContext?.columnSortingState, colIdx);
const orderButtonRef = useRef<HTMLButtonElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const draggable = dndHeaderContext?.getCanDrag?.(colIdx) ?? false;
const drag = useDrag({
draggable,
@@ -55,26 +54,32 @@ export const HeaderCellContentRenderer = memo(function HeaderCellContentRenderer
},
});
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Tab' && !e.shiftKey && isColumnSortable && onColumnSort && orderButtonRef.current !== document.activeElement) {
e.preventDefault();
e.stopPropagation();
containerRef.current?.parentElement?.setAttribute('aria-selected', 'false');
orderButtonRef.current?.focus();
function handleSort(e: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>) {
if (!onColumnSort) return;
const nextSortState = sortingState === 'asc' ? 'desc' : sortingState === 'desc' ? null : 'asc';
onColumnSort(colIdx, nextSortState, e.ctrlKey || e.metaKey);
}
function onKeyDown(event: React.KeyboardEvent<HTMLElement>) {
onHeaderKeyDown?.(event);
if ((event.key === 'Enter' || event.key === ' ') && isColumnSortable && onColumnSort) {
event.preventDefault();
handleSort(event);
}
};
}
return (
<div
ref={containerRef}
onKeyDown={onKeyDown}
tabIndex={tabIndex}
onKeyDown={handleKeyDown}
className="tw:w-full tw:h-full tw:content-center tw:flex tw:items-center tw:justify-between tw:gap-1 tw:outline-none tw:group"
{...drag.props}
{...drop.props}
>
<span className="tw:overflow-hidden tw:text-ellipsis">{headerElement ?? getHeaderText ?? ''}</span>
{isColumnSortable && onColumnSort && <OrderButton ref={orderButtonRef} colIdx={colIdx} sortState={sortingState} onSort={onColumnSort} />}
{isColumnSortable && onColumnSort && <OrderButton sortState={sortingState} onClick={handleSort} />}
</div>
);
});
@@ -1,19 +1,12 @@
import { clsx, IconButton, Icon } from '@dbeaver/ui-kit';
interface OrderButtonProps {
colIdx: number;
sortState?: 'asc' | 'desc' | null;
onSort: (attributePosition: number, order: 'asc' | 'desc' | null, isMultiple: boolean) => void;
onClick: (e: React.MouseEvent<HTMLElement>) => void;
tabIndex?: number;
ref: React.Ref<HTMLButtonElement>;
}
export function OrderButton({ colIdx, sortState, onSort, tabIndex, ref }: OrderButtonProps) {
function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
const nextSortState = sortState === 'asc' ? 'desc' : sortState === 'desc' ? null : 'asc';
onSort(colIdx, nextSortState, e.ctrlKey || e.metaKey);
}
export function OrderButton({ sortState, onClick, tabIndex }: OrderButtonProps) {
const svgSortAsc = 'sort-asc';
const svgSortDesc = 'sort-desc';
const svgSortUnknown = 'sort-unknown';
@@ -24,9 +17,8 @@ export function OrderButton({ colIdx, sortState, onSort, tabIndex, ref }: OrderB
<IconButton
variant="secondary"
size="small"
onClick={handleClick}
onClick={onClick}
tabIndex={tabIndex}
ref={ref}
title="Sort by column"
aria-label="Sort by column"
className={clsx(
@@ -84,11 +84,14 @@ export const DataGridTable = observer<IDataPresentationProps>(function DataGridT
const tableData = useTableData(model as unknown as IDatabaseDataModel<ResultSetDataSource>, resultIndex, dataGridDivRef);
const gridSelectionContext = useGridSelectionContext(tableData, selectionAction);
function restoreFocus() {
const gridDiv = gridContainerRef.current;
const focusSink = gridDiv?.querySelector<HTMLDivElement>('[aria-selected="true"]');
focusSink?.focus();
}
const restoreFocus = useCallback(
function restoreFocus() {
const gridDiv = gridContainerRef.current;
const focusSink = gridDiv?.querySelector<HTMLDivElement>('[aria-selected="true"]');
focusSink?.focus();
},
[gridContainerRef],
);
function isGridInFocus(): boolean {
const gridDiv = gridContainerRef.current;
@@ -544,6 +547,7 @@ export const DataGridTable = observer<IDataPresentationProps>(function DataGridT
onColumnSort={handleSort}
onCellChange={handleCellChange}
onCellKeyDown={handleCellKeyDown}
onHeaderKeyDown={gridSelectedCellCopy.onKeydownHandler}
/>
</div>
</TableDataContext.Provider>
@@ -6,9 +6,16 @@
* you may not use this file except in compliance with the License.
*/
.container {
.clickAreaOverlay {
cursor: pointer;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.iconOrImage {
cursor: auto;
width: 10px;
@@ -36,10 +36,16 @@ export const TableIndexColumnHeader = observer(function TableIndexColumnHeader()
}
return (
<div title={translate('data_grid_table_index_column_tooltip')} className={s(styles, { container: true })} onClick={handleClick}>
<>
{readonly && (
<IconOrImage title={translate('data_grid_table_readonly_tooltip')} icon="/icons/lock.png" className={s(styles, { iconOrImage: true })} />
)}
</div>
<div
role="button"
title={translate('data_grid_table_index_column_tooltip')}
className={s(styles, { clickAreaOverlay: true })}
onClick={handleClick}
/>
</>
);
});