From 68fd721d57c32222cde3cfecefd132205a333c58 Mon Sep 17 00:00:00 2001 From: Ruslan Farkhutdinov Date: Tue, 17 Mar 2026 13:04:31 +0200 Subject: [PATCH 1/4] Popover: Support WCAG - Hoverable --- .../stories/tooltip/Tooltip.stories.tsx | 92 +++++++++++ .../js/__internal/ui/popover/m_popover.ts | 67 +++++++- .../DevExpress.ui.widgets/popover.tests.js | 155 +++++++++++++----- .../DevExpress.ui.widgets/tooltip.tests.js | 69 ++++++-- 4 files changed, 333 insertions(+), 50 deletions(-) create mode 100644 apps/react-storybook/stories/tooltip/Tooltip.stories.tsx diff --git a/apps/react-storybook/stories/tooltip/Tooltip.stories.tsx b/apps/react-storybook/stories/tooltip/Tooltip.stories.tsx new file mode 100644 index 000000000000..14d343df2fca --- /dev/null +++ b/apps/react-storybook/stories/tooltip/Tooltip.stories.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react-webpack5'; +import { Tooltip } from 'devextreme-react/tooltip'; + +const meta: Meta = { + title: 'Components/Tooltip', + component: Tooltip, + parameters: { + layout: 'padded', + }, +}; + +export default meta; + +type Story = StoryObj; + +const HoverableExample: Story['render'] = (args) => ( +
+

+ Hover the button to show the tooltip, then move the pointer onto + the tooltip content — it must stay open. +

+ + +
+ WCAG — Hoverable +

+ Move your pointer here from the button. +
+ The tooltip stays open as long as the pointer +
+ is over the target or this content. +

