dropdown.spec.js 73 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142
  1. import Dropdown from '../../src/dropdown'
  2. import EventHandler from '../../src/dom/event-handler'
  3. import { noop } from '../../src/util'
  4. /** Test helpers */
  5. import { clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fixture'
  6. describe('Dropdown', () => {
  7. let fixtureEl
  8. beforeAll(() => {
  9. fixtureEl = getFixture()
  10. })
  11. afterEach(() => {
  12. clearFixture()
  13. })
  14. describe('VERSION', () => {
  15. it('should return plugin version', () => {
  16. expect(Dropdown.VERSION).toEqual(jasmine.any(String))
  17. })
  18. })
  19. describe('Default', () => {
  20. it('should return plugin default config', () => {
  21. expect(Dropdown.Default).toEqual(jasmine.any(Object))
  22. })
  23. })
  24. describe('DefaultType', () => {
  25. it('should return plugin default type config', () => {
  26. expect(Dropdown.DefaultType).toEqual(jasmine.any(Object))
  27. })
  28. })
  29. describe('DATA_KEY', () => {
  30. it('should return plugin data key', () => {
  31. expect(Dropdown.DATA_KEY).toEqual('bs.dropdown')
  32. })
  33. })
  34. describe('constructor', () => {
  35. it('should take care of element either passed as a CSS selector or DOM element', () => {
  36. fixtureEl.innerHTML = [
  37. '<div class="dropdown">',
  38. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  39. ' <div class="dropdown-menu">',
  40. ' <a class="dropdown-item" href="#">Link</a>',
  41. ' </div>',
  42. '</div>'
  43. ].join('')
  44. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  45. const dropdownBySelector = new Dropdown('[data-bs-toggle="dropdown"]')
  46. const dropdownByElement = new Dropdown(btnDropdown)
  47. expect(dropdownBySelector._element).toEqual(btnDropdown)
  48. expect(dropdownByElement._element).toEqual(btnDropdown)
  49. })
  50. it('should add a listener on trigger which do not have data-bs-toggle="dropdown"', () => {
  51. fixtureEl.innerHTML = [
  52. '<div class="dropdown">',
  53. ' <button class="btn">Dropdown</button>',
  54. ' <div class="dropdown-menu">',
  55. ' <a class="dropdown-item" href="#">Secondary link</a>',
  56. ' </div>',
  57. '</div>'
  58. ].join('')
  59. const btnDropdown = fixtureEl.querySelector('.btn')
  60. const dropdown = new Dropdown(btnDropdown)
  61. spyOn(dropdown, 'toggle')
  62. btnDropdown.click()
  63. expect(dropdown.toggle).toHaveBeenCalled()
  64. })
  65. it('should create offset modifier correctly when offset option is a function', done => {
  66. fixtureEl.innerHTML = [
  67. '<div class="dropdown">',
  68. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  69. ' <div class="dropdown-menu">',
  70. ' <a class="dropdown-item" href="#">Secondary link</a>',
  71. ' </div>',
  72. '</div>'
  73. ].join('')
  74. const getOffset = jasmine.createSpy('getOffset').and.returnValue([10, 20])
  75. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  76. const dropdown = new Dropdown(btnDropdown, {
  77. offset: getOffset,
  78. popperConfig: {
  79. onFirstUpdate: state => {
  80. expect(getOffset).toHaveBeenCalledWith({
  81. popper: state.rects.popper,
  82. reference: state.rects.reference,
  83. placement: state.placement
  84. }, btnDropdown)
  85. done()
  86. }
  87. }
  88. })
  89. const offset = dropdown._getOffset()
  90. expect(typeof offset).toEqual('function')
  91. dropdown.show()
  92. })
  93. it('should create offset modifier correctly when offset option is a string into data attribute', () => {
  94. fixtureEl.innerHTML = [
  95. '<div class="dropdown">',
  96. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-offset="10,20">Dropdown</button>',
  97. ' <div class="dropdown-menu">',
  98. ' <a class="dropdown-item" href="#">Secondary link</a>',
  99. ' </div>',
  100. '</div>'
  101. ].join('')
  102. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  103. const dropdown = new Dropdown(btnDropdown)
  104. expect(dropdown._getOffset()).toEqual([10, 20])
  105. })
  106. it('should allow to pass config to Popper with `popperConfig`', () => {
  107. fixtureEl.innerHTML = [
  108. '<div class="dropdown">',
  109. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  110. ' <div class="dropdown-menu">',
  111. ' <a class="dropdown-item" href="#">Secondary link</a>',
  112. ' </div>',
  113. '</div>'
  114. ].join('')
  115. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  116. const dropdown = new Dropdown(btnDropdown, {
  117. popperConfig: {
  118. placement: 'left'
  119. }
  120. })
  121. const popperConfig = dropdown._getPopperConfig()
  122. expect(popperConfig.placement).toEqual('left')
  123. })
  124. it('should allow to pass config to Popper with `popperConfig` as a function', () => {
  125. fixtureEl.innerHTML = [
  126. '<div class="dropdown">',
  127. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-placement="right" >Dropdown</button>',
  128. ' <div class="dropdown-menu">',
  129. ' <a class="dropdown-item" href="#">Secondary link</a>',
  130. ' </div>',
  131. '</div>'
  132. ].join('')
  133. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  134. const getPopperConfig = jasmine.createSpy('getPopperConfig').and.returnValue({ placement: 'left' })
  135. const dropdown = new Dropdown(btnDropdown, {
  136. popperConfig: getPopperConfig
  137. })
  138. const popperConfig = dropdown._getPopperConfig()
  139. expect(getPopperConfig).toHaveBeenCalled()
  140. expect(popperConfig.placement).toEqual('left')
  141. })
  142. })
  143. describe('toggle', () => {
  144. it('should toggle a dropdown', done => {
  145. fixtureEl.innerHTML = [
  146. '<div class="dropdown">',
  147. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>',
  148. ' <div class="dropdown-menu">',
  149. ' <a class="dropdown-item" href="#">Secondary link</a>',
  150. ' </div>',
  151. '</div>'
  152. ].join('')
  153. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  154. const dropdown = new Dropdown(btnDropdown)
  155. btnDropdown.addEventListener('shown.bs.dropdown', () => {
  156. expect(btnDropdown.classList.contains('show')).toEqual(true)
  157. expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true')
  158. done()
  159. })
  160. dropdown.toggle()
  161. })
  162. it('should destroy old popper references on toggle', done => {
  163. fixtureEl.innerHTML = [
  164. '<div class="first dropdown">',
  165. ' <button class="firstBtn btn" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>',
  166. ' <div class="dropdown-menu">',
  167. ' <a class="dropdown-item" href="#">Secondary link</a>',
  168. ' </div>',
  169. '</div>',
  170. '<div class="second dropdown">',
  171. ' <button class="secondBtn btn" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>',
  172. ' <div class="dropdown-menu">',
  173. ' <a class="dropdown-item" href="#">Secondary link</a>',
  174. ' </div>',
  175. '</div>'
  176. ].join('')
  177. const btnDropdown1 = fixtureEl.querySelector('.firstBtn')
  178. const btnDropdown2 = fixtureEl.querySelector('.secondBtn')
  179. const firstDropdownEl = fixtureEl.querySelector('.first')
  180. const secondDropdownEl = fixtureEl.querySelector('.second')
  181. const dropdown1 = new Dropdown(btnDropdown1)
  182. firstDropdownEl.addEventListener('shown.bs.dropdown', () => {
  183. expect(btnDropdown1.classList.contains('show')).toEqual(true)
  184. spyOn(dropdown1._popper, 'destroy')
  185. btnDropdown2.click()
  186. })
  187. secondDropdownEl.addEventListener('shown.bs.dropdown', () => setTimeout(() => {
  188. expect(dropdown1._popper.destroy).toHaveBeenCalled()
  189. done()
  190. }))
  191. dropdown1.toggle()
  192. })
  193. it('should toggle a dropdown and add/remove event listener on mobile', done => {
  194. fixtureEl.innerHTML = [
  195. '<div class="dropdown">',
  196. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>',
  197. ' <div class="dropdown-menu">',
  198. ' <a class="dropdown-item" href="#">Secondary link</a>',
  199. ' </div>',
  200. '</div>'
  201. ].join('')
  202. const defaultValueOnTouchStart = document.documentElement.ontouchstart
  203. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  204. const dropdown = new Dropdown(btnDropdown)
  205. document.documentElement.ontouchstart = () => {}
  206. spyOn(EventHandler, 'on')
  207. spyOn(EventHandler, 'off')
  208. btnDropdown.addEventListener('shown.bs.dropdown', () => {
  209. expect(btnDropdown.classList.contains('show')).toEqual(true)
  210. expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true')
  211. expect(EventHandler.on).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop)
  212. dropdown.toggle()
  213. })
  214. btnDropdown.addEventListener('hidden.bs.dropdown', () => {
  215. expect(btnDropdown.classList.contains('show')).toEqual(false)
  216. expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false')
  217. expect(EventHandler.off).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop)
  218. document.documentElement.ontouchstart = defaultValueOnTouchStart
  219. done()
  220. })
  221. dropdown.toggle()
  222. })
  223. it('should toggle a dropdown at the right', done => {
  224. fixtureEl.innerHTML = [
  225. '<div class="dropdown">',
  226. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>',
  227. ' <div class="dropdown-menu dropdown-menu-end">',
  228. ' <a class="dropdown-item" href="#">Secondary link</a>',
  229. ' </div>',
  230. '</div>'
  231. ].join('')
  232. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  233. const dropdown = new Dropdown(btnDropdown)
  234. btnDropdown.addEventListener('shown.bs.dropdown', () => {
  235. expect(btnDropdown.classList.contains('show')).toEqual(true)
  236. expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true')
  237. done()
  238. })
  239. dropdown.toggle()
  240. })
  241. it('should toggle a dropup', done => {
  242. fixtureEl.innerHTML = [
  243. '<div class="dropup">',
  244. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>',
  245. ' <div class="dropdown-menu">',
  246. ' <a class="dropdown-item" href="#">Secondary link</a>',
  247. ' </div>',
  248. '</div>'
  249. ].join('')
  250. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  251. const dropupEl = fixtureEl.querySelector('.dropup')
  252. const dropdown = new Dropdown(btnDropdown)
  253. dropupEl.addEventListener('shown.bs.dropdown', () => {
  254. expect(btnDropdown.classList.contains('show')).toEqual(true)
  255. expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true')
  256. done()
  257. })
  258. dropdown.toggle()
  259. })
  260. it('should toggle a dropup at the right', done => {
  261. fixtureEl.innerHTML = [
  262. '<div class="dropup">',
  263. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>',
  264. ' <div class="dropdown-menu dropdown-menu-end">',
  265. ' <a class="dropdown-item" href="#">Secondary link</a>',
  266. ' </div>',
  267. '</div>'
  268. ].join('')
  269. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  270. const dropupEl = fixtureEl.querySelector('.dropup')
  271. const dropdown = new Dropdown(btnDropdown)
  272. dropupEl.addEventListener('shown.bs.dropdown', () => {
  273. expect(btnDropdown.classList.contains('show')).toEqual(true)
  274. expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true')
  275. done()
  276. })
  277. dropdown.toggle()
  278. })
  279. it('should toggle a dropend', done => {
  280. fixtureEl.innerHTML = [
  281. '<div class="dropend">',
  282. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>',
  283. ' <div class="dropdown-menu">',
  284. ' <a class="dropdown-item" href="#">Secondary link</a>',
  285. ' </div>',
  286. '</div>'
  287. ].join('')
  288. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  289. const dropendEl = fixtureEl.querySelector('.dropend')
  290. const dropdown = new Dropdown(btnDropdown)
  291. dropendEl.addEventListener('shown.bs.dropdown', () => {
  292. expect(btnDropdown.classList.contains('show')).toEqual(true)
  293. expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true')
  294. done()
  295. })
  296. dropdown.toggle()
  297. })
  298. it('should toggle a dropstart', done => {
  299. fixtureEl.innerHTML = [
  300. '<div class="dropstart">',
  301. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>',
  302. ' <div class="dropdown-menu">',
  303. ' <a class="dropdown-item" href="#">Secondary link</a>',
  304. ' </div>',
  305. '</div>'
  306. ].join('')
  307. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  308. const dropstartEl = fixtureEl.querySelector('.dropstart')
  309. const dropdown = new Dropdown(btnDropdown)
  310. dropstartEl.addEventListener('shown.bs.dropdown', () => {
  311. expect(btnDropdown.classList.contains('show')).toEqual(true)
  312. expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true')
  313. done()
  314. })
  315. dropdown.toggle()
  316. })
  317. it('should toggle a dropdown with parent reference', done => {
  318. fixtureEl.innerHTML = [
  319. '<div class="dropdown">',
  320. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>',
  321. ' <div class="dropdown-menu">',
  322. ' <a class="dropdown-item" href="#">Secondary link</a>',
  323. ' </div>',
  324. '</div>'
  325. ].join('')
  326. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  327. const dropdown = new Dropdown(btnDropdown, {
  328. reference: 'parent'
  329. })
  330. btnDropdown.addEventListener('shown.bs.dropdown', () => {
  331. expect(btnDropdown.classList.contains('show')).toEqual(true)
  332. expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true')
  333. done()
  334. })
  335. dropdown.toggle()
  336. })
  337. it('should toggle a dropdown with a dom node reference', done => {
  338. fixtureEl.innerHTML = [
  339. '<div class="dropdown">',
  340. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>',
  341. ' <div class="dropdown-menu">',
  342. ' <a class="dropdown-item" href="#">Secondary link</a>',
  343. ' </div>',
  344. '</div>'
  345. ].join('')
  346. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  347. const dropdown = new Dropdown(btnDropdown, {
  348. reference: fixtureEl
  349. })
  350. btnDropdown.addEventListener('shown.bs.dropdown', () => {
  351. expect(btnDropdown.classList.contains('show')).toEqual(true)
  352. expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true')
  353. done()
  354. })
  355. dropdown.toggle()
  356. })
  357. it('should toggle a dropdown with a jquery object reference', done => {
  358. fixtureEl.innerHTML = [
  359. '<div class="dropdown">',
  360. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>',
  361. ' <div class="dropdown-menu">',
  362. ' <a class="dropdown-item" href="#">Secondary link</a>',
  363. ' </div>',
  364. '</div>'
  365. ].join('')
  366. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  367. const dropdown = new Dropdown(btnDropdown, {
  368. reference: { 0: fixtureEl, jquery: 'jQuery' }
  369. })
  370. btnDropdown.addEventListener('shown.bs.dropdown', () => {
  371. expect(btnDropdown.classList.contains('show')).toEqual(true)
  372. expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true')
  373. done()
  374. })
  375. dropdown.toggle()
  376. })
  377. it('should toggle a dropdown with a valid virtual element reference', done => {
  378. fixtureEl.innerHTML = [
  379. '<div class="dropdown">',
  380. ' <button class="btn dropdown-toggle visually-hidden" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>',
  381. ' <div class="dropdown-menu">',
  382. ' <a class="dropdown-item" href="#">Secondary link</a>',
  383. ' </div>',
  384. '</div>'
  385. ].join('')
  386. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  387. const virtualElement = {
  388. nodeType: 1,
  389. getBoundingClientRect() {
  390. return {
  391. width: 0,
  392. height: 0,
  393. top: 0,
  394. right: 0,
  395. bottom: 0,
  396. left: 0
  397. }
  398. }
  399. }
  400. expect(() => new Dropdown(btnDropdown, {
  401. reference: {}
  402. })).toThrowError(TypeError, 'DROPDOWN: Option "reference" provided type "object" without a required "getBoundingClientRect" method.')
  403. expect(() => new Dropdown(btnDropdown, {
  404. reference: {
  405. getBoundingClientRect: 'not-a-function'
  406. }
  407. })).toThrowError(TypeError, 'DROPDOWN: Option "reference" provided type "object" without a required "getBoundingClientRect" method.')
  408. // use onFirstUpdate as Poppers internal update is executed async
  409. const dropdown = new Dropdown(btnDropdown, {
  410. reference: virtualElement,
  411. popperConfig: {
  412. onFirstUpdate() {
  413. expect(virtualElement.getBoundingClientRect).toHaveBeenCalled()
  414. expect(btnDropdown.classList.contains('show')).toEqual(true)
  415. expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true')
  416. done()
  417. }
  418. }
  419. })
  420. spyOn(virtualElement, 'getBoundingClientRect').and.callThrough()
  421. dropdown.toggle()
  422. })
  423. it('should not toggle a dropdown if the element is disabled', done => {
  424. fixtureEl.innerHTML = [
  425. '<div class="dropdown">',
  426. ' <button disabled class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  427. ' <div class="dropdown-menu">',
  428. ' <a class="dropdown-item" href="#">Secondary link</a>',
  429. ' </div>',
  430. '</div>'
  431. ].join('')
  432. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  433. const dropdown = new Dropdown(btnDropdown)
  434. btnDropdown.addEventListener('shown.bs.dropdown', () => {
  435. throw new Error('should not throw shown.bs.dropdown event')
  436. })
  437. dropdown.toggle()
  438. setTimeout(() => {
  439. expect().nothing()
  440. done()
  441. })
  442. })
  443. it('should not toggle a dropdown if the element contains .disabled', done => {
  444. fixtureEl.innerHTML = [
  445. '<div class="dropdown">',
  446. ' <button class="btn dropdown-toggle disabled" data-bs-toggle="dropdown">Dropdown</button>',
  447. ' <div class="dropdown-menu">',
  448. ' <a class="dropdown-item" href="#">Secondary link</a>',
  449. ' </div>',
  450. '</div>'
  451. ].join('')
  452. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  453. const dropdown = new Dropdown(btnDropdown)
  454. btnDropdown.addEventListener('shown.bs.dropdown', () => {
  455. throw new Error('should not throw shown.bs.dropdown event')
  456. })
  457. dropdown.toggle()
  458. setTimeout(() => {
  459. expect().nothing()
  460. done()
  461. })
  462. })
  463. it('should not toggle a dropdown if the menu is shown', done => {
  464. fixtureEl.innerHTML = [
  465. '<div class="dropdown">',
  466. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  467. ' <div class="dropdown-menu show">',
  468. ' <a class="dropdown-item" href="#">Secondary link</a>',
  469. ' </div>',
  470. '</div>'
  471. ].join('')
  472. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  473. const dropdown = new Dropdown(btnDropdown)
  474. btnDropdown.addEventListener('shown.bs.dropdown', () => {
  475. throw new Error('should not throw shown.bs.dropdown event')
  476. })
  477. dropdown.toggle()
  478. setTimeout(() => {
  479. expect().nothing()
  480. done()
  481. })
  482. })
  483. it('should not toggle a dropdown if show event is prevented', done => {
  484. fixtureEl.innerHTML = [
  485. '<div class="dropdown">',
  486. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  487. ' <div class="dropdown-menu">',
  488. ' <a class="dropdown-item" href="#">Secondary link</a>',
  489. ' </div>',
  490. '</div>'
  491. ].join('')
  492. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  493. const dropdown = new Dropdown(btnDropdown)
  494. btnDropdown.addEventListener('show.bs.dropdown', e => {
  495. e.preventDefault()
  496. })
  497. btnDropdown.addEventListener('shown.bs.dropdown', () => {
  498. throw new Error('should not throw shown.bs.dropdown event')
  499. })
  500. dropdown.toggle()
  501. setTimeout(() => {
  502. expect().nothing()
  503. done()
  504. })
  505. })
  506. })
  507. describe('show', () => {
  508. it('should show a dropdown', done => {
  509. fixtureEl.innerHTML = [
  510. '<div class="dropdown">',
  511. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  512. ' <div class="dropdown-menu">',
  513. ' <a class="dropdown-item" href="#">Secondary link</a>',
  514. ' </div>',
  515. '</div>'
  516. ].join('')
  517. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  518. const dropdown = new Dropdown(btnDropdown)
  519. btnDropdown.addEventListener('shown.bs.dropdown', () => {
  520. expect(btnDropdown.classList.contains('show')).toEqual(true)
  521. done()
  522. })
  523. dropdown.show()
  524. })
  525. it('should not show a dropdown if the element is disabled', done => {
  526. fixtureEl.innerHTML = [
  527. '<div class="dropdown">',
  528. ' <button disabled class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  529. ' <div class="dropdown-menu">',
  530. ' <a class="dropdown-item" href="#">Secondary link</a>',
  531. ' </div>',
  532. '</div>'
  533. ].join('')
  534. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  535. const dropdown = new Dropdown(btnDropdown)
  536. btnDropdown.addEventListener('shown.bs.dropdown', () => {
  537. throw new Error('should not throw shown.bs.dropdown event')
  538. })
  539. dropdown.show()
  540. setTimeout(() => {
  541. expect().nothing()
  542. done()
  543. }, 10)
  544. })
  545. it('should not show a dropdown if the element contains .disabled', done => {
  546. fixtureEl.innerHTML = [
  547. '<div class="dropdown">',
  548. ' <button class="btn dropdown-toggle disabled" data-bs-toggle="dropdown">Dropdown</button>',
  549. ' <div class="dropdown-menu">',
  550. ' <a class="dropdown-item" href="#">Secondary link</a>',
  551. ' </div>',
  552. '</div>'
  553. ].join('')
  554. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  555. const dropdown = new Dropdown(btnDropdown)
  556. btnDropdown.addEventListener('shown.bs.dropdown', () => {
  557. throw new Error('should not throw shown.bs.dropdown event')
  558. })
  559. dropdown.show()
  560. setTimeout(() => {
  561. expect().nothing()
  562. done()
  563. }, 10)
  564. })
  565. it('should not show a dropdown if the menu is shown', done => {
  566. fixtureEl.innerHTML = [
  567. '<div class="dropdown">',
  568. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  569. ' <div class="dropdown-menu show">',
  570. ' <a class="dropdown-item" href="#">Secondary link</a>',
  571. ' </div>',
  572. '</div>'
  573. ].join('')
  574. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  575. const dropdown = new Dropdown(btnDropdown)
  576. btnDropdown.addEventListener('shown.bs.dropdown', () => {
  577. throw new Error('should not throw shown.bs.dropdown event')
  578. })
  579. dropdown.show()
  580. setTimeout(() => {
  581. expect().nothing()
  582. done()
  583. }, 10)
  584. })
  585. it('should not show a dropdown if show event is prevented', done => {
  586. fixtureEl.innerHTML = [
  587. '<div class="dropdown">',
  588. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  589. ' <div class="dropdown-menu">',
  590. ' <a class="dropdown-item" href="#">Secondary link</a>',
  591. ' </div>',
  592. '</div>'
  593. ].join('')
  594. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  595. const dropdown = new Dropdown(btnDropdown)
  596. btnDropdown.addEventListener('show.bs.dropdown', e => {
  597. e.preventDefault()
  598. })
  599. btnDropdown.addEventListener('shown.bs.dropdown', () => {
  600. throw new Error('should not throw shown.bs.dropdown event')
  601. })
  602. dropdown.show()
  603. setTimeout(() => {
  604. expect().nothing()
  605. done()
  606. }, 10)
  607. })
  608. })
  609. describe('hide', () => {
  610. it('should hide a dropdown', done => {
  611. fixtureEl.innerHTML = [
  612. '<div class="dropdown">',
  613. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="true">Dropdown</button>',
  614. ' <div class="dropdown-menu show">',
  615. ' <a class="dropdown-item" href="#">Secondary link</a>',
  616. ' </div>',
  617. '</div>'
  618. ].join('')
  619. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  620. const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
  621. const dropdown = new Dropdown(btnDropdown)
  622. btnDropdown.addEventListener('hidden.bs.dropdown', () => {
  623. expect(dropdownMenu.classList.contains('show')).toEqual(false)
  624. expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false')
  625. done()
  626. })
  627. dropdown.hide()
  628. })
  629. it('should hide a dropdown and destroy popper', done => {
  630. fixtureEl.innerHTML = [
  631. '<div class="dropdown">',
  632. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  633. ' <div class="dropdown-menu">',
  634. ' <a class="dropdown-item" href="#">Secondary link</a>',
  635. ' </div>',
  636. '</div>'
  637. ].join('')
  638. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  639. const dropdown = new Dropdown(btnDropdown)
  640. btnDropdown.addEventListener('shown.bs.dropdown', () => {
  641. spyOn(dropdown._popper, 'destroy')
  642. dropdown.hide()
  643. })
  644. btnDropdown.addEventListener('hidden.bs.dropdown', () => {
  645. expect(dropdown._popper.destroy).toHaveBeenCalled()
  646. done()
  647. })
  648. dropdown.show()
  649. })
  650. it('should not hide a dropdown if the element is disabled', done => {
  651. fixtureEl.innerHTML = [
  652. '<div class="dropdown">',
  653. ' <button disabled class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  654. ' <div class="dropdown-menu show">',
  655. ' <a class="dropdown-item" href="#">Secondary link</a>',
  656. ' </div>',
  657. '</div>'
  658. ].join('')
  659. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  660. const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
  661. const dropdown = new Dropdown(btnDropdown)
  662. btnDropdown.addEventListener('hidden.bs.dropdown', () => {
  663. throw new Error('should not throw hidden.bs.dropdown event')
  664. })
  665. dropdown.hide()
  666. setTimeout(() => {
  667. expect(dropdownMenu.classList.contains('show')).toEqual(true)
  668. done()
  669. }, 10)
  670. })
  671. it('should not hide a dropdown if the element contains .disabled', done => {
  672. fixtureEl.innerHTML = [
  673. '<div class="dropdown">',
  674. ' <button class="btn dropdown-toggle disabled" data-bs-toggle="dropdown">Dropdown</button>',
  675. ' <div class="dropdown-menu show">',
  676. ' <a class="dropdown-item" href="#">Secondary link</a>',
  677. ' </div>',
  678. '</div>'
  679. ].join('')
  680. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  681. const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
  682. const dropdown = new Dropdown(btnDropdown)
  683. btnDropdown.addEventListener('hidden.bs.dropdown', () => {
  684. throw new Error('should not throw hidden.bs.dropdown event')
  685. })
  686. dropdown.hide()
  687. setTimeout(() => {
  688. expect(dropdownMenu.classList.contains('show')).toEqual(true)
  689. done()
  690. }, 10)
  691. })
  692. it('should not hide a dropdown if the menu is not shown', done => {
  693. fixtureEl.innerHTML = [
  694. '<div class="dropdown">',
  695. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  696. ' <div class="dropdown-menu">',
  697. ' <a class="dropdown-item" href="#">Secondary link</a>',
  698. ' </div>',
  699. '</div>'
  700. ].join('')
  701. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  702. const dropdown = new Dropdown(btnDropdown)
  703. btnDropdown.addEventListener('hidden.bs.dropdown', () => {
  704. throw new Error('should not throw hidden.bs.dropdown event')
  705. })
  706. dropdown.hide()
  707. setTimeout(() => {
  708. expect().nothing()
  709. done()
  710. }, 10)
  711. })
  712. it('should not hide a dropdown if hide event is prevented', done => {
  713. fixtureEl.innerHTML = [
  714. '<div class="dropdown">',
  715. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  716. ' <div class="dropdown-menu show">',
  717. ' <a class="dropdown-item" href="#">Secondary link</a>',
  718. ' </div>',
  719. '</div>'
  720. ].join('')
  721. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  722. const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
  723. const dropdown = new Dropdown(btnDropdown)
  724. btnDropdown.addEventListener('hide.bs.dropdown', e => {
  725. e.preventDefault()
  726. })
  727. btnDropdown.addEventListener('hidden.bs.dropdown', () => {
  728. throw new Error('should not throw hidden.bs.dropdown event')
  729. })
  730. dropdown.hide()
  731. setTimeout(() => {
  732. expect(dropdownMenu.classList.contains('show')).toEqual(true)
  733. done()
  734. })
  735. })
  736. it('should remove event listener on touch-enabled device that was added in show method', done => {
  737. fixtureEl.innerHTML = [
  738. '<div class="dropdown">',
  739. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  740. ' <div class="dropdown-menu">',
  741. ' <a class="dropdown-item" href="#">Dropdwon item</a>',
  742. ' </div>',
  743. '</div>'
  744. ].join('')
  745. const defaultValueOnTouchStart = document.documentElement.ontouchstart
  746. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  747. const dropdown = new Dropdown(btnDropdown)
  748. document.documentElement.ontouchstart = () => {}
  749. spyOn(EventHandler, 'off')
  750. btnDropdown.addEventListener('shown.bs.dropdown', () => {
  751. dropdown.hide()
  752. })
  753. btnDropdown.addEventListener('hidden.bs.dropdown', () => {
  754. expect(btnDropdown.classList.contains('show')).toEqual(false)
  755. expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false')
  756. expect(EventHandler.off).toHaveBeenCalled()
  757. document.documentElement.ontouchstart = defaultValueOnTouchStart
  758. done()
  759. })
  760. dropdown.show()
  761. })
  762. })
  763. describe('dispose', () => {
  764. it('should dispose dropdown', () => {
  765. fixtureEl.innerHTML = [
  766. '<div class="dropdown">',
  767. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  768. ' <div class="dropdown-menu">',
  769. ' <a class="dropdown-item" href="#">Secondary link</a>',
  770. ' </div>',
  771. '</div>'
  772. ].join('')
  773. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  774. spyOn(btnDropdown, 'addEventListener').and.callThrough()
  775. spyOn(btnDropdown, 'removeEventListener').and.callThrough()
  776. const dropdown = new Dropdown(btnDropdown)
  777. expect(dropdown._popper).toBeNull()
  778. expect(dropdown._menu).not.toBeNull()
  779. expect(dropdown._element).not.toBeNull()
  780. expect(btnDropdown.addEventListener).toHaveBeenCalledWith('click', jasmine.any(Function), jasmine.any(Boolean))
  781. dropdown.dispose()
  782. expect(dropdown._menu).toBeNull()
  783. expect(dropdown._element).toBeNull()
  784. expect(btnDropdown.removeEventListener).toHaveBeenCalledWith('click', jasmine.any(Function), jasmine.any(Boolean))
  785. })
  786. it('should dispose dropdown with Popper', () => {
  787. fixtureEl.innerHTML = [
  788. '<div class="dropdown">',
  789. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  790. ' <div class="dropdown-menu">',
  791. ' <a class="dropdown-item" href="#">Secondary link</a>',
  792. ' </div>',
  793. '</div>'
  794. ].join('')
  795. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  796. const dropdown = new Dropdown(btnDropdown)
  797. dropdown.toggle()
  798. expect(dropdown._popper).not.toBeNull()
  799. expect(dropdown._menu).not.toBeNull()
  800. expect(dropdown._element).not.toBeNull()
  801. dropdown.dispose()
  802. expect(dropdown._popper).toBeNull()
  803. expect(dropdown._menu).toBeNull()
  804. expect(dropdown._element).toBeNull()
  805. })
  806. })
  807. describe('update', () => {
  808. it('should call Popper and detect navbar on update', () => {
  809. fixtureEl.innerHTML = [
  810. '<div class="dropdown">',
  811. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  812. ' <div class="dropdown-menu">',
  813. ' <a class="dropdown-item" href="#">Secondary link</a>',
  814. ' </div>',
  815. '</div>'
  816. ].join('')
  817. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  818. const dropdown = new Dropdown(btnDropdown)
  819. dropdown.toggle()
  820. expect(dropdown._popper).not.toBeNull()
  821. spyOn(dropdown._popper, 'update')
  822. spyOn(dropdown, '_detectNavbar')
  823. dropdown.update()
  824. expect(dropdown._popper.update).toHaveBeenCalled()
  825. expect(dropdown._detectNavbar).toHaveBeenCalled()
  826. })
  827. it('should just detect navbar on update', () => {
  828. fixtureEl.innerHTML = [
  829. '<div class="dropdown">',
  830. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  831. ' <div class="dropdown-menu">',
  832. ' <a class="dropdown-item" href="#">Secondary link</a>',
  833. ' </div>',
  834. '</div>'
  835. ].join('')
  836. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  837. const dropdown = new Dropdown(btnDropdown)
  838. spyOn(dropdown, '_detectNavbar')
  839. dropdown.update()
  840. expect(dropdown._popper).toBeNull()
  841. expect(dropdown._detectNavbar).toHaveBeenCalled()
  842. })
  843. })
  844. describe('data-api', () => {
  845. it('should show and hide a dropdown', done => {
  846. fixtureEl.innerHTML = [
  847. '<div class="dropdown">',
  848. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>',
  849. ' <div class="dropdown-menu">',
  850. ' <a class="dropdown-item" href="#">Secondary link</a>',
  851. ' </div>',
  852. '</div>'
  853. ].join('')
  854. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  855. let showEventTriggered = false
  856. let hideEventTriggered = false
  857. btnDropdown.addEventListener('show.bs.dropdown', () => {
  858. showEventTriggered = true
  859. })
  860. btnDropdown.addEventListener('shown.bs.dropdown', e => setTimeout(() => {
  861. expect(btnDropdown.classList.contains('show')).toEqual(true)
  862. expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true')
  863. expect(showEventTriggered).toEqual(true)
  864. expect(e.relatedTarget).toEqual(btnDropdown)
  865. document.body.click()
  866. }))
  867. btnDropdown.addEventListener('hide.bs.dropdown', () => {
  868. hideEventTriggered = true
  869. })
  870. btnDropdown.addEventListener('hidden.bs.dropdown', e => {
  871. expect(btnDropdown.classList.contains('show')).toEqual(false)
  872. expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false')
  873. expect(hideEventTriggered).toEqual(true)
  874. expect(e.relatedTarget).toEqual(btnDropdown)
  875. done()
  876. })
  877. btnDropdown.click()
  878. })
  879. it('should not use Popper in navbar', done => {
  880. fixtureEl.innerHTML = [
  881. '<nav class="navbar navbar-expand-md navbar-light bg-light">',
  882. ' <div class="dropdown">',
  883. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>',
  884. ' <div class="dropdown-menu">',
  885. ' <a class="dropdown-item" href="#">Secondary link</a>',
  886. ' </div>',
  887. ' </div>',
  888. '</nav>'
  889. ].join('')
  890. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  891. const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
  892. const dropdown = new Dropdown(btnDropdown)
  893. btnDropdown.addEventListener('shown.bs.dropdown', () => {
  894. expect(dropdown._popper).toBeNull()
  895. expect(dropdownMenu.getAttribute('style')).toEqual(null, 'no inline style applied by Popper')
  896. done()
  897. })
  898. dropdown.show()
  899. })
  900. it('should not collapse the dropdown when clicking a select option nested in the dropdown', done => {
  901. fixtureEl.innerHTML = [
  902. '<div class="dropdown">',
  903. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>',
  904. ' <div class="dropdown-menu">',
  905. ' <select>',
  906. ' <option selected>Open this select menu</option>',
  907. ' <option value="1">One</option>',
  908. ' </select>',
  909. ' </div>',
  910. '</div>'
  911. ].join('')
  912. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  913. const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
  914. const dropdown = new Dropdown(btnDropdown)
  915. const hideSpy = spyOn(dropdown, '_completeHide')
  916. btnDropdown.addEventListener('shown.bs.dropdown', () => {
  917. const clickEvent = new MouseEvent('click', {
  918. bubbles: true
  919. })
  920. dropdownMenu.querySelector('option').dispatchEvent(clickEvent)
  921. })
  922. dropdownMenu.addEventListener('click', event => {
  923. expect(event.target.tagName).toMatch(/select|option/i)
  924. Dropdown.clearMenus(event)
  925. setTimeout(() => {
  926. expect(hideSpy).not.toHaveBeenCalled()
  927. done()
  928. }, 10)
  929. })
  930. dropdown.show()
  931. })
  932. it('should manage bs attribute `data-bs-popper`="none" when dropdown is in navbar', done => {
  933. fixtureEl.innerHTML = [
  934. '<nav class="navbar navbar-expand-md navbar-light bg-light">',
  935. ' <div class="dropdown">',
  936. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>',
  937. ' <div class="dropdown-menu">',
  938. ' <a class="dropdown-item" href="#">Secondary link</a>',
  939. ' </div>',
  940. ' </div>',
  941. '</nav>'
  942. ].join('')
  943. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  944. const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
  945. const dropdown = new Dropdown(btnDropdown)
  946. btnDropdown.addEventListener('shown.bs.dropdown', () => {
  947. expect(dropdownMenu.getAttribute('data-bs-popper')).toEqual('none')
  948. dropdown.hide()
  949. })
  950. btnDropdown.addEventListener('hidden.bs.dropdown', () => {
  951. expect(dropdownMenu.getAttribute('data-bs-popper')).toBeNull()
  952. done()
  953. })
  954. dropdown.show()
  955. })
  956. it('should not use Popper if display set to static', done => {
  957. fixtureEl.innerHTML = [
  958. '<div class="dropdown">',
  959. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-display="static">Dropdown</button>',
  960. ' <div class="dropdown-menu">',
  961. ' <a class="dropdown-item" href="#">Secondary link</a>',
  962. ' </div>',
  963. '</div>'
  964. ].join('')
  965. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  966. const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
  967. btnDropdown.addEventListener('shown.bs.dropdown', () => {
  968. // Popper adds this attribute when we use it
  969. expect(dropdownMenu.getAttribute('data-popper-placement')).toEqual(null)
  970. done()
  971. })
  972. btnDropdown.click()
  973. })
  974. it('should manage bs attribute `data-bs-popper`="static" when display set to static', done => {
  975. fixtureEl.innerHTML = [
  976. '<div class="dropdown">',
  977. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-display="static">Dropdown</button>',
  978. ' <div class="dropdown-menu">',
  979. ' <a class="dropdown-item" href="#">Secondary link</a>',
  980. ' </div>',
  981. '</div>'
  982. ].join('')
  983. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  984. const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
  985. const dropdown = new Dropdown(btnDropdown)
  986. btnDropdown.addEventListener('shown.bs.dropdown', () => {
  987. expect(dropdownMenu.getAttribute('data-bs-popper')).toEqual('static')
  988. dropdown.hide()
  989. })
  990. btnDropdown.addEventListener('hidden.bs.dropdown', () => {
  991. expect(dropdownMenu.getAttribute('data-bs-popper')).toBeNull()
  992. done()
  993. })
  994. dropdown.show()
  995. })
  996. it('should remove "show" class if tabbing outside of menu', done => {
  997. fixtureEl.innerHTML = [
  998. '<div class="dropdown">',
  999. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  1000. ' <div class="dropdown-menu">',
  1001. ' <a class="dropdown-item" href="#">Secondary link</a>',
  1002. ' </div>',
  1003. '</div>'
  1004. ].join('')
  1005. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  1006. btnDropdown.addEventListener('shown.bs.dropdown', () => {
  1007. expect(btnDropdown.classList.contains('show')).toEqual(true)
  1008. const keyup = createEvent('keyup')
  1009. keyup.key = 'Tab'
  1010. document.dispatchEvent(keyup)
  1011. })
  1012. btnDropdown.addEventListener('hidden.bs.dropdown', () => {
  1013. expect(btnDropdown.classList.contains('show')).toEqual(false)
  1014. done()
  1015. })
  1016. btnDropdown.click()
  1017. })
  1018. it('should remove "show" class if body is clicked, with multiple dropdowns', done => {
  1019. fixtureEl.innerHTML = [
  1020. '<div class="nav">',
  1021. ' <div class="dropdown" id="testmenu">',
  1022. ' <a class="dropdown-toggle" data-bs-toggle="dropdown" href="#testmenu">Test menu</a>',
  1023. ' <div class="dropdown-menu">',
  1024. ' <a class="dropdown-item" href="#sub1">Submenu 1</a>',
  1025. ' </div>',
  1026. ' </div>',
  1027. '</div>',
  1028. '<div class="btn-group">',
  1029. ' <button class="btn">Actions</button>',
  1030. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown"></button>',
  1031. ' <div class="dropdown-menu">',
  1032. ' <a class="dropdown-item" href="#">Action 1</a>',
  1033. ' </div>',
  1034. '</div>'
  1035. ].join('')
  1036. const triggerDropdownList = fixtureEl.querySelectorAll('[data-bs-toggle="dropdown"]')
  1037. expect(triggerDropdownList.length).toEqual(2)
  1038. const [triggerDropdownFirst, triggerDropdownLast] = triggerDropdownList
  1039. triggerDropdownFirst.addEventListener('shown.bs.dropdown', () => {
  1040. expect(triggerDropdownFirst.classList.contains('show')).toEqual(true)
  1041. expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(1)
  1042. document.body.click()
  1043. })
  1044. triggerDropdownFirst.addEventListener('hidden.bs.dropdown', () => {
  1045. expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(0)
  1046. triggerDropdownLast.click()
  1047. })
  1048. triggerDropdownLast.addEventListener('shown.bs.dropdown', () => {
  1049. expect(triggerDropdownLast.classList.contains('show')).toEqual(true)
  1050. expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(1)
  1051. document.body.click()
  1052. })
  1053. triggerDropdownLast.addEventListener('hidden.bs.dropdown', () => {
  1054. expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(0)
  1055. done()
  1056. })
  1057. triggerDropdownFirst.click()
  1058. })
  1059. it('should remove "show" class if body if tabbing outside of menu, with multiple dropdowns', done => {
  1060. fixtureEl.innerHTML = [
  1061. '<div class="dropdown">',
  1062. ' <a class="dropdown-toggle" data-bs-toggle="dropdown" href="#testmenu">Test menu</a>',
  1063. ' <div class="dropdown-menu">',
  1064. ' <a class="dropdown-item" href="#sub1">Submenu 1</a>',
  1065. ' </div>',
  1066. '</div>',
  1067. '<div class="btn-group">',
  1068. ' <button class="btn">Actions</button>',
  1069. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown"></button>',
  1070. ' <div class="dropdown-menu">',
  1071. ' <a class="dropdown-item" href="#">Action 1</a>',
  1072. ' </div>',
  1073. '</div>'
  1074. ].join('')
  1075. const triggerDropdownList = fixtureEl.querySelectorAll('[data-bs-toggle="dropdown"]')
  1076. expect(triggerDropdownList.length).toEqual(2)
  1077. const [triggerDropdownFirst, triggerDropdownLast] = triggerDropdownList
  1078. triggerDropdownFirst.addEventListener('shown.bs.dropdown', () => {
  1079. expect(triggerDropdownFirst.classList.contains('show')).toEqual(true, '"show" class added on click')
  1080. expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(1, 'only one dropdown is shown')
  1081. const keyup = createEvent('keyup')
  1082. keyup.key = 'Tab'
  1083. document.dispatchEvent(keyup)
  1084. })
  1085. triggerDropdownFirst.addEventListener('hidden.bs.dropdown', () => {
  1086. expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(0, '"show" class removed')
  1087. triggerDropdownLast.click()
  1088. })
  1089. triggerDropdownLast.addEventListener('shown.bs.dropdown', () => {
  1090. expect(triggerDropdownLast.classList.contains('show')).toEqual(true, '"show" class added on click')
  1091. expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(1, 'only one dropdown is shown')
  1092. const keyup = createEvent('keyup')
  1093. keyup.key = 'Tab'
  1094. document.dispatchEvent(keyup)
  1095. })
  1096. triggerDropdownLast.addEventListener('hidden.bs.dropdown', () => {
  1097. expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(0, '"show" class removed')
  1098. done()
  1099. })
  1100. triggerDropdownFirst.click()
  1101. })
  1102. it('should fire hide and hidden event without a clickEvent if event type is not click', done => {
  1103. fixtureEl.innerHTML = [
  1104. '<div class="dropdown">',
  1105. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  1106. ' <div class="dropdown-menu">',
  1107. ' <a class="dropdown-item" href="#sub1">Submenu 1</a>',
  1108. ' </div>',
  1109. '</div>'
  1110. ].join('')
  1111. const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  1112. triggerDropdown.addEventListener('hide.bs.dropdown', e => {
  1113. expect(e.clickEvent).toBeUndefined()
  1114. })
  1115. triggerDropdown.addEventListener('hidden.bs.dropdown', e => {
  1116. expect(e.clickEvent).toBeUndefined()
  1117. done()
  1118. })
  1119. triggerDropdown.addEventListener('shown.bs.dropdown', () => {
  1120. const keydown = createEvent('keydown')
  1121. keydown.key = 'Escape'
  1122. triggerDropdown.dispatchEvent(keydown)
  1123. })
  1124. triggerDropdown.click()
  1125. })
  1126. it('should bubble up the events to the parent elements', done => {
  1127. fixtureEl.innerHTML = [
  1128. '<div class="dropdown">',
  1129. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  1130. ' <div class="dropdown-menu">',
  1131. ' <a class="dropdown-item" href="#subMenu">Sub menu</a>',
  1132. ' </div>',
  1133. '</div>'
  1134. ].join('')
  1135. const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  1136. const dropdownParent = fixtureEl.querySelector('.dropdown')
  1137. const dropdown = new Dropdown(triggerDropdown)
  1138. const showFunction = jasmine.createSpy('showFunction')
  1139. dropdownParent.addEventListener('show.bs.dropdown', showFunction)
  1140. const shownFunction = jasmine.createSpy('shownFunction')
  1141. dropdownParent.addEventListener('shown.bs.dropdown', () => {
  1142. shownFunction()
  1143. dropdown.hide()
  1144. })
  1145. const hideFunction = jasmine.createSpy('hideFunction')
  1146. dropdownParent.addEventListener('hide.bs.dropdown', hideFunction)
  1147. dropdownParent.addEventListener('hidden.bs.dropdown', () => {
  1148. expect(showFunction).toHaveBeenCalled()
  1149. expect(shownFunction).toHaveBeenCalled()
  1150. expect(hideFunction).toHaveBeenCalled()
  1151. done()
  1152. })
  1153. dropdown.show()
  1154. })
  1155. it('should ignore keyboard events within <input>s and <textarea>s', done => {
  1156. fixtureEl.innerHTML = [
  1157. '<div class="dropdown">',
  1158. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  1159. ' <div class="dropdown-menu">',
  1160. ' <a class="dropdown-item" href="#sub1">Submenu 1</a>',
  1161. ' <input type="text">',
  1162. ' <textarea></textarea>',
  1163. ' </div>',
  1164. '</div>'
  1165. ].join('')
  1166. const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  1167. const input = fixtureEl.querySelector('input')
  1168. const textarea = fixtureEl.querySelector('textarea')
  1169. triggerDropdown.addEventListener('shown.bs.dropdown', () => {
  1170. input.focus()
  1171. const keydown = createEvent('keydown')
  1172. keydown.key = 'ArrowUp'
  1173. input.dispatchEvent(keydown)
  1174. expect(document.activeElement).toEqual(input, 'input still focused')
  1175. textarea.focus()
  1176. textarea.dispatchEvent(keydown)
  1177. expect(document.activeElement).toEqual(textarea, 'textarea still focused')
  1178. done()
  1179. })
  1180. triggerDropdown.click()
  1181. })
  1182. it('should skip disabled element when using keyboard navigation', done => {
  1183. fixtureEl.innerHTML = [
  1184. '<div class="dropdown">',
  1185. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  1186. ' <div class="dropdown-menu">',
  1187. ' <a class="dropdown-item disabled" href="#sub1">Submenu 1</a>',
  1188. ' <button class="dropdown-item" type="button" disabled>Disabled button</button>',
  1189. ' <a id="item1" class="dropdown-item" href="#">Another link</a>',
  1190. ' </div>',
  1191. '</div>'
  1192. ].join('')
  1193. const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  1194. triggerDropdown.addEventListener('shown.bs.dropdown', () => {
  1195. const keydown = createEvent('keydown')
  1196. keydown.key = 'ArrowDown'
  1197. triggerDropdown.dispatchEvent(keydown)
  1198. triggerDropdown.dispatchEvent(keydown)
  1199. expect(document.activeElement.classList.contains('disabled')).toEqual(false, '.disabled not focused')
  1200. expect(document.activeElement.hasAttribute('disabled')).toEqual(false, ':disabled not focused')
  1201. done()
  1202. })
  1203. triggerDropdown.click()
  1204. })
  1205. it('should skip hidden element when using keyboard navigation', done => {
  1206. fixtureEl.innerHTML = [
  1207. '<style>',
  1208. ' .d-none {',
  1209. ' display: none;',
  1210. ' }',
  1211. '</style>',
  1212. '<div class="dropdown">',
  1213. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  1214. ' <div class="dropdown-menu">',
  1215. ' <button class="dropdown-item d-none" type="button">Hidden button by class</button>',
  1216. ' <a class="dropdown-item" href="#sub1" style="display: none">Hidden link</a>',
  1217. ' <a class="dropdown-item" href="#sub1" style="visibility: hidden">Hidden link</a>',
  1218. ' <a id="item1" class="dropdown-item" href="#">Another link</a>',
  1219. ' </div>',
  1220. '</div>'
  1221. ].join('')
  1222. const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  1223. triggerDropdown.addEventListener('shown.bs.dropdown', () => {
  1224. const keydown = createEvent('keydown')
  1225. keydown.key = 'ArrowDown'
  1226. triggerDropdown.dispatchEvent(keydown)
  1227. expect(document.activeElement.classList.contains('d-none')).toEqual(false, '.d-none not focused')
  1228. expect(document.activeElement.style.display).not.toBe('none', '"display: none" not focused')
  1229. expect(document.activeElement.style.visibility).not.toBe('hidden', '"visibility: hidden" not focused')
  1230. done()
  1231. })
  1232. triggerDropdown.click()
  1233. })
  1234. it('should focus next/previous element when using keyboard navigation', done => {
  1235. fixtureEl.innerHTML = [
  1236. '<div class="dropdown">',
  1237. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  1238. ' <div class="dropdown-menu">',
  1239. ' <a id="item1" class="dropdown-item" href="#">A link</a>',
  1240. ' <a id="item2" class="dropdown-item" href="#">Another link</a>',
  1241. ' </div>',
  1242. '</div>'
  1243. ].join('')
  1244. const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  1245. const item1 = fixtureEl.querySelector('#item1')
  1246. const item2 = fixtureEl.querySelector('#item2')
  1247. triggerDropdown.addEventListener('shown.bs.dropdown', () => {
  1248. const keydownArrowDown = createEvent('keydown')
  1249. keydownArrowDown.key = 'ArrowDown'
  1250. triggerDropdown.dispatchEvent(keydownArrowDown)
  1251. expect(document.activeElement).toEqual(item1, 'item1 is focused')
  1252. document.activeElement.dispatchEvent(keydownArrowDown)
  1253. expect(document.activeElement).toEqual(item2, 'item2 is focused')
  1254. const keydownArrowUp = createEvent('keydown')
  1255. keydownArrowUp.key = 'ArrowUp'
  1256. document.activeElement.dispatchEvent(keydownArrowUp)
  1257. expect(document.activeElement).toEqual(item1, 'item1 is focused')
  1258. done()
  1259. })
  1260. triggerDropdown.click()
  1261. })
  1262. it('should open the dropdown and focus on the last item when using ArrowUp for the first time', done => {
  1263. fixtureEl.innerHTML = [
  1264. '<div class="dropdown">',
  1265. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  1266. ' <div class="dropdown-menu">',
  1267. ' <a id="item1" class="dropdown-item" href="#">A link</a>',
  1268. ' <a id="item2" class="dropdown-item" href="#">Another link</a>',
  1269. ' </div>',
  1270. '</div>'
  1271. ].join('')
  1272. const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  1273. const lastItem = fixtureEl.querySelector('#item2')
  1274. triggerDropdown.addEventListener('shown.bs.dropdown', () => {
  1275. setTimeout(() => {
  1276. expect(document.activeElement).toEqual(lastItem, 'item2 is focused')
  1277. done()
  1278. })
  1279. })
  1280. const keydown = createEvent('keydown')
  1281. keydown.key = 'ArrowUp'
  1282. triggerDropdown.dispatchEvent(keydown)
  1283. })
  1284. it('should open the dropdown and focus on the first item when using ArrowDown for the first time', done => {
  1285. fixtureEl.innerHTML = [
  1286. '<div class="dropdown">',
  1287. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  1288. ' <div class="dropdown-menu">',
  1289. ' <a id="item1" class="dropdown-item" href="#">A link</a>',
  1290. ' <a id="item2" class="dropdown-item" href="#">Another link</a>',
  1291. ' </div>',
  1292. '</div>'
  1293. ].join('')
  1294. const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  1295. const firstItem = fixtureEl.querySelector('#item1')
  1296. triggerDropdown.addEventListener('shown.bs.dropdown', () => {
  1297. setTimeout(() => {
  1298. expect(document.activeElement).toEqual(firstItem, 'item1 is focused')
  1299. done()
  1300. })
  1301. })
  1302. const keydown = createEvent('keydown')
  1303. keydown.key = 'ArrowDown'
  1304. triggerDropdown.dispatchEvent(keydown)
  1305. })
  1306. it('should not close the dropdown if the user clicks on a text field within dropdown-menu', done => {
  1307. fixtureEl.innerHTML = [
  1308. '<div class="dropdown">',
  1309. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  1310. ' <div class="dropdown-menu">',
  1311. ' <input type="text">',
  1312. ' </div>',
  1313. '</div>'
  1314. ].join('')
  1315. const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  1316. const input = fixtureEl.querySelector('input')
  1317. input.addEventListener('click', () => {
  1318. expect(triggerDropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown')
  1319. done()
  1320. })
  1321. triggerDropdown.addEventListener('shown.bs.dropdown', () => {
  1322. expect(triggerDropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown')
  1323. input.dispatchEvent(createEvent('click'))
  1324. })
  1325. triggerDropdown.click()
  1326. })
  1327. it('should not close the dropdown if the user clicks on a textarea within dropdown-menu', done => {
  1328. fixtureEl.innerHTML = [
  1329. '<div class="dropdown">',
  1330. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  1331. ' <div class="dropdown-menu">',
  1332. ' <textarea></textarea>',
  1333. ' </div>',
  1334. '</div>'
  1335. ].join('')
  1336. const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  1337. const textarea = fixtureEl.querySelector('textarea')
  1338. textarea.addEventListener('click', () => {
  1339. expect(triggerDropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown')
  1340. done()
  1341. })
  1342. triggerDropdown.addEventListener('shown.bs.dropdown', () => {
  1343. expect(triggerDropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown')
  1344. textarea.dispatchEvent(createEvent('click'))
  1345. })
  1346. triggerDropdown.click()
  1347. })
  1348. it('should close the dropdown if the user clicks on a text field that is not contained within dropdown-menu', done => {
  1349. fixtureEl.innerHTML = [
  1350. '<div class="dropdown">',
  1351. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  1352. ' <div class="dropdown-menu">',
  1353. ' </div>',
  1354. '</div>',
  1355. '<input type="text">'
  1356. ]
  1357. const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  1358. const input = fixtureEl.querySelector('input')
  1359. triggerDropdown.addEventListener('hidden.bs.dropdown', () => {
  1360. expect().nothing()
  1361. done()
  1362. })
  1363. triggerDropdown.addEventListener('shown.bs.dropdown', () => {
  1364. input.dispatchEvent(createEvent('click', {
  1365. bubbles: true
  1366. }))
  1367. })
  1368. triggerDropdown.click()
  1369. })
  1370. it('should ignore keyboard events for <input>s and <textarea>s within dropdown-menu, except for escape key', done => {
  1371. fixtureEl.innerHTML = [
  1372. '<div class="dropdown">',
  1373. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  1374. ' <div class="dropdown-menu">',
  1375. ' <a class="dropdown-item" href="#sub1">Submenu 1</a>',
  1376. ' <input type="text">',
  1377. ' <textarea></textarea>',
  1378. ' </div>',
  1379. '</div>'
  1380. ].join('')
  1381. const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  1382. const input = fixtureEl.querySelector('input')
  1383. const textarea = fixtureEl.querySelector('textarea')
  1384. const keydownSpace = createEvent('keydown')
  1385. keydownSpace.key = 'Space'
  1386. const keydownArrowUp = createEvent('keydown')
  1387. keydownArrowUp.key = 'ArrowUp'
  1388. const keydownArrowDown = createEvent('keydown')
  1389. keydownArrowDown.key = 'ArrowDown'
  1390. const keydownEscape = createEvent('keydown')
  1391. keydownEscape.key = 'Escape'
  1392. triggerDropdown.addEventListener('shown.bs.dropdown', () => {
  1393. // Key Space
  1394. input.focus()
  1395. input.dispatchEvent(keydownSpace)
  1396. expect(document.activeElement).toEqual(input, 'input still focused')
  1397. textarea.focus()
  1398. textarea.dispatchEvent(keydownSpace)
  1399. expect(document.activeElement).toEqual(textarea, 'textarea still focused')
  1400. // Key ArrowUp
  1401. input.focus()
  1402. input.dispatchEvent(keydownArrowUp)
  1403. expect(document.activeElement).toEqual(input, 'input still focused')
  1404. textarea.focus()
  1405. textarea.dispatchEvent(keydownArrowUp)
  1406. expect(document.activeElement).toEqual(textarea, 'textarea still focused')
  1407. // Key ArrowDown
  1408. input.focus()
  1409. input.dispatchEvent(keydownArrowDown)
  1410. expect(document.activeElement).toEqual(input, 'input still focused')
  1411. textarea.focus()
  1412. textarea.dispatchEvent(keydownArrowDown)
  1413. expect(document.activeElement).toEqual(textarea, 'textarea still focused')
  1414. // Key Escape
  1415. input.focus()
  1416. input.dispatchEvent(keydownEscape)
  1417. expect(triggerDropdown.classList.contains('show')).toEqual(false, 'dropdown menu is not shown')
  1418. done()
  1419. })
  1420. triggerDropdown.click()
  1421. })
  1422. it('should not open dropdown if escape key was pressed on the toggle', done => {
  1423. fixtureEl.innerHTML = [
  1424. '<div class="tabs">',
  1425. ' <div class="dropdown">',
  1426. ' <button disabled class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  1427. ' <div class="dropdown-menu">',
  1428. ' <a class="dropdown-item" href="#">Secondary link</a>',
  1429. ' <a class="dropdown-item" href="#">Something else here</a>',
  1430. ' <div class="divider"></div>',
  1431. ' <a class="dropdown-item" href="#">Another link</a>',
  1432. ' </div>',
  1433. ' </div>',
  1434. '</div>'
  1435. ]
  1436. const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  1437. const dropdown = new Dropdown(triggerDropdown)
  1438. const button = fixtureEl.querySelector('button[data-bs-toggle="dropdown"]')
  1439. spyOn(dropdown, 'toggle')
  1440. // Key escape
  1441. button.focus()
  1442. // Key escape
  1443. const keydownEscape = createEvent('keydown')
  1444. keydownEscape.key = 'Escape'
  1445. button.dispatchEvent(keydownEscape)
  1446. setTimeout(() => {
  1447. expect(dropdown.toggle).not.toHaveBeenCalled()
  1448. expect(triggerDropdown.classList.contains('show')).toEqual(false)
  1449. done()
  1450. }, 20)
  1451. })
  1452. it('should propagate escape key events if dropdown is closed', done => {
  1453. fixtureEl.innerHTML = [
  1454. '<div class="parent">',
  1455. ' <div class="dropdown">',
  1456. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  1457. ' <div class="dropdown-menu">',
  1458. ' <a class="dropdown-item" href="#">Some Item</a>',
  1459. ' </div>',
  1460. ' </div>',
  1461. '</div>'
  1462. ]
  1463. const parent = fixtureEl.querySelector('.parent')
  1464. const toggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  1465. const parentKeyHandler = jasmine.createSpy('parentKeyHandler')
  1466. parent.addEventListener('keydown', parentKeyHandler)
  1467. parent.addEventListener('keyup', () => {
  1468. expect(parentKeyHandler).toHaveBeenCalled()
  1469. done()
  1470. })
  1471. const keydownEscape = createEvent('keydown', { bubbles: true })
  1472. keydownEscape.key = 'Escape'
  1473. const keyupEscape = createEvent('keyup', { bubbles: true })
  1474. keyupEscape.key = 'Escape'
  1475. toggle.focus()
  1476. toggle.dispatchEvent(keydownEscape)
  1477. toggle.dispatchEvent(keyupEscape)
  1478. })
  1479. it('should close dropdown (only) by clicking inside the dropdown menu when it has data-attribute `data-bs-auto-close="inside"`', done => {
  1480. fixtureEl.innerHTML = [
  1481. '<div class="dropdown">',
  1482. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="inside">Dropdown toggle</button>',
  1483. ' <div class="dropdown-menu">',
  1484. ' <a class="dropdown-item" href="#">Dropdown item</a>',
  1485. ' </div>',
  1486. '</div>'
  1487. ]
  1488. const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  1489. const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
  1490. const expectDropdownToBeOpened = () => setTimeout(() => {
  1491. expect(dropdownToggle.classList.contains('show')).toEqual(true)
  1492. dropdownMenu.click()
  1493. }, 150)
  1494. dropdownToggle.addEventListener('shown.bs.dropdown', () => {
  1495. document.documentElement.click()
  1496. expectDropdownToBeOpened()
  1497. })
  1498. dropdownToggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => {
  1499. expect(dropdownToggle.classList.contains('show')).toEqual(false)
  1500. done()
  1501. }))
  1502. dropdownToggle.click()
  1503. })
  1504. it('should close dropdown (only) by clicking outside the dropdown menu when it has data-attribute `data-bs-auto-close="outside"`', done => {
  1505. fixtureEl.innerHTML = [
  1506. '<div class="dropdown">',
  1507. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="outside">Dropdown toggle</button>',
  1508. ' <div class="dropdown-menu">',
  1509. ' <a class="dropdown-item" href="#">Dropdown item</a>',
  1510. ' </div>',
  1511. '</div>'
  1512. ]
  1513. const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  1514. const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
  1515. const expectDropdownToBeOpened = () => setTimeout(() => {
  1516. expect(dropdownToggle.classList.contains('show')).toEqual(true)
  1517. document.documentElement.click()
  1518. }, 150)
  1519. dropdownToggle.addEventListener('shown.bs.dropdown', () => {
  1520. dropdownMenu.click()
  1521. expectDropdownToBeOpened()
  1522. })
  1523. dropdownToggle.addEventListener('hidden.bs.dropdown', () => {
  1524. expect(dropdownToggle.classList.contains('show')).toEqual(false)
  1525. done()
  1526. })
  1527. dropdownToggle.click()
  1528. })
  1529. it('should not close dropdown by clicking inside or outside the dropdown menu when it has data-attribute `data-bs-auto-close="false"`', done => {
  1530. fixtureEl.innerHTML = [
  1531. '<div class="dropdown">',
  1532. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="false">Dropdown toggle</button>',
  1533. ' <div class="dropdown-menu">',
  1534. ' <a class="dropdown-item" href="#">Dropdown item</a>',
  1535. ' </div>',
  1536. '</div>'
  1537. ]
  1538. const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  1539. const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
  1540. const expectDropdownToBeOpened = (shouldTriggerClick = true) => setTimeout(() => {
  1541. expect(dropdownToggle.classList.contains('show')).toEqual(true)
  1542. if (shouldTriggerClick) {
  1543. document.documentElement.click()
  1544. } else {
  1545. done()
  1546. }
  1547. expectDropdownToBeOpened(false)
  1548. }, 150)
  1549. dropdownToggle.addEventListener('shown.bs.dropdown', () => {
  1550. dropdownMenu.click()
  1551. expectDropdownToBeOpened()
  1552. })
  1553. dropdownToggle.click()
  1554. })
  1555. })
  1556. describe('jQueryInterface', () => {
  1557. it('should create a dropdown', () => {
  1558. fixtureEl.innerHTML = '<div></div>'
  1559. const div = fixtureEl.querySelector('div')
  1560. jQueryMock.fn.dropdown = Dropdown.jQueryInterface
  1561. jQueryMock.elements = [div]
  1562. jQueryMock.fn.dropdown.call(jQueryMock)
  1563. expect(Dropdown.getInstance(div)).not.toBeNull()
  1564. })
  1565. it('should not re create a dropdown', () => {
  1566. fixtureEl.innerHTML = '<div></div>'
  1567. const div = fixtureEl.querySelector('div')
  1568. const dropdown = new Dropdown(div)
  1569. jQueryMock.fn.dropdown = Dropdown.jQueryInterface
  1570. jQueryMock.elements = [div]
  1571. jQueryMock.fn.dropdown.call(jQueryMock)
  1572. expect(Dropdown.getInstance(div)).toEqual(dropdown)
  1573. })
  1574. it('should throw error on undefined method', () => {
  1575. fixtureEl.innerHTML = '<div></div>'
  1576. const div = fixtureEl.querySelector('div')
  1577. const action = 'undefinedMethod'
  1578. jQueryMock.fn.dropdown = Dropdown.jQueryInterface
  1579. jQueryMock.elements = [div]
  1580. expect(() => {
  1581. jQueryMock.fn.dropdown.call(jQueryMock, action)
  1582. }).toThrowError(TypeError, `No method named "${action}"`)
  1583. })
  1584. })
  1585. describe('getInstance', () => {
  1586. it('should return dropdown instance', () => {
  1587. fixtureEl.innerHTML = '<div></div>'
  1588. const div = fixtureEl.querySelector('div')
  1589. const dropdown = new Dropdown(div)
  1590. expect(Dropdown.getInstance(div)).toEqual(dropdown)
  1591. expect(Dropdown.getInstance(div)).toBeInstanceOf(Dropdown)
  1592. })
  1593. it('should return null when there is no dropdown instance', () => {
  1594. fixtureEl.innerHTML = '<div></div>'
  1595. const div = fixtureEl.querySelector('div')
  1596. expect(Dropdown.getInstance(div)).toEqual(null)
  1597. })
  1598. })
  1599. describe('getOrCreateInstance', () => {
  1600. it('should return dropdown instance', () => {
  1601. fixtureEl.innerHTML = '<div></div>'
  1602. const div = fixtureEl.querySelector('div')
  1603. const dropdown = new Dropdown(div)
  1604. expect(Dropdown.getOrCreateInstance(div)).toEqual(dropdown)
  1605. expect(Dropdown.getInstance(div)).toEqual(Dropdown.getOrCreateInstance(div, {}))
  1606. expect(Dropdown.getOrCreateInstance(div)).toBeInstanceOf(Dropdown)
  1607. })
  1608. it('should return new instance when there is no dropdown instance', () => {
  1609. fixtureEl.innerHTML = '<div></div>'
  1610. const div = fixtureEl.querySelector('div')
  1611. expect(Dropdown.getInstance(div)).toEqual(null)
  1612. expect(Dropdown.getOrCreateInstance(div)).toBeInstanceOf(Dropdown)
  1613. })
  1614. it('should return new instance when there is no dropdown instance with given configuration', () => {
  1615. fixtureEl.innerHTML = '<div></div>'
  1616. const div = fixtureEl.querySelector('div')
  1617. expect(Dropdown.getInstance(div)).toEqual(null)
  1618. const dropdown = Dropdown.getOrCreateInstance(div, {
  1619. display: 'dynamic'
  1620. })
  1621. expect(dropdown).toBeInstanceOf(Dropdown)
  1622. expect(dropdown._config.display).toEqual('dynamic')
  1623. })
  1624. it('should return the instance when exists without given configuration', () => {
  1625. fixtureEl.innerHTML = '<div></div>'
  1626. const div = fixtureEl.querySelector('div')
  1627. const dropdown = new Dropdown(div, {
  1628. display: 'dynamic'
  1629. })
  1630. expect(Dropdown.getInstance(div)).toEqual(dropdown)
  1631. const dropdown2 = Dropdown.getOrCreateInstance(div, {
  1632. display: 'static'
  1633. })
  1634. expect(dropdown).toBeInstanceOf(Dropdown)
  1635. expect(dropdown2).toEqual(dropdown)
  1636. expect(dropdown2._config.display).toEqual('dynamic')
  1637. })
  1638. })
  1639. it('should open dropdown when pressing keydown or keyup', done => {
  1640. fixtureEl.innerHTML = [
  1641. '<div class="dropdown">',
  1642. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  1643. ' <div class="dropdown-menu">',
  1644. ' <a class="dropdown-item disabled" href="#sub1">Submenu 1</a>',
  1645. ' <button class="dropdown-item" type="button" disabled>Disabled button</button>',
  1646. ' <a id="item1" class="dropdown-item" href="#">Another link</a>',
  1647. ' </div>',
  1648. '</div>'
  1649. ].join('')
  1650. const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  1651. const dropdown = fixtureEl.querySelector('.dropdown')
  1652. const keydown = createEvent('keydown')
  1653. keydown.key = 'ArrowDown'
  1654. const keyup = createEvent('keyup')
  1655. keyup.key = 'ArrowUp'
  1656. const handleArrowDown = () => {
  1657. expect(triggerDropdown.classList.contains('show')).toEqual(true)
  1658. expect(triggerDropdown.getAttribute('aria-expanded')).toEqual('true')
  1659. setTimeout(() => {
  1660. dropdown.hide()
  1661. keydown.key = 'ArrowUp'
  1662. triggerDropdown.dispatchEvent(keyup)
  1663. }, 20)
  1664. }
  1665. const handleArrowUp = () => {
  1666. expect(triggerDropdown.classList.contains('show')).toEqual(true)
  1667. expect(triggerDropdown.getAttribute('aria-expanded')).toEqual('true')
  1668. done()
  1669. }
  1670. dropdown.addEventListener('shown.bs.dropdown', event => {
  1671. if (event.target.key === 'ArrowDown') {
  1672. handleArrowDown()
  1673. } else {
  1674. handleArrowUp()
  1675. }
  1676. })
  1677. triggerDropdown.dispatchEvent(keydown)
  1678. })
  1679. it('should allow `data-bs-toggle="dropdown"` click events to bubble up', () => {
  1680. fixtureEl.innerHTML = [
  1681. '<div class="dropdown">',
  1682. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
  1683. ' <div class="dropdown-menu">',
  1684. ' <a class="dropdown-item" href="#">Secondary link</a>',
  1685. ' </div>',
  1686. '</div>'
  1687. ].join('')
  1688. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  1689. const clickListener = jasmine.createSpy('clickListener')
  1690. const delegatedClickListener = jasmine.createSpy('delegatedClickListener')
  1691. btnDropdown.addEventListener('click', clickListener)
  1692. document.addEventListener('click', delegatedClickListener)
  1693. btnDropdown.click()
  1694. expect(clickListener).toHaveBeenCalled()
  1695. expect(delegatedClickListener).toHaveBeenCalled()
  1696. })
  1697. it('should open the dropdown when clicking the child element inside `data-bs-toggle="dropdown"`', done => {
  1698. fixtureEl.innerHTML = [
  1699. '<div class="container">',
  1700. ' <div class="dropdown">',
  1701. ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown"><span id="childElement">Dropdown</span></button>',
  1702. ' <div class="dropdown-menu">',
  1703. ' <a class="dropdown-item" href="#subMenu">Sub menu</a>',
  1704. ' </div>',
  1705. ' </div>',
  1706. '</div>'
  1707. ].join('')
  1708. const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
  1709. const childElement = fixtureEl.querySelector('#childElement')
  1710. btnDropdown.addEventListener('shown.bs.dropdown', () => setTimeout(() => {
  1711. expect(btnDropdown.classList.contains('show')).toEqual(true)
  1712. expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true')
  1713. done()
  1714. }))
  1715. childElement.click()
  1716. })
  1717. })