Skip to content

Commit db32b23

Browse files
authored
fix: make EventHandler better handle mouseenter/mouseleave events (#33310)
* fix: make EventHandler better handle mouseenter/mouseleave events * refactor: simplify custom events regex and move it to a variable
1 parent 49df4c8 commit db32b23

File tree

2 files changed

+105
-11
lines changed

2 files changed

+105
-11
lines changed

js/src/dom/event-handler.js

+28-10
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const customEvents = {
2222
mouseenter: 'mouseover',
2323
mouseleave: 'mouseout'
2424
}
25+
const customEventsRegex = /^(mouseenter|mouseleave)/i
2526
const nativeEvents = new Set([
2627
'click',
2728
'dblclick',
@@ -113,7 +114,7 @@ function bootstrapDelegationHandler(element, selector, fn) {
113114

114115
if (handler.oneOff) {
115116
// eslint-disable-next-line unicorn/consistent-destructuring
116-
EventHandler.off(element, event.type, fn)
117+
EventHandler.off(element, event.type, selector, fn)
117118
}
118119

119120
return fn.apply(target, [event])
@@ -144,14 +145,7 @@ function normalizeParams(originalTypeEvent, handler, delegationFn) {
144145
const delegation = typeof handler === 'string'
145146
const originalHandler = delegation ? delegationFn : handler
146147

147-
// allow to get the native events from namespaced events ('click.bs.button' --> 'click')
148-
let typeEvent = originalTypeEvent.replace(stripNameRegex, '')
149-
const custom = customEvents[typeEvent]
150-
151-
if (custom) {
152-
typeEvent = custom
153-
}
154-
148+
let typeEvent = getTypeEvent(originalTypeEvent)
155149
const isNative = nativeEvents.has(typeEvent)
156150

157151
if (!isNative) {
@@ -171,6 +165,24 @@ function addHandler(element, originalTypeEvent, handler, delegationFn, oneOff) {
171165
delegationFn = null
172166
}
173167

168+
// in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position
169+
// this prevents the handler from being dispatched the same way as mouseover or mouseout does
170+
if (customEventsRegex.test(originalTypeEvent)) {
171+
const wrapFn = fn => {
172+
return function (event) {
173+
if (!event.relatedTarget || (event.relatedTarget !== event.delegateTarget && event.relatedTarget.contains(event.delegateTarget))) {
174+
return fn.call(this, event)
175+
}
176+
}
177+
}
178+
179+
if (delegationFn) {
180+
delegationFn = wrapFn(delegationFn)
181+
} else {
182+
handler = wrapFn(handler)
183+
}
184+
}
185+
174186
const [delegation, originalHandler, typeEvent] = normalizeParams(originalTypeEvent, handler, delegationFn)
175187
const events = getEvent(element)
176188
const handlers = events[typeEvent] || (events[typeEvent] = {})
@@ -219,6 +231,12 @@ function removeNamespacedHandlers(element, events, typeEvent, namespace) {
219231
})
220232
}
221233

234+
function getTypeEvent(event) {
235+
// allow to get the native events from namespaced events ('click.bs.button' --> 'click')
236+
event = event.replace(stripNameRegex, '')
237+
return customEvents[event] || event
238+
}
239+
222240
const EventHandler = {
223241
on(element, event, handler, delegationFn) {
224242
addHandler(element, event, handler, delegationFn, false)
@@ -272,7 +290,7 @@ const EventHandler = {
272290
}
273291

274292
const $ = getjQuery()
275-
const typeEvent = event.replace(stripNameRegex, '')
293+
const typeEvent = getTypeEvent(event)
276294
const inNamespace = event !== typeEvent
277295
const isNative = nativeEvents.has(typeEvent)
278296

js/tests/unit/dom/event-handler.spec.js

+77-1
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,64 @@ describe('EventHandler', () => {
7777

7878
div.click()
7979
})
80+
81+
it('should handle mouseenter/mouseleave like the native counterpart', done => {
82+
fixtureEl.innerHTML = [
83+
'<div class="outer">',
84+
'<div class="inner">',
85+
'<div class="nested">',
86+
'<div class="deep"></div>',
87+
'</div>',
88+
'</div>',
89+
'</div>'
90+
]
91+
92+
const outer = fixtureEl.querySelector('.outer')
93+
const inner = fixtureEl.querySelector('.inner')
94+
const nested = fixtureEl.querySelector('.nested')
95+
const deep = fixtureEl.querySelector('.deep')
96+
97+
const enterSpy = jasmine.createSpy('mouseenter')
98+
const leaveSpy = jasmine.createSpy('mouseleave')
99+
const delegateEnterSpy = jasmine.createSpy('mouseenter')
100+
const delegateLeaveSpy = jasmine.createSpy('mouseleave')
101+
102+
EventHandler.on(inner, 'mouseenter', enterSpy)
103+
EventHandler.on(inner, 'mouseleave', leaveSpy)
104+
EventHandler.on(outer, 'mouseenter', '.inner', delegateEnterSpy)
105+
EventHandler.on(outer, 'mouseleave', '.inner', delegateLeaveSpy)
106+
107+
const moveMouse = (from, to) => {
108+
from.dispatchEvent(new MouseEvent('mouseout', {
109+
bubbles: true,
110+
relatedTarget: to
111+
}))
112+
113+
to.dispatchEvent(new MouseEvent('mouseover', {
114+
bubbles: true,
115+
relatedTarget: from
116+
}))
117+
}
118+
119+
moveMouse(outer, inner)
120+
moveMouse(inner, nested)
121+
moveMouse(nested, deep)
122+
moveMouse(deep, nested)
123+
moveMouse(nested, inner)
124+
moveMouse(inner, outer)
125+
126+
setTimeout(() => {
127+
expect(enterSpy.calls.count()).toBe(1)
128+
expect(leaveSpy.calls.count()).toBe(1)
129+
expect(delegateEnterSpy.calls.count()).toBe(1)
130+
expect(delegateLeaveSpy.calls.count()).toBe(1)
131+
done()
132+
}, 20)
133+
})
80134
})
81135

82136
describe('one', () => {
83-
it('should call listener just one', done => {
137+
it('should call listener just once', done => {
84138
fixtureEl.innerHTML = '<div></div>'
85139

86140
let called = 0
@@ -101,6 +155,28 @@ describe('EventHandler', () => {
101155
done()
102156
}, 20)
103157
})
158+
159+
it('should call delegated listener just once', done => {
160+
fixtureEl.innerHTML = '<div></div>'
161+
162+
let called = 0
163+
const div = fixtureEl.querySelector('div')
164+
const obj = {
165+
oneListener() {
166+
called++
167+
}
168+
}
169+
170+
EventHandler.one(fixtureEl, 'bootstrap', 'div', obj.oneListener)
171+
172+
EventHandler.trigger(div, 'bootstrap')
173+
EventHandler.trigger(div, 'bootstrap')
174+
175+
setTimeout(() => {
176+
expect(called).toEqual(1)
177+
done()
178+
}, 20)
179+
})
104180
})
105181

106182
describe('off', () => {

0 commit comments

Comments
 (0)