clickoutsidehandler.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  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 clickOutsideHandler from '../../src/bindings/clickoutsidehandler';
  7. import DomEmitterMixin from '@ckeditor/ckeditor5-utils/src/dom/emittermixin';
  8. import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
  9. describe( 'clickOutsideHandler', () => {
  10. let activator, actionSpy, contextElement1, contextElement2;
  11. let shadowRootContainer, shadowContextElement1, shadowContextElement2;
  12. testUtils.createSinonSandbox();
  13. beforeEach( () => {
  14. activator = testUtils.sinon.stub().returns( false );
  15. contextElement1 = document.createElement( 'div' );
  16. contextElement2 = document.createElement( 'div' );
  17. shadowRootContainer = document.createElement( 'div' );
  18. shadowRootContainer.attachShadow( { mode: 'open' } );
  19. shadowContextElement1 = document.createElement( 'div' );
  20. shadowContextElement2 = document.createElement( 'div' );
  21. actionSpy = testUtils.sinon.spy();
  22. document.body.appendChild( contextElement1 );
  23. document.body.appendChild( contextElement2 );
  24. shadowRootContainer.shadowRoot.appendChild( shadowContextElement1 );
  25. shadowRootContainer.shadowRoot.appendChild( shadowContextElement2 );
  26. document.body.appendChild( shadowRootContainer );
  27. clickOutsideHandler( {
  28. emitter: Object.create( DomEmitterMixin ),
  29. activator,
  30. contextElements: [ contextElement1, contextElement2, shadowContextElement1, shadowContextElement2 ],
  31. callback: actionSpy
  32. } );
  33. } );
  34. afterEach( () => {
  35. document.body.removeChild( contextElement1 );
  36. document.body.removeChild( contextElement2 );
  37. document.body.removeChild( shadowRootContainer );
  38. } );
  39. it( 'should execute upon #mousedown outside of the contextElements (activator is active)', () => {
  40. activator.returns( true );
  41. document.body.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
  42. sinon.assert.calledOnce( actionSpy );
  43. } );
  44. it( 'should execute upon #mousedown in the shadow root but outside the contextElements (activator is active)', () => {
  45. activator.returns( true );
  46. shadowRootContainer.shadowRoot.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
  47. sinon.assert.notCalled( actionSpy );
  48. } );
  49. it( 'should not execute upon #mousedown outside of the contextElements (activator is inactive)', () => {
  50. activator.returns( false );
  51. document.body.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
  52. sinon.assert.notCalled( actionSpy );
  53. } );
  54. it( 'should not execute upon #mousedown in the shadow root but outside of the contextElements (activator is inactive)', () => {
  55. activator.returns( false );
  56. shadowRootContainer.shadowRoot.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
  57. sinon.assert.notCalled( actionSpy );
  58. } );
  59. it( 'should not execute upon #mousedown from one of the contextElements (activator is active)', () => {
  60. activator.returns( true );
  61. contextElement1.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
  62. sinon.assert.notCalled( actionSpy );
  63. contextElement2.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
  64. sinon.assert.notCalled( actionSpy );
  65. shadowContextElement1.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
  66. sinon.assert.notCalled( actionSpy );
  67. shadowContextElement2.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
  68. sinon.assert.notCalled( actionSpy );
  69. } );
  70. it( 'should not execute upon #mousedown from one of the contextElements (activator is inactive)', () => {
  71. activator.returns( false );
  72. contextElement1.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
  73. sinon.assert.notCalled( actionSpy );
  74. contextElement2.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
  75. sinon.assert.notCalled( actionSpy );
  76. shadowContextElement1.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
  77. sinon.assert.notCalled( actionSpy );
  78. shadowContextElement2.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
  79. sinon.assert.notCalled( actionSpy );
  80. } );
  81. it( 'should execute if the activator function returns `true`', () => {
  82. const spy = testUtils.sinon.spy();
  83. activator.returns( true );
  84. clickOutsideHandler( {
  85. emitter: Object.create( DomEmitterMixin ),
  86. activator,
  87. contextElements: [ contextElement1 ],
  88. callback: spy
  89. } );
  90. document.body.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
  91. sinon.assert.calledOnce( spy );
  92. } );
  93. it( 'should not execute if the activator function returns `false`', () => {
  94. const spy = testUtils.sinon.spy();
  95. activator.returns( false );
  96. clickOutsideHandler( {
  97. emitter: Object.create( DomEmitterMixin ),
  98. activator,
  99. contextElements: [ contextElement1 ],
  100. callback: spy
  101. } );
  102. document.body.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
  103. sinon.assert.notCalled( spy );
  104. } );
  105. it( 'should react to the activator\'s return value change', () => {
  106. activator.returns( true );
  107. document.body.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
  108. sinon.assert.calledOnce( actionSpy );
  109. activator.returns( false );
  110. document.body.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
  111. // Still called once, was not called second time.
  112. sinon.assert.calledOnce( actionSpy );
  113. activator.returns( true );
  114. document.body.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
  115. // Called one more time.
  116. sinon.assert.calledTwice( actionSpy );
  117. } );
  118. it( 'should not execute if one of contextElements contains the DOM event target', () => {
  119. const target = document.createElement( 'div' );
  120. activator.returns( true );
  121. contextElement2.appendChild( target );
  122. target.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
  123. sinon.assert.notCalled( actionSpy );
  124. } );
  125. it( 'should not execute if one of contextElements in the shadow root contains the DOM event target', () => {
  126. const target = document.createElement( 'div' );
  127. activator.returns( true );
  128. shadowContextElement1.appendChild( target );
  129. target.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
  130. sinon.assert.notCalled( actionSpy );
  131. } );
  132. it( 'should not execute if one of contextElements in the shadow root is the DOM event target', () => {
  133. const target = document.createElement( 'div' );
  134. activator.returns( true );
  135. shadowRootContainer.shadowRoot.appendChild( target );
  136. target.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
  137. sinon.assert.notCalled( actionSpy );
  138. } );
  139. } );