Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import {
afterEach, beforeEach, describe, expect, it, jest,
} from '@jest/globals';
import type { Properties as DataGridProperties } from '@js/ui/data_grid';
import type { DataGridInstance } from '@ts/grids/grid_core/__tests__/__mock__/helpers/utils';
import {
afterTest,
beforeTest,
createDataGrid,
flushAsync,
} from '@ts/grids/grid_core/__tests__/__mock__/helpers/utils';
import type { DataController } from '@ts/grids/grid_core/data_controller/data_controller';
import { triggerKeyDown, triggerPointerDown } from '@ts/grids/grid_core/keyboard_navigation/__tests__/helpers/utils';

import type { GroupingDataControllerExtension } from '../../types';

const DATA = [
{ id: 1, group: 'A', value: 10 },
{ id: 2, group: 'A', value: 20 },
{ id: 3, group: 'A', value: 30 },
{ id: 4, group: 'B', value: 40 },
];

const createGroupedGrid = (
options: DataGridProperties = {},
): ReturnType<typeof createDataGrid> => createDataGrid({
dataSource: DATA,
columns: [{ dataField: 'group', groupIndex: 0 }, 'value'],
grouping: { autoExpandAll: true },
paging: { enabled: false },
...options,
});

const createMasterDetailGrid = (): ReturnType<typeof createDataGrid> => createDataGrid({
dataSource: DATA,
columns: ['group', 'value'],
masterDetail: { enabled: true },
});

const focusCell = (cell: HTMLElement | null): void => {
triggerPointerDown(cell as HTMLElement);
jest.runAllTimers();
};

const pressEnter = async (instance: DataGridInstance): Promise<void> => {
triggerKeyDown(instance, 'enter');
await flushAsync();
};