+
+
+
+); + +export const Hoverable: Story = { + args: { + position: 'bottom', + }, + argTypes: { + position: { + control: 'select', + options: ['top', 'bottom', 'left', 'right'], + }, + }, + render: HoverableExample, +}; + +const HoverableWithDelayExample: Story['render'] = (args) => ( +
+

+ Show delay: 500 ms — Hide delay: 300 ms. + Moving onto the tooltip content cancels the hide timeout. +

+ + +
+ Move here to cancel the hide timeout. +
+
+
+); + +export const HoverableWithDelay: Story = { + args: { + position: 'bottom', + }, + argTypes: { + position: { + control: 'select', + options: ['top', 'bottom', 'left', 'right'], + }, + }, + render: HoverableWithDelayExample, +}; diff --git a/packages/devextreme/js/__internal/ui/popover/m_popover.ts b/packages/devextreme/js/__internal/ui/popover/m_popover.ts index 60cdca1c40b0..5bb1f93746ea 100644 --- a/packages/devextreme/js/__internal/ui/popover/m_popover.ts +++ b/packages/devextreme/js/__internal/ui/popover/m_popover.ts @@ -47,6 +47,9 @@ const POSITION_FLIP_MAP = { center: 'center', }; +const HOVER_HIDE_EVENTS = ['mouseleave', 'mouseout']; +const HOVER_HIDE_DELAY = 50; + const ESC_KEY_NAME = 'escape'; type PopoverTarget = string | dxElementWrapper | Element | undefined; @@ -204,6 +207,8 @@ class Popover< super._render.apply(this, arguments); this._detachEvents(this.option('target')); this._attachEvents(); + this._detachHoverableOverlay(); + this._attachHoverableOverlay(); } _detachEvents(target): void { @@ -216,11 +221,63 @@ class Popover< this._attachEvent('hide'); } - _createEventHandler(name) { + // eslint-disable-next-line class-methods-use-this + _isHoverHideEventName(eventName: string): boolean { + return HOVER_HIDE_EVENTS.some((hoverEvent) => eventName.split(/\s+/).includes(hoverEvent)); + } + + _attachHoverableOverlay(): void { + const hideEventName = this._getEventName('hideEvent'); + if (!hideEventName || !this._isHoverHideEventName(hideEventName)) { + return; + } + const $overlayContent = this.$overlayContent(); + if (!$overlayContent.length) { + return; + } + + const namespace = `${this.NAME as string}Hoverable`; + const hoverInEventName = addNamespace('mouseenter', namespace); + const hoverOutEventName = addNamespace('mouseleave', namespace); + + eventsEngine.off($overlayContent, hoverInEventName); + eventsEngine.on($overlayContent, hoverInEventName, () => { + this._clearEventsTimeouts(); + }); + + eventsEngine.off($overlayContent, hoverOutEventName); + eventsEngine.on($overlayContent, hoverOutEventName, (e) => { + const { target } = this.option(); + + if (target && $(e.relatedTarget).closest(target).length) { + return; + } + this._clearEventsTimeouts(); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.hide(); + }); + } + + _detachHoverableOverlay(): void { + const $overlayContent = this.$overlayContent(); + if (!$overlayContent.length) { + return; + } + const namespace = `${this.NAME as string}Hoverable`; + eventsEngine.off($overlayContent, addNamespace('mouseenter', namespace)); + eventsEngine.off($overlayContent, addNamespace('mouseleave', namespace)); + } + + _createEventHandler(name: string) { const action = this._createAction(() => { - const delay = this._getEventDelay(`${name}Event`); + const explicitDelay = this._getEventDelay(`${name}Event`); this._clearEventsTimeouts(); + const hideEventName = name === 'hide' ? this._getEventName('hideEvent') : null; + const isHoverHide = hideEventName && this._isHoverHideEventName(hideEventName); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const delay = explicitDelay || (isHoverHide ? HOVER_HIDE_DELAY : 0); + if (delay) { this._timeouts[name] = setTimeout(() => { this[name](); @@ -566,6 +623,7 @@ class Popover< _clean(): void { this._detachEscapeKeyHandler(); this._detachEvents(this.option('target')); + this._detachHoverableOverlay(); // @ts-expect-error ts-error super._clean.apply(this, arguments); } @@ -603,6 +661,11 @@ class Popover< const { target } = this.option(); this._detachEvent(target, eventName, event); this._attachEvent(eventName); + + if (name === 'hideEvent') { + this._detachHoverableOverlay(); + this._attachHoverableOverlay(); + } break; } case 'visible': diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/popover.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/popover.tests.js index e640571dac81..4657dfef2022 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/popover.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/popover.tests.js @@ -2345,62 +2345,141 @@ QUnit.module('accessibility', { assert.strictEqual($overlay.attr('role'), 'dialog'); }); - QUnit.test('should hide visible popover on esc press', function(assert) { - const popover = new Popover($('#what'), { - target: '#where', - visible: true, - }); - const $target = $('#where').attr('tabindex', 0); - const keyboard = keyboardMock($target); + QUnit.module('WCAG - dismissible', () => { + QUnit.test('should hide visible popover on esc press', function(assert) { + const popover = new Popover($('#what'), { + target: '#where', + visible: true, + }); + const $target = $('#where').attr('tabindex', 0); + const keyboard = keyboardMock($target); - keyboard.keyDown('esc'); + keyboard.keyDown('esc'); - assert.strictEqual(popover.option('visible'), false, 'popover is hidden'); - }); + assert.strictEqual(popover.option('visible'), false, 'popover is hidden'); + }); - QUnit.test('should hide only topmost popover on esc press', function(assert) { - const $markup = $('
' + + QUnit.test('should hide only topmost popover on esc press', function(assert) { + const $markup = $('
' + '
' + '
' + '
') - .appendTo('body'); + .appendTo('body'); - const bottomPopover = new Popover($('#popover1'), { - target: '#target1', - visible: true, - }); - const topPopover = new Popover($('#popover2'), { - target: '#target2', - visible: true, + const bottomPopover = new Popover($('#popover1'), { + target: '#target1', + visible: true, + }); + const topPopover = new Popover($('#popover2'), { + target: '#target2', + visible: true, + }); + + const keyboard = keyboardMock($('#target2')); + + keyboard.keyDown('esc'); + + assert.strictEqual(topPopover.option('visible'), false, 'top popover is hidden'); + assert.strictEqual(bottomPopover.option('visible'), true, 'bottom popover is still visible'); + + $markup.remove(); }); - const keyboard = keyboardMock($('#target2')); + QUnit.test('should not call hide for hidden popover on esc press', function(assert) { + const popover = new Popover($('#what'), { + target: '#where', + visible: true, + animation: null, + }); + const hideSpy = sinon.spy(popover, 'hide'); + const $target = $('#where').attr('tabindex', 0); + const keyboard = keyboardMock($target); - keyboard.keyDown('esc'); + popover.hide(); + hideSpy.resetHistory(); - assert.strictEqual(topPopover.option('visible'), false, 'top popover is hidden'); - assert.strictEqual(bottomPopover.option('visible'), true, 'bottom popover is still visible'); + keyboard.keyDown('esc'); - $markup.remove(); + assert.strictEqual(hideSpy.callCount, 0, 'hide is not called'); + assert.strictEqual(popover.option('visible'), false, 'popover remains hidden'); + }); }); - QUnit.test('should not call hide for hidden popover on esc press', function(assert) { - const popover = new Popover($('#what'), { - target: '#where', - visible: true, - animation: null, + QUnit.module('WCAG - hoverable', { + beforeEach: function() { + this.clock = sinon.useFakeTimers(); + }, + afterEach: function() { + this.clock.restore(); + } + }, () => { + QUnit.test('should stay visible when pointer moves from target to overlay content', function(assert) { + const instance = new Popover($('#what'), { + target: '#where', + showEvent: 'mouseenter', + hideEvent: 'mouseleave', + visible: true, + }); + + const $overlayContent = wrapper().find(`.${OVERLAY_CONTENT_CLASS}`); + + $('#where').trigger('mouseleave'); + $overlayContent.trigger('mouseenter'); + this.clock.tick(200); + + assert.ok(instance.option('visible'), 'popover remains visible after pointer moves to overlay content'); + }); + + QUnit.test('should hide when pointer leaves overlay content', function(assert) { + const instance = new Popover($('#what'), { + target: '#where', + showEvent: 'mouseenter', + hideEvent: 'mouseleave', + visible: true, + }); + + const $overlayContent = wrapper().find(`.${OVERLAY_CONTENT_CLASS}`); + + $overlayContent.trigger('mouseenter'); + $overlayContent.trigger('mouseleave'); + + assert.notOk(instance.option('visible'), 'popover hides when pointer leaves overlay content'); + }); + + QUnit.test('should stay visible when pointer moves from overlay back to target', function(assert) { + const instance = new Popover($('#what'), { + target: '#where', + showEvent: 'mouseenter', + hideEvent: 'mouseleave', + visible: true, + }); + + const $overlayContent = wrapper().find(`.${OVERLAY_CONTENT_CLASS}`); + + $overlayContent.trigger('mouseenter'); + + const mouseLeaveEvent = $.Event('mouseleave'); + mouseLeaveEvent.relatedTarget = $('#where')[0]; + $overlayContent.trigger(mouseLeaveEvent); + + this.clock.tick(200); + + assert.ok(instance.option('visible'), 'popover stays visible when pointer moves from overlay back to target'); }); - const hideSpy = sinon.spy(popover, 'hide'); - const $target = $('#where').attr('tabindex', 0); - const keyboard = keyboardMock($target); - popover.hide(); - hideSpy.resetHistory(); + QUnit.test('should not apply hoverable behavior for non-hover hide events', function(assert) { + const instance = new Popover($('#what'), { + target: '#where', + showEvent: 'dxclick', + hideEvent: 'dxclick', + visible: true, + }); - keyboard.keyDown('esc'); + $('#where').trigger('dxclick'); + this.clock.tick(0); - assert.strictEqual(hideSpy.callCount, 0, 'hide is not called'); - assert.strictEqual(popover.option('visible'), false, 'popover remains hidden'); + assert.notOk(instance.option('visible'), 'popover hides immediately for non-hover hide event'); + }); }); }); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/tooltip.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/tooltip.tests.js index 9ac02a4ac2be..b117b7e50137 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/tooltip.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/tooltip.tests.js @@ -8,6 +8,7 @@ import keyboardMock from '../../helpers/keyboardMock.js'; const TOOLTIP_CLASS = 'dx-tooltip'; const TOOLTIP_WRAPPER_CLASS = 'dx-tooltip-wrapper'; const DX_INVISIBILITY_CLASS = 'dx-state-invisible'; +const OVERLAY_CONTENT_CLASS = 'dx-overlay-content'; const wrapper = function() { return $('body').find('.' + TOOLTIP_WRAPPER_CLASS); @@ -167,7 +168,7 @@ QUnit.module('accessibility', () => { QUnit.test('role="tooltip" attribute should be added to tooltip', function(assert) { const $tooltip = $('#tooltip'); new Tooltip($tooltip); - const $overlayContent = $tooltip.find('.dx-overlay-content'); + const $overlayContent = $tooltip.find(`.${OVERLAY_CONTENT_CLASS}`); assert.equal($overlayContent.attr('role'), 'tooltip'); }); @@ -176,23 +177,71 @@ QUnit.module('accessibility', () => { const $target = $('#target'); const $element = $('#tooltip'); new Tooltip($element, { target: $target, visible: false }); - const $overlay = $element.find('.dx-overlay-content'); + const $overlay = $element.find(`.${OVERLAY_CONTENT_CLASS}`); assert.notEqual($target.attr('aria-describedby'), undefined, 'aria-describedby exists on target'); assert.equal($target.attr('aria-describedby'), $overlay.attr('id'), 'aria-describedby and overlay\'s id are equal'); }); - QUnit.test('visible tooltip should be hidden on Escape key press', function(assert) { - const tooltip = new Tooltip($('#tooltip'), { - target: '#target', - visible: true, + QUnit.module('WCAG - dismissible', () => { + QUnit.test('should hide visible tooltip on Escape key press', function(assert) { + const tooltip = new Tooltip($('#tooltip'), { + target: '#target', + visible: true, + }); + const $target = $('#target').attr('tabindex', 0); + const keyboard = keyboardMock($target); + + keyboard.keyDown('esc'); + + assert.strictEqual(tooltip.option('visible'), false, 'tooltip is hidden'); + }); + }); + + QUnit.module('WCAG - hoverable', { + beforeEach: function() { + this.clock = sinon.useFakeTimers(); + fx.off = true; + }, + afterEach: function() { + this.clock.restore(); + fx.off = false; + } + }, () => { + QUnit.test('should stay visible when pointer moves from target to tooltip content', function(assert) { + const instance = new Tooltip($('#tooltip'), { + target: '#target', + showEvent: 'mouseenter', + hideEvent: 'mouseleave', + visible: true, + animation: null, + }); + + const $overlayContent = wrapper().find(`.${OVERLAY_CONTENT_CLASS}`); + + $('#target').trigger('mouseleave'); + $overlayContent.trigger('mouseenter'); + this.clock.tick(200); + + assert.ok(instance.option('visible'), 'tooltip remains visible after pointer moves to tooltip content'); }); - const $target = $('#target').attr('tabindex', 0); - const keyboard = keyboardMock($target); - keyboard.keyDown('esc'); + QUnit.test('should hide when pointer leaves tooltip content', function(assert) { + const instance = new Tooltip($('#tooltip'), { + target: '#target', + showEvent: 'mouseenter', + hideEvent: 'mouseleave', + visible: true, + animation: null, + }); + + const $overlayContent = wrapper().find(`.${OVERLAY_CONTENT_CLASS}`); - assert.strictEqual(tooltip.option('visible'), false, 'tooltip is hidden'); + $overlayContent.trigger('mouseenter'); + $overlayContent.trigger('mouseleave'); + + assert.notOk(instance.option('visible'), 'tooltip hides when pointer leaves tooltip content'); + }); }); }); From dcda1c5f51e56f12b089b6411aa0ac5149265380 Mon Sep 17 00:00:00 2001 From: Ruslan Farkhutdinov Date: Tue, 17 Mar 2026 14:59:21 +0200 Subject: [PATCH 2/4] Popover: Support all hover hide event types & fix default delay issue --- .../js/__internal/ui/popover/m_popover.ts | 44 ++++++++++++++----- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/packages/devextreme/js/__internal/ui/popover/m_popover.ts b/packages/devextreme/js/__internal/ui/popover/m_popover.ts index 5bb1f93746ea..305942283201 100644 --- a/packages/devextreme/js/__internal/ui/popover/m_popover.ts +++ b/packages/devextreme/js/__internal/ui/popover/m_popover.ts @@ -47,12 +47,24 @@ const POSITION_FLIP_MAP = { center: 'center', }; -const HOVER_HIDE_EVENTS = ['mouseleave', 'mouseout']; +const HOVER_EVENT_PAIRS: Record = { + // eslint-disable-next-line spellcheck/spell-checker + mouseleave: 'mouseenter', + // eslint-disable-next-line spellcheck/spell-checker + mouseout: 'mouseover', + // eslint-disable-next-line spellcheck/spell-checker + pointerleave: 'pointerenter', + // eslint-disable-next-line spellcheck/spell-checker + dxhoverend: 'dxhoverstart', +}; + +const HOVER_HIDE_EVENTS = Object.keys(HOVER_EVENT_PAIRS); const HOVER_HIDE_DELAY = 50; const ESC_KEY_NAME = 'escape'; type PopoverTarget = string | dxElementWrapper | Element | undefined; +type PopoverEventOption = 'showEvent' | 'hideEvent'; export interface PopoverProperties extends Omit eventName in HOVER_EVENT_PAIRS); + + const hoverInEventName = activeHideEvents + .map((eventName: string) => addNamespace(HOVER_EVENT_PAIRS[eventName], namespace)) + .join(' '); + const hoverOutEventName = activeHideEvents + .map((eventName: string) => addNamespace(eventName, namespace)) + .join(' '); eventsEngine.off($overlayContent, hoverInEventName); eventsEngine.on($overlayContent, hoverInEventName, () => { @@ -264,19 +282,21 @@ class Popover< return; } const namespace = `${this.NAME as string}Hoverable`; - eventsEngine.off($overlayContent, addNamespace('mouseenter', namespace)); - eventsEngine.off($overlayContent, addNamespace('mouseleave', namespace)); + const allEventNames = [ + ...Object.keys(HOVER_EVENT_PAIRS), + ...Object.values(HOVER_EVENT_PAIRS), + ].map((e) => addNamespace(e, namespace)).join(' '); + eventsEngine.off($overlayContent, allEventNames); } _createEventHandler(name: string) { const action = this._createAction(() => { - const explicitDelay = this._getEventDelay(`${name}Event`); + const explicitDelay = this._getEventDelay(`${name}Event` as PopoverEventOption); this._clearEventsTimeouts(); const hideEventName = name === 'hide' ? this._getEventName('hideEvent') : null; const isHoverHide = hideEventName && this._isHoverHideEventName(hideEventName); - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const delay = explicitDelay || (isHoverHide ? HOVER_HIDE_DELAY : 0); + const delay = explicitDelay ?? (isHoverHide ? HOVER_HIDE_DELAY : 0); if (delay) { this._timeouts[name] = setTimeout(() => { @@ -355,10 +375,10 @@ class Popover< return this._getEventNameByOption(optionValue); } - _getEventDelay(optionName) { - const optionValue = this.option(optionName); - // @ts-expect-error - return isObject(optionValue) && optionValue.delay; + _getEventDelay(optionName: PopoverEventOption): number | undefined { + const { [optionName]: optionValue } = this.option(); + + return isObject(optionValue) ? (optionValue.delay) : undefined; } _renderArrow(): void { From 7c642009347e7f86a9c300e0e23a5c0529462231 Mon Sep 17 00:00:00 2001 From: Ruslan Farkhutdinov Date: Tue, 17 Mar 2026 15:36:49 +0200 Subject: [PATCH 3/4] Popover: Preserve hideEvent.delay behavior on hover hide --- .../js/__internal/ui/popover/m_popover.ts | 21 ++++++++-- .../DevExpress.ui.widgets/popover.tests.js | 40 +++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/packages/devextreme/js/__internal/ui/popover/m_popover.ts b/packages/devextreme/js/__internal/ui/popover/m_popover.ts index 305942283201..06ecaf0b1a9d 100644 --- a/packages/devextreme/js/__internal/ui/popover/m_popover.ts +++ b/packages/devextreme/js/__internal/ui/popover/m_popover.ts @@ -233,6 +233,22 @@ class Popover< this._attachEvent('hide'); } + _scheduleHoverHide(): void { + this._clearEventsTimeouts(); + const hideDelay = this._getEventDelay('hideEvent'); + + if (hideDelay) { + // eslint-disable-next-line no-restricted-globals + this._timeouts.hide = setTimeout(() => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.hide(); + }, hideDelay); + } else { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.hide(); + } + } + // eslint-disable-next-line class-methods-use-this _isHoverHideEventName(eventName: string): boolean { return HOVER_HIDE_EVENTS.some((hoverEvent) => eventName.split(/\s+/).includes(hoverEvent)); @@ -270,9 +286,8 @@ class Popover< if (target && $(e.relatedTarget).closest(target).length) { return; } - this._clearEventsTimeouts(); - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.hide(); + + this._scheduleHoverHide(); }); } diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/popover.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/popover.tests.js index 4657dfef2022..fab230bd20cf 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/popover.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/popover.tests.js @@ -2481,5 +2481,45 @@ QUnit.module('accessibility', { assert.notOk(instance.option('visible'), 'popover hides immediately for non-hover hide event'); }); + + QUnit.test('should respect hideEvent.delay when pointer leaves overlay content', function(assert) { + const instance = new Popover($('#what'), { + target: '#where', + showEvent: 'mouseenter', + hideEvent: { name: 'mouseleave', delay: 300 }, + visible: true, + }); + + const $overlayContent = wrapper().find(`.${OVERLAY_CONTENT_CLASS}`); + + $overlayContent.trigger('mouseenter'); + $overlayContent.trigger('mouseleave'); + + assert.ok(instance.option('visible'), 'popover is still visible during delay period'); + + this.clock.tick(300); + + assert.notOk(instance.option('visible'), 'popover hides after the configured delay'); + }); + + QUnit.test('should cancel scheduled overlay hide when pointer re-enters overlay content before delay expires', function(assert) { + const instance = new Popover($('#what'), { + target: '#where', + showEvent: 'mouseenter', + hideEvent: { name: 'mouseleave', delay: 300 }, + visible: true, + }); + + const $overlayContent = wrapper().find(`.${OVERLAY_CONTENT_CLASS}`); + + $overlayContent.trigger('mouseenter'); + $overlayContent.trigger('mouseleave'); + + this.clock.tick(150); + $overlayContent.trigger('mouseenter'); + this.clock.tick(300); + + assert.ok(instance.option('visible'), 'popover remains visible when pointer re-enters overlay before delay expires'); + }); }); }); From 8951ed8bb0863a1c54c128336e3afd1b25ac7c89 Mon Sep 17 00:00:00 2001 From: Ruslan Farkhutdinov Date: Tue, 17 Mar 2026 16:14:33 +0200 Subject: [PATCH 4/4] Popover: Add type signature to hoverOut callback --- packages/devextreme/js/__internal/ui/popover/m_popover.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/devextreme/js/__internal/ui/popover/m_popover.ts b/packages/devextreme/js/__internal/ui/popover/m_popover.ts index 06ecaf0b1a9d..908801798f29 100644 --- a/packages/devextreme/js/__internal/ui/popover/m_popover.ts +++ b/packages/devextreme/js/__internal/ui/popover/m_popover.ts @@ -280,10 +280,11 @@ class Popover< }); eventsEngine.off($overlayContent, hoverOutEventName); - eventsEngine.on($overlayContent, hoverOutEventName, (e) => { + eventsEngine.on($overlayContent, hoverOutEventName, (e: PointerEvent | MouseEvent) => { const { target } = this.option(); + const { relatedTarget } = e; - if (target && $(e.relatedTarget).closest(target).length) { + if (target && relatedTarget instanceof Element && $(relatedTarget).closest(target).length) { return; }