diff --git a/js/src/collapse.js b/js/src/collapse.js index b308863f468f..983ea67318b0 100644 --- a/js/src/collapse.js +++ b/js/src/collapse.js @@ -39,7 +39,7 @@ const CLASS_NAME_HORIZONTAL = 'collapse-horizontal' const WIDTH = 'width' const HEIGHT = 'height' -const SELECTOR_ACTIVES = '.collapse.show, .collapse.collapsing' +const SELECTOR_ACTIVES = '.collapse.show, .collapsing' const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="collapse"]' const Default = { @@ -61,6 +61,7 @@ class Collapse extends BaseComponent { super(element, config) this._isTransitioning = false + this._isExpanding = false this._triggerArray = [] const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE) @@ -120,9 +121,10 @@ class Collapse extends BaseComponent { activeChildren = this._getFirstLevelChildren(SELECTOR_ACTIVES) .filter(element => element !== this._element) .map(element => Collapse.getOrCreateInstance(element, { toggle: false })) + .filter(activeInstance => !this._triggerArray.some(trigger => activeInstance._triggerArray.includes(trigger))) } - if (activeChildren.length && activeChildren[0]._isTransitioning) { + if (activeChildren.some(activeChild => activeChild._isTransitioning && activeChild._isExpanding)) { return } @@ -144,9 +146,11 @@ class Collapse extends BaseComponent { this._addAriaAndCollapsedClass(this._triggerArray, true) this._isTransitioning = true + this._isExpanding = true const complete = () => { this._isTransitioning = false + this._isExpanding = false this._element.classList.remove(CLASS_NAME_COLLAPSING) this._element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW) diff --git a/js/tests/unit/collapse.spec.js b/js/tests/unit/collapse.spec.js index 58c5367526b9..73ea9b35eede 100644 --- a/js/tests/unit/collapse.spec.js +++ b/js/tests/unit/collapse.spec.js @@ -202,6 +202,36 @@ describe('Collapse', () => { expect(spy).not.toHaveBeenCalled() }) + it('should do nothing if any active sibling is transitioning', () => { + fixtureEl.innerHTML = [ + '
', + '
', + '
', + '
', + '
' + ].join('') + + const collapseEl1 = fixtureEl.querySelector('#collapse1') + const collapseEl2 = fixtureEl.querySelector('#collapse2') + const collapseEl3 = fixtureEl.querySelector('#collapse3') + + const collapse1 = new Collapse(collapseEl1, { toggle: false }) + const collapse2 = new Collapse(collapseEl2, { toggle: false }) + const collapse3 = new Collapse(collapseEl3, { toggle: false }) + + collapse1._isTransitioning = false + collapse2._isTransitioning = true + collapse2._isExpanding = true + + const spy = spyOn(EventHandler, 'trigger').and.callThrough() + + collapse3.show() + + expect(spy).not.toHaveBeenCalled() + expect(collapse3._isTransitioning).toBeFalse() + expect(collapseEl3).not.toHaveClass('show') + }) + it('should show a collapsed element', () => { return new Promise(resolve => { fixtureEl.innerHTML = '
' @@ -941,6 +971,49 @@ describe('Collapse', () => { trigger3.click() }) }) + + it('should not allow multiple items to open when triggers are clicked rapidly in succession', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + '
', + ' ', + '
', + '
', + '
', + ' ', + '
', + '
', + '
', + ' ', + '
', + '
', + '
' + ].join('') + + const btn1 = fixtureEl.querySelector('#btn1') + const btn2 = fixtureEl.querySelector('#btn2') + const btn3 = fixtureEl.querySelector('#btn3') + const collapse1 = fixtureEl.querySelector('#collapse1') + const collapse2 = fixtureEl.querySelector('#collapse2') + const collapse3 = fixtureEl.querySelector('#collapse3') + + collapse3.addEventListener('shown.bs.collapse', () => { + setTimeout(() => { + const shownCollapses = fixtureEl.querySelectorAll('.collapse.show') + expect(shownCollapses.length).toEqual(1) + expect(collapse3).toHaveClass('show') + expect(collapse1).not.toHaveClass('show') + expect(collapse2).not.toHaveClass('show') + resolve() + }, 10) + }) + + btn3.click() + btn2.click() + btn1.click() + }) + }) }) describe('jQueryInterface', () => {