@@ -47,9 +47,24 @@ const POSITION_FLIP_MAP = {
4747 center : 'center' ,
4848} ;
4949
50+ const HOVER_EVENT_PAIRS : Record < string , string > = {
51+ // eslint-disable-next-line spellcheck/spell-checker
52+ mouseleave : 'mouseenter' ,
53+ // eslint-disable-next-line spellcheck/spell-checker
54+ mouseout : 'mouseover' ,
55+ // eslint-disable-next-line spellcheck/spell-checker
56+ pointerleave : 'pointerenter' ,
57+ // eslint-disable-next-line spellcheck/spell-checker
58+ dxhoverend : 'dxhoverstart' ,
59+ } ;
60+
61+ const HOVER_HIDE_EVENTS = Object . keys ( HOVER_EVENT_PAIRS ) ;
62+ const HOVER_HIDE_DELAY = 50 ;
63+
5064const ESC_KEY_NAME = 'escape' ;
5165
5266type PopoverTarget = string | dxElementWrapper | Element | undefined ;
67+ type PopoverEventOption = 'showEvent' | 'hideEvent' ;
5368
5469export interface PopoverProperties extends Omit < Properties ,
5570'onTitleRendered' | 'onHidden' | 'onHiding' | 'onShowing' | 'onShown'
@@ -204,6 +219,8 @@ class Popover<
204219 super . _render . apply ( this , arguments ) ;
205220 this . _detachEvents ( this . option ( 'target' ) ) ;
206221 this . _attachEvents ( ) ;
222+ this . _detachHoverableOverlay ( ) ;
223+ this . _attachHoverableOverlay ( ) ;
207224 }
208225
209226 _detachEvents ( target ) : void {
@@ -216,11 +233,87 @@ class Popover<
216233 this . _attachEvent ( 'hide' ) ;
217234 }
218235
219- _createEventHandler ( name ) {
236+ _scheduleHoverHide ( ) : void {
237+ this . _clearEventsTimeouts ( ) ;
238+ const hideDelay = this . _getEventDelay ( 'hideEvent' ) ;
239+
240+ if ( hideDelay ) {
241+ // eslint-disable-next-line no-restricted-globals
242+ this . _timeouts . hide = setTimeout ( ( ) => {
243+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
244+ this . hide ( ) ;
245+ } , hideDelay ) ;
246+ } else {
247+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
248+ this . hide ( ) ;
249+ }
250+ }
251+
252+ // eslint-disable-next-line class-methods-use-this
253+ _isHoverHideEventName ( eventName : string ) : boolean {
254+ return HOVER_HIDE_EVENTS . some ( ( hoverEvent ) => eventName . split ( / \s + / ) . includes ( hoverEvent ) ) ;
255+ }
256+
257+ _attachHoverableOverlay ( ) : void {
258+ const hideEventName = this . _getEventName ( 'hideEvent' ) ;
259+ if ( ! hideEventName || ! this . _isHoverHideEventName ( hideEventName ) ) {
260+ return ;
261+ }
262+ const $overlayContent = this . $overlayContent ( ) ;
263+ if ( ! $overlayContent . length ) {
264+ return ;
265+ }
266+
267+ const namespace = `${ this . NAME as string } Hoverable` ;
268+ const activeHideEvents = hideEventName . split ( / \s + / ) . filter ( ( eventName : string ) => eventName in HOVER_EVENT_PAIRS ) ;
269+
270+ const hoverInEventName = activeHideEvents
271+ . map ( ( eventName : string ) => addNamespace ( HOVER_EVENT_PAIRS [ eventName ] , namespace ) )
272+ . join ( ' ' ) ;
273+ const hoverOutEventName = activeHideEvents
274+ . map ( ( eventName : string ) => addNamespace ( eventName , namespace ) )
275+ . join ( ' ' ) ;
276+
277+ eventsEngine . off ( $overlayContent , hoverInEventName ) ;
278+ eventsEngine . on ( $overlayContent , hoverInEventName , ( ) => {
279+ this . _clearEventsTimeouts ( ) ;
280+ } ) ;
281+
282+ eventsEngine . off ( $overlayContent , hoverOutEventName ) ;
283+ eventsEngine . on ( $overlayContent , hoverOutEventName , ( e : PointerEvent | MouseEvent ) => {
284+ const { target } = this . option ( ) ;
285+ const { relatedTarget } = e ;
286+
287+ if ( target && relatedTarget instanceof Element && $ ( relatedTarget ) . closest ( target ) . length ) {
288+ return ;
289+ }
290+
291+ this . _scheduleHoverHide ( ) ;
292+ } ) ;
293+ }
294+
295+ _detachHoverableOverlay ( ) : void {
296+ const $overlayContent = this . $overlayContent ( ) ;
297+ if ( ! $overlayContent . length ) {
298+ return ;
299+ }
300+ const namespace = `${ this . NAME as string } Hoverable` ;
301+ const allEventNames = [
302+ ...Object . keys ( HOVER_EVENT_PAIRS ) ,
303+ ...Object . values ( HOVER_EVENT_PAIRS ) ,
304+ ] . map ( ( e ) => addNamespace ( e , namespace ) ) . join ( ' ' ) ;
305+ eventsEngine . off ( $overlayContent , allEventNames ) ;
306+ }
307+
308+ _createEventHandler ( name : string ) {
220309 const action = this . _createAction ( ( ) => {
221- const delay = this . _getEventDelay ( `${ name } Event` ) ;
310+ const explicitDelay = this . _getEventDelay ( `${ name } Event` as PopoverEventOption ) ;
222311 this . _clearEventsTimeouts ( ) ;
223312
313+ const hideEventName = name === 'hide' ? this . _getEventName ( 'hideEvent' ) : null ;
314+ const isHoverHide = hideEventName && this . _isHoverHideEventName ( hideEventName ) ;
315+ const delay = explicitDelay ?? ( isHoverHide ? HOVER_HIDE_DELAY : 0 ) ;
316+
224317 if ( delay ) {
225318 this . _timeouts [ name ] = setTimeout ( ( ) => {
226319 this [ name ] ( ) ;
@@ -298,10 +391,10 @@ class Popover<
298391 return this . _getEventNameByOption ( optionValue ) ;
299392 }
300393
301- _getEventDelay ( optionName ) {
302- const optionValue = this . option ( optionName ) ;
303- // @ts -expect-error
304- return isObject ( optionValue ) && optionValue . delay ;
394+ _getEventDelay ( optionName : PopoverEventOption ) : number | undefined {
395+ const { [ optionName ] : optionValue } = this . option ( ) ;
396+
397+ return isObject ( optionValue ) ? ( optionValue . delay ) : undefined ;
305398 }
306399
307400 _renderArrow ( ) : void {
@@ -566,6 +659,7 @@ class Popover<
566659 _clean ( ) : void {
567660 this . _detachEscapeKeyHandler ( ) ;
568661 this . _detachEvents ( this . option ( 'target' ) ) ;
662+ this . _detachHoverableOverlay ( ) ;
569663 // @ts -expect-error ts-error
570664 super . _clean . apply ( this , arguments ) ;
571665 }
@@ -603,6 +697,11 @@ class Popover<
603697 const { target } = this . option ( ) ;
604698 this . _detachEvent ( target , eventName , event ) ;
605699 this . _attachEvent ( eventName ) ;
700+
701+ if ( name === 'hideEvent' ) {
702+ this . _detachHoverableOverlay ( ) ;
703+ this . _attachHoverableOverlay ( ) ;
704+ }
606705 break ;
607706 }
608707 case 'visible' :
0 commit comments