Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
Expand Up @@ -321,3 +321,137 @@ test('DataGrid should scroll to the last cell of the previous row and focus it w
}));
});
});

test('DataGrid should move focus from Save to Cancel button on Tab press in row editing mode with virtual columns (T1326106)', async (t) => {
// arrange
const dataGrid = new DataGrid('#container');

await t
.expect(dataGrid.isReady())
.ok();

const commandCell = dataGrid.getDataRow(0).getCommandCell(5);

// act - click Edit button on first row
await t.click(commandCell.getButton(0));

const saveButton = commandCell.getButton(0);
const cancelButton = commandCell.getButton(1);

// assert
await t
.expect(saveButton.exists)
.ok()
.expect(cancelButton.exists)
.ok();

const lastDataCell = dataGrid.getDataCell(0, 4);

// act
await t.click(lastDataCell.element);

// assert
await t
.expect(lastDataCell.isFocused)
.ok();

// act
await t.pressKey('tab');

// assert
await t
.expect(saveButton.focused)
.ok();

// act
await t.pressKey('tab');

// assert
await t
.expect(cancelButton.focused)
.ok();

// act
await t.pressKey('tab');

// assert - First cell of the second row should be focused
await t
.expect(dataGrid.getDataCell(1, 0).isFocused)
.ok();
}).before(async () => createWidget('dxDataGrid', {
width: 800,
dataSource: generateData(10, 5),
columnWidth: 100,
keyExpr: 'field1',
editing: {
mode: 'row',
allowUpdating: true,
allowDeleting: true,
},
scrolling: {
columnRenderingMode: 'virtual',
},
}));

test('DataGrid should move focus from Save to Cancel button on Shift + Tab press in row editing mode with virtual columns (T1326106)', async (t) => {
// arrange
const dataGrid = new DataGrid('#container');

await t
.expect(dataGrid.isReady())
.ok();

const commandCell = dataGrid.getDataRow(1).getCommandCell(5);

// act - click Edit button on second row
await t.click(commandCell.getButton(0));

const saveButton = commandCell.getButton(0);
const cancelButton = commandCell.getButton(1);

// assert
await t
.expect(saveButton.exists)
.ok()
.expect(cancelButton.exists)
.ok();

const firstDataCell = dataGrid.getDataCell(1, 0);

// act
await t.click(firstDataCell.element);

// assert
await t
.expect(firstDataCell.isFocused)
.ok();

// act
await t.pressKey('shift+tab');

// assert
await t
.expect(saveButton.focused)
.ok();

// act
await t.pressKey('shift+tab');

// assert - Last cell of the first row should be focused
await t
.expect(dataGrid.getDataCell(0, 4).isFocused)
.ok();
}).before(async () => createWidget('dxDataGrid', {
width: 800,
dataSource: generateData(10, 5),
columnWidth: 100,
keyExpr: 'field1',
editing: {
mode: 'row',
allowUpdating: true,
allowDeleting: true,
},
scrolling: {
columnRenderingMode: 'virtual',
},
}));
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ import {
} from './m_keyboard_navigation_utils';
import { keyboardNavigationScrollableA11yExtender } from './scrollable_a11y';
import type { NavigationDirection, NavigationElementType, NavigationKeyCode } from './types';
import { getNextColumnIndex } from './utils';

