clickoutsidehandler.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  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 outside of the contextElements (activator is active, unsupported shadow DOM)', () => {
  45. activator.returns( true );
  46. const event = new Event( 'mousedown', { bubbles: true } );
  47. event.composedPath = undefined;
  48. document.body.dispatchEvent( event );
  49. sinon.assert.calledOnce( actionSpy );
  50. } );
  51. it( 'should execute upon #mousedown in the shadow root but outside the contextElements (activator is active)', () => {
  52. activator.returns( true );
  53. shadowRootContainer.shadowRoot.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
  54. sinon.assert.notCalled( actionSpy );
  55. } );
  56. it( 'should not execute upon #mousedown outside of the contextElements (activator is inactive)', () => {
  57. activator.returns( false );
  58. document.body.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
  59. sinon.assert.notCalled( actionSpy );
  60. } );
  61. it( 'should not execute upon #mousedown outside of the contextElements (activator is inactive, unsupported shadow DOM)', () => {
  62. activator.returns( false );
  63. const event = new Event( 'mousedown', { bubbles: true } );
  64. event.composedPath = undefined;
  65. document.body.dispatchEvent( event );
  66. sinon.assert.notCalled( actionSpy );
  67. } );
  68. it( 'should not execute upon #mousedown in the shadow root but outside of the contextElements (activator is inactive)', () => {
  69. activator.returns( false );
  70. shadowRootContainer.shadowRoot.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
  71. sinon.assert.notCalled( actionSpy );
  72. } );
  73. it( 'should not execute upon #mousedown from one of the contextElements (activator is active)', () => {
  74. activator.returns( true );
  75. contextElement1.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
  76. sinon.assert.notCalled( actionSpy );
  77. contextElement2.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
  78. sinon.assert.notCalled( actionSpy );
  79. shadowContextElement1.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
  80. sinon.assert.notCalled( actionSpy );
  81. shadowContextElement2.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
  82. sinon.assert.notCalled( actionSpy );
  83. } );
  84. it( 'should not execute upon #mousedown from one of the contextElements (activator is inactive)', () => {
  85. activator.returns( false );
  86. contextElement1.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
  87. sinon.assert.notCalled( actionSpy );
  88. contextElement2.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
  89. sinon.assert.notCalled( actionSpy );
  90. shadowContextElement1.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
  91. sinon.assert.notCalled( actionSpy );
  92. shadowContextElement2.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
  93. sinon.assert.notCalled( actionSpy );
  94. } );
  95. it( 'should execute if the activator function returns `true`', () => {
  96. const spy = testUtils.sinon.spy();
  97. activator.returns( true );
  98. clickOutsideHandler( {
  99. emitter: Object.create( DomEmitterMixin ),
  100. activator,
  101. contextElements: [ contextElement1 ],
  102. callback: spy
  103. } );
  104. document.body.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
  105. sinon.assert.calledOnce( spy );
  106. } );
  107. it( 'should not execute if the activator function returns `false`', () => {
  108. const spy = testUtils.sinon.spy();
  109. activator.returns( false );
  110. clickOutsideHandler( {
  111. emitter: Object.create( DomEmitterMixin ),
  112. activator,
  113. contextElements: [ contextElement1 ],
  114. callback: spy
  115. } );
  116. document.body.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
  117. sinon.assert.notCalled( spy );
  118. } );
  119. it( 'should react to the activator\'s return value change', () => {
  120. activator.returns( true );
  121. document.body.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
  122. sinon.assert.calledOnce( actionSpy );
  123. activator.returns( false );
  124. document.body.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
  125. // Still called once, was not called second time.
  126. sinon.assert.calledOnce( actionSpy );
  127. activator.returns( true );
  128. document.body.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
  129. // Called one more time.
  130. sinon.assert.calledTwice( actionSpy );
  131. } );
  132. it( 'should not execute if one of contextElements contains the DOM event target', () => {
  133. const target = document.createElement( 'div' );
  134. activator.returns( true );
  135. contextElement2.appendChild( target );
  136. target.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
  137. sinon.assert.notCalled( actionSpy );
  138. } );
  139. it( 'should not execute if one of contextElements in the shadow root contains the DOM event target', () => {
  140. const target = document.createElement( 'div' );
  141. activator.returns( true );
  142. shadowContextElement1.appendChild( target );
  143. target.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
  144. sinon.assert.notCalled( actionSpy );
  145. } );
  146. it( 'should not execute if one of contextElements in the shadow root is the DOM event target', () => {
  147. const target = document.createElement( 'div' );
  148. activator.returns( true );
  149. shadowRootContainer.shadowRoot.appendChild( target );
  150. target.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
  151. sinon.assert.notCalled( actionSpy );
  152. } );
  153. } );