|
|
@@ -3,8 +3,15 @@
|
|
|
* For licensing, see LICENSE.md.
|
|
|
*/
|
|
|
|
|
|
+/* global document */
|
|
|
+
|
|
|
import ViewCollection from 'ckeditor5-ui/src/viewcollection';
|
|
|
import ListView from 'ckeditor5-ui/src/list/listview';
|
|
|
+import KeystrokeHandler from 'ckeditor5-utils/src/keystrokehandler';
|
|
|
+import FocusTracker from 'ckeditor5-utils/src/focustracker';
|
|
|
+import FocusCycler from 'ckeditor5-utils/src/focuscycler';
|
|
|
+import { keyCodes } from 'ckeditor5-utils/src/keyboard';
|
|
|
+import View from 'ckeditor5-ui/src/view';
|
|
|
|
|
|
describe( 'ListView', () => {
|
|
|
let view;
|
|
|
@@ -24,5 +31,149 @@ describe( 'ListView', () => {
|
|
|
expect( view.template.children ).to.have.length( 1 );
|
|
|
expect( view.template.children.get( 0 ) ).to.equal( view.items );
|
|
|
} );
|
|
|
+
|
|
|
+ it( 'creates #focusTracker instance', () => {
|
|
|
+ expect( view.focusTracker ).to.be.instanceOf( FocusTracker );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'creates #keystrokeHandler instance', () => {
|
|
|
+ expect( view.keystrokes ).to.be.instanceOf( KeystrokeHandler );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'creates #_focusCycler instance', () => {
|
|
|
+ expect( view._focusCycler ).to.be.instanceOf( FocusCycler );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'registers #items in #focusTracker', () => {
|
|
|
+ const spyAdd = sinon.spy( view.focusTracker, 'add' );
|
|
|
+ const spyRemove = sinon.spy( view.focusTracker, 'remove' );
|
|
|
+
|
|
|
+ view.items.add( focusable() );
|
|
|
+ view.items.add( focusable() );
|
|
|
+
|
|
|
+ sinon.assert.calledTwice( spyAdd );
|
|
|
+
|
|
|
+ view.items.remove( 1 );
|
|
|
+ sinon.assert.calledOnce( spyRemove );
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+
|
|
|
+ describe( 'init()', () => {
|
|
|
+ it( 'starts listening for #keystrokes coming from #element', () => {
|
|
|
+ view = new ListView();
|
|
|
+
|
|
|
+ const spy = sinon.spy( view.keystrokes, 'listenTo' );
|
|
|
+
|
|
|
+ return view.init().then( () => {
|
|
|
+ sinon.assert.calledOnce( spy );
|
|
|
+ sinon.assert.calledWithExactly( spy, view.element );
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+
|
|
|
+ describe( 'activates keyboard navigation for the list', () => {
|
|
|
+ it( 'so "arrowup" focuses previous focusable item', () => {
|
|
|
+ const keyEvtData = {
|
|
|
+ keyCode: keyCodes.arrowup,
|
|
|
+ preventDefault: sinon.spy(),
|
|
|
+ stopPropagation: sinon.spy()
|
|
|
+ };
|
|
|
+
|
|
|
+ // No children to focus.
|
|
|
+ view.keystrokes.press( keyEvtData );
|
|
|
+ sinon.assert.calledOnce( keyEvtData.preventDefault );
|
|
|
+ sinon.assert.calledOnce( keyEvtData.stopPropagation );
|
|
|
+
|
|
|
+ view.items.add( nonFocusable() );
|
|
|
+ view.items.add( nonFocusable() );
|
|
|
+
|
|
|
+ // No focusable children.
|
|
|
+ view.keystrokes.press( keyEvtData );
|
|
|
+ sinon.assert.calledTwice( keyEvtData.preventDefault );
|
|
|
+ sinon.assert.calledTwice( keyEvtData.stopPropagation );
|
|
|
+
|
|
|
+ view.items.add( focusable() );
|
|
|
+ view.items.add( nonFocusable() );
|
|
|
+ view.items.add( focusable() );
|
|
|
+
|
|
|
+ // Mock the last item is focused.
|
|
|
+ view.focusTracker.isFocused = true;
|
|
|
+ view.focusTracker.focusedElement = view.items.get( 4 ).element;
|
|
|
+
|
|
|
+ const spy = sinon.spy( view.items.get( 2 ), 'focus' );
|
|
|
+ view.keystrokes.press( keyEvtData );
|
|
|
+
|
|
|
+ sinon.assert.calledThrice( keyEvtData.preventDefault );
|
|
|
+ sinon.assert.calledThrice( keyEvtData.stopPropagation );
|
|
|
+ sinon.assert.calledOnce( spy );
|
|
|
+ } );
|
|
|
+
|
|
|
+ it( 'so "arrowdown" focuses next focusable item', () => {
|
|
|
+ const keyEvtData = {
|
|
|
+ keyCode: keyCodes.arrowdown,
|
|
|
+ preventDefault: sinon.spy(),
|
|
|
+ stopPropagation: sinon.spy()
|
|
|
+ };
|
|
|
+
|
|
|
+ // No children to focus.
|
|
|
+ view.keystrokes.press( keyEvtData );
|
|
|
+ sinon.assert.calledOnce( keyEvtData.preventDefault );
|
|
|
+ sinon.assert.calledOnce( keyEvtData.stopPropagation );
|
|
|
+
|
|
|
+ view.items.add( nonFocusable() );
|
|
|
+ view.items.add( nonFocusable() );
|
|
|
+
|
|
|
+ // No focusable children.
|
|
|
+ view.keystrokes.press( keyEvtData );
|
|
|
+ sinon.assert.calledTwice( keyEvtData.preventDefault );
|
|
|
+ sinon.assert.calledTwice( keyEvtData.stopPropagation );
|
|
|
+
|
|
|
+ view.items.add( focusable() );
|
|
|
+ view.items.add( nonFocusable() );
|
|
|
+ view.items.add( focusable() );
|
|
|
+
|
|
|
+ // Mock the last item is focused.
|
|
|
+ view.focusTracker.isFocused = true;
|
|
|
+ view.focusTracker.focusedElement = view.items.get( 4 ).element;
|
|
|
+
|
|
|
+ const spy = sinon.spy( view.items.get( 2 ), 'focus' );
|
|
|
+ view.keystrokes.press( keyEvtData );
|
|
|
+
|
|
|
+ sinon.assert.calledThrice( keyEvtData.preventDefault );
|
|
|
+ sinon.assert.calledThrice( keyEvtData.stopPropagation );
|
|
|
+ sinon.assert.calledOnce( spy );
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+
|
|
|
+ describe( 'focus()', () => {
|
|
|
+ it( 'focuses the first focusable item in DOM', () => {
|
|
|
+ // No children to focus.
|
|
|
+ view.focus();
|
|
|
+
|
|
|
+ // The second child is focusable.
|
|
|
+ view.items.add( nonFocusable() );
|
|
|
+ view.items.add( focusable() );
|
|
|
+ view.items.add( nonFocusable() );
|
|
|
+
|
|
|
+ const spy = sinon.spy( view.items.get( 1 ), 'focus' );
|
|
|
+ view.focus();
|
|
|
+
|
|
|
+ sinon.assert.calledOnce( spy );
|
|
|
+ } );
|
|
|
} );
|
|
|
} );
|
|
|
+
|
|
|
+function focusable() {
|
|
|
+ const view = nonFocusable();
|
|
|
+
|
|
|
+ view.focus = () => {};
|
|
|
+
|
|
|
+ return view;
|
|
|
+}
|
|
|
+
|
|
|
+function nonFocusable() {
|
|
|
+ const view = new View();
|
|
|
+ view.element = document.createElement( 'li' );
|
|
|
+
|
|
|
+ return view;
|
|
|
+}
|