focustrap.spec.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. import EventHandler from '../../../src/dom/event-handler.js'
  2. import SelectorEngine from '../../../src/dom/selector-engine.js'
  3. import FocusTrap from '../../../src/util/focustrap.js'
  4. import { clearFixture, createEvent, getFixture } from '../../helpers/fixture.js'
  5. describe('FocusTrap', () => {
  6. let fixtureEl
  7. beforeAll(() => {
  8. fixtureEl = getFixture()
  9. })
  10. afterEach(() => {
  11. clearFixture()
  12. })
  13. describe('activate', () => {
  14. it('should autofocus itself by default', () => {
  15. fixtureEl.innerHTML = '<div id="focustrap" tabindex="-1"></div>'
  16. const trapElement = fixtureEl.querySelector('div')
  17. const spy = spyOn(trapElement, 'focus')
  18. const focustrap = new FocusTrap({ trapElement })
  19. focustrap.activate()
  20. expect(spy).toHaveBeenCalled()
  21. })
  22. it('if configured not to autofocus, should not autofocus itself', () => {
  23. fixtureEl.innerHTML = '<div id="focustrap" tabindex="-1"></div>'
  24. const trapElement = fixtureEl.querySelector('div')
  25. const spy = spyOn(trapElement, 'focus')
  26. const focustrap = new FocusTrap({ trapElement, autofocus: false })
  27. focustrap.activate()
  28. expect(spy).not.toHaveBeenCalled()
  29. })
  30. it('should force focus inside focus trap if it can', () => {
  31. return new Promise(resolve => {
  32. fixtureEl.innerHTML = [
  33. '<a href="#" id="outside">outside</a>',
  34. '<div id="focustrap" tabindex="-1">',
  35. ' <a href="#" id="inside">inside</a>',
  36. '</div>'
  37. ].join('')
  38. const trapElement = fixtureEl.querySelector('div')
  39. const focustrap = new FocusTrap({ trapElement })
  40. focustrap.activate()
  41. const inside = document.getElementById('inside')
  42. const focusInListener = () => {
  43. expect(spy).toHaveBeenCalled()
  44. document.removeEventListener('focusin', focusInListener)
  45. resolve()
  46. }
  47. const spy = spyOn(inside, 'focus')
  48. spyOn(SelectorEngine, 'focusableChildren').and.callFake(() => [inside])
  49. document.addEventListener('focusin', focusInListener)
  50. const focusInEvent = createEvent('focusin', { bubbles: true })
  51. Object.defineProperty(focusInEvent, 'target', {
  52. value: document.getElementById('outside')
  53. })
  54. document.dispatchEvent(focusInEvent)
  55. })
  56. })
  57. it('should wrap focus around forward on tab', () => {
  58. return new Promise(resolve => {
  59. fixtureEl.innerHTML = [
  60. '<a href="#" id="outside">outside</a>',
  61. '<div id="focustrap" tabindex="-1">',
  62. ' <a href="#" id="first">first</a>',
  63. ' <a href="#" id="inside">inside</a>',
  64. ' <a href="#" id="last">last</a>',
  65. '</div>'
  66. ].join('')
  67. const trapElement = fixtureEl.querySelector('div')
  68. const focustrap = new FocusTrap({ trapElement })
  69. focustrap.activate()
  70. const first = document.getElementById('first')
  71. const inside = document.getElementById('inside')
  72. const last = document.getElementById('last')
  73. const outside = document.getElementById('outside')
  74. spyOn(SelectorEngine, 'focusableChildren').and.callFake(() => [first, inside, last])
  75. const spy = spyOn(first, 'focus').and.callThrough()
  76. const focusInListener = () => {
  77. expect(spy).toHaveBeenCalled()
  78. first.removeEventListener('focusin', focusInListener)
  79. resolve()
  80. }
  81. first.addEventListener('focusin', focusInListener)
  82. const keydown = createEvent('keydown')
  83. keydown.key = 'Tab'
  84. document.dispatchEvent(keydown)
  85. outside.focus()
  86. })
  87. })
  88. it('should wrap focus around backwards on shift-tab', () => {
  89. return new Promise(resolve => {
  90. fixtureEl.innerHTML = [
  91. '<a href="#" id="outside">outside</a>',
  92. '<div id="focustrap" tabindex="-1">',
  93. ' <a href="#" id="first">first</a>',
  94. ' <a href="#" id="inside">inside</a>',
  95. ' <a href="#" id="last">last</a>',
  96. '</div>'
  97. ].join('')
  98. const trapElement = fixtureEl.querySelector('div')
  99. const focustrap = new FocusTrap({ trapElement })
  100. focustrap.activate()
  101. const first = document.getElementById('first')
  102. const inside = document.getElementById('inside')
  103. const last = document.getElementById('last')
  104. const outside = document.getElementById('outside')
  105. spyOn(SelectorEngine, 'focusableChildren').and.callFake(() => [first, inside, last])
  106. const spy = spyOn(last, 'focus').and.callThrough()
  107. const focusInListener = () => {
  108. expect(spy).toHaveBeenCalled()
  109. last.removeEventListener('focusin', focusInListener)
  110. resolve()
  111. }
  112. last.addEventListener('focusin', focusInListener)
  113. const keydown = createEvent('keydown')
  114. keydown.key = 'Tab'
  115. keydown.shiftKey = true
  116. document.dispatchEvent(keydown)
  117. outside.focus()
  118. })
  119. })
  120. it('should force focus on itself if there is no focusable content', () => {
  121. return new Promise(resolve => {
  122. fixtureEl.innerHTML = [
  123. '<a href="#" id="outside">outside</a>',
  124. '<div id="focustrap" tabindex="-1"></div>'
  125. ].join('')
  126. const trapElement = fixtureEl.querySelector('div')
  127. const focustrap = new FocusTrap({ trapElement })
  128. focustrap.activate()
  129. const focusInListener = () => {
  130. expect(spy).toHaveBeenCalled()
  131. document.removeEventListener('focusin', focusInListener)
  132. resolve()
  133. }
  134. const spy = spyOn(focustrap._config.trapElement, 'focus')
  135. document.addEventListener('focusin', focusInListener)
  136. const focusInEvent = createEvent('focusin', { bubbles: true })
  137. Object.defineProperty(focusInEvent, 'target', {
  138. value: document.getElementById('outside')
  139. })
  140. document.dispatchEvent(focusInEvent)
  141. })
  142. })
  143. })
  144. describe('deactivate', () => {
  145. it('should flag itself as no longer active', () => {
  146. const focustrap = new FocusTrap({ trapElement: fixtureEl })
  147. focustrap.activate()
  148. expect(focustrap._isActive).toBeTrue()
  149. focustrap.deactivate()
  150. expect(focustrap._isActive).toBeFalse()
  151. })
  152. it('should remove all event listeners', () => {
  153. const focustrap = new FocusTrap({ trapElement: fixtureEl })
  154. focustrap.activate()
  155. const spy = spyOn(EventHandler, 'off')
  156. focustrap.deactivate()
  157. expect(spy).toHaveBeenCalled()
  158. })
  159. it('doesn\'t try removing event listeners unless it needs to (in case it hasn\'t been activated)', () => {
  160. const focustrap = new FocusTrap({ trapElement: fixtureEl })
  161. const spy = spyOn(EventHandler, 'off')
  162. focustrap.deactivate()
  163. expect(spy).not.toHaveBeenCalled()
  164. })
  165. })
  166. })