Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
975def9
Toolbar: support keyboard navigation according to APG W3C
EugeniyKiyashko Jun 10, 2026
957caf0
fix Copilot remarks
EugeniyKiyashko Jun 10, 2026
841b6fb
Merge branch '26_1' into 26_1_toolbar_kbn
pharret31 Jun 11, 2026
319c6ec
Merge branch '26_1' into 26_1_toolbar_kbn
pharret31 Jun 11, 2026
d1577d8
Merge branch '26_1' into 26_1_toolbar_kbn
pharret31 Jun 12, 2026
119c52d
fix copilot's review issues
pharret31 Jun 12, 2026
e96abeb
revert default behavior to DataGrid demos
dmlvr Jun 16, 2026
2202e7d
remove wa from e2e tests
dmlvr Jun 16, 2026
440ad6d
leftovers
dmlvr Jun 16, 2026
1321dfb
leftovers
dmlvr Jun 16, 2026
ce9ad6f
update etalons
dmlvr Jun 16, 2026
ce8065f
add more etalons
dmlvr Jun 16, 2026
9128adc
Merge branch '26_1' into 26_1_toolbar_kbn
dmlvr Jun 17, 2026
6060967
remove import
dmlvr Jun 17, 2026
4547048
Merge branch '26_1_toolbar_kbn' of github.com:EugeniyKiyashko/DevExtr…
dmlvr Jun 17, 2026
8f16cd1
update import
dmlvr Jun 17, 2026
18ceedd
update etalon
dmlvr Jun 17, 2026
ecba17a
set global rtlEnabled
dmlvr Jun 17, 2026
d2cc260
remove test.only
dmlvr Jun 17, 2026
dcac03f
revert global rtl
dmlvr Jun 17, 2026
07e73ce
allowKeyboardNavigation: false for DataGrid's groupPanel
dmlvr Jun 18, 2026
334e271
add early return when modifier button used
dmlvr Jun 18, 2026
0a286f6
revert some etalons
dmlvr Jun 18, 2026
3c5c413
update early return for commandKey
dmlvr Jun 18, 2026
c1cda02
add testcases for grid group panel
dmlvr Jun 18, 2026
1f49738
Merge branch '26_1' into 26_1_toolbar_kbn
dmlvr Jun 19, 2026
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please recheck this case and 3 below - it seems the functionality changed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello, we found the reason, fix it and update etalons one more time, please, check it again

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
170 changes: 170 additions & 0 deletions e2e/testcafe-devextreme/tests/navigation/toolbar/keyboard.nonAPG.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { ClientFunction, Selector } from 'testcafe';
import Toolbar from 'devextreme-testcafe-models/toolbar/toolbar';
import url from '../../../helpers/getPageUrl';
import { createWidget } from '../../../helpers/createWidget';
import { appendElementTo } from '../../../helpers/domUtils';

fixture.disablePageReloads`Toolbar_keyboard_navigation_nonAPG`
.page(url(__dirname, '../../container.html'));

const itemHasFocus = (item: Selector): Selector => item.filter(
(node) => node === document.activeElement
|| node.contains(document.activeElement as Node | null),
);

const setupThreeButtonsFixture = async (): Promise<void> => {
await appendElementTo('#container', 'div', 'externalBefore');
await appendElementTo('#container', 'div', 'toolbar');
await appendElementTo('#container', 'div', 'externalAfter');

await createWidget('dxButton', { text: 'External Before' }, '#externalBefore');

await createWidget('dxToolbar', {
allowKeyboardNavigation: false,
items: [
{
location: 'before',
widget: 'dxButton',
options: { text: 'A', focusStateEnabled: true },
},
{
location: 'before',
widget: 'dxButton',
options: { text: 'B', focusStateEnabled: true },
},
{
location: 'before',
widget: 'dxButton',
options: { text: 'C', focusStateEnabled: true },
},
],
}, '#toolbar');

await createWidget('dxButton', { text: 'External After' }, '#externalAfter');
};

