diff options
| author | Patrick H. Lauke <[email protected]> | 2021-05-04 12:46:06 +0100 |
|---|---|---|
| committer | GitHub <[email protected]> | 2021-05-04 12:46:06 +0100 |
| commit | 8865a8ab1c7157ab81bf49afa62b75f36daee46d (patch) | |
| tree | 97ef78f2ea8e07aab50014176d061fe3c1d49134 /js/tests/unit/dropdown.spec.js | |
| parent | 018ee6a3b50b958ddb49657086cd9168abf5a485 (diff) | |
| parent | 7ea6578773cb1b7f5cfb8fb41321b3fa10349daf (diff) | |
| download | bootstrap-jo-docs-thanks-page.tar.xz bootstrap-jo-docs-thanks-page.zip | |
Merge branch 'main' into jo-docs-thanks-pagejo-docs-thanks-page
Diffstat (limited to 'js/tests/unit/dropdown.spec.js')
| -rw-r--r-- | js/tests/unit/dropdown.spec.js | 773 |
1 files changed, 591 insertions, 182 deletions
diff --git a/js/tests/unit/dropdown.spec.js b/js/tests/unit/dropdown.spec.js index f6a5feb1b..b0f225140 100644 --- a/js/tests/unit/dropdown.spec.js +++ b/js/tests/unit/dropdown.spec.js @@ -1,7 +1,6 @@ -import Popper from 'popper.js' - import Dropdown from '../../src/dropdown' import EventHandler from '../../src/dom/event-handler' +import { noop } from '../../src/util' /** Test helpers */ import { getFixture, clearFixture, createEvent, jQueryMock } from '../helpers/fixture' @@ -35,30 +34,52 @@ describe('Dropdown', () => { }) }) + describe('DATA_KEY', () => { + it('should return plugin data key', () => { + expect(Dropdown.DATA_KEY).toEqual('bs.dropdown') + }) + }) + describe('constructor', () => { - it('should create offset modifier correctly when offset option is a function', () => { + it('should take care of element either passed as a CSS selector or DOM element', () => { fixtureEl.innerHTML = [ '<div class="dropdown">', ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', + ' <a class="dropdown-item" href="#">Link</a>', ' </div>', '</div>' ].join('') - const getOffset = offsets => offsets const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown, { - offset: getOffset - }) + const dropdownBySelector = new Dropdown('[data-bs-toggle="dropdown"]') + const dropdownByElement = new Dropdown(btnDropdown) - const offset = dropdown._getOffset() + expect(dropdownBySelector._element).toEqual(btnDropdown) + expect(dropdownByElement._element).toEqual(btnDropdown) + }) + + it('should add a listener on trigger which do not have data-bs-toggle="dropdown"', () => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - expect(offset.offset).toBeUndefined() - expect(typeof offset.fn).toEqual('function') + const btnDropdown = fixtureEl.querySelector('.btn') + const dropdown = new Dropdown(btnDropdown) + + spyOn(dropdown, 'toggle') + + btnDropdown.click() + + expect(dropdown.toggle).toHaveBeenCalled() }) - it('should create offset modifier correctly when offset option is not a function', () => { + it('should create offset modifier correctly when offset option is a function', done => { fixtureEl.innerHTML = [ '<div class="dropdown">', ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', @@ -68,36 +89,42 @@ describe('Dropdown', () => { '</div>' ].join('') - const myOffset = 7 + const getOffset = jasmine.createSpy('getOffset').and.returnValue([10, 20]) const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdown = new Dropdown(btnDropdown, { - offset: myOffset + offset: getOffset, + popperConfig: { + onFirstUpdate: state => { + expect(getOffset).toHaveBeenCalledWith({ + popper: state.rects.popper, + reference: state.rects.reference, + placement: state.placement + }, btnDropdown) + done() + } + } }) - const offset = dropdown._getOffset() - expect(offset.offset).toEqual(myOffset) - expect(offset.fn).toBeUndefined() + expect(typeof offset).toEqual('function') + + dropdown.show() }) - it('should add a listener on trigger which do not have data-bs-toggle="dropdown"', () => { + it('should create offset modifier correctly when offset option is a string into data attribute', () => { fixtureEl.innerHTML = [ '<div class="dropdown">', - ' <button class="btn">Dropdown</button>', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-offset="10,20">Dropdown</button>', ' <div class="dropdown-menu">', ' <a class="dropdown-item" href="#">Secondary link</a>', ' </div>', '</div>' ].join('') - const btnDropdown = fixtureEl.querySelector('.btn') + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdown = new Dropdown(btnDropdown) - spyOn(dropdown, 'toggle') - - btnDropdown.click() - - expect(dropdown.toggle).toHaveBeenCalled() + expect(dropdown._getOffset()).toEqual([10, 20]) }) it('should allow to pass config to Popper with `popperConfig`', () => { @@ -121,6 +148,28 @@ describe('Dropdown', () => { expect(popperConfig.placement).toEqual('left') }) + + it('should allow to pass config to Popper with `popperConfig` as a function', () => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-placement="right" >Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const getPopperConfig = jasmine.createSpy('getPopperConfig').and.returnValue({ placement: 'left' }) + const dropdown = new Dropdown(btnDropdown, { + popperConfig: getPopperConfig + }) + + const popperConfig = dropdown._getPopperConfig() + + expect(getPopperConfig).toHaveBeenCalled() + expect(popperConfig.placement).toEqual('left') + }) }) describe('toggle', () => { @@ -135,10 +184,9 @@ describe('Dropdown', () => { ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') const dropdown = new Dropdown(btnDropdown) - dropdownEl.addEventListener('shown.bs.dropdown', () => { + btnDropdown.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown.classList.contains('show')).toEqual(true) expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') done() @@ -168,18 +216,17 @@ describe('Dropdown', () => { const firstDropdownEl = fixtureEl.querySelector('.first') const secondDropdownEl = fixtureEl.querySelector('.second') const dropdown1 = new Dropdown(btnDropdown1) - const dropdown2 = new Dropdown(btnDropdown2) firstDropdownEl.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown1.classList.contains('show')).toEqual(true) spyOn(dropdown1._popper, 'destroy') - dropdown2.toggle() + btnDropdown2.click() }) - secondDropdownEl.addEventListener('shown.bs.dropdown', () => { + secondDropdownEl.addEventListener('shown.bs.dropdown', () => setTimeout(() => { expect(dropdown1._popper.destroy).toHaveBeenCalled() done() - }) + })) dropdown1.toggle() }) @@ -196,25 +243,24 @@ describe('Dropdown', () => { const defaultValueOnTouchStart = document.documentElement.ontouchstart const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') const dropdown = new Dropdown(btnDropdown) document.documentElement.ontouchstart = () => {} spyOn(EventHandler, 'on') spyOn(EventHandler, 'off') - dropdownEl.addEventListener('shown.bs.dropdown', () => { + btnDropdown.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown.classList.contains('show')).toEqual(true) expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - expect(EventHandler.on).toHaveBeenCalled() + expect(EventHandler.on).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop) dropdown.toggle() }) - dropdownEl.addEventListener('hidden.bs.dropdown', () => { + btnDropdown.addEventListener('hidden.bs.dropdown', () => { expect(btnDropdown.classList.contains('show')).toEqual(false) expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') - expect(EventHandler.off).toHaveBeenCalled() + expect(EventHandler.off).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop) document.documentElement.ontouchstart = defaultValueOnTouchStart done() @@ -234,10 +280,9 @@ describe('Dropdown', () => { ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') const dropdown = new Dropdown(btnDropdown) - dropdownEl.addEventListener('shown.bs.dropdown', () => { + btnDropdown.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown.classList.contains('show')).toEqual(true) expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') done() @@ -349,12 +394,11 @@ describe('Dropdown', () => { ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') const dropdown = new Dropdown(btnDropdown, { reference: 'parent' }) - dropdownEl.addEventListener('shown.bs.dropdown', () => { + btnDropdown.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown.classList.contains('show')).toEqual(true) expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') done() @@ -374,12 +418,11 @@ describe('Dropdown', () => { ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') const dropdown = new Dropdown(btnDropdown, { reference: fixtureEl }) - dropdownEl.addEventListener('shown.bs.dropdown', () => { + btnDropdown.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown.classList.contains('show')).toEqual(true) expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') done() @@ -399,12 +442,11 @@ describe('Dropdown', () => { ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') const dropdown = new Dropdown(btnDropdown, { reference: { 0: fixtureEl, jquery: 'jQuery' } }) - dropdownEl.addEventListener('shown.bs.dropdown', () => { + btnDropdown.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown.classList.contains('show')).toEqual(true) expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') done() @@ -413,6 +455,58 @@ describe('Dropdown', () => { dropdown.toggle() }) + it('should toggle a dropdown with a valid virtual element reference', done => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle visually-hidden" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const virtualElement = { + getBoundingClientRect() { + return { + width: 0, + height: 0, + top: 0, + right: 0, + bottom: 0, + left: 0 + } + } + } + + expect(() => new Dropdown(btnDropdown, { + reference: {} + })).toThrowError(TypeError, 'DROPDOWN: Option "reference" provided type "object" without a required "getBoundingClientRect" method.') + + expect(() => new Dropdown(btnDropdown, { + reference: { + getBoundingClientRect: 'not-a-function' + } + })).toThrowError(TypeError, 'DROPDOWN: Option "reference" provided type "object" without a required "getBoundingClientRect" method.') + + // use onFirstUpdate as Poppers internal update is executed async + const dropdown = new Dropdown(btnDropdown, { + reference: virtualElement, + popperConfig: { + onFirstUpdate() { + expect(virtualElement.getBoundingClientRect).toHaveBeenCalled() + expect(btnDropdown.classList.contains('show')).toEqual(true) + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + done() + } + } + }) + + spyOn(virtualElement, 'getBoundingClientRect').and.callThrough() + + dropdown.toggle() + }) + it('should not toggle a dropdown if the element is disabled', done => { fixtureEl.innerHTML = [ '<div class="dropdown">', @@ -424,10 +518,9 @@ describe('Dropdown', () => { ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') const dropdown = new Dropdown(btnDropdown) - dropdownEl.addEventListener('shown.bs.dropdown', () => { + btnDropdown.addEventListener('shown.bs.dropdown', () => { throw new Error('should not throw shown.bs.dropdown event') }) @@ -450,10 +543,9 @@ describe('Dropdown', () => { ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') const dropdown = new Dropdown(btnDropdown) - dropdownEl.addEventListener('shown.bs.dropdown', () => { + btnDropdown.addEventListener('shown.bs.dropdown', () => { throw new Error('should not throw shown.bs.dropdown event') }) @@ -476,10 +568,9 @@ describe('Dropdown', () => { ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') const dropdown = new Dropdown(btnDropdown) - dropdownEl.addEventListener('shown.bs.dropdown', () => { + btnDropdown.addEventListener('shown.bs.dropdown', () => { throw new Error('should not throw shown.bs.dropdown event') }) @@ -502,14 +593,13 @@ describe('Dropdown', () => { ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') const dropdown = new Dropdown(btnDropdown) - dropdownEl.addEventListener('show.bs.dropdown', e => { + btnDropdown.addEventListener('show.bs.dropdown', e => { e.preventDefault() }) - dropdownEl.addEventListener('shown.bs.dropdown', () => { + btnDropdown.addEventListener('shown.bs.dropdown', () => { throw new Error('should not throw shown.bs.dropdown event') }) @@ -534,10 +624,9 @@ describe('Dropdown', () => { ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') const dropdown = new Dropdown(btnDropdown) - dropdownEl.addEventListener('shown.bs.dropdown', () => { + btnDropdown.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown.classList.contains('show')).toEqual(true) done() }) @@ -556,10 +645,9 @@ describe('Dropdown', () => { ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') const dropdown = new Dropdown(btnDropdown) - dropdownEl.addEventListener('shown.bs.dropdown', () => { + btnDropdown.addEventListener('shown.bs.dropdown', () => { throw new Error('should not throw shown.bs.dropdown event') }) @@ -582,10 +670,9 @@ describe('Dropdown', () => { ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') const dropdown = new Dropdown(btnDropdown) - dropdownEl.addEventListener('shown.bs.dropdown', () => { + btnDropdown.addEventListener('shown.bs.dropdown', () => { throw new Error('should not throw shown.bs.dropdown event') }) @@ -608,10 +695,9 @@ describe('Dropdown', () => { ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') const dropdown = new Dropdown(btnDropdown) - dropdownEl.addEventListener('shown.bs.dropdown', () => { + btnDropdown.addEventListener('shown.bs.dropdown', () => { throw new Error('should not throw shown.bs.dropdown event') }) @@ -634,14 +720,13 @@ describe('Dropdown', () => { ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') const dropdown = new Dropdown(btnDropdown) - dropdownEl.addEventListener('show.bs.dropdown', e => { + btnDropdown.addEventListener('show.bs.dropdown', e => { e.preventDefault() }) - dropdownEl.addEventListener('shown.bs.dropdown', () => { + btnDropdown.addEventListener('shown.bs.dropdown', () => { throw new Error('should not throw shown.bs.dropdown event') }) @@ -658,7 +743,7 @@ describe('Dropdown', () => { it('should hide a dropdown', done => { fixtureEl.innerHTML = [ '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="true">Dropdown</button>', ' <div class="dropdown-menu show">', ' <a class="dropdown-item" href="#">Secondary link</a>', ' </div>', @@ -666,12 +751,12 @@ describe('Dropdown', () => { ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') const dropdown = new Dropdown(btnDropdown) - dropdownEl.addEventListener('hidden.bs.dropdown', () => { + btnDropdown.addEventListener('hidden.bs.dropdown', () => { expect(dropdownMenu.classList.contains('show')).toEqual(false) + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') done() }) @@ -689,15 +774,14 @@ describe('Dropdown', () => { ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') const dropdown = new Dropdown(btnDropdown) - dropdownEl.addEventListener('shown.bs.dropdown', () => { + btnDropdown.addEventListener('shown.bs.dropdown', () => { spyOn(dropdown._popper, 'destroy') dropdown.hide() }) - dropdownEl.addEventListener('hidden.bs.dropdown', () => { + btnDropdown.addEventListener('hidden.bs.dropdown', () => { expect(dropdown._popper.destroy).toHaveBeenCalled() done() }) @@ -716,11 +800,10 @@ describe('Dropdown', () => { ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') const dropdown = new Dropdown(btnDropdown) - dropdownEl.addEventListener('hidden.bs.dropdown', () => { + btnDropdown.addEventListener('hidden.bs.dropdown', () => { throw new Error('should not throw hidden.bs.dropdown event') }) @@ -743,11 +826,10 @@ describe('Dropdown', () => { ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') const dropdown = new Dropdown(btnDropdown) - dropdownEl.addEventListener('hidden.bs.dropdown', () => { + btnDropdown.addEventListener('hidden.bs.dropdown', () => { throw new Error('should not throw hidden.bs.dropdown event') }) @@ -770,10 +852,9 @@ describe('Dropdown', () => { ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') const dropdown = new Dropdown(btnDropdown) - dropdownEl.addEventListener('hidden.bs.dropdown', () => { + btnDropdown.addEventListener('hidden.bs.dropdown', () => { throw new Error('should not throw hidden.bs.dropdown event') }) @@ -796,15 +877,14 @@ describe('Dropdown', () => { ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') const dropdown = new Dropdown(btnDropdown) - dropdownEl.addEventListener('hide.bs.dropdown', e => { + btnDropdown.addEventListener('hide.bs.dropdown', e => { e.preventDefault() }) - dropdownEl.addEventListener('hidden.bs.dropdown', () => { + btnDropdown.addEventListener('hidden.bs.dropdown', () => { throw new Error('should not throw hidden.bs.dropdown event') }) @@ -815,6 +895,39 @@ describe('Dropdown', () => { done() }) }) + + it('should remove event listener on touch-enabled device that was added in show method', done => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Dropdwon item</a>', + ' </div>', + '</div>' + ].join('') + + const defaultValueOnTouchStart = document.documentElement.ontouchstart + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) + + document.documentElement.ontouchstart = () => {} + spyOn(EventHandler, 'off') + + btnDropdown.addEventListener('shown.bs.dropdown', () => { + dropdown.hide() + }) + + btnDropdown.addEventListener('hidden.bs.dropdown', () => { + expect(btnDropdown.classList.contains('show')).toEqual(false) + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') + expect(EventHandler.off).toHaveBeenCalled() + + document.documentElement.ontouchstart = defaultValueOnTouchStart + done() + }) + + dropdown.show() + }) }) describe('dispose', () => { @@ -829,16 +942,21 @@ describe('Dropdown', () => { ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + spyOn(btnDropdown, 'addEventListener').and.callThrough() + spyOn(btnDropdown, 'removeEventListener').and.callThrough() + const dropdown = new Dropdown(btnDropdown) expect(dropdown._popper).toBeNull() expect(dropdown._menu).toBeDefined() expect(dropdown._element).toBeDefined() + expect(btnDropdown.addEventListener).toHaveBeenCalledWith('click', jasmine.any(Function), jasmine.any(Boolean)) dropdown.dispose() expect(dropdown._menu).toBeNull() expect(dropdown._element).toBeNull() + expect(btnDropdown.removeEventListener).toHaveBeenCalledWith('click', jasmine.any(Function), jasmine.any(Boolean)) }) it('should dispose dropdown with Popper', () => { @@ -860,14 +978,11 @@ describe('Dropdown', () => { expect(dropdown._menu).toBeDefined() expect(dropdown._element).toBeDefined() - spyOn(Popper.prototype, 'destroy') - dropdown.dispose() expect(dropdown._popper).toBeNull() expect(dropdown._menu).toBeNull() expect(dropdown._element).toBeNull() - expect(Popper.prototype.destroy).toHaveBeenCalled() }) }) @@ -889,12 +1004,12 @@ describe('Dropdown', () => { expect(dropdown._popper).toBeDefined() - spyOn(dropdown._popper, 'scheduleUpdate') + spyOn(dropdown._popper, 'update') spyOn(dropdown, '_detectNavbar') dropdown.update() - expect(dropdown._popper.scheduleUpdate).toHaveBeenCalled() + expect(dropdown._popper.update).toHaveBeenCalled() expect(dropdown._detectNavbar).toHaveBeenCalled() }) @@ -921,10 +1036,10 @@ describe('Dropdown', () => { }) describe('data-api', () => { - it('should not add class position-static to dropdown if boundary not set', done => { + it('should show and hide a dropdown', done => { fixtureEl.innerHTML = [ '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', ' <div class="dropdown-menu">', ' <a class="dropdown-item" href="#">Secondary link</a>', ' </div>', @@ -932,80 +1047,103 @@ describe('Dropdown', () => { ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') + let showEventTriggered = false + let hideEventTriggered = false + + btnDropdown.addEventListener('show.bs.dropdown', () => { + showEventTriggered = true + }) + + btnDropdown.addEventListener('shown.bs.dropdown', e => setTimeout(() => { + expect(btnDropdown.classList.contains('show')).toEqual(true) + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + expect(showEventTriggered).toEqual(true) + expect(e.relatedTarget).toEqual(btnDropdown) + document.body.click() + })) + + btnDropdown.addEventListener('hide.bs.dropdown', () => { + hideEventTriggered = true + }) - dropdownEl.addEventListener('shown.bs.dropdown', () => { - expect(dropdownEl.classList.contains('position-static')).toEqual(false) + btnDropdown.addEventListener('hidden.bs.dropdown', e => { + expect(btnDropdown.classList.contains('show')).toEqual(false) + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') + expect(hideEventTriggered).toEqual(true) + expect(e.relatedTarget).toEqual(btnDropdown) done() }) btnDropdown.click() }) - it('should add class position-static to dropdown if boundary not scrollParent', done => { + it('should not use Popper in navbar', done => { fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-boundary="viewport">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', + '<nav class="navbar navbar-expand-md navbar-light bg-light">', + ' <div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', ' </div>', - '</div>' + '</nav>' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(btnDropdown) - dropdownEl.addEventListener('shown.bs.dropdown', () => { - expect(dropdownEl.classList.contains('position-static')).toEqual(true) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(dropdown._popper).toBeNull() + expect(dropdownMenu.getAttribute('style')).toEqual(null, 'no inline style applied by Popper') done() }) - btnDropdown.click() + dropdown.show() }) - it('should show and hide a dropdown', done => { + it('should not collapse the dropdown when clicking a select option nested in the dropdown', done => { fixtureEl.innerHTML = [ '<div class="dropdown">', ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', + ' <select>', + ' <option selected>Open this select menu</option>', + ' <option value="1">One</option>', + ' </select>', ' </div>', '</div>' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') - let showEventTriggered = false - let hideEventTriggered = false + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(btnDropdown) - dropdownEl.addEventListener('show.bs.dropdown', () => { - showEventTriggered = true - }) + const hideSpy = spyOn(dropdown, '_completeHide') - dropdownEl.addEventListener('shown.bs.dropdown', e => { - expect(btnDropdown.classList.contains('show')).toEqual(true) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - expect(showEventTriggered).toEqual(true) - expect(e.relatedTarget).toEqual(btnDropdown) - document.body.click() - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + const clickEvent = new MouseEvent('click', { + bubbles: true + }) - dropdownEl.addEventListener('hide.bs.dropdown', () => { - hideEventTriggered = true + dropdownMenu.querySelector('option').dispatchEvent(clickEvent) }) - dropdownEl.addEventListener('hidden.bs.dropdown', e => { - expect(btnDropdown.classList.contains('show')).toEqual(false) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') - expect(hideEventTriggered).toEqual(true) - expect(e.relatedTarget).toEqual(btnDropdown) - done() + dropdownMenu.addEventListener('click', event => { + expect(event.target.tagName).toMatch(/select|option/i) + + Dropdown.clearMenus(event) + + setTimeout(() => { + expect(hideSpy).not.toHaveBeenCalled() + done() + }, 10) }) - btnDropdown.click() + dropdown.show() }) - it('should not use Popper in navbar', done => { + it('should manage bs attribute `data-bs-popper`="none" when dropdown is in navbar', done => { fixtureEl.innerHTML = [ '<nav class="navbar navbar-expand-md navbar-light bg-light">', ' <div class="dropdown">', @@ -1018,15 +1156,20 @@ describe('Dropdown', () => { ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(btnDropdown) - dropdownEl.addEventListener('shown.bs.dropdown', () => { - expect(dropdownMenu.getAttribute('style')).toEqual(null, 'no inline style applied by Popper') + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(dropdownMenu.getAttribute('data-bs-popper')).toEqual('none') + dropdown.hide() + }) + + btnDropdown.addEventListener('hidden.bs.dropdown', () => { + expect(dropdownMenu.getAttribute('data-bs-popper')).toBeNull() done() }) - btnDropdown.click() + dropdown.show() }) it('should not use Popper if display set to static', done => { @@ -1040,18 +1183,44 @@ describe('Dropdown', () => { ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - dropdownEl.addEventListener('shown.bs.dropdown', () => { + btnDropdown.addEventListener('shown.bs.dropdown', () => { // Popper adds this attribute when we use it - expect(dropdownMenu.getAttribute('x-placement')).toEqual(null) + expect(dropdownMenu.getAttribute('data-popper-placement')).toEqual(null) done() }) btnDropdown.click() }) + it('should manage bs attribute `data-bs-popper`="static" when display set to static', done => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-display="static">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(btnDropdown) + + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(dropdownMenu.getAttribute('data-bs-popper')).toEqual('static') + dropdown.hide() + }) + + btnDropdown.addEventListener('hidden.bs.dropdown', () => { + expect(dropdownMenu.getAttribute('data-bs-popper')).toBeNull() + done() + }) + + dropdown.show() + }) + it('should remove "show" class if tabbing outside of menu', done => { fixtureEl.innerHTML = [ '<div class="dropdown">', @@ -1063,9 +1232,8 @@ describe('Dropdown', () => { ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownEl = fixtureEl.querySelector('.dropdown') - dropdownEl.addEventListener('shown.bs.dropdown', () => { + btnDropdown.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown.classList.contains('show')).toEqual(true) const keyup = createEvent('keyup') @@ -1074,7 +1242,7 @@ describe('Dropdown', () => { document.dispatchEvent(keyup) }) - dropdownEl.addEventListener('hidden.bs.dropdown', () => { + btnDropdown.addEventListener('hidden.bs.dropdown', () => { expect(btnDropdown.classList.contains('show')).toEqual(false) done() }) @@ -1105,34 +1273,31 @@ describe('Dropdown', () => { expect(triggerDropdownList.length).toEqual(2) - const first = triggerDropdownList[0] - const last = triggerDropdownList[1] - const dropdownTestMenu = first.parentNode - const btnGroup = last.parentNode + const [triggerDropdownFirst, triggerDropdownLast] = triggerDropdownList - dropdownTestMenu.addEventListener('shown.bs.dropdown', () => { - expect(first.classList.contains('show')).toEqual(true) + triggerDropdownFirst.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdownFirst.classList.contains('show')).toEqual(true) expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(1) document.body.click() }) - dropdownTestMenu.addEventListener('hidden.bs.dropdown', () => { + triggerDropdownFirst.addEventListener('hidden.bs.dropdown', () => { expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(0) - last.click() + triggerDropdownLast.click() }) - btnGroup.addEventListener('shown.bs.dropdown', () => { - expect(last.classList.contains('show')).toEqual(true) + triggerDropdownLast.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdownLast.classList.contains('show')).toEqual(true) expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(1) document.body.click() }) - btnGroup.addEventListener('hidden.bs.dropdown', () => { + triggerDropdownLast.addEventListener('hidden.bs.dropdown', () => { expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(0) done() }) - first.click() + triggerDropdownFirst.click() }) it('should remove "show" class if body if tabbing outside of menu, with multiple dropdowns', done => { @@ -1156,13 +1321,10 @@ describe('Dropdown', () => { expect(triggerDropdownList.length).toEqual(2) - const first = triggerDropdownList[0] - const last = triggerDropdownList[1] - const dropdownTestMenu = first.parentNode - const btnGroup = last.parentNode + const [triggerDropdownFirst, triggerDropdownLast] = triggerDropdownList - dropdownTestMenu.addEventListener('shown.bs.dropdown', () => { - expect(first.classList.contains('show')).toEqual(true, '"show" class added on click') + triggerDropdownFirst.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdownFirst.classList.contains('show')).toEqual(true, '"show" class added on click') expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(1, 'only one dropdown is shown') const keyup = createEvent('keyup') @@ -1171,13 +1333,13 @@ describe('Dropdown', () => { document.dispatchEvent(keyup) }) - dropdownTestMenu.addEventListener('hidden.bs.dropdown', () => { + triggerDropdownFirst.addEventListener('hidden.bs.dropdown', () => { expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(0, '"show" class removed') - last.click() + triggerDropdownLast.click() }) - btnGroup.addEventListener('shown.bs.dropdown', () => { - expect(last.classList.contains('show')).toEqual(true, '"show" class added on click') + triggerDropdownLast.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdownLast.classList.contains('show')).toEqual(true, '"show" class added on click') expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(1, 'only one dropdown is shown') const keyup = createEvent('keyup') @@ -1186,12 +1348,12 @@ describe('Dropdown', () => { document.dispatchEvent(keyup) }) - btnGroup.addEventListener('hidden.bs.dropdown', () => { + triggerDropdownLast.addEventListener('hidden.bs.dropdown', () => { expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(0, '"show" class removed') done() }) - first.click() + triggerDropdownFirst.click() }) it('should fire hide and hidden event without a clickEvent if event type is not click', done => { @@ -1205,18 +1367,17 @@ describe('Dropdown', () => { ].join('') const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = fixtureEl.querySelector('.dropdown') - dropdown.addEventListener('hide.bs.dropdown', e => { + triggerDropdown.addEventListener('hide.bs.dropdown', e => { expect(e.clickEvent).toBeUndefined() }) - dropdown.addEventListener('hidden.bs.dropdown', e => { + triggerDropdown.addEventListener('hidden.bs.dropdown', e => { expect(e.clickEvent).toBeUndefined() done() }) - dropdown.addEventListener('shown.bs.dropdown', () => { + triggerDropdown.addEventListener('shown.bs.dropdown', () => { const keydown = createEvent('keydown') keydown.key = 'Escape' @@ -1226,6 +1387,42 @@ describe('Dropdown', () => { triggerDropdown.click() }) + it('should bubble up the events to the parent elements', done => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#subMenu">Sub menu</a>', + ' </div>', + '</div>' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownParent = fixtureEl.querySelector('.dropdown') + const dropdown = new Dropdown(triggerDropdown) + + const showFunction = jasmine.createSpy('showFunction') + dropdownParent.addEventListener('show.bs.dropdown', showFunction) + + const shownFunction = jasmine.createSpy('shownFunction') + dropdownParent.addEventListener('shown.bs.dropdown', () => { + shownFunction() + dropdown.hide() + }) + + const hideFunction = jasmine.createSpy('hideFunction') + dropdownParent.addEventListener('hide.bs.dropdown', hideFunction) + + dropdownParent.addEventListener('hidden.bs.dropdown', () => { + expect(showFunction).toHaveBeenCalled() + expect(shownFunction).toHaveBeenCalled() + expect(hideFunction).toHaveBeenCalled() + done() + }) + + dropdown.show() + }) + it('should ignore keyboard events within <input>s and <textarea>s', done => { fixtureEl.innerHTML = [ '<div class="dropdown">', @@ -1239,11 +1436,10 @@ describe('Dropdown', () => { ].join('') const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = fixtureEl.querySelector('.dropdown') const input = fixtureEl.querySelector('input') const textarea = fixtureEl.querySelector('textarea') - dropdown.addEventListener('shown.bs.dropdown', () => { + triggerDropdown.addEventListener('shown.bs.dropdown', () => { input.focus() const keydown = createEvent('keydown') @@ -1275,9 +1471,8 @@ describe('Dropdown', () => { ].join('') const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = fixtureEl.querySelector('.dropdown') - dropdown.addEventListener('shown.bs.dropdown', () => { + triggerDropdown.addEventListener('shown.bs.dropdown', () => { const keydown = createEvent('keydown') keydown.key = 'ArrowDown' @@ -1311,9 +1506,8 @@ describe('Dropdown', () => { ].join('') const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = fixtureEl.querySelector('.dropdown') - dropdown.addEventListener('shown.bs.dropdown', () => { + triggerDropdown.addEventListener('shown.bs.dropdown', () => { const keydown = createEvent('keydown') keydown.key = 'ArrowDown' @@ -1341,11 +1535,10 @@ describe('Dropdown', () => { ].join('') const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = fixtureEl.querySelector('.dropdown') const item1 = fixtureEl.querySelector('#item1') const item2 = fixtureEl.querySelector('#item2') - dropdown.addEventListener('shown.bs.dropdown', () => { + triggerDropdown.addEventListener('shown.bs.dropdown', () => { const keydownArrowDown = createEvent('keydown') keydownArrowDown.key = 'ArrowDown' @@ -1379,10 +1572,9 @@ describe('Dropdown', () => { ].join('') const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = fixtureEl.querySelector('.dropdown') const item1 = fixtureEl.querySelector('#item1') - dropdown.addEventListener('shown.bs.dropdown', () => { + triggerDropdown.addEventListener('shown.bs.dropdown', () => { const keydown = createEvent('keydown') keydown.key = 'ArrowUp' @@ -1406,7 +1598,6 @@ describe('Dropdown', () => { ].join('') const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = fixtureEl.querySelector('.dropdown') const input = fixtureEl.querySelector('input') input.addEventListener('click', () => { @@ -1414,7 +1605,7 @@ describe('Dropdown', () => { done() }) - dropdown.addEventListener('shown.bs.dropdown', () => { + triggerDropdown.addEventListener('shown.bs.dropdown', () => { expect(triggerDropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') input.dispatchEvent(createEvent('click')) }) @@ -1433,7 +1624,6 @@ describe('Dropdown', () => { ].join('') const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = fixtureEl.querySelector('.dropdown') const textarea = fixtureEl.querySelector('textarea') textarea.addEventListener('click', () => { @@ -1441,7 +1631,7 @@ describe('Dropdown', () => { done() }) - dropdown.addEventListener('shown.bs.dropdown', () => { + triggerDropdown.addEventListener('shown.bs.dropdown', () => { expect(triggerDropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') textarea.dispatchEvent(createEvent('click')) }) @@ -1462,7 +1652,6 @@ describe('Dropdown', () => { ].join('') const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = fixtureEl.querySelector('.dropdown') const input = fixtureEl.querySelector('input') const textarea = fixtureEl.querySelector('textarea') @@ -1478,7 +1667,7 @@ describe('Dropdown', () => { const keydownEscape = createEvent('keydown') keydownEscape.key = 'Escape' - dropdown.addEventListener('shown.bs.dropdown', () => { + triggerDropdown.addEventListener('shown.bs.dropdown', () => { // Key Space input.focus() input.dispatchEvent(keydownSpace) @@ -1557,6 +1746,133 @@ describe('Dropdown', () => { done() }, 20) }) + + it('should propagate escape key events if dropdown is closed', done => { + fixtureEl.innerHTML = [ + '<div class="parent">', + ' <div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Some Item</a>', + ' </div>', + ' </div>', + '</div>' + ] + + const parent = fixtureEl.querySelector('.parent') + const toggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + + const parentKeyHandler = jasmine.createSpy('parentKeyHandler') + + parent.addEventListener('keydown', parentKeyHandler) + parent.addEventListener('keyup', () => { + expect(parentKeyHandler).toHaveBeenCalled() + done() + }) + + const keydownEscape = createEvent('keydown', { bubbles: true }) + keydownEscape.key = 'Escape' + const keyupEscape = createEvent('keyup', { bubbles: true }) + keyupEscape.key = 'Escape' + + toggle.focus() + toggle.dispatchEvent(keydownEscape) + toggle.dispatchEvent(keyupEscape) + }) + + it('should close dropdown (only) by clicking inside the dropdown menu when it has data-attribute `data-bs-auto-close="inside"`', done => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="inside">Dropdown toggle</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Dropdown item</a>', + ' </div>', + '</div>' + ] + + const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + + const expectDropdownToBeOpened = () => setTimeout(() => { + expect(dropdownToggle.classList.contains('show')).toEqual(true) + dropdownMenu.click() + }, 150) + + dropdownToggle.addEventListener('shown.bs.dropdown', () => { + document.documentElement.click() + expectDropdownToBeOpened() + }) + + dropdownToggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => { + expect(dropdownToggle.classList.contains('show')).toEqual(false) + done() + })) + + dropdownToggle.click() + }) + + it('should close dropdown (only) by clicking outside the dropdown menu when it has data-attribute `data-bs-auto-close="outside"`', done => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="outside">Dropdown toggle</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Dropdown item</a>', + ' </div>', + '</div>' + ] + + const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + + const expectDropdownToBeOpened = () => setTimeout(() => { + expect(dropdownToggle.classList.contains('show')).toEqual(true) + document.documentElement.click() + }, 150) + + dropdownToggle.addEventListener('shown.bs.dropdown', () => { + dropdownMenu.click() + expectDropdownToBeOpened() + }) + + dropdownToggle.addEventListener('hidden.bs.dropdown', () => { + expect(dropdownToggle.classList.contains('show')).toEqual(false) + done() + }) + + dropdownToggle.click() + }) + + it('should not close dropdown by clicking inside or outside the dropdown menu when it has data-attribute `data-bs-auto-close="false"`', done => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="false">Dropdown toggle</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Dropdown item</a>', + ' </div>', + '</div>' + ] + + const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + + const expectDropdownToBeOpened = (shouldTriggerClick = true) => setTimeout(() => { + expect(dropdownToggle.classList.contains('show')).toEqual(true) + if (shouldTriggerClick) { + document.documentElement.click() + } else { + done() + } + + expectDropdownToBeOpened(false) + }, 150) + + dropdownToggle.addEventListener('shown.bs.dropdown', () => { + dropdownMenu.click() + expectDropdownToBeOpened() + }) + + dropdownToggle.click() + }) }) describe('jQueryInterface', () => { @@ -1596,11 +1912,9 @@ describe('Dropdown', () => { jQueryMock.fn.dropdown = Dropdown.jQueryInterface jQueryMock.elements = [div] - try { + expect(() => { jQueryMock.fn.dropdown.call(jQueryMock, action) - } catch (error) { - expect(error.message).toEqual(`No method named "${action}"`) - } + }).toThrowError(TypeError, `No method named "${action}"`) }) }) @@ -1623,4 +1937,99 @@ describe('Dropdown', () => { expect(Dropdown.getInstance(div)).toEqual(null) }) }) + + it('should open dropdown when pressing keydown or keyup', done => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item disabled" href="#sub1">Submenu 1</a>', + ' <button class="dropdown-item" type="button" disabled>Disabled button</button>', + ' <a id="item1" class="dropdown-item" href="#">Another link</a>', + ' </div>', + '</div>' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = fixtureEl.querySelector('.dropdown') + + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' + + const keyup = createEvent('keyup') + keyup.key = 'ArrowUp' + + const handleArrowDown = () => { + expect(triggerDropdown.classList.contains('show')).toEqual(true) + expect(triggerDropdown.getAttribute('aria-expanded')).toEqual('true') + setTimeout(() => { + dropdown.hide() + keydown.key = 'ArrowUp' + triggerDropdown.dispatchEvent(keyup) + }, 20) + } + + const handleArrowUp = () => { + expect(triggerDropdown.classList.contains('show')).toEqual(true) + expect(triggerDropdown.getAttribute('aria-expanded')).toEqual('true') + done() + } + + dropdown.addEventListener('shown.bs.dropdown', event => { + if (event.target.key === 'ArrowDown') { + handleArrowDown() + } else { + handleArrowUp() + } + }) + + triggerDropdown.dispatchEvent(keydown) + }) + + it('should allow `data-bs-toggle="dropdown"` click events to bubble up', () => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const clickListener = jasmine.createSpy('clickListener') + const delegatedClickListener = jasmine.createSpy('delegatedClickListener') + + btnDropdown.addEventListener('click', clickListener) + document.addEventListener('click', delegatedClickListener) + + btnDropdown.click() + + expect(clickListener).toHaveBeenCalled() + expect(delegatedClickListener).toHaveBeenCalled() + }) + + it('should open the dropdown when clicking the child element inside `data-bs-toggle="dropdown"`', done => { + fixtureEl.innerHTML = [ + '<div class="container">', + ' <div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown"><span id="childElement">Dropdown</span></button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#subMenu">Sub menu</a>', + ' </div>', + ' </div>', + '</div>' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const childElement = fixtureEl.querySelector('#childElement') + + btnDropdown.addEventListener('shown.bs.dropdown', () => setTimeout(() => { + expect(btnDropdown.classList.contains('show')).toEqual(true) + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + done() + })) + + childElement.click() + }) }) |
