Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
f0fca41
Toolbar: support keyboard navigation according to APG W3C
EugeniyKiyashko May 14, 2026
e926260
Toolbar: support keyboard navigation according to APG W3C
EugeniyKiyashko May 14, 2026
e3ff633
update imports for classnames
dmlvr May 15, 2026
b98de3d
add testfile
dmlvr May 15, 2026
84c694f
add tests
pharret31 May 15, 2026
46ae4b1
remove extra tests and unskipped some
dmlvr May 15, 2026
87d48e7
merge test part
dmlvr May 15, 2026
0a7da03
fix qunit scenarios
EugeniyKiyashko May 18, 2026
cf7e016
Merge branch '26_1' into 26_1_toolbar_kbn
EugeniyKiyashko May 18, 2026
3208739
fix old tests
EugeniyKiyashko May 18, 2026
dfdc9ee
exclude disabled items
EugeniyKiyashko May 18, 2026
bb24433
fix jquery related tests
dmlvr May 18, 2026
720a174
Merge branch '26_1_toolbar_kbn' of github.com:EugeniyKiyashko/DevExtr…
dmlvr May 18, 2026
84c6bf3
consider more scenarios
EugeniyKiyashko May 18, 2026
9584e39
prevent item focusing inside overflow menu
EugeniyKiyashko May 18, 2026
86e5c76
add fallback strategy using focusStateEnabled option
EugeniyKiyashko May 18, 2026
a953640
add focusStateEnabled tests
pharret31 May 18, 2026
301115e
remove extra comments
dmlvr May 18, 2026
a840a5d
non-focusable service items
pharret31 May 18, 2026
0bcb3db
add templates tests
pharret31 May 18, 2026
1e0a66a
add enter/exit tests
pharret31 May 18, 2026
d1f4e57
repair tests
EugeniyKiyashko May 19, 2026
ba22bb2
Merge branch '26_1' into 26_1_toolbar_kbn
EugeniyKiyashko May 19, 2026
5b360ed
Merge remote-tracking branch 'EugeniyKiyashko/26_1_toolbar_kbn' into …
pharret31 May 19, 2026
6ee9c86
add tab/shift-tab testcafe tests
pharret31 May 19, 2026
23765ef
remove only
pharret31 May 19, 2026
a3092f3
add some extra test cases
dmlvr May 19, 2026
d615427
Merge branch '26_1_toolbar_kbn' of github.com:EugeniyKiyashko/DevExtr…
dmlvr May 19, 2026
f59577b
fix one tab stop on editors according to APG
EugeniyKiyashko May 19, 2026
ec821cb
add more tests
EugeniyKiyashko May 20, 2026
00f218e
add focus outline with base color
pharret31 May 20, 2026
0a75daa
consider more scenarios
EugeniyKiyashko May 20, 2026
c527f2c
Merge branch '26_1_toolbar_kbn' of https://github.com/EugeniyKiyashko…
EugeniyKiyashko May 20, 2026
3cf14f8
fix ts
EugeniyKiyashko May 20, 2026
b5e431b
Merge branch '26_1' into 26_1_toolbar_kbn
EugeniyKiyashko May 20, 2026
be2517e
turn on fallback mode in components
EugeniyKiyashko May 20, 2026
03e512d
Merge branch '26_1_toolbar_kbn' of https://github.com/EugeniyKiyashko…
EugeniyKiyashko May 20, 2026
680b105
updates
EugeniyKiyashko May 21, 2026
0f24557
use focus outline only in new kbn mode
pharret31 May 21, 2026
df9d87f
update focusStateEnabled at runtime
pharret31 May 21, 2026
59abdac
fix scss
pharret31 May 21, 2026
c374f7a
refactor menu implementation
EugeniyKiyashko May 22, 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
179 changes: 179 additions & 0 deletions e2e/testcafe-devextreme/tests/navigation/toolbar/keyboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { 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`
.page(url(__dirname, '../../container.html'));

const toolbarWidgets = [
{
widget: 'dxButton',
options: { text: 'Button' },
},
{
widget: 'dxTextBox',
options: { value: 'text', showClearButton: false },
},
{
widget: 'dxAutocomplete',
options: { value: 'auto', showClearButton: false },
},
{
widget: 'dxCheckBox',
options: { value: true },
},
{
widget: 'dxDateBox',
options: {
value: new Date(2021, 9, 17),
openOnFieldClick: false,
showClearButton: false,
showDropDownButton: false,
},
},
{
widget: 'dxSelectBox',
options: {
items: ['Item 1', 'Item 2'],
value: 'Item 1',
showClearButton: false,
showDropDownButton: false,
},
},
{
widget: 'dxMenu',
options: {
items: [{ text: 'Menu Item 1' }, { text: 'Menu Item 2' }],
},
},
{
widget: 'dxTabs',
options: {
items: [{ text: 'Tab 1' }, { text: 'Tab 2' }],
},
},
{
widget: 'dxButtonGroup',
options: {
items: [{ text: 'Left' }, { text: 'Right' }],
},
},
{
widget: 'dxDropDownButton',
options: {
text: 'Drop',
items: [{ text: 'Action 1' }, { text: 'Action 2' }],
},
},
] as const;

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

await createWidget('dxToolbar', {
items: [
{ widget: 'dxButton', locateInMenu: 'never', options: { text: 'Visible' } },
{ widget: 'dxButton', locateInMenu: 'always', options: { text: 'Menu A' } },
{ widget: 'dxButton', locateInMenu: 'always', options: { text: 'Menu B' } },
],
}, '#toolbar');

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

test('Tab inside overflow menu closes popup and moves focus past the toolbar', async (t) => {
const externalAfter = Selector('#externalAfter');
const toolbar = new Toolbar('#toolbar');
const menu = toolbar.getOverflowMenu();

await t.click(menu.element);
await t.expect(menu.option('opened')).eql(true);

await t.pressKey('tab');

await t.expect(menu.option('opened')).eql(false);
await t.expect(externalAfter.focused).ok();
}).before(setupOverflowMenuFixture);

test('Outside click closes overflow menu without stealing focus to overflow button', async (t) => {
const externalAfter = Selector('#externalAfter');
const toolbar = new Toolbar('#toolbar');
const menu = toolbar.getOverflowMenu();

await t.click(menu.element);
await t.expect(menu.option('opened')).eql(true);

await t.click(externalAfter);

await t.expect(menu.option('opened')).eql(false);
await t.expect(externalAfter.focused).ok();
await t.expect(menu.isFocused).notOk();
}).before(setupOverflowMenuFixture);

toolbarWidgets.forEach(({ widget, options }) => {
test(`${widget}: Tab leaves and Shift+Tab returns focus`, 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 button should be focused');

await t.pressKey('tab');
await t
.expect(toolbar.getItem(0).find(':focus').exists)
.ok('first toolbar item should be focused after Tab');

await t.pressKey('right');
await t
.expect(toolbar.getItem(1).find(':focus').exists)
.ok(`${widget} should be focused after arrow right`);

await t.pressKey('tab');
await t
.expect(externalAfter.focused)
.ok('external after button should be focused after Tab');

await t.pressKey('shift+tab');
await t
.expect(toolbar.getItem(1).find(':focus').exists)
.ok(`${widget} should be focused after Shift+Tab`);
}).before(async () => {
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', {
items: [
{
location: 'before',
widget: 'dxButton',
options: { text: 'Prev', focusStateEnabled: true },
},
{
location: 'before',
widget,
options: { ...options, focusStateEnabled: true },
},
{
location: 'before',
widget: 'dxButton',
options: { text: 'Next', focusStateEnabled: true },
},
],
}, '#toolbar');

await createWidget('dxButton', {
text: 'External After',
}, '#externalAfter');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ test('Toolbar buttons in menu appearance', async (t) => {
await createWidget('dxToolbar', {
width: 50,
multiline: false,
focusStateEnabled: false,
items,
});
});
Expand Down Expand Up @@ -260,6 +261,7 @@ test('Toolbar buttons as custom template appearance', async (t) => {
}));

await createWidget('dxToolbar', {
focusStateEnabled: false,
width: 50,
multiline: false,
items,
Expand Down Expand Up @@ -311,6 +313,7 @@ test('Toolbar button group appearance', async (t) => {
});

await createWidget('dxToolbar', {
focusStateEnabled: false,
width: 50,
items,
});
Expand Down Expand Up @@ -363,6 +366,7 @@ test('Toolbar button group as custom template appearance', async (t) => {
});

await createWidget('dxToolbar', {
focusStateEnabled: false,
width: 50,
items,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@use "./mixins" as *;
@use "../mixins" as *;

// adduse

Expand Down
49 changes: 49 additions & 0 deletions packages/devextreme-scss/scss/widgets/base/toolbar/_mixins.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
@mixin dx-toolbar-focus-outline(
$accent-color,
$border-radius,
) {
.dx-toolbar-item.dx-state-focused {
[tabindex="0"]:focus-visible {
outline: 2px solid $accent-color;
outline-offset: 1px;
border-radius: $border-radius;
}
}

.dx-toolbar-menu-container {
[tabindex="0"]:focus-visible {
outline: 2px solid $accent-color;
outline-offset: 1px;
border-radius: $border-radius;
}
}

.dx-toolbar-item[tabindex="0"].dx-state-focused:has(.dx-menu:not(.dx-state-focused)) {
.dx-menu {
outline: 2px solid $accent-color;
outline-offset: 1px;
border-radius: $border-radius;
}
}

.dx-dropdownmenu-popup-wrapper.dx-toolbar-focus-state-enabled {
.dx-dropdownmenu-list {
.dx-list-item {
[tabindex="0"]:focus-visible {
outline: 2px solid $accent-color;
outline-offset: 1px;
border-radius: $border-radius;
}
}

.dx-list-item[tabindex="0"]:has(.dx-menu:not(.dx-state-focused)) {
.dx-menu {
outline: 2px solid $accent-color;
outline-offset: 1px;
border-radius: $border-radius;
}
}
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
@use "../checkBox/sizes" as *;
@use "mixins" as *;
@use "../../base/toolbar";
@use "../../base/toolbar/mixins" as *;

// adduse
@use "../dropDownMenu";
Expand Down Expand Up @@ -180,3 +181,5 @@
line-height: 0;
}
}

@include dx-toolbar-focus-outline($base-accent, $fluent-base-border-radius);
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@use "sizes" as *;
@use "../typography/sizes" as *;
@use "../colors" as *;

@mixin dx-toolbar-sizing($height, $padding, $label-font-size, $item-spacing) {
padding: $padding;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
@use "../button/sizes" as *;
@use "mixins" as *;
@use "../../base/toolbar";
@use "../../base/toolbar/mixins" as *;

// adduse
@use "../dropDownMenu";
Expand Down Expand Up @@ -99,3 +100,5 @@
}
}
}

@include dx-toolbar-focus-outline($base-accent, $base-border-radius);
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@use "sizes" as *;
@use "../colors" as *;

@mixin dx-toolbar-sizing($height, $padding, $label-font-size, $item-spacing) {
padding: $padding;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
@use "../checkBox/sizes" as *;
@use "mixins" as *;
@use "../../base/toolbar";
@use "../../base/toolbar/mixins" as *;

// adduse
@use "../dropDownMenu";
Expand Down Expand Up @@ -207,3 +208,4 @@
padding: 4px;
}

@include dx-toolbar-focus-outline($base-accent, $material-base-border-radius);
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@use "sizes" as *;
@use "../typography/sizes" as *;
@use "../colors" as *;

@mixin dx-toolbar-sizing($height, $padding, $label-font-size, $item-spacing) {
padding: $padding;
Expand Down
2 changes: 1 addition & 1 deletion packages/devextreme/js/__internal/core/widget/widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import type { OptionChanged } from '@ts/core/widget/types';
import type { KeyboardKeyDownEvent } from '@ts/events/core/m_keyboard_processor';

export const WIDGET_CLASS = 'dx-widget';
const DISABLED_STATE_CLASS = 'dx-state-disabled';
export const DISABLED_STATE_CLASS = 'dx-state-disabled';
export const ACTIVE_STATE_CLASS = 'dx-state-active';
export const FOCUSED_STATE_CLASS = 'dx-state-focused';
export const HOVER_STATE_CLASS = 'dx-state-hover';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export class HeaderPanel extends ColumnsView {
const options: { toolbarOptions: ToolbarProperties<DefaultToolbarItem | ToolbarItem> } = {
toolbarOptions: {
items: sortedToolbarItems,
focusStateEnabled: false,
visible: userToolbarOptions?.visible,
disabled: userToolbarOptions?.disabled,
onItemRendered(e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ export class SchedulerHeader extends Widget<HeaderOptions> {

return {
...toolbar,
// @ts-expect-error ts-error
focusStateEnabled: false,
items: parsedItems,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ class ChatTextArea extends TextArea<Properties> {

const toolbarOptions = {
items: toolbarItems,
focusStateEnabled: false,
};

this._$toolbar = $('<div>')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ class DiagramToolbar extends DiagramPanel {
this._prepareToolbarItems(afterCommands, 'after', this._executeCommand),
);
this._toolbarInstance = this._createComponent($toolbar, Toolbar, {
focusStateEnabled: false,
dataSource,
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,12 @@ class DropDownEditor<
: this._getFirstPopupElement();

if ($focusableElement) {
const $input = $focusableElement.hasClass('dx-texteditor') ? $focusableElement.find('.dx-texteditor-input').first() : $();
const $focusTarget = $input.length ? $input : $focusableElement;
// @ts-expect-error ts-error
eventsEngine.trigger($focusableElement, 'focus');
eventsEngine.trigger($focusTarget, 'focus');
// @ts-expect-error ts-error
$focusableElement.select();
$focusTarget.select();
}
e.preventDefault();
},
Expand Down
Loading
Loading