Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/twbs/bootstrap.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/js
diff options
context:
space:
mode:
authoralpadev <2838324+alpadev@users.noreply.github.com>2021-04-13 06:25:58 +0300
committerGitHub <noreply@github.com>2021-04-13 06:25:58 +0300
commitdb32b2380c3040936b8e88f6d6dae5998750ddf6 (patch)
treeb54f2f2aaee725d0b9ac7befaf11e262f439b692 /js
parent49df4c89c5bb644f89a197b1ac2006bc1d20b246 (diff)
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
Diffstat (limited to 'js')
-rw-r--r--js/src/dom/event-handler.js38
-rw-r--r--js/tests/unit/dom/event-handler.spec.js78
2 files changed, 105 insertions, 11 deletions
diff --git a/js/src/dom/event-handler.js b/js/src/dom/event-handler.js
index 26f6a1e3f2..8ccb887fc3 100644
--- a/js/src/dom/event-handler.js
+++ b/js/src/dom/event-handler.js
@@ -22,6 +22,7 @@ const customEvents = {
mouseenter: 'mouseover',
mouseleave: 'mouseout'
}
+const customEventsRegex = /^(mouseenter|mouseleave)/i
const nativeEvents = new Set([
'click',
'dblclick',
@@ -113,7 +114,7 @@ function bootstrapDelegationHandler(element, selector, fn) {
if (handler.oneOff) {
// 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])
@@ -144,14 +145,7 @@ function normalizeParams(originalTypeEvent, handler, delegationFn) {
const delegation = typeof handler === 'string'
const originalHandler = delegation ? delegationFn : handler
- // allow to get the native events from namespaced events ('click.bs.button' --> 'click')
- let typeEvent = originalTypeEvent.replace(stripNameRegex, '')
- const custom = customEvents[typeEvent]
-
- if (custom) {
- typeEvent = custom
- }
-
+ let typeEvent = getTypeEvent(originalTypeEvent)
const isNative = nativeEvents.has(typeEvent)
if (!isNative) {
@@ -171,6 +165,24 @@ function addHandler(element, originalTypeEvent, handler, delegationFn, oneOff) {
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 events = getEvent(element)
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 = {
on(element, event, handler, delegationFn) {
addHandler(element, event, handler, delegationFn, false)
@@ -272,7 +290,7 @@ const EventHandler = {
}
const $ = getjQuery()
- const typeEvent = event.replace(stripNameRegex, '')
+ const typeEvent = getTypeEvent(event)
const inNamespace = event !== typeEvent
const isNative = nativeEvents.has(typeEvent)
diff --git a/js/tests/unit/dom/event-handler.spec.js b/js/tests/unit/dom/event-handler.spec.js
index e596a49b59..5fb1f01956 100644
--- a/js/tests/unit/dom/event-handler.spec.js
+++ b/js/tests/unit/dom/event-handler.spec.js
@@ -77,10 +77,64 @@ describe('EventHandler', () => {
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', () => {
- it('should call listener just one', done => {
+ it('should call listener just once', done => {
fixtureEl.innerHTML = '<div></div>'
let called = 0
@@ -101,6 +155,28 @@ describe('EventHandler', () => {
done()
}, 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', () => {