Skip to content

Commit 813a1c2

Browse files
authored
Toolbar: close menu when a focused item is selected via the enter/space key (T1301705) (DevExpress#30749)
1 parent 801390c commit 813a1c2

File tree

3 files changed

+177
-8
lines changed

3 files changed

+177
-8
lines changed

packages/devextreme/js/__internal/ui/toolbar/internal/toolbar.menu.list.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type { ActionConfig } from '@ts/core/widget/component';
99
import type { ItemRenderInfo, ItemTemplate } from '@ts/ui/collection/collection_widget.base';
1010
import { ListBase } from '@ts/ui/list/list.base';
1111

12-
const TOOLBAR_MENU_ACTION_CLASS = 'dx-toolbar-menu-action';
12+
export const TOOLBAR_MENU_ACTION_CLASS = 'dx-toolbar-menu-action';
1313
const TOOLBAR_HIDDEN_BUTTON_CLASS = 'dx-toolbar-hidden-button';
1414
const TOOLBAR_HIDDEN_BUTTON_GROUP_CLASS = 'dx-toolbar-hidden-button-group';
1515
const TOOLBAR_MENU_SECTION_CLASS = 'dx-toolbar-menu-section';

packages/devextreme/js/__internal/ui/toolbar/internal/toolbar.menu.ts

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import Widget from '@ts/core/widget/widget';
1818
import Button from '@ts/ui/button/wrapper';
1919
import type { ListBase } from '@ts/ui/list/list.base';
2020
import Popup from '@ts/ui/popup/m_popup';
21-
import ToolbarMenuList from '@ts/ui/toolbar/internal/toolbar.menu.list';
21+
import ToolbarMenuList, { TOOLBAR_MENU_ACTION_CLASS } from '@ts/ui/toolbar/internal/toolbar.menu.list';
2222
import { toggleItemFocusableElementTabIndex } from '@ts/ui/toolbar/toolbar.utils';
2323

2424
const DROP_DOWN_MENU_CLASS = 'dx-dropdownmenu';
@@ -277,8 +277,26 @@ export default class DropDownMenu extends Widget<DropDownMenuProperties> {
277277
dragEnabled: false,
278278
showTitle: false,
279279
fullScreen: false,
280+
ignoreChildEvents: false,
280281
_fixWrapperPosition: true,
281282
});
283+
this._popup.registerKeyHandler('space', (
284+
e: DxEvent<KeyboardEvent>,
285+
) => {
286+
this._popupKeyHandler(e);
287+
});
288+
this._popup.registerKeyHandler('enter', (
289+
e: DxEvent<KeyboardEvent>,
290+
) => {
291+
this._popupKeyHandler(e);
292+
});
293+
this._popup.registerKeyHandler('escape', (
294+
e: DxEvent<KeyboardEvent>,
295+
): void => {
296+
if (this._popup?.$overlayContent().is($(e.target))) {
297+
this.option('opened', false);
298+
}
299+
});
282300
}
283301

