diff --git a/packages/devextreme/js/__internal/grids/grid_core/focus/focus.ts b/packages/devextreme/js/__internal/grids/grid_core/focus/focus.ts index ba5f89e37378..8894b1f24803 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/focus/focus.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/focus/focus.ts @@ -120,11 +120,37 @@ export class FocusController extends core.ViewController { const isFocusedRowVisible = focusedRowIndex >= 0; const isFocusedRowInChange = !!e.rowIndices?.includes(focusedRowIndex); - if (isFocusedRowVisible && isFocusedRowInChange) { + if (!isFocusedRowVisible || !isFocusedRowInChange) { + return; + } + + if (this.hasStaleFocusedRow(focusedRowIndex)) { this.updateFocusedRow({ focusedRowKey, preventScroll: true }); + } else { + this.restoreFocusedRowTabIndex(focusedRowIndex); } } + private restoreFocusedRowTabIndex(focusedRowIndex: number): void { + const $focusedRows = $(this.getRowsView().getRowElement(focusedRowIndex)); + const hasFocusableCell = $focusedRows.children('[tabindex]').length > 0; + + if (!hasFocusableCell) { + $focusedRows.attr('tabindex', this.option('tabIndex') ?? 0); + } + } + + private hasStaleFocusedRow(focusedRowIndex: number): boolean { + const rowsView = this.getRowsView(); + + return rowsView.getTableElements().toArray().some((tableElement) => { + const $tableElement = $(tableElement); + const $focusedRow = rowsView._getRowElements($tableElement).eq(focusedRowIndex); + + return this.findFocusedRows($tableElement).not($focusedRow).length > 0; + }); + } + private updateFocusedRowIfNeeded(e: DataChange, forceUpdate = false): void { const operationTypes = e.operationTypes ?? {}; const { @@ -536,15 +562,19 @@ export class FocusController extends core.ViewController { } } - private _clearPreviousFocusedRow($tableElement: dxElementWrapper, focusedRowIndex: number): void { + private findFocusedRows($tableElement: dxElementWrapper): dxElementWrapper { const isNotMasterDetailFocusedRow = (_: number, focusedRow: Element): boolean => { const $focusedRowTable = $(focusedRow).closest(`.${this.addWidgetPrefix(VIEW_CLASSES.table)}`); return $tableElement.is($focusedRowTable); }; - const $prevRowFocusedElement = $tableElement + return $tableElement .find(FOCUSED_ROW_SELECTOR) .filter(isNotMasterDetailFocusedRow); + } + + private _clearPreviousFocusedRow($tableElement: dxElementWrapper, focusedRowIndex: number): void { + const $prevRowFocusedElement = this.findFocusedRows($tableElement); $prevRowFocusedElement .removeClass(CLASSES.rowFocused) diff --git a/packages/devextreme/js/__internal/grids/tree_list/__tests__/focus_navigation.integration.test.ts b/packages/devextreme/js/__internal/grids/tree_list/__tests__/focus_navigation.integration.test.ts index ff4ea488369f..62fc9bfa5391 100644 --- a/packages/devextreme/js/__internal/grids/tree_list/__tests__/focus_navigation.integration.test.ts +++ b/packages/devextreme/js/__internal/grids/tree_list/__tests__/focus_navigation.integration.test.ts @@ -1,6 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, } from '@jest/globals'; +import $ from '@js/core/renderer'; import { flushAsync } from '../../grid_core/__tests__/__mock__/helpers/utils'; import { @@ -89,3 +90,62 @@ describe('TreeList focus — navigating to the focused row', () => { expect(instance.option('focusedRowIndex')).toBe(1); }); }); + +describe('TreeList focus — partial update of the focused row', () => { + beforeEach(beforeTest); + afterEach(afterTest); + + it('keeps DOM focus on the focused cell when the focused row is expanded with repaintChangesOnly (T1338023)', async () => { + const { instance } = await createTreeList({ + dataSource: DATA, + columns: ['id', 'name'], + focusedRowEnabled: true, + focusedRowKey: 1, + repaintChangesOnly: true, + autoExpandAll: false, + }); + await flushAsync(); + + const cell = instance.getCellElement(0, 'name') as HTMLElement; + instance.focus(cell); + await flushAsync(); + cell.dispatchEvent(new KeyboardEvent('keydown', { key: 'Shift', bubbles: true })); + + expect(document.activeElement).toBe(cell); + + const expanding = instance.expandRow(1); + await flushAsync(); + await expanding; + + expect(visibleKeys(instance)).toEqual([1, 11, 2]); + expect(cell.hasAttribute('tabindex')).toBe(true); + expect(document.activeElement).toBe(cell); + }); + + it('keeps the focused row tab stop when the focused row is expanded with repaintChangesOnly (T1338023)', async () => { + const { instance } = await createTreeList({ + dataSource: DATA, + columns: ['id', 'name'], + focusedRowEnabled: true, + focusedRowKey: 1, + repaintChangesOnly: true, + autoExpandAll: false, + tabIndex: 3, + }); + await flushAsync(); + + const row = $(instance.getRowElement(0)).get(0) as HTMLElement; + instance.focus(row); + await flushAsync(); + row.dispatchEvent(new KeyboardEvent('keydown', { key: 'Shift', bubbles: true })); + + expect(document.activeElement).toBe(row); + + const expanding = instance.expandRow(1); + await flushAsync(); + await expanding; + + expect(visibleKeys(instance)).toEqual([1, 11, 2]); + expect(row.getAttribute('tabindex')).toBe('3'); + }); +});