|
|
@@ -0,0 +1,399 @@
|
|
|
+/**
|
|
|
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
|
|
|
+ * For licensing, see LICENSE.md.
|
|
|
+ */
|
|
|
+
|
|
|
+import ViewCollection from '../src/viewcollection';
|
|
|
+import View from '../src/view';
|
|
|
+import FocusCycler from '../src/focuscycler';
|
|
|
+import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
|
|
|
+import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
|
|
|
+
|
|
|
+describe( 'FocusCycler', () => {
|
|
|
+ let focusables, focusTracker, cycler;
|
|
|
+
|
|
|
+ beforeEach( () => {
|
|
|
+ focusables = new ViewCollection();
|
|
|
+ focusTracker = {
|
|
|
+ focusedElement: null
|
|
|
+ };
|
|
|
+ cycler = new FocusCycler( {
|
|
|
+ focusables: focusables,
|
|
|
+ focusTracker: focusTracker
|
|
|
+ } );
|
|
|
+
|
|
|
+ focusables.add( nonFocusable() );
|
|
|
+ focusables.add( focusable() );
|
|
|
+ focusables.add( focusable() );
|
|
|
+ focusables.add( focusable() );
|
|
|
+ focusables.add( nonFocusable() );
|
|
|
+ } );
|
|
|
+
|
|
|
+ describe( 'constructor()', () => {
|
|
|
+ it( 'sets class properties', () => {
|
|
|
+ expect( cycler.focusables ).to.equal( focusables );
|
|
|
+ expect( cycler.focusTracker ).to.equal( focusTracker );
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+
|
|
|
+ describe( 'current()', () => {
|
|
|
+ it( 'returns null when no view is focused', () => {
|
|
|
+ expect( cycler.current ).to.equal( null );
|
|
|
+
|
|
|
+ focusTracker.focusedElement = focusables.get( 2 ).element;
|
|
|
+ expect( cycler.current ).to.equal( 2 );
|
|
|
+
|
|
|
+ focusTracker.focusedElement = null;
|
|
|
+ expect( cycler.current ).to.equal( null );
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+
|
|
|
+ describe( 'first()', () => {
|
|
|
+ it( 'returns first focusable view', () => {
|
|
|
+ expect( cycler.first ).to.equal( focusables.get( 1 ) );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'returns null when no focusable items', () => {
|
|
|
+ focusables = new ViewCollection();
|
|
|
+ cycler = new FocusCycler( { focusables, focusTracker } );
|
|
|
+
|
|
|
+ focusables.add( nonFocusable() );
|
|
|
+ focusables.add( nonFocusable() );
|
|
|
+
|
|
|
+ expect( cycler.first ).to.be.null;
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'returns null when no items', () => {
|
|
|
+ focusables = new ViewCollection();
|
|
|
+ cycler = new FocusCycler( { focusables, focusTracker } );
|
|
|
+
|
|
|
+ expect( cycler.first ).to.be.null;
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+
|
|
|
+ describe( 'last()', () => {
|
|
|
+ it( 'returns last focusable view', () => {
|
|
|
+ expect( cycler.last ).to.equal( focusables.get( 3 ) );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'returns null when no focusable items', () => {
|
|
|
+ focusables = new ViewCollection();
|
|
|
+ cycler = new FocusCycler( { focusables, focusTracker } );
|
|
|
+
|
|
|
+ focusables.add( nonFocusable() );
|
|
|
+ focusables.add( nonFocusable() );
|
|
|
+
|
|
|
+ expect( cycler.last ).to.be.null;
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'returns null when no items', () => {
|
|
|
+ focusables = new ViewCollection();
|
|
|
+ cycler = new FocusCycler( { focusables, focusTracker } );
|
|
|
+
|
|
|
+ expect( cycler.last ).to.be.null;
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+
|
|
|
+ describe( 'next()', () => {
|
|
|
+ it( 'cycles to return the next focusable view', () => {
|
|
|
+ focusTracker.focusedElement = focusables.get( 2 ).element;
|
|
|
+ expect( cycler.next ).to.equal( focusables.get( 3 ) );
|
|
|
+
|
|
|
+ focusTracker.focusedElement = focusables.get( 3 ).element;
|
|
|
+ expect( cycler.next ).to.equal( focusables.get( 1 ) );
|
|
|
+
|
|
|
+ focusTracker.focusedElement = focusables.get( 1 ).element;
|
|
|
+ expect( cycler.next ).to.equal( focusables.get( 2 ) );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'returns null when no view is focused', () => {
|
|
|
+ focusTracker.focusedElement = null;
|
|
|
+
|
|
|
+ expect( cycler.next ).to.be.null;
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'returns null when no items', () => {
|
|
|
+ focusables = new ViewCollection();
|
|
|
+ cycler = new FocusCycler( { focusables, focusTracker } );
|
|
|
+
|
|
|
+ expect( cycler.next ).to.be.null;
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'returns null when no focusable items', () => {
|
|
|
+ focusables = new ViewCollection();
|
|
|
+ cycler = new FocusCycler( { focusables, focusTracker } );
|
|
|
+
|
|
|
+ focusables.add( nonFocusable() );
|
|
|
+ focusables.add( nonFocusable() );
|
|
|
+
|
|
|
+ expect( cycler.next ).to.be.null;
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'returns null if the only focusable in focusables', () => {
|
|
|
+ focusables = new ViewCollection();
|
|
|
+ cycler = new FocusCycler( { focusables, focusTracker } );
|
|
|
+
|
|
|
+ focusables.add( nonFocusable() );
|
|
|
+ focusables.add( focusable() );
|
|
|
+ focusables.add( nonFocusable() );
|
|
|
+
|
|
|
+ focusTracker.focusedElement = focusables.get( 1 ).element;
|
|
|
+
|
|
|
+ expect( cycler.first ).to.equal( focusables.get( 1 ) );
|
|
|
+ expect( cycler.next ).to.be.null;
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+
|
|
|
+ describe( 'previous()', () => {
|
|
|
+ it( 'cycles to return the previous focusable view', () => {
|
|
|
+ focusTracker.focusedElement = focusables.get( 1 ).element;
|
|
|
+ expect( cycler.previous ).to.equal( focusables.get( 3 ) );
|
|
|
+
|
|
|
+ focusTracker.focusedElement = focusables.get( 2 ).element;
|
|
|
+ expect( cycler.previous ).to.equal( focusables.get( 1 ) );
|
|
|
+
|
|
|
+ focusTracker.focusedElement = focusables.get( 3 ).element;
|
|
|
+ expect( cycler.previous ).to.equal( focusables.get( 2 ) );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'returns null when no view is focused', () => {
|
|
|
+ focusTracker.focusedElement = null;
|
|
|
+
|
|
|
+ expect( cycler.previous ).to.be.null;
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'returns null when no items', () => {
|
|
|
+ focusables = new ViewCollection();
|
|
|
+ cycler = new FocusCycler( { focusables, focusTracker } );
|
|
|
+
|
|
|
+ expect( cycler.previous ).to.be.null;
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'returns null when no focusable items', () => {
|
|
|
+ focusables = new ViewCollection();
|
|
|
+ cycler = new FocusCycler( { focusables, focusTracker } );
|
|
|
+
|
|
|
+ focusables.add( nonFocusable() );
|
|
|
+ focusables.add( nonFocusable() );
|
|
|
+
|
|
|
+ expect( cycler.previous ).to.be.null;
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'returns null if the only focusable in focusables', () => {
|
|
|
+ focusables = new ViewCollection();
|
|
|
+ cycler = new FocusCycler( { focusables, focusTracker } );
|
|
|
+
|
|
|
+ focusables.add( nonFocusable() );
|
|
|
+ focusables.add( focusable() );
|
|
|
+ focusables.add( nonFocusable() );
|
|
|
+
|
|
|
+ focusTracker.focusedElement = focusables.get( 1 ).element;
|
|
|
+
|
|
|
+ expect( cycler.first ).to.equal( focusables.get( 1 ) );
|
|
|
+ expect( cycler.previous ).to.be.null;
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+
|
|
|
+ describe( 'focusFirst()', () => {
|
|
|
+ it( 'focuses first focusable view', () => {
|
|
|
+ const spy = sinon.spy( focusables.get( 1 ), 'focus' );
|
|
|
+
|
|
|
+ cycler.focusFirst();
|
|
|
+
|
|
|
+ sinon.assert.calledOnce( spy );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'does not throw when no focusable items', () => {
|
|
|
+ focusables = new ViewCollection();
|
|
|
+ cycler = new FocusCycler( { focusables, focusTracker } );
|
|
|
+
|
|
|
+ focusables.add( nonFocusable() );
|
|
|
+ focusables.add( nonFocusable() );
|
|
|
+
|
|
|
+ expect( () => {
|
|
|
+ cycler.focusFirst();
|
|
|
+ } ).to.not.throw();
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'does not throw when no items', () => {
|
|
|
+ focusables = new ViewCollection();
|
|
|
+ cycler = new FocusCycler( { focusables, focusTracker } );
|
|
|
+
|
|
|
+ expect( () => {
|
|
|
+ cycler.focusFirst();
|
|
|
+ } ).to.not.throw();
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+
|
|
|
+ describe( 'focusLast()', () => {
|
|
|
+ it( 'focuses last focusable view', () => {
|
|
|
+ const spy = sinon.spy( focusables.get( 3 ), 'focus' );
|
|
|
+
|
|
|
+ cycler.focusLast();
|
|
|
+
|
|
|
+ sinon.assert.calledOnce( spy );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'does not throw when no focusable items', () => {
|
|
|
+ focusables = new ViewCollection();
|
|
|
+ cycler = new FocusCycler( { focusables, focusTracker } );
|
|
|
+
|
|
|
+ focusables.add( nonFocusable() );
|
|
|
+ focusables.add( nonFocusable() );
|
|
|
+
|
|
|
+ expect( () => {
|
|
|
+ cycler.focusLast();
|
|
|
+ } ).to.not.throw();
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'does not throw when no items', () => {
|
|
|
+ focusables = new ViewCollection();
|
|
|
+ cycler = new FocusCycler( { focusables, focusTracker } );
|
|
|
+
|
|
|
+ expect( () => {
|
|
|
+ cycler.focusLast();
|
|
|
+ } ).to.not.throw();
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+
|
|
|
+ describe( 'focusNext()', () => {
|
|
|
+ it( 'focuses next focusable view', () => {
|
|
|
+ const spy = sinon.spy( focusables.get( 3 ), 'focus' );
|
|
|
+
|
|
|
+ focusTracker.focusedElement = focusables.get( 2 ).element;
|
|
|
+ cycler.focusNext();
|
|
|
+
|
|
|
+ sinon.assert.calledOnce( spy );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'does not throw when no focusable items', () => {
|
|
|
+ focusables = new ViewCollection();
|
|
|
+ cycler = new FocusCycler( { focusables, focusTracker } );
|
|
|
+
|
|
|
+ focusables.add( nonFocusable() );
|
|
|
+ focusables.add( nonFocusable() );
|
|
|
+
|
|
|
+ expect( () => {
|
|
|
+ cycler.focusNext();
|
|
|
+ } ).to.not.throw();
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'does not throw when no items', () => {
|
|
|
+ focusables = new ViewCollection();
|
|
|
+ cycler = new FocusCycler( { focusables, focusTracker } );
|
|
|
+
|
|
|
+ expect( () => {
|
|
|
+ cycler.focusNext();
|
|
|
+ } ).to.not.throw();
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+
|
|
|
+ describe( 'focusPrevious()', () => {
|
|
|
+ it( 'focuses previous focusable view', () => {
|
|
|
+ const spy = sinon.spy( focusables.get( 3 ), 'focus' );
|
|
|
+
|
|
|
+ focusTracker.focusedElement = focusables.get( 1 ).element;
|
|
|
+ cycler.focusPrevious();
|
|
|
+
|
|
|
+ sinon.assert.calledOnce( spy );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'does not throw when no focusable items', () => {
|
|
|
+ focusables = new ViewCollection();
|
|
|
+ cycler = new FocusCycler( { focusables, focusTracker } );
|
|
|
+
|
|
|
+ focusables.add( nonFocusable() );
|
|
|
+ focusables.add( nonFocusable() );
|
|
|
+
|
|
|
+ expect( () => {
|
|
|
+ cycler.focusPrevious();
|
|
|
+ } ).to.not.throw();
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'does not throw when no items', () => {
|
|
|
+ focusables = new ViewCollection();
|
|
|
+ cycler = new FocusCycler( { focusables, focusTracker } );
|
|
|
+
|
|
|
+ expect( () => {
|
|
|
+ cycler.focusPrevious();
|
|
|
+ } ).to.not.throw();
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+
|
|
|
+ describe( 'keystrokes', () => {
|
|
|
+ it( 'creates event listeners', () => {
|
|
|
+ const keystrokeHandler = new KeystrokeHandler();
|
|
|
+
|
|
|
+ cycler = new FocusCycler( {
|
|
|
+ focusables, focusTracker, keystrokeHandler,
|
|
|
+ actions: {
|
|
|
+ focusPrevious: 'arrowup',
|
|
|
+ focusNext: 'arrowdown'
|
|
|
+ }
|
|
|
+ } );
|
|
|
+
|
|
|
+ const keyEvtData = {
|
|
|
+ keyCode: keyCodes.arrowup,
|
|
|
+ preventDefault: sinon.spy(),
|
|
|
+ stopPropagation: sinon.spy()
|
|
|
+ };
|
|
|
+
|
|
|
+ const spy1 = sinon.spy( cycler, 'focusPrevious' );
|
|
|
+ const spy2 = sinon.spy( cycler, 'focusNext' );
|
|
|
+
|
|
|
+ keystrokeHandler.press( keyEvtData );
|
|
|
+
|
|
|
+ sinon.assert.calledOnce( spy1 );
|
|
|
+ sinon.assert.calledOnce( keyEvtData.preventDefault );
|
|
|
+ sinon.assert.calledOnce( keyEvtData.stopPropagation );
|
|
|
+ sinon.assert.notCalled( spy2 );
|
|
|
+
|
|
|
+ keyEvtData.keyCode = keyCodes.arrowdown;
|
|
|
+
|
|
|
+ keystrokeHandler.press( keyEvtData );
|
|
|
+
|
|
|
+ sinon.assert.calledOnce( spy1 );
|
|
|
+ sinon.assert.calledTwice( keyEvtData.preventDefault );
|
|
|
+ sinon.assert.calledTwice( keyEvtData.stopPropagation );
|
|
|
+ sinon.assert.calledOnce( spy2 );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'supports array keystroke syntax', () => {
|
|
|
+ const keystrokeHandler = new KeystrokeHandler();
|
|
|
+
|
|
|
+ cycler = new FocusCycler( {
|
|
|
+ focusables, focusTracker, keystrokeHandler,
|
|
|
+ actions: {
|
|
|
+ focusPrevious: [ 'arrowup', 'arrowleft' ],
|
|
|
+ }
|
|
|
+ } );
|
|
|
+
|
|
|
+ const keyEvtData = {
|
|
|
+ keyCode: keyCodes.arrowleft,
|
|
|
+ preventDefault: sinon.spy(),
|
|
|
+ stopPropagation: sinon.spy()
|
|
|
+ };
|
|
|
+
|
|
|
+ const spy = sinon.spy( cycler, 'focusPrevious' );
|
|
|
+
|
|
|
+ keystrokeHandler.press( keyEvtData );
|
|
|
+
|
|
|
+ sinon.assert.calledOnce( spy );
|
|
|
+ sinon.assert.calledOnce( keyEvtData.preventDefault );
|
|
|
+ sinon.assert.calledOnce( keyEvtData.stopPropagation );
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+} );
|
|
|
+
|
|
|
+function focusable() {
|
|
|
+ const view = new View();
|
|
|
+
|
|
|
+ view.focus = () => {};
|
|
|
+ view.element = {};
|
|
|
+
|
|
|
+ return view;
|
|
|
+}
|
|
|
+
|
|
|
+function nonFocusable() {
|
|
|
+ return new View();
|
|
|
+}
|