test('Tab walks through every toolbar item; Shift+Tab walks back', async (t) => {
const externalBefore = Selector('#externalBefore');
const externalAfter = Selector('#externalAfter');
const toolbar = new Toolbar('#toolbar');

await t.click(externalBefore);
await t.expect(externalBefore.focused).ok('external before is focused');

await t.pressKey('tab');
await t.expect(itemHasFocus(toolbar.getItem(0)).exists)
.ok('Tab #1 -> item[0]');

await t.pressKey('tab');
await t.expect(itemHasFocus(toolbar.getItem(1)).exists)
.ok('Tab #2 -> item[1]');

await t.pressKey('tab');
await t.expect(itemHasFocus(toolbar.getItem(2)).exists)
.ok('Tab #3 -> item[2]');

await t.pressKey('tab');
await t.expect(externalAfter.focused)
.ok('Tab #4 -> external after');

await t.pressKey('shift+tab');
await t.expect(itemHasFocus(toolbar.getItem(2)).exists)
.ok('Shift+Tab #1 -> item[2]');

await t.pressKey('shift+tab');
await t.expect(itemHasFocus(toolbar.getItem(1)).exists)
.ok('Shift+Tab #2 -> item[1]');

await t.pressKey('shift+tab');
await t.expect(itemHasFocus(toolbar.getItem(0)).exists)
.ok('Shift+Tab #3 -> item[0]');

await t.pressKey('shift+tab');
await t.expect(externalBefore.focused)
.ok('Shift+Tab #4 -> external before');
}).before(setupThreeButtonsFixture);

test('Arrow keys do not move focus across toolbar items', async (t) => {
const externalBefore = Selector('#externalBefore');
const toolbar = new Toolbar('#toolbar');

await t.click(externalBefore);
await t.pressKey('tab');
await t.expect(itemHasFocus(toolbar.getItem(0)).exists)
.ok('item[0] focused');

const keys = ['right', 'left', 'home', 'end', 'down', 'up'];
await keys.reduce(async (prev, key) => {
await prev;
await t.pressKey(key);
await t.expect(itemHasFocus(toolbar.getItem(0)).exists)
.ok(`focus stays on item[0] after "${key}"`);
await t.expect(itemHasFocus(toolbar.getItem(1)).exists)
.notOk(`focus does not jump to item[1] on "${key}"`);
}, Promise.resolve());
}).before(setupThreeButtonsFixture);

const getTabsSelectedIndex = ClientFunction(
() => ($('#toolbar .dx-tabs') as unknown as {
dxTabs: (m: string, n: string) => number;
}).dxTabs('option', 'selectedIndex'),
);

test('Arrow keys are forwarded to the focused widget (dxTabs changes selectedIndex)', async (t) => {
const externalBefore = Selector('#externalBefore');
const toolbar = new Toolbar('#toolbar');

await t.click(externalBefore);
await t.pressKey('tab tab');
await t.expect(itemHasFocus(toolbar.getItem(1)).exists)
.ok('focus is inside dxTabs item');

await t.expect(getTabsSelectedIndex()).eql(0, 'initial selectedIndex is 0');

await t.pressKey('right');
await t.expect(getTabsSelectedIndex()).eql(
1,
'ArrowRight reaches dxTabs and advances selectedIndex',
);
await t.expect(itemHasFocus(toolbar.getItem(1)).exists)
.ok('focus stays inside dxTabs, did not jump to item[2]');

await t.pressKey('left');
await t.expect(getTabsSelectedIndex()).eql(
0,
'ArrowLeft reaches dxTabs and decreases selectedIndex',
);
await t.expect(itemHasFocus(toolbar.getItem(1)).exists)
.ok('focus still inside dxTabs');
}).before(async () => {
await appendElementTo('#container', 'div', 'externalBefore');
await appendElementTo('#container', 'div', 'toolbar');

await createWidget('dxButton', { text: 'External Before' }, '#externalBefore');

await createWidget('dxToolbar', {
allowKeyboardNavigation: false,
items: [
{
location: 'before',
widget: 'dxButton',
options: { text: 'Prev', focusStateEnabled: true },
},
{
location: 'before',
widget: 'dxTabs',
options: {
items: [{ text: 'Home' }, { text: 'Insert' }, { text: 'Layout' }],
selectedIndex: 0,
width: 'auto',
focusStateEnabled: true,
},
},
{
location: 'before',
widget: 'dxButton',
options: { text: 'Next', focusStateEnabled: true },
},
],
}, '#toolbar');
});
Loading
Loading