diff --git a/packages/devextreme/js/__internal/ui/overlay/overlay.ts b/packages/devextreme/js/__internal/ui/overlay/overlay.ts index 3c421693ef7d..c1a9e9123a2f 100644 --- a/packages/devextreme/js/__internal/ui/overlay/overlay.ts +++ b/packages/devextreme/js/__internal/ui/overlay/overlay.ts @@ -565,14 +565,13 @@ class Overlay< super._initTemplates(); } - _isTopOverlay(): boolean { + _isTopFocusableOverlay(): boolean { const overlayStack = this._overlayStack(); for (let i = overlayStack.length - 1; i >= 0; i -= 1) { const tabbableElements = overlayStack[i]._findTabbableBounds(); if (tabbableElements.$first || tabbableElements.$last) { - // @ts-ignore expected: types Overlay and this have no overlap return overlayStack[i] === this; } } @@ -580,6 +579,25 @@ class Overlay< return false; } + _isLastInOverlayStack(): boolean { + const overlayStack = this._overlayStack(); + + return overlayStack[overlayStack.length - 1] === this; + } + + _handlesDocumentEscapeKey(): boolean { + return false; + } + + _isEscapeHandledByOverlayAbove(): boolean { + const overlayStack = this._overlayStack(); + const index = overlayStack.indexOf(this); + + return overlayStack + .slice(index + 1) + .some((overlay) => overlay._handlesDocumentEscapeKey()); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any _overlayStack(): Overlay[] { return OVERLAY_STACK; @@ -1045,7 +1063,7 @@ class Overlay< } _tabKeyHandler(e: KeyboardEvent): void { - if (normalizeKeyName(e) !== TAB_KEY || !this._isTopOverlay()) { + if (normalizeKeyName(e) !== TAB_KEY || !this._isTopFocusableOverlay()) { return; } diff --git a/packages/devextreme/js/__internal/ui/popover/popover.ts b/packages/devextreme/js/__internal/ui/popover/popover.ts index fd035ad9ba86..ccd6279eba83 100644 --- a/packages/devextreme/js/__internal/ui/popover/popover.ts +++ b/packages/devextreme/js/__internal/ui/popover/popover.ts @@ -215,18 +215,19 @@ class Popover< _initEscapeKeyHandler(): void { this._documentEscapeKeyHandler = (e: KeyboardEvent): void => { - const { visible } = this.option(); - - const overlayStack = this._overlayStack(); - const isTopOverlay = overlayStack[overlayStack.length - 1] === this; - - if (normalizeKeyName(e) === ESC_KEY_NAME && visible && isTopOverlay) { + if (normalizeKeyName(e) === ESC_KEY_NAME && this._handlesDocumentEscapeKey()) { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.hide(); } }; } + _handlesDocumentEscapeKey(): boolean { + const { visible } = this.option(); + + return Boolean(visible) && this._isLastInOverlayStack(); + } + _attachEscapeKeyHandler(): void { const eventName = addNamespace('keydown', this.NAME as string); diff --git a/packages/devextreme/js/__internal/ui/popup/popup.ts b/packages/devextreme/js/__internal/ui/popup/popup.ts index 0c67ef664cd9..ea0276e5b231 100644 --- a/packages/devextreme/js/__internal/ui/popup/popup.ts +++ b/packages/devextreme/js/__internal/ui/popup/popup.ts @@ -246,10 +246,14 @@ class Popup< const e = options.originalEvent; const $target = $(e.target); - if (this._$content && !$target.is(this._$content) - && options.keyName === ESC_KEY_NAME - && !e.isDefaultPrevented() - && !_ignoreCloseOnChildEscape) { + const shouldCloseOnChildEscape = !!this._$content + && !$target.is(this._$content) + && options.keyName === ESC_KEY_NAME + && !e.isDefaultPrevented() + && !_ignoreCloseOnChildEscape + && !this._isEscapeHandledByOverlayAbove(); + + if (shouldCloseOnChildEscape) { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.hide(); } diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/popup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/popup.tests.js index 468828b4ea61..c4b9ce3c76a2 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/popup.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/popup.tests.js @@ -33,6 +33,8 @@ import { BUTTON_CLASS } from '__internal/ui/button/button'; import 'fluent_blue_light.css!'; import 'ui/popup'; +import 'ui/tooltip'; +import 'ui/load_panel'; import 'ui/tab_panel'; import 'ui/scroll_view'; import 'ui/date_box'; @@ -2725,6 +2727,41 @@ QUnit.module('keyboard navigation', { assert.strictEqual(this.popup.option('visible'), true, 'popup remains visible when _ignoreCloseOnChildEscape is true'); }); + + QUnit.test('should remain visible when child element presses escape and a tooltip is shown above it (T1334708)', function(assert) { + this.init({ dragEnabled: false }); + + const $input = $('').appendTo(this.popup.$content()); + const tooltip = $('
') + .appendTo('#qunit-fixture') + .dxTooltip({ target: $input, visible: true, animation: null }) + .dxTooltip('instance'); + + assert.strictEqual(tooltip.option('visible'), true, 'tooltip is shown'); + + keyboardMock($input).keyDown('esc'); + + assert.strictEqual(tooltip.option('visible'), false, 'tooltip is hidden'); + assert.strictEqual(this.popup.option('visible'), true, 'popup remains visible'); + + tooltip.dispose(); + }); + + QUnit.test('should be closed on child element escape key press when an overlay that ignores escape is shown above it', function(assert) { + this.init({ dragEnabled: false }); + + const $input = $('').appendTo(this.popup.$content()); + const loadPanel = $('
') + .appendTo('#qunit-fixture') + .dxLoadPanel({ visible: true, animation: null }) + .dxLoadPanel('instance'); + + keyboardMock($input).keyDown('esc'); + + assert.strictEqual(this.popup.option('visible'), false, 'popup is closed'); + + loadPanel.dispose(); + }); }); QUnit.module('rendering', {