focustracker.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /* global document, Event */
  6. import FocusTracker from '../src/focustracker';
  7. import global from '../src/dom/global';
  8. import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
  9. import { expectToThrowCKEditorError } from './_utils/utils';
  10. describe( 'FocusTracker', () => {
  11. let focusTracker, container, containerFirstInput, containerSecondInput;
  12. testUtils.createSinonSandbox();
  13. beforeEach( () => {
  14. container = document.createElement( 'div' );
  15. containerFirstInput = document.createElement( 'input' );
  16. containerSecondInput = document.createElement( 'input' );
  17. container.appendChild( containerFirstInput );
  18. container.appendChild( containerSecondInput );
  19. testUtils.sinon.useFakeTimers();
  20. focusTracker = new FocusTracker();
  21. } );
  22. describe( 'constructor', () => {
  23. describe( 'isFocused', () => {
  24. it( 'should be false at default', () => {
  25. expect( focusTracker.isFocused ).to.false;
  26. } );
  27. it( 'should be observable', () => {
  28. const observableSpy = testUtils.sinon.spy();
  29. focusTracker.listenTo( focusTracker, 'change:isFocused', observableSpy );
  30. focusTracker.isFocused = true;
  31. expect( observableSpy.calledOnce ).to.true;
  32. } );
  33. } );
  34. describe( 'focusedElement', () => {
  35. it( 'should be null at default', () => {
  36. expect( focusTracker.focusedElement ).to.be.null;
  37. } );
  38. it( 'should be observable', () => {
  39. const observableSpy = testUtils.sinon.spy();
  40. focusTracker.listenTo( focusTracker, 'change:focusedElement', observableSpy );
  41. focusTracker.focusedElement = global.document.body;
  42. expect( observableSpy.calledOnce ).to.true;
  43. } );
  44. } );
  45. } );
  46. describe( 'add', () => {
  47. it( 'should throw an error when element has been already added', () => {
  48. focusTracker.add( containerFirstInput );
  49. expectToThrowCKEditorError( () => {
  50. focusTracker.add( containerFirstInput );
  51. }, 'focustracker-add-element-already-exist', focusTracker );
  52. } );
  53. describe( 'single element', () => {
  54. it( 'should start listening on element focus and update `isFocused` property', () => {
  55. focusTracker.add( containerFirstInput );
  56. expect( focusTracker.isFocused ).to.false;
  57. containerFirstInput.dispatchEvent( new Event( 'focus' ) );
  58. expect( focusTracker.isFocused ).to.true;
  59. expect( focusTracker.focusedElement ).to.equal( containerFirstInput );
  60. } );
  61. it( 'should start listening on element blur and update `isFocused` property', () => {
  62. focusTracker.add( containerFirstInput );
  63. containerFirstInput.dispatchEvent( new Event( 'focus' ) );
  64. expect( focusTracker.focusedElement ).to.equal( containerFirstInput );
  65. containerFirstInput.dispatchEvent( new Event( 'blur' ) );
  66. testUtils.sinon.clock.tick( 0 );
  67. expect( focusTracker.isFocused ).to.false;
  68. expect( focusTracker.focusedElement ).to.be.null;
  69. } );
  70. } );
  71. describe( 'container element', () => {
  72. it( 'should start listening on element focus using event capturing and update `isFocused` property', () => {
  73. focusTracker.add( container );
  74. expect( focusTracker.isFocused ).to.false;
  75. containerFirstInput.dispatchEvent( new Event( 'focus' ) );
  76. expect( focusTracker.isFocused ).to.true;
  77. expect( focusTracker.focusedElement ).to.equal( container );
  78. } );
  79. it( 'should start listening on element blur using event capturing and update `isFocused` property', () => {
  80. focusTracker.add( container );
  81. containerFirstInput.dispatchEvent( new Event( 'focus' ) );
  82. expect( focusTracker.focusedElement ).to.equal( container );
  83. containerFirstInput.dispatchEvent( new Event( 'blur' ) );
  84. testUtils.sinon.clock.tick( 0 );
  85. expect( focusTracker.isFocused ).to.false;
  86. expect( focusTracker.focusedElement ).to.be.null;
  87. } );
  88. it( 'should not change `isFocused` property when focus is going between child elements', () => {
  89. const changeSpy = testUtils.sinon.spy();
  90. focusTracker.add( container );
  91. containerFirstInput.dispatchEvent( new Event( 'focus' ) );
  92. expect( focusTracker.focusedElement ).to.equal( container );
  93. expect( focusTracker.isFocused ).to.true;
  94. focusTracker.listenTo( focusTracker, 'change:isFocused', changeSpy );
  95. containerFirstInput.dispatchEvent( new Event( 'blur' ) );
  96. containerSecondInput.dispatchEvent( new Event( 'focus' ) );
  97. testUtils.sinon.clock.tick( 0 );
  98. expect( focusTracker.focusedElement ).to.equal( container );
  99. expect( focusTracker.isFocused ).to.true;
  100. expect( changeSpy.notCalled ).to.true;
  101. } );
  102. // https://github.com/ckeditor/ckeditor5-utils/issues/159
  103. it( 'should keep `isFocused` synced when multiple blur events are followed by the focus', () => {
  104. focusTracker.add( container );
  105. container.dispatchEvent( new Event( 'focus' ) );
  106. expect( focusTracker.focusedElement ).to.equal( container );
  107. container.dispatchEvent( new Event( 'blur' ) );
  108. containerFirstInput.dispatchEvent( new Event( 'blur' ) );
  109. containerSecondInput.dispatchEvent( new Event( 'focus' ) );
  110. testUtils.sinon.clock.tick( 0 );
  111. expect( focusTracker.isFocused ).to.be.true;
  112. expect( focusTracker.focusedElement ).to.equal( container );
  113. } );
  114. } );
  115. } );
  116. describe( 'remove', () => {
  117. it( 'should do nothing when element was not added', () => {
  118. expect( () => {
  119. focusTracker.remove( container );
  120. } ).to.not.throw();
  121. } );
  122. it( 'should stop listening on element focus', () => {
  123. focusTracker.add( containerFirstInput );
  124. focusTracker.remove( containerFirstInput );
  125. containerFirstInput.dispatchEvent( new Event( 'focus' ) );
  126. expect( focusTracker.isFocused ).to.false;
  127. expect( focusTracker.focusedElement ).to.be.null;
  128. } );
  129. it( 'should stop listening on element blur', () => {
  130. focusTracker.add( containerFirstInput );
  131. focusTracker.remove( containerFirstInput );
  132. focusTracker.isFocused = true;
  133. containerFirstInput.dispatchEvent( new Event( 'blur' ) );
  134. testUtils.sinon.clock.tick( 0 );
  135. expect( focusTracker.isFocused ).to.true;
  136. } );
  137. it( 'should blur element before removing when is focused', () => {
  138. focusTracker.add( containerFirstInput );
  139. containerFirstInput.dispatchEvent( new Event( 'focus' ) );
  140. expect( focusTracker.focusedElement ).to.equal( containerFirstInput );
  141. expect( focusTracker.isFocused ).to.true;
  142. focusTracker.remove( containerFirstInput );
  143. testUtils.sinon.clock.tick( 0 );
  144. expect( focusTracker.isFocused ).to.false;
  145. expect( focusTracker.focusedElement ).to.be.null;
  146. } );
  147. } );
  148. describe( 'destroy()', () => {
  149. it( 'should stop listening', () => {
  150. const stopListeningSpy = sinon.spy( focusTracker, 'stopListening' );
  151. focusTracker.destroy();
  152. sinon.assert.calledOnce( stopListeningSpy );
  153. } );
  154. } );
  155. } );