Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detect target inside a shadow root for clickOutsideHandler #1

Merged
merged 10 commits into from
Aug 7, 2020
5 changes: 3 additions & 2 deletions packages/ckeditor5-ui/src/bindings/clickoutsidehandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@
* @param {Function} options.callback An action executed by the handler.
*/
export default function clickOutsideHandler( { emitter, activator, callback, contextElements } ) {
emitter.listenTo( document, 'mousedown', ( evt, { target } ) => {
emitter.listenTo( document, 'mousedown', ( evt, domEvt ) => {
justinfagnani marked this conversation as resolved.
Show resolved Hide resolved
if ( !activator() ) {
return;
}

for ( const contextElement of contextElements ) {
if ( contextElement.contains( target ) ) {
if ( contextElement.contains( domEvt.target ) ||
( 'composedPath' in domEvt && domEvt.composedPath().includes( contextElement ) ) ) {
ywsang marked this conversation as resolved.
Show resolved Hide resolved
return;
}
}
Expand Down
59 changes: 58 additions & 1 deletion packages/ckeditor5-ui/tests/bindings/clickoutsidehandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,38 @@ import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';

describe( 'clickOutsideHandler', () => {
let activator, actionSpy, contextElement1, contextElement2;
let shadowRootContainer, shadowContextElement1, shadowContextElement2;

testUtils.createSinonSandbox();

beforeEach( () => {
activator = testUtils.sinon.stub().returns( false );
contextElement1 = document.createElement( 'div' );
contextElement2 = document.createElement( 'div' );
shadowRootContainer = document.createElement( 'div' );
shadowRootContainer.attachShadow( { mode: 'open' } );
ywsang marked this conversation as resolved.
Show resolved Hide resolved
shadowContextElement1 = document.createElement( 'div' );
shadowContextElement2 = document.createElement( 'div' );
actionSpy = testUtils.sinon.spy();

document.body.appendChild( contextElement1 );
document.body.appendChild( contextElement2 );
shadowRootContainer.shadowRoot.appendChild( shadowContextElement1 );
shadowRootContainer.shadowRoot.appendChild( shadowContextElement2 );
document.body.appendChild( shadowRootContainer );

clickOutsideHandler( {
emitter: Object.create( DomEmitterMixin ),
activator,
contextElements: [ contextElement1, contextElement2 ],
contextElements: [ contextElement1, contextElement2, shadowContextElement1, shadowContextElement2 ],
callback: actionSpy
} );
} );

afterEach( () => {
document.body.removeChild( contextElement1 );
document.body.removeChild( contextElement2 );
document.body.removeChild( shadowRootContainer );
} );

it( 'should execute upon #mousedown outside of the contextElements (activator is active)', () => {
Expand All @@ -46,6 +55,14 @@ describe( 'clickOutsideHandler', () => {
sinon.assert.calledOnce( actionSpy );
} );

it( 'should execute upon #mousedown in the shadow root but outside the contextElements (activator is active)', () => {
activator.returns( true );

shadowRootContainer.shadowRoot.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );

sinon.assert.notCalled( actionSpy );
} );

it( 'should not execute upon #mousedown outside of the contextElements (activator is inactive)', () => {
activator.returns( false );

Expand All @@ -54,6 +71,14 @@ describe( 'clickOutsideHandler', () => {
sinon.assert.notCalled( actionSpy );
} );

it( 'should not execute upon #mousedown in the shadow root but outside of the contextElements (activator is inactive)', () => {
activator.returns( false );

shadowRootContainer.shadowRoot.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );

sinon.assert.notCalled( actionSpy );
} );

it( 'should not execute upon #mousedown from one of the contextElements (activator is active)', () => {
activator.returns( true );

Expand All @@ -62,6 +87,12 @@ describe( 'clickOutsideHandler', () => {

contextElement2.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
sinon.assert.notCalled( actionSpy );

shadowContextElement1.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
sinon.assert.notCalled( actionSpy );

shadowContextElement2.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
sinon.assert.notCalled( actionSpy );
} );

it( 'should not execute upon #mousedown from one of the contextElements (activator is inactive)', () => {
Expand All @@ -72,6 +103,12 @@ describe( 'clickOutsideHandler', () => {

contextElement2.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
sinon.assert.notCalled( actionSpy );

shadowContextElement1.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
sinon.assert.notCalled( actionSpy );

shadowContextElement2.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
sinon.assert.notCalled( actionSpy );
} );

it( 'should execute if the activator function returns `true`', () => {
Expand Down Expand Up @@ -139,4 +176,24 @@ describe( 'clickOutsideHandler', () => {

sinon.assert.notCalled( actionSpy );
} );

it( 'should not execute if one of contextElements in the shadow root contains the DOM event target', () => {
const target = document.createElement( 'div' );
activator.returns( true );

shadowContextElement1.appendChild( target );
target.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );

sinon.assert.notCalled( actionSpy );
} );

it( 'should not execute if one of contextElements in the shadow root is the DOM event target', () => {
const target = document.createElement( 'div' );
activator.returns( true );

shadowRootContainer.shadowRoot.appendChild( target );
target.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );

sinon.assert.notCalled( actionSpy );
} );
} );