Skip to content

Commit

Permalink
Fix handling of transitionend events dispatched by nested elements(#3…
Browse files Browse the repository at this point in the history
…3845)

Fix handling of transitionend events dispatched by nested elements
Properly handle events from nested elements

Change `emulateTransitionEnd` to `executeAfterTransition` &&
  • Loading branch information
alpadev committed Jun 3, 2021
1 parent 071a288 commit 4a5029e
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 87 deletions.
16 changes: 3 additions & 13 deletions js/src/base-component.js
Expand Up @@ -7,10 +7,8 @@

import Data from './dom/data'
import {
emulateTransitionEnd,
execute,
getElement,
getTransitionDurationFromElement
executeAfterTransition,
getElement
} from './util/index'
import EventHandler from './dom/event-handler'

Expand Down Expand Up @@ -44,15 +42,7 @@ class BaseComponent {
}

_queueCallback(callback, element, isAnimated = true) {
if (!isAnimated) {
execute(callback)
return
}

const transitionDuration = getTransitionDurationFromElement(element)
EventHandler.one(element, 'transitionend', () => execute(callback))

emulateTransitionEnd(element, transitionDuration)
executeAfterTransition(callback, element, isAnimated)
}

/** Static */
Expand Down
31 changes: 16 additions & 15 deletions js/src/modal.js
Expand Up @@ -7,9 +7,7 @@

import {
defineJQueryPlugin,
emulateTransitionEnd,
getElementFromSelector,
getTransitionDurationFromElement,
isRTL,
isVisible,
reflow,
Expand Down Expand Up @@ -339,25 +337,28 @@ class Modal extends BaseComponent {
return
}

const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight
const { classList, scrollHeight, style } = this._element
const isModalOverflowing = scrollHeight > document.documentElement.clientHeight

// return if the following background transition hasn't yet completed
if ((!isModalOverflowing && style.overflowY === 'hidden') || classList.contains(CLASS_NAME_STATIC)) {
return
}

if (!isModalOverflowing) {
this._element.style.overflowY = 'hidden'
style.overflowY = 'hidden'
}

this._element.classList.add(CLASS_NAME_STATIC)
const modalTransitionDuration = getTransitionDurationFromElement(this._dialog)
EventHandler.off(this._element, 'transitionend')
EventHandler.one(this._element, 'transitionend', () => {
this._element.classList.remove(CLASS_NAME_STATIC)
classList.add(CLASS_NAME_STATIC)
this._queueCallback(() => {
classList.remove(CLASS_NAME_STATIC)
if (!isModalOverflowing) {
EventHandler.one(this._element, 'transitionend', () => {
this._element.style.overflowY = ''
})
emulateTransitionEnd(this._element, modalTransitionDuration)
this._queueCallback(() => {
style.overflowY = ''
}, this._dialog)
}
})
emulateTransitionEnd(this._element, modalTransitionDuration)
}, this._dialog)

this._element.focus()
}

Expand Down
11 changes: 2 additions & 9 deletions js/src/util/backdrop.js
Expand Up @@ -6,7 +6,7 @@
*/

import EventHandler from '../dom/event-handler'
import { emulateTransitionEnd, execute, getElement, getTransitionDurationFromElement, reflow, typeCheckConfig } from './index'
import { execute, executeAfterTransition, getElement, reflow, typeCheckConfig } from './index'

const Default = {
isVisible: true, // if false, we use the backdrop helper without adding any element to the dom
Expand Down Expand Up @@ -122,14 +122,7 @@ class Backdrop {
}

_emulateAnimation(callback) {
if (!this._config.isAnimated) {
execute(callback)
return
}

const backdropTransitionDuration = getTransitionDurationFromElement(this._getElement())
EventHandler.one(this._getElement(), 'transitionend', () => execute(callback))
emulateTransitionEnd(this._getElement(), backdropTransitionDuration)
executeAfterTransition(callback, this._getElement(), this._config.isAnimated)
}
}

Expand Down
51 changes: 31 additions & 20 deletions js/src/util/index.js
Expand Up @@ -126,24 +126,6 @@ const getElement = obj => {
return null
}

const emulateTransitionEnd = (element, duration) => {
let called = false
const durationPadding = 5
const emulatedDuration = duration + durationPadding

function listener() {
called = true
element.removeEventListener(TRANSITION_END, listener)
}

element.addEventListener(TRANSITION_END, listener)
setTimeout(() => {
if (!called) {
triggerTransitionEnd(element)
}
}, emulatedDuration)
}

const typeCheckConfig = (componentName, config, configTypes) => {
Object.keys(configTypes).forEach(property => {
const expectedTypes = configTypes[property]
Expand Down Expand Up @@ -252,6 +234,35 @@ const execute = callback => {
}
}

const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => {
if (!waitForTransition) {
execute(callback)
return
}

const durationPadding = 5
const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding

let called = false

const handler = ({ target }) => {
if (target !== transitionElement) {
return
}

called = true
transitionElement.removeEventListener(TRANSITION_END, handler)
execute(callback)
}

transitionElement.addEventListener(TRANSITION_END, handler)
setTimeout(() => {
if (!called) {
triggerTransitionEnd(transitionElement)
}
}, emulatedDuration)
}

/**
* Return the previous/next element of a list.
*
Expand Down Expand Up @@ -288,7 +299,6 @@ export {
getTransitionDurationFromElement,
triggerTransitionEnd,
isElement,
emulateTransitionEnd,
typeCheckConfig,
isVisible,
isDisabled,
Expand All @@ -300,5 +310,6 @@ export {
onDOMContentLoaded,
isRTL,
defineJQueryPlugin,
execute
execute,
executeAfterTransition
}
23 changes: 23 additions & 0 deletions js/tests/unit/modal.spec.js
Expand Up @@ -539,6 +539,29 @@ describe('Modal', () => {
modal.show()
})

it('should not queue multiple callbacks when clicking outside of modal-content and backdrop = static', done => {
fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" style="transition-duration: 50ms;"></div></div>'

const modalEl = fixtureEl.querySelector('.modal')
const modal = new Modal(modalEl, {
backdrop: 'static'
})

modalEl.addEventListener('shown.bs.modal', () => {
const spy = spyOn(modal, '_queueCallback').and.callThrough()

modalEl.click()
modalEl.click()

setTimeout(() => {
expect(spy).toHaveBeenCalledTimes(1)
done()
}, 20)
})

modal.show()
})

it('should enforce focus', done => {
fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'

Expand Down
2 changes: 1 addition & 1 deletion js/tests/unit/tooltip.spec.js
Expand Up @@ -446,7 +446,7 @@ describe('Tooltip', () => {
const tooltip = new Tooltip(tooltipEl)
document.documentElement.ontouchstart = noop

spyOn(EventHandler, 'on')
spyOn(EventHandler, 'on').and.callThrough()

tooltipEl.addEventListener('shown.bs.tooltip', () => {
expect(document.querySelector('.tooltip')).not.toBeNull()
Expand Down

0 comments on commit 4a5029e

Please sign in to comment.