From e841eac7b8ca016e7ef2fa6987dc13db98832069 Mon Sep 17 00:00:00 2001 From: "anna.shakhova" Date: Fri, 19 Dec 2025 10:31:15 +0100 Subject: [PATCH] Menu: fix focus logic on submenu hiding (T1304251) (24_2) --- .../js/__internal/ui/menu/m_menu.ts | 28 +-- .../tests/DevExpress.ui.widgets/menu.tests.js | 177 ++++++++++++------ 2 files changed, 138 insertions(+), 67 deletions(-) diff --git a/packages/devextreme/js/__internal/ui/menu/m_menu.ts b/packages/devextreme/js/__internal/ui/menu/m_menu.ts index 2b2a1f7e1742..ffbf10f95ff4 100644 --- a/packages/devextreme/js/__internal/ui/menu/m_menu.ts +++ b/packages/devextreme/js/__internal/ui/menu/m_menu.ts @@ -692,23 +692,29 @@ class Menu extends MenuBase { this._actions.onSubmenuHiding(eventArgs); + if (eventArgs.cancel) { + return; + } + const { focusedElement } = this.option(); - const { focusedElement: submenuFocusedElement } = submenu.option(); + const submenuContainerElement = $(eventArgs.submenuContainer).get(0); + const focusedDomElement = $(focusedElement).get(0); + const isFocusedElementInsideSubmenu = focusedDomElement && submenuContainerElement + ? submenuContainerElement.contains(focusedDomElement) + : false; + + if (isFocusedElementInsideSubmenu) { + this.option('focusedElement', getPublicElement($menuAnchorItem)); + } const isVisibleSubmenuHiding = this._visibleSubmenu === submenu; - const isFocusedElementHiding = focusedElement === submenuFocusedElement; - if (isVisibleSubmenuHiding && isFocusedElementHiding) { - this.option('focusedElement', $menuAnchorItem); + if (isVisibleSubmenuHiding) { + this._visibleSubmenu = null; } - if (!eventArgs.cancel) { - if (isVisibleSubmenuHiding) { - this._visibleSubmenu = null; - } - $border.hide(); - $menuAnchorItem.removeClass(DX_MENU_ITEM_EXPANDED_CLASS); - } + $border.hide(); + $menuAnchorItem.removeClass(DX_MENU_ITEM_EXPANDED_CLASS); } _submenuOnHiddenHandler($menuAnchorItem, submenu, { rootItem }) { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/menu.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/menu.tests.js index 9a305dcada17..b24c6377b026 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/menu.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/menu.tests.js @@ -15,6 +15,7 @@ import ArrayStore from 'common/data/array_store'; import eventsEngine from 'common/core/events/core/events_engine'; import { DataSource } from 'common/data/data_source/data_source'; import * as checkStyleHelper from '../../helpers/checkStyleHelper.js'; +import { shouldSkipOnMobile } from '../../helpers/device.js'; import 'generic_light.css!'; import { implementationsMap, getHeight, getWidth, getOuterHeight } from 'core/utils/size'; @@ -2502,6 +2503,126 @@ QUnit.module('Menu tests', { submenu = getSubMenuInstance($rootMenuItem); assert.ok(submenu.option('visible'), 'submenu shown'); }); + + QUnit.test('focusedElement should be set to main menu item after hiding submenu (T1291581)', function(assert) { + const menu = $('#menu').dxMenu({ + orientation: 'horizontal', + focusStateEnabled: true, + items: [ + { + text: 'Item 1', + items: [ + { text: 'Item 11', items: [ { text: 'Item 111' }, { text: 'Item 112' }, { text: 'Item 113' } ] }, + { text: 'Item 12' } + ], + }, + ] + }).dxMenu('instance'); + const keyboard = keyboardMock(menu.itemsContainer()); + + keyboard.press('enter') + .press('down') + .press('down'); + + assert.strictEqual($(menu.option('focusedElement')).text(), 'Item 12', 'focusedElement is submenu item'); + + keyboard.press('enter'); + + const mainMenuItemText = $(menu.itemElements()[0]).text(); + + assert.strictEqual($(menu.option('focusedElement')).text(), mainMenuItemText, 'focusedElement is main menu item'); + }); + + QUnit.test('focusedElement should be set to main menu item after hiding nested submenu (T1291581)', function(assert) { + const menu = $('#menu').dxMenu({ + orientation: 'horizontal', + focusStateEnabled: true, + items: [ + { + text: 'Item 1', + items: [ + { text: 'Item 11', items: [ { text: 'Item 111' }, { text: 'Item 112' }, { text: 'Item 113' } ] }, + { text: 'Item 12' } + ], + }, + ] + }).dxMenu('instance'); + const keyboard = keyboardMock(menu.itemsContainer()); + + keyboard.press('enter') + .press('down') + .press('enter') + .press('right') + .press('down'); + + assert.strictEqual($(menu.option('focusedElement')).text(), 'Item 112', 'focusedElement is submenu item'); + + keyboard.press('enter'); + + const mainMenuItemText = $(menu.itemElements()[0]).text(); + + assert.strictEqual($(menu.option('focusedElement')).text(), mainMenuItemText, 'focusedElement is main menu item'); + }); + + QUnit.test('focusedElement should not be moved if submenu hiding was cancelled (T1291581)', function(assert) { + const menu = $('#menu').dxMenu({ + orientation: 'horizontal', + focusStateEnabled: true, + items: [ + { + text: 'Item 1', + items: [ + { text: 'Item 11', items: [ { text: 'Item 111' }, { text: 'Item 112' }, { text: 'Item 113' } ] }, + { text: 'Item 12' } + ], + }, + ], + onSubmenuHiding: function(e) { + e.cancel = true; + } + }).dxMenu('instance'); + const keyboard = keyboardMock(menu.itemsContainer()); + + keyboard.press('enter') + .press('down') + .press('down'); + + const { focusedElement: initialFocusedElement } = menu.option(); + + keyboard.press('enter'); + + assert.strictEqual($(menu.option('focusedElement')).text(), 'Item 12', 'focusedElement is submenu item'); + assert.strictEqual(menu.option('focusedElement'), initialFocusedElement, 'focusedElement not changed'); + }); + + QUnit.test('focusedElement should not be set to main menu item after hiding nested submenu if no item was focused (T1304251)', function(assert) { + if(shouldSkipOnMobile(assert)) { + return; + } + + const menu = createMenu({ + items: [{ text: 'Item 1', items: [{ text: 'Item 11' }, { text: 'Item 12' }, { text: 'Item 13' }] }], + showFirstSubmenuMode: { name: 'onHover', delay: 0 }, + hideSubmenuOnMouseLeave: true + }); + const $rootMenuItem = $(menu.element).find(`.${DX_MENU_ITEM_CLASS}`); + + $(menu.element).trigger($.Event('dxhoverstart', { target: $rootMenuItem.get(0) })); + $($rootMenuItem).trigger('dxpointermove'); + this.clock.tick(0); + + const submenu = getSubMenuInstance($rootMenuItem); + const $item = $(submenu._overlay.content()).find(`.${DX_MENU_ITEM_CLASS}`); + + $(menu.element).trigger($.Event('dxhoverstart', { target: $item.get(1) })); + $(menu.element).trigger($.Event('dxhoverend', { target: $item.get(1) })); + $(menu.element).trigger($.Event('dxhoverstart', { target: window })); + $($(submenu._overlay.content()).find('.dx-submenu')).trigger('dxhoverend'); + this.clock.tick(0); + + assert.strictEqual($rootMenuItem.eq(0).hasClass(DX_STATE_FOCUSED_CLASS), false, 'root menu item has not focused class'); + assert.strictEqual(menu.instance.option('focusedElement'), null, 'menu focusedElement is null'); + }); }); QUnit.module('keyboard navigation', { @@ -2952,62 +3073,6 @@ QUnit.module('keyboard navigation', { assert.equal($(this.instance._visibleSubmenu.option('focusedElement')).text(), 'Item 113'); }); - - QUnit.test('focusedElement should be set to main menu item after hiding submenu', function(assert) { - this.instance.option({ - orientation: 'horizontal', - items: [ - { - text: 'Item 1', - items: [ - { text: 'Item 11', items: [ { text: 'Item 111' }, { text: 'Item 112' }, { text: 'Item 113' } ] }, - { text: 'Item 12' } - ], - }, - ] - }); - - this.keyboard.press('enter') - .press('down') - .press('down'); - - assert.strictEqual($(this.instance.option('focusedElement')).text(), 'Item 12', 'focusedElement is submenu item'); - - this.keyboard.press('enter'); - - const mainMenuItemText = $(this.instance.itemElements()[0]).text(); - - assert.strictEqual($(this.instance.option('focusedElement')).text(), mainMenuItemText, 'focusedElement is main menu item'); - }); - - QUnit.test('focusedElement should be set to main menu item after hiding nested submenu', function(assert) { - this.instance.option({ - orientation: 'horizontal', - items: [ - { - text: 'Item 1', - items: [ - { text: 'Item 11', items: [ { text: 'Item 111' }, { text: 'Item 112' }, { text: 'Item 113' } ] }, - { text: 'Item 12' } - ], - }, - ] - }); - - this.keyboard.press('enter') - .press('down') - .press('enter') - .press('right') - .press('down'); - - assert.strictEqual($(this.instance.option('focusedElement')).text(), 'Item 112', 'focusedElement is submenu item'); - - this.keyboard.press('enter'); - - const mainMenuItemText = $(this.instance.itemElements()[0]).text(); - - assert.strictEqual($(this.instance.option('focusedElement')).text(), mainMenuItemText, 'focusedElement is main menu item'); - }); }); QUnit.module('Menu with templates', {