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:
parent
49df4c89c5
commit
db32b2380c
@ -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)
|
||||||
|
|
||||||
|
@ -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', () => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user