From e20e7d0cc55873b806cb3ac80f8798131efb1682 Mon Sep 17 00:00:00 2001 From: alpadev Date: Tue, 2 Mar 2021 10:30:10 +0100 Subject: [PATCH 1/2] fix: make EventHandler better handle mouseenter/mouseleave events --- js/src/dom/event-handler.js | 37 ++++++++---- js/tests/unit/dom/event-handler.spec.js | 78 ++++++++++++++++++++++++- 2 files changed, 104 insertions(+), 11 deletions(-) diff --git a/js/src/dom/event-handler.js b/js/src/dom/event-handler.js index 26f6a1e3f286..834f5b2fe8bf 100644 --- a/js/src/dom/event-handler.js +++ b/js/src/dom/event-handler.js @@ -113,7 +113,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 +144,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 +164,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 getting triggered the same way as mouseover or mouseout does + if (/^mouse(enter|leave)/i.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 +230,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 +289,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 e596a49b59b9..5fb1f01956da 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 = [ + '
', + '
', + '
', + '
', + '
', + '
', + '
' + ] + + 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 = '
' let called = 0 @@ -101,6 +155,28 @@ describe('EventHandler', () => { done() }, 20) }) + + it('should call delegated listener just once', done => { + fixtureEl.innerHTML = '
' + + 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', () => { From ef82822b422b2764c9fa045323a0349c042da839 Mon Sep 17 00:00:00 2001 From: alpadev Date: Wed, 31 Mar 2021 14:29:31 +0200 Subject: [PATCH 2/2] refactor: simplify custom events regex and move it to a variable requested by GeoSot - https://github.com/twbs/bootstrap/pull/33310#discussion_r601925640 --- js/src/dom/event-handler.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/js/src/dom/event-handler.js b/js/src/dom/event-handler.js index 834f5b2fe8bf..8ccb887fc3bf 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', @@ -165,8 +166,8 @@ function addHandler(element, originalTypeEvent, handler, delegationFn, oneOff) { } // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position - // this prevents the handler getting triggered the same way as mouseover or mouseout does - if (/^mouse(enter|leave)/i.test(originalTypeEvent)) { + // 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))) {