0
0
mirror of https://github.com/twbs/bootstrap.git synced 2025-01-18 10:52:19 +01:00

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
This commit is contained in:
alpadev 2021-04-13 05:25:58 +02:00 committed by GitHub
parent 49df4c89c5
commit db32b2380c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 105 additions and 11 deletions

View File

@ -22,6 +22,7 @@ const customEvents = {
mouseenter: 'mouseover', mouseenter: 'mouseover',
mouseleave: 'mouseout' mouseleave: 'mouseout'
} }
const customEventsRegex = /^(mouseenter|mouseleave)/i
const nativeEvents = new Set([ const nativeEvents = new Set([
'click', 'click',
'dblclick', 'dblclick',
@ -113,7 +114,7 @@ function bootstrapDelegationHandler(element, selector, fn) {
if (handler.oneOff) { if (handler.oneOff) {
// eslint-disable-next-line unicorn/consistent-destructuring // eslint-disable-next-line unicorn/consistent-destructuring
EventHandler.off(element, event.type, fn) EventHandler.off(element, event.type, selector, fn)
} }
return fn.apply(target, [event]) return fn.apply(target, [event])
@ -144,14 +145,7 @@ function normalizeParams(originalTypeEvent, handler, delegationFn) {
const delegation = typeof handler === 'string' const delegation = typeof handler === 'string'
const originalHandler = delegation ? delegationFn : handler const originalHandler = delegation ? delegationFn : handler
// allow to get the native events from namespaced events ('click.bs.button' --> 'click') let typeEvent = getTypeEvent(originalTypeEvent)
let typeEvent = originalTypeEvent.replace(stripNameRegex, '')
const custom = customEvents[typeEvent]
if (custom) {
typeEvent = custom
}
const isNative = nativeEvents.has(typeEvent) const isNative = nativeEvents.has(typeEvent)
if (!isNative) { if (!isNative) {
@ -171,6 +165,24 @@ function addHandler(element, originalTypeEvent, handler, delegationFn, oneOff) {
delegationFn = null delegationFn = null
} }
// in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position
// this prevents the handler from being dispatched the same way as mouseover or mouseout does
if (customEventsRegex.test(originalTypeEvent)) {
const wrapFn = fn => {
return function (event) {
if (!event.relatedTarget || (event.relatedTarget !== event.delegateTarget && event.relatedTarget.contains(event.delegateTarget))) {
return fn.call(this, event)
}
}
}
if (delegationFn) {
delegationFn = wrapFn(delegationFn)
} else {
handler = wrapFn(handler)
}
}
const [delegation, originalHandler, typeEvent] = normalizeParams(originalTypeEvent, handler, delegationFn) const [delegation, originalHandler, typeEvent] = normalizeParams(originalTypeEvent, handler, delegationFn)
const events = getEvent(element) const events = getEvent(element)
const handlers = events[typeEvent] || (events[typeEvent] = {}) const handlers = events[typeEvent] || (events[typeEvent] = {})
@ -219,6 +231,12 @@ function removeNamespacedHandlers(element, events, typeEvent, namespace) {
}) })
} }
function getTypeEvent(event) {
// allow to get the native events from namespaced events ('click.bs.button' --> 'click')
event = event.replace(stripNameRegex, '')
return customEvents[event] || event
}
const EventHandler = { const EventHandler = {
on(element, event, handler, delegationFn) { on(element, event, handler, delegationFn) {
addHandler(element, event, handler, delegationFn, false) addHandler(element, event, handler, delegationFn, false)
@ -272,7 +290,7 @@ const EventHandler = {
} }
const $ = getjQuery() const $ = getjQuery()
const typeEvent = event.replace(stripNameRegex, '') const typeEvent = getTypeEvent(event)
const inNamespace = event !== typeEvent const inNamespace = event !== typeEvent
const isNative = nativeEvents.has(typeEvent) const isNative = nativeEvents.has(typeEvent)

View File

@ -77,10 +77,64 @@ describe('EventHandler', () => {
div.click() div.click()
}) })
it('should handle mouseenter/mouseleave like the native counterpart', done => {
fixtureEl.innerHTML = [
'<div class="outer">',
'<div class="inner">',
'<div class="nested">',
'<div class="deep"></div>',
'</div>',
'</div>',
'</div>'
]
const outer = fixtureEl.querySelector('.outer')
const inner = fixtureEl.querySelector('.inner')
const nested = fixtureEl.querySelector('.nested')
const deep = fixtureEl.querySelector('.deep')
const enterSpy = jasmine.createSpy('mouseenter')
const leaveSpy = jasmine.createSpy('mouseleave')
const delegateEnterSpy = jasmine.createSpy('mouseenter')
const delegateLeaveSpy = jasmine.createSpy('mouseleave')
EventHandler.on(inner, 'mouseenter', enterSpy)
EventHandler.on(inner, 'mouseleave', leaveSpy)
EventHandler.on(outer, 'mouseenter', '.inner', delegateEnterSpy)
EventHandler.on(outer, 'mouseleave', '.inner', delegateLeaveSpy)
const moveMouse = (from, to) => {
from.dispatchEvent(new MouseEvent('mouseout', {
bubbles: true,
relatedTarget: to
}))
to.dispatchEvent(new MouseEvent('mouseover', {
bubbles: true,
relatedTarget: from
}))
}
moveMouse(outer, inner)
moveMouse(inner, nested)
moveMouse(nested, deep)
moveMouse(deep, nested)
moveMouse(nested, inner)
moveMouse(inner, outer)
setTimeout(() => {
expect(enterSpy.calls.count()).toBe(1)
expect(leaveSpy.calls.count()).toBe(1)
expect(delegateEnterSpy.calls.count()).toBe(1)
expect(delegateLeaveSpy.calls.count()).toBe(1)
done()
}, 20)
})
}) })
describe('one', () => { describe('one', () => {
it('should call listener just one', done => { it('should call listener just once', done => {
fixtureEl.innerHTML = '<div></div>' fixtureEl.innerHTML = '<div></div>'
let called = 0 let called = 0
@ -101,6 +155,28 @@ describe('EventHandler', () => {
done() done()
}, 20) }, 20)
}) })
it('should call delegated listener just once', done => {
fixtureEl.innerHTML = '<div></div>'
let called = 0
const div = fixtureEl.querySelector('div')
const obj = {
oneListener() {
called++
}
}
EventHandler.one(fixtureEl, 'bootstrap', 'div', obj.oneListener)
EventHandler.trigger(div, 'bootstrap')
EventHandler.trigger(div, 'bootstrap')
setTimeout(() => {
expect(called).toEqual(1)
done()
}, 20)
})
}) })
describe('off', () => { describe('off', () => {