284302
_getMaxHeight(): number {
@@ -317,12 +335,10 @@ export default class DropDownMenu extends Widget<DropDownMenuProperties> {
317335
indicateLoading: false,
318336
noDataText: '',
319337
itemTemplate,
320-
onItemClick: (e: ItemClickEvent<ListItem>): void => {
321-
const { closeOnClick } = this.option();
322-
if (closeOnClick) {
323-
this.option('opened', false);
324-
}
325-
this._itemClickAction?.(e);
338+
onItemClick: (
339+
e: ItemClickEvent<ListItem>,
340+
) => {
341+
this._itemClickHandler(e);
326342
},
327343
tabIndex: -1,
328344
focusStateEnabled: false,
@@ -332,6 +348,24 @@ export default class DropDownMenu extends Widget<DropDownMenuProperties> {
332348
});
333349
}
334350

351+
_popupKeyHandler(e: DxEvent<KeyboardEvent>): void {
352+
if ($(e.target).closest(`.${TOOLBAR_MENU_ACTION_CLASS}`).length) {
353+
this._closePopup();
354+
}
355+
}
356+
357+
_closePopup(): void {
358+
const { closeOnClick } = this.option();
359+
if (closeOnClick) {
360+
this.option('opened', false);
361+
}
362+
}
363+
364+
_itemClickHandler(e: ItemClickEvent<ListItem>): void {
365+
this._closePopup();
366+
this._itemClickAction?.(e);
367+
}
368+
335369
_itemOptionChanged(
336370
item: Item,
337371
property: 'disabled',

packages/devextreme/testing/tests/DevExpress.ui.widgets/toolbar.menu.tests.js

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import $ from 'jquery';
33
import ArrayStore from 'common/data/array_store';
44
import fx from 'common/core/animation/fx';
55
import Button from 'ui/button';
6+
import 'ui/check_box';
67
import Popup from '__internal/ui/popup/m_popup';
78
import DropDownMenu from '__internal/ui/toolbar/internal/toolbar.menu';
89
import ToolbarMenuList from '__internal/ui/toolbar/internal/toolbar.menu.list';
@@ -13,6 +14,7 @@ import config from 'core/config';
1314
import { DataSource } from 'common/data/data_source/data_source';
1415
import { isRenderer } from 'core/utils/type';
1516
import themes from 'ui/themes';
17+
import { shouldSkipOnMobile } from '../../helpers/device.js';
1618

1719
import 'generic_light.css!';
1820

@@ -32,6 +34,8 @@ const DROP_DOWN_MENU_POPUP_CLASS = 'dx-dropdownmenu-popup';
3234
const DROP_DOWN_MENU_POPUP_WRAPPER_CLASS = 'dx-dropdownmenu-popup-wrapper';
3335
const LIST_ITEM_CLASS = 'dx-list-item';
3436
const SCROLLVIEW_CONTENT_CLASS = 'dx-scrollview-content';
37+
const CHECK_BOX_CLASS = 'dx-checkbox';
38+
const BUTTON_CLASS = 'dx-button';
3539

3640

3741
const moduleConfig = {
@@ -595,6 +599,137 @@ QUnit.module('widget sizing render', moduleConfig, () => {
595599

596600
});
597601

602+
QUnit.module('esc on popup', {
603+
beforeEach: function() {
604+
this.instance.option({
605+
opened: false,
606+
items: [{ template: () => $('<div>').dxCheckBox({ value: false }) }],
607+
});
608+
},
609+
afterEach: function() {
610+
this.instance.option('opened', false);
611+
}
612+
}, () => {
613+
QUnit.test('esc on popup overlay should close popup', function(assert) {
614+
if(shouldSkipOnMobile(assert)) {
615+
return;
616+
}
617+
618+
this.overflowMenu.$button().focusin();
619+
this.keyboard.keyDown('enter');
620+
621+
const popup = this.overflowMenu.popup();
622+
assert.equal(popup.option('visible'), true, 'popup is visible');
623+
624+
const $popupContent = popup.$overlayContent();
625+
626+
keyboardMock($popupContent).keyDown('esc');
627+
assert.equal(popup.option('visible'), false, 'esc on popup overlay closed popup');
628+
});
629+
630+
QUnit.test('esc on nested items should NOT close popup', function(assert) {
631+
if(shouldSkipOnMobile(assert)) {
632+
return;
633+
}
634+
635+
this.overflowMenu.$button().focusin();
636+
this.keyboard.keyDown('enter');
637+
638+
const popup = this.overflowMenu.popup();
639+
assert.equal(popup.option('visible'), true, 'popup is visible');
640+
641+
const $checkBox = this.overflowMenu.$popupContent().find(`.${CHECK_BOX_CLASS}`).first();
642+
643+
keyboardMock($checkBox).keyDown('esc');
644+
assert.equal(popup.option('visible'), true, 'esc on nested item did not close popup');
645+
});
646+
});
647+
648+
QUnit.module('space/enter on list item (T1301705)', {
649+
beforeEach: function() {
650+
this.instance.option({
651+
closeOnClick: true,
652+
opened: false,
653+
items: [
654+
{
655+
template: () => $('<div>').dxCheckBox({ value: false }),
656+
widget: 'dxCheckBox',
657+
},
658+
{
659+
template: () => $('<div>').dxButton({ text: 'Button' })
660+
},
661+
]
662+
});
663+
},
664+
afterEach: function() {
665+
this.instance.option('opened', false);
666+
}
667+
}, () => {
668+
QUnit.test('space on a not dxButton in nested list item should NOT close popup', function(assert) {
669+
if(shouldSkipOnMobile(assert)) {
670+
return;
671+
}
672+
673+
this.overflowMenu.$button().focusin();
674+
this.keyboard.keyDown('enter');
675+
676+
const popup = this.overflowMenu.popup();
677+
assert.equal(popup.option('visible'), true, 'popup is visible');
678+
679+
const $checkBox = this.overflowMenu.$popupContent().find(`.${CHECK_BOX_CLASS}`).first();
680+
681+
keyboardMock($checkBox).keyDown('space');
682+
assert.equal(popup.option('visible'), true, 'space on a dxCheckBox did not close popup');
683+
});
684+
685+
QUnit.test('enter on a not dxButton in nested list item should NOT close popup', function(assert) {
686+
if(shouldSkipOnMobile(assert)) {
687+
return;
688+
}
689+
690+
this.overflowMenu.$button().focusin();
691+
this.keyboard.keyDown('enter');
692+
693+
const popup = this.overflowMenu.popup();
694+
assert.equal(popup.option('visible'), true, 'popup is visible');
695+
696+
const $checkBox = this.overflowMenu.$popupContent().find(`.${CHECK_BOX_CLASS}`).first();
697+
698+
keyboardMock($checkBox).keyDown('enter');
699+
assert.equal(popup.option('visible'), true, 'enter on a dxCheckBox did not close popup');
700+
});
701+
702+
QUnit.test('space on a dxButton in nested list item should close popup', function(assert) {
703+
if(shouldSkipOnMobile(assert)) {
704+
return;
705+
}
706+
this.overflowMenu.$button().focusin();
707+
this.keyboard.keyDown('enter');
708+
709+
const popup = this.overflowMenu.popup();
710+
assert.equal(popup.option('visible'), true, 'popup is visible');
711+
712+
const $button = this.overflowMenu.$popupContent().find(`.${BUTTON_CLASS}`).first();
713+
keyboardMock($button).keyDown('space');
714+
assert.equal(popup.option('visible'), false, 'space on a dxButton closed popup');
715+
});
716+
717+
QUnit.test('enter on a dxButton in nested list item should close popup', function(assert) {
718+
if(shouldSkipOnMobile(assert)) {
719+
return;
720+
}
721+
this.overflowMenu.$button().focusin();
722+
this.keyboard.keyDown('enter');
723+
724+
const popup = this.overflowMenu.popup();
725+
assert.equal(popup.option('visible'), true, 'popup is visible');
726+
727+
const $button = this.overflowMenu.$popupContent().find(`.${BUTTON_CLASS}`).first();
728+
keyboardMock($button).keyDown('enter');
729+
assert.equal(popup.option('visible'), false, 'enter on a dxButton closed popup');
730+
});
731+
});
732+
598733
QUnit.test('Enter or space press should call onItemClick (T318240)', function(assert) {
599734
let itemClicked = 0;
600735

0 commit comments

Comments
 (0)