浏览代码

Enabled toolbar focus and editable re–focus (blur toolbar) keystrokes in the ClassicEditorUI.

Aleksander Nowodzinski 9 年之前
父节点
当前提交
fc22b66993

+ 34 - 0
packages/ckeditor5-editor-classic/src/classiceditorui.js

@@ -9,6 +9,7 @@
 
 import ComponentFactory from 'ckeditor5-ui/src/componentfactory';
 import FocusTracker from 'ckeditor5-utils/src/focustracker';
+import KeystrokeHandler from 'ckeditor5-utils/src/keystrokehandler';
 
 /**
  * The classic editor UI class.
@@ -53,6 +54,14 @@ export default class ClassicEditorUI {
 		 */
 		this.focusTracker = new FocusTracker();
 
+		/**
+		 * Instance of the {@link module:core/keystrokehandler~KeystrokeHandler}.
+		 *
+		 * @readonly
+		 * @member {module:core/keystrokehandler~KeystrokeHandler}
+		 */
+		this.keystrokes = new KeystrokeHandler();
+
 		// Set–up the view.
 		view.set( 'width', editor.config.get( 'ui.width' ) );
 		view.set( 'height', editor.config.get( 'ui.height' ) );
@@ -66,6 +75,7 @@ export default class ClassicEditorUI {
 		view.editable.bind( 'isReadOnly', 'isFocused' ).to( editingRoot );
 		view.editable.name = editingRoot.rootName;
 		this.focusTracker.add( view.editableElement );
+		this.focusTracker.add( this.view.toolbar.element );
 	}
 
 	/**
@@ -75,6 +85,7 @@ export default class ClassicEditorUI {
 	 */
 	init() {
 		const editor = this.editor;
+		const toolbarFocusTracker = this.view.toolbar.focusTracker;
 
 		return this.view.init()
 			.then( () => {
@@ -88,6 +99,29 @@ export default class ClassicEditorUI {
 				}
 
 				return Promise.all( promises );
+			} )
+			.then( () => {
+				// Listen on the keystrokes from the main UI.
+				this.keystrokes.listenTo( this.view.element );
+
+				// Listen on the keystrokes from the floating panels, toolbars and the such.
+				this.keystrokes.listenTo( this.view._bodyCollectionContainer );
+
+				// Focus the toolbar on the keystroke, if not already focused.
+				this.keystrokes.set( 'alt + f10', ( data, cancel ) => {
+					if ( this.focusTracker.isFocused && !toolbarFocusTracker.isFocused ) {
+						this.view.toolbar.focus();
+						cancel();
+					}
+				} );
+
+				// Blur the toolbar and bring the focus back to editable on the keystroke.
+				this.keystrokes.set( 'esc', ( data, cancel ) => {
+					if ( toolbarFocusTracker.isFocused ) {
+						editor.editing.view.focus();
+						cancel();
+					}
+				} );
 			} );
 	}
 

+ 77 - 0
packages/ckeditor5-editor-classic/tests/classiceditorui.js

@@ -10,6 +10,8 @@ import FocusTracker from 'ckeditor5-utils/src/focustracker';
 import ClassicEditorUI from 'ckeditor5-editor-classic/src/classiceditorui';
 import ClassicEditorUIView from 'ckeditor5-editor-classic/src/classiceditoruiview';
 import ClassicTestEditor from 'ckeditor5-core/tests/_utils/classictesteditor';
+import KeystrokeHandler from 'ckeditor5-utils/src/keystrokehandler';
+import { keyCodes } from 'ckeditor5-utils/src/keyboard';
 import View from 'ckeditor5-ui/src/view';
 
 import testUtils from 'ckeditor5-core/tests/_utils/utils';
@@ -57,6 +59,10 @@ describe( 'ClassicEditorUI', () => {
 			expect( ui.focusTracker ).to.be.instanceOf( FocusTracker );
 		} );
 
+		it( 'creates #keystrokes', () => {
+			expect( ui.keystrokes ).to.be.instanceOf( KeystrokeHandler );
+		} );
+
 		it( 'sets view#width and view#height', () => {
 			expect( view.width ).to.equal( 100 );
 			expect( view.height ).to.equal( 200 );
@@ -142,6 +148,76 @@ describe( 'ClassicEditorUI', () => {
 				expect( view.toolbar.items.get( 1 ).name ).to.equal( 'bar' );
 			} );
 		} );
+
+		describe( 'activates keyboard navigation for the toolbar', () => {
+			it( 'listens for keystrokes coming from various parts of the UI', () => {
+				const spy = sinon.spy( ui.keystrokes, 'listenTo' );
+
+				return ui.init().then( () => {
+					sinon.assert.calledTwice( spy );
+					sinon.assert.calledWithExactly( spy.firstCall, ui.view.element );
+					sinon.assert.calledWithExactly( spy.secondCall, ui.view._bodyCollectionContainer );
+				} );
+			} );
+
+			it( 'alt + f10: focus the first focusable toolbar item', () => {
+				return ui.init().then( () => {
+					const spy = sinon.spy( view.toolbar, 'focus' );
+					const toolbarFocusTracker = view.toolbar.focusTracker;
+					const keyEvtData = {
+						keyCode: keyCodes.f10,
+						altKey: true,
+						preventDefault: sinon.spy(),
+						stopPropagation: sinon.spy()
+					};
+
+					toolbarFocusTracker.isFocused = false;
+					ui.focusTracker.isFocused = false;
+
+					ui.keystrokes.press( keyEvtData );
+					sinon.assert.notCalled( spy );
+
+					toolbarFocusTracker.isFocused = true;
+					ui.focusTracker.isFocused = true;
+
+					ui.keystrokes.press( keyEvtData );
+					sinon.assert.notCalled( spy );
+
+					toolbarFocusTracker.isFocused = false;
+					ui.focusTracker.isFocused = true;
+
+					ui.keystrokes.press( keyEvtData );
+					sinon.assert.calledOnce( spy );
+
+					sinon.assert.calledOnce( keyEvtData.preventDefault );
+					sinon.assert.calledOnce( keyEvtData.stopPropagation );
+				} );
+			} );
+
+			it( 'esc: re–foucus editable when toolbar is focused', () => {
+				return ui.init().then( () => {
+					const spy = sinon.spy( editor.editing.view, 'focus' );
+					const toolbarFocusTracker = view.toolbar.focusTracker;
+					const keyEvtData = { keyCode: keyCodes.esc,
+						preventDefault: sinon.spy(),
+						stopPropagation: sinon.spy()
+					};
+
+					toolbarFocusTracker.isFocused = false;
+
+					ui.keystrokes.press( keyEvtData );
+					sinon.assert.notCalled( spy );
+
+					toolbarFocusTracker.isFocused = true;
+
+					ui.keystrokes.press( keyEvtData );
+
+					sinon.assert.calledOnce( spy );
+					sinon.assert.calledOnce( keyEvtData.preventDefault );
+					sinon.assert.calledOnce( keyEvtData.stopPropagation );
+				} );
+			} );
+		} );
 	} );
 
 	describe( 'destroy()', () => {
@@ -176,6 +252,7 @@ function viewCreator( name ) {
 		const view = new View( locale );
 
 		view.name = name;
+		view.element = document.createElement( 'a' );
 
 		return view;
 	};