describe('Grouping keyboard navigation — Enter', () => {
beforeEach(beforeTest);
afterEach(afterTest);

it('collapses and then expands a focused group row', async () => {
const onRowCollapsing = jest.fn();
const onRowExpanding = jest.fn();
const { instance, component } = await createGroupedGrid({ onRowCollapsing, onRowExpanding });
focusCell(component.getGroupRow(0).getExpandCell());

await pressEnter(instance);

expect(instance.isRowExpanded(['A'])).toBe(false);
expect(onRowCollapsing).toHaveBeenCalledTimes(1);

await pressEnter(instance);

expect(instance.isRowExpanded(['A'])).toBe(true);
expect(onRowExpanding).toHaveBeenCalledTimes(1);
});

it('keeps a group row expanded when grouping.allowCollapsing is false', async () => {
const { instance, component } = await createGroupedGrid({
grouping: { autoExpandAll: true, allowCollapsing: false },
});
focusCell(component.getGroupRow(0).getExpandCell());

await pressEnter(instance);

expect(instance.isRowExpanded(['A'])).toBe(true);
});

it('opens and then closes the detail row from a focused expand cell', async () => {
const { instance, component } = await createMasterDetailGrid();
focusCell(component.getDataRow(0).getCell(0));

await pressEnter(instance);

expect(instance.isRowExpanded(1)).toBe(true);

await pressEnter(instance);

expect(instance.isRowExpanded(1)).toBe(false);
});

it('keeps a group row continued from the previous page expanded', async () => {
const onRowCollapsing = jest.fn();
const { instance, component } = await createGroupedGrid({
paging: { pageSize: 3, pageIndex: 1 },
onRowCollapsing,
});
await flushAsync();

expect(instance.getVisibleRows()[0]).toMatchObject({
rowType: 'group',
key: ['A'],
data: { isContinuation: true },
});

focusCell(component.getGroupRow(0).getExpandCell());

await pressEnter(instance);

expect(instance.isRowExpanded(['A'])).toBe(true);
expect(onRowCollapsing).not.toHaveBeenCalled();
});

it('calls changeRowExpand with the group key only', async () => {
const { instance, component } = await createGroupedGrid();
const dataController = instance.getController('data') as DataController
& GroupingDataControllerExtension;
const changeRowExpand = jest.spyOn(dataController, 'changeRowExpand');
focusCell(component.getGroupRow(0).getExpandCell());

await pressEnter(instance);

expect(changeRowExpand.mock.calls).toStrictEqual([[['A']]]);
});

it('starts and then ends editing of a data cell in a grouped grid', async () => {
const { instance, component } = await createGroupedGrid({
editing: { mode: 'cell', allowUpdating: true },
});
focusCell(component.getDataCell(1, 1).getElement());

await pressEnter(instance);

expect(component.getDataCell(1, 1).isEditCell).toBe(true);

await pressEnter(instance);

expect(component.getDataCell(1, 1).isEditCell).toBe(false);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { isDefined } from '@js/core/utils/type';
import type { Grouping, MasterDetail } from '@js/ui/data_grid';
import type { KeyboardKeyDownEvent } from '@ts/events/core/keyboard_processor';
import { CLASSES as COLUMNS_CONTROLLER_CLASSES } from '@ts/grids/grid_core/columns_controller/const';
import type { KeyboardNavigationController } from '@ts/grids/grid_core/keyboard_navigation/m_keyboard_navigation';
import type { KeyboardNavigationDataController } from '@ts/grids/grid_core/keyboard_navigation/types';
import { isGroupRow } from '@ts/grids/grid_core/keyboard_navigation/utils';
import type { ModuleType } from '@ts/grids/grid_core/types';

import type { GroupingDataControllerExtension } from '../types';

export const groupingKeyboardNavigationControllerExtender = (
Base: ModuleType<KeyboardNavigationController>,
): ModuleType<KeyboardNavigationController> => class GroupingKeyboardNavigationControllerExtender
extends Base {
protected declare _dataController: KeyboardNavigationDataController
& GroupingDataControllerExtension;

private needToggleRowExpand(rowIndex: number): boolean {
const $row = this._focusedView?.getRow(rowIndex);
const $cell = this._getFocusedCell();
const allowCollapsing: Grouping['allowCollapsing'] = this.option('grouping.allowCollapsing');
const masterDetailEnabled: MasterDetail['enabled'] = this.option('masterDetail.enabled');

return Boolean(allowCollapsing && isGroupRow($row))
|| Boolean(masterDetailEnabled && $cell.hasClass(COLUMNS_CONTROLLER_CLASSES.commandExpand));
}

protected _enterKeyHandler(eventArgs: KeyboardKeyDownEvent, isEditing: boolean): void {
const rowIndex = this.getVisibleRowIndex();

if (!this.needToggleRowExpand(rowIndex)) {
super._enterKeyHandler(eventArgs, isEditing);
return;
}

const key = this._dataController.getKeyByRowIndex(rowIndex);
const item = this._dataController.items()[rowIndex];
const isNotContinuation = item?.data && !item.data.isContinuation;

if (isDefined(key) && isNotContinuation) {
this._dataController.changeRowExpand(key);
}
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import gridCore from '@ts/grids/data_grid/core';
import { groupingColumnsControllerExtender } from './extenders/grouping_columns_controller';
import { groupingDataControllerExtender } from './extenders/grouping_data_controller';
import { groupingEditingControllerExtender } from './extenders/grouping_editing_controller';
import { groupingKeyboardNavigationControllerExtender } from './extenders/grouping_keyboard_navigation_controller';
import {
columnHeadersViewExtender,
GroupingHeaderPanelExtender,
Expand Down Expand Up @@ -39,6 +40,7 @@ gridCore.registerModule('grouping', {
data: groupingDataControllerExtender,
columns: groupingColumnsControllerExtender,
editing: groupingEditingControllerExtender,
keyboardNavigation: groupingKeyboardNavigationControllerExtender,
},
views: {
headerPanel: GroupingHeaderPanelExtender,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {
afterEach, beforeEach, describe, expect, it, jest,
} from '@jest/globals';
import eventsEngine from '@js/common/core/events/core/events_engine';
import $ from '@js/core/renderer';
import { isDefined } from '@js/core/utils/type';
import type { Properties as DataGridProperties } from '@js/ui/data_grid';
import type { DataGridInstance } from '@ts/grids/grid_core/__tests__/__mock__/helpers/utils';
import {
afterTest,
beforeTest,
createDataGrid,
flushAsync,
} from '@ts/grids/grid_core/__tests__/__mock__/helpers/utils';
import {
getHeadersKeyboardNavigationController,
triggerHeadersKeyDown,
} from '@ts/grids/grid_core/keyboard_navigation/__tests__/helpers/utils';
import { KEY_CODES } from '@ts/grids/grid_core/keyboard_navigation/const';

const GROUPED: DataGridProperties = {
dataSource: [
{ id: 1, group: 'A', value: 10 },
{ id: 2, group: 'B', value: 20 },
],
keyExpr: 'id',
columns: ['id', { dataField: 'group', groupIndex: 0 }, 'value'],
};

const GROUP_KEYS = { ctrlKey: true, which: KEY_CODES.G };
const UNGROUP_ALL_KEYS = { shiftKey: true, altKey: true, which: KEY_CODES.G };

const getGroupedDataFields = (instance: DataGridInstance): string[] => ['id', 'group', 'value']
.filter((dataField) => isDefined(instance.columnOption(dataField, 'groupIndex')));

const focusHeaderCell = (cell: HTMLElement | null): void => {
// @ts-expect-error - eventsEngine.trigger is not fully typed
eventsEngine.trigger($(cell), 'focusin');
};

describe('DataGrid headers keyboard navigation — grouping keys', () => {
beforeEach(beforeTest);
afterEach(() => {
afterTest();
jest.restoreAllMocks();
});

it('Shift+Alt+G in a filter row editor ungroups all columns and focuses the first header cell', async () => {
// JSDOM has no layout: without client rects the grid isn't :visible,
// so no resizeCompleted fires to restore the focus
jest.spyOn(Element.prototype, 'getClientRects').mockReturnValue([{}] as unknown as DOMRectList);
const { instance, component } = await createDataGrid({
...GROUPED,
filterRow: { visible: true },
});
const editor = component.getFilterRow().getFilterCell(1).getElement()
?.querySelector('input') ?? null;
editor?.focus();

triggerHeadersKeyDown(instance, 'g', editor, UNGROUP_ALL_KEYS);
await flushAsync();

expect(instance.getVisibleRows().map(({ rowType }) => rowType)).toEqual(['data', 'data']);
expect(document.activeElement).toBe(component.getHeaderCell(0).getElement());
});

it('Shift+Alt+G keeps focus on the focused header column', async () => {
const { instance, component } = await createDataGrid(GROUPED);
const valueHeaderCell = component.getHeaderCell(2).getElement();
focusHeaderCell(valueHeaderCell);

triggerHeadersKeyDown(instance, 'g', valueHeaderCell, UNGROUP_ALL_KEYS);
await flushAsync();

expect(getGroupedDataFields(instance)).toEqual([]);
expect(getHeadersKeyboardNavigationController(instance)._getFocusedCell().text()).toBe('Value');
});

it('Ctrl+G outside a header cell groups no column', async () => {
const { instance, $container } = await createDataGrid(GROUPED);
const headersElement = $container.find('.dx-datagrid-headers').get(0) ?? null;

triggerHeadersKeyDown(instance, 'g', headersElement, GROUP_KEYS);
await flushAsync();

expect(getGroupedDataFields(instance)).toEqual(['group']);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { isDefined } from '@js/core/utils/type';
import type { KeyboardKeyDownEvent } from '@ts/events/core/keyboard_processor';
import type { Column } from '@ts/grids/grid_core/columns_controller/types';
import { KEY_CODES } from '@ts/grids/grid_core/keyboard_navigation/const';
import type { FocusedCellPosition } from '@ts/grids/grid_core/keyboard_navigation/types';
import type { CellPosition } from '@ts/grids/grid_core/views/types';

import type { MixinBase } from './types';

Expand All @@ -22,7 +22,7 @@ export const ColumnKeyboardNavigationMixin = <T extends MixinBase>(Base: T) => c

protected getFocusedCellPositionByColumn(
column: Column | undefined,
): FocusedCellPosition | undefined {
): CellPosition | undefined {
if (!column) {
return undefined;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ import type { KeyboardKeyDownEvent } from '@ts/events/core/keyboard_processor';
import type { Column, GroupColumn } from '@ts/grids/grid_core/columns_controller/types';
import { ColumnKeyboardNavigationController } from '@ts/grids/grid_core/keyboard_navigation/column_keyboard_navigation_core';
import { Direction } from '@ts/grids/grid_core/keyboard_navigation/const';
import type { FocusedCellPosition } from '@ts/grids/grid_core/keyboard_navigation/types';
import type { Views } from '@ts/grids/grid_core/types';
import type { ViewDataChange } from '@ts/grids/grid_core/views/types';
import type { CellPosition, ViewDataChange } from '@ts/grids/grid_core/views/types';

import gridCore from '../core';
import { CLASSES as GROUPING_CLASSES } from '../grouping/const';
Expand Down Expand Up @@ -91,10 +90,16 @@ export class GroupPanelKeyboardNavigationController
: groupColumns[visibleColumnIndex + 1];
}

protected _getCell(cellPosition: FocusedCellPosition): dxElementWrapper | undefined {
protected _getCell(cellPosition: Partial<CellPosition>): dxElementWrapper | undefined {
const { columnIndex } = cellPosition;

if (columnIndex === undefined) {
return undefined;
}

const $groupColumnElements = this.headerPanel?.getColumnElements();

return $groupColumnElements?.eq(cellPosition.columnIndex);
return $groupColumnElements?.eq(columnIndex);
}

protected getFocusedView(): Views['headerPanel'] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ const headersKeyboardNavigation: (
return isHandled;
}

public groupColumn(column, rowIndex = 0): void {
if (!isDefined(column.groupIndex) && column?.allowGrouping) {
public groupColumn(column: Column | undefined, rowIndex = 0): void {
if (column && !isDefined(column.groupIndex) && column.allowGrouping) {
const newGroupIndex = this._columnsController.getGroupColumns()?.length ?? 0;
const newFocusedColumn = this.getNewFocusedColumnBeforeGrouping(
column,
Expand All @@ -100,13 +100,17 @@ const headersKeyboardNavigation: (
this._columnsController.beginUpdate();
super.ungroupAllColumns();

const rowIndex = this._columnsController.getRowIndex(focusedColumn.index ?? -1, true);
const newVisibleIndex = this.getVisibleIndex(focusedColumn);
if (focusedColumn) {
const rowIndex = this._columnsController.getRowIndex(focusedColumn.index ?? -1, true);
const newVisibleIndex = this.getVisibleIndex(focusedColumn);

this.updateFocusPosition({
rowIndex,
columnIndex: newVisibleIndex,
});
this.updateFocusPosition({
rowIndex,
columnIndex: newVisibleIndex,
});
} else {
this.updateFocusPosition();
}
this._columnsController.endUpdate();
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ export const adaptivityKeyboardNavigationViewControllerExtender = (
Base: ModuleType<KeyboardNavigationController>,
): ModuleType<KeyboardNavigationController> => class AdaptivityKeyboardNavigationViewControllerExtender
extends Base {
public _isCellValid($cell, isClick?) {
public isCellValid($cell, isClick?) {
return (
super._isCellValid($cell, isClick)
super.isCellValid($cell, isClick)
&& !$cell.hasClass(this.addWidgetPrefix(CLASSES.hiddenColumn))
&& !$cell.hasClass(CLASSES.commandAdaptiveHidden)
);
Expand Down Expand Up @@ -53,7 +53,7 @@ export const adaptivityKeyboardNavigationViewControllerExtender = (
return super.isFocusableColumn(column) && column.visibleWidth !== HIDDEN_COLUMNS_WIDTH;
}

public _isCellElement($cell) {
return super._isCellElement($cell) || $cell.hasClass(CLASSES.adaptiveItemText);
public isCellElement($cell) {
return super.isCellElement($cell) || $cell.hasClass(CLASSES.adaptiveItemText);
}
};
Loading
Loading