export class KeyboardNavigationController extends KeyboardNavigationControllerCore {
private _updateFocusTimeout: any;
Expand Down Expand Up @@ -817,8 +818,9 @@ export class KeyboardNavigationController extends KeyboardNavigationControllerCo

const tabOptions = { hasEditingOptions, isLastValidCell, isOriginalHandlerRequired };

// Virtual columns require horizontal scrolling before executing tab navigation
if (canHandleNavigation && this._isVirtualColumnRender()) {
// For virtual columns, scroll to reveal the next column before navigating.
// Skip scrolling when focus should cycle through interactive elements within the current cell.
if (canHandleNavigation && this.needVirtualColumnScroll(eventTarget, event)) {
this._processVirtualHorizontalPosition(direction, event)
.done(() => this.executeTabKey(event, tabOptions));
return;
Expand All @@ -827,6 +829,20 @@ export class KeyboardNavigationController extends KeyboardNavigationControllerCo
this.executeTabKey(event, tabOptions);
}

/**
* Determines whether horizontal scrolling is needed for virtual column tab navigation.
* Returns false when focus should cycle through interactive elements within the current cell.
*/
private needVirtualColumnScroll(eventTarget: Element, event: KeyDownEvent): boolean {
if (!this._isVirtualColumnRender()) {
return false;
}

const $cell = this.getCellElementFromTarget(eventTarget);

return !this.isOriginalTabHandlerRequired($cell, event);
}

/**
* Determines whether the focused cell is at the boundary of the grid.
* When at the boundary, Tab/Shift+Tab should let focus leave the grid.
Expand Down Expand Up @@ -899,62 +915,19 @@ export class KeyboardNavigationController extends KeyboardNavigationControllerCo
event: KeyDownEvent,
): DeferredObj<void> {
const columnIndex = this.getColumnIndex();
let nextColumnIndex;
let horizontalScrollPosition = 0;
let needToScroll = false;
const nextColumnIndex = getNextColumnIndex(direction, columnIndex);

// eslint-disable-next-line default-case
switch (direction) {
case 'next':
case 'nextInRow': {
const columnsCount = this._getVisibleColumnCount();
nextColumnIndex = columnIndex + 1;
horizontalScrollPosition = this.option('rtlEnabled')
? this._getMaxHorizontalOffset()
: 0;
if (direction === 'next') {
needToScroll = columnsCount === nextColumnIndex
|| (this._isFixedColumn(columnIndex)
&& !this._isColumnRendered(nextColumnIndex));
} else {
needToScroll = columnsCount > nextColumnIndex
&& this._isFixedColumn(columnIndex)
&& !this._isColumnRendered(nextColumnIndex);
}
break;
}
case 'previous':
case 'previousInRow': {
nextColumnIndex = columnIndex - 1;
horizontalScrollPosition = this.option('rtlEnabled')
? 0
: this._getMaxHorizontalOffset();
if (direction === 'previous') {
const columnIndexOffset = this._columnsController.getColumnIndexOffset();
const leftEdgePosition = nextColumnIndex < 0 && columnIndexOffset === 0;
needToScroll = leftEdgePosition
|| (this._isFixedColumn(columnIndex)
&& !this._isColumnRendered(nextColumnIndex));
} else {
needToScroll = nextColumnIndex >= 0
&& this._isFixedColumn(columnIndex)
&& !this._isColumnRendered(nextColumnIndex);
}
break;
}
}
// Strategy 1: scroll to the grid's edge (beginning or end)
if (this.needScrollToEdge(direction, columnIndex, nextColumnIndex)) {
const scrollPosition = this.getEdgeScrollPosition(direction);

if (needToScroll) {
event.originalEvent.preventDefault();

return this.scrollLeft(horizontalScrollPosition);
return this.scrollLeft(scrollPosition);
}

if (
isDefined(nextColumnIndex)
&& isDefined(direction)
&& this._isColumnVirtual(nextColumnIndex)
) {
// Strategy 2: scroll to reveal the next virtual column
if (this._isColumnVirtual(nextColumnIndex)) {
event.originalEvent.preventDefault();

return this.scrollToNextCell(null, direction);
Expand All @@ -964,6 +937,45 @@ export class KeyboardNavigationController extends KeyboardNavigationControllerCo
return Deferred().resolve().promise();
}

private getEdgeScrollPosition(direction: NavigationDirection): number {
const isForward = direction === 'next' || direction === 'nextInRow';

if (this.option('rtlEnabled')) {
return isForward ? this._getMaxHorizontalOffset() : 0;
}

return isForward ? 0 : this._getMaxHorizontalOffset();
}

private needScrollToEdge(
direction: NavigationDirection,
columnIndex: number,
nextColumnIndex: number,
): boolean {
const isFixedColumn = this._isFixedColumn(columnIndex);
const isNextColumnRendered = this._isColumnRendered(nextColumnIndex);
const needScrollPastFixed = isFixedColumn && !isNextColumnRendered;

switch (direction) {
case 'next': {
const isLastColumn = this._getVisibleColumnCount() === nextColumnIndex;
return isLastColumn || needScrollPastFixed;
}
case 'nextInRow':
return this._getVisibleColumnCount() > nextColumnIndex
&& needScrollPastFixed;
case 'previous': {
const columnIndexOffset = this._columnsController.getColumnIndexOffset();
const isAtLeftEdge = nextColumnIndex < 0 && columnIndexOffset === 0;
return isAtLeftEdge || needScrollPastFixed;
}
case 'previousInRow':
return nextColumnIndex >= 0 && needScrollPastFixed;
default:
return false;
}
}

/**
* Handles Tab key when a cell is being edited.
* Moves focus and editing to the next/previous editable cell.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { NavigationDirection } from './types';
Comment thread
Alyar666 marked this conversation as resolved.

export const getNextColumnIndex = (
direction: NavigationDirection,
columnIndex: number,
): number => (direction === 'next' || direction === 'nextInRow'
? columnIndex + 1
: columnIndex - 1);
Loading