8
0
فهرست منبع

Implemented DropdownView#focus() with keyboard navigation using arrow[left,right,down] and esc keystrokes.

Aleksander Nowodzinski 9 سال پیش
والد
کامیت
0006bc24a2
2فایلهای تغییر یافته به همراه202 افزوده شده و 0 حذف شده
  1. 65 0
      packages/ckeditor5-ui/src/dropdown/dropdownview.js
  2. 137 0
      packages/ckeditor5-ui/tests/dropdown/dropdownview.js

+ 65 - 0
packages/ckeditor5-ui/src/dropdown/dropdownview.js

@@ -9,6 +9,8 @@
 
 import View from '../view';
 import Template from '../template';
+import FocusTracker from 'ckeditor5-utils/src/focustracker';
+import KeystrokeHandler from 'ckeditor5-utils/src/keystrokehandler';
 
 /**
  * The dropdown view class.
@@ -47,6 +49,22 @@ export default class DropdownView extends View {
 		 */
 		this.set( 'isOpen', false );
 
+		/**
+		 * Tracks information about DOM focus in the list.
+		 *
+		 * @readonly
+		 * @member {module:utils/focustracker~FocusTracker}
+		 */
+		this.focusTracker = new FocusTracker();
+
+		/**
+		 * Instance of the {@link module:core/keystrokehandler~KeystrokeHandler}.
+		 *
+		 * @readonly
+		 * @member {module:core/keystrokehandler~KeystrokeHandler}
+		 */
+		this.keystrokes = new KeystrokeHandler();
+
 		this.template = new Template( {
 			tag: 'div',
 
@@ -104,4 +122,51 @@ export default class DropdownView extends View {
 		 * @member {Boolean} #withText
 		 */
 	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		// Listen for keystrokes coming from within #element.
+		this.keystrokes.listenTo( this.element );
+
+		// Register #element in the focus tracker.
+		this.focusTracker.add( this.element );
+
+		const closeDropdown = ( data, cancel ) => {
+			if ( this.isOpen ) {
+				this.buttonView.focus();
+				this.isOpen = false;
+				cancel();
+			}
+		};
+
+		// Open the dropdown panel using the arrow down key, just like with return or space.
+		this.keystrokes.set( 'arrowdown', ( data, cancel ) => {
+			if ( !this.isOpen ) {
+				this.isOpen = true;
+				cancel();
+			}
+		} );
+
+		// Block the right arrow key (until nested dropdowns are implemented).
+		this.keystrokes.set( 'arrowright', ( data, cancel ) => {
+			if ( this.isOpen ) {
+				cancel();
+			}
+		} );
+
+		// Close the dropdown using the arrow left/escape key.
+		this.keystrokes.set( 'arrowleft', closeDropdown );
+		this.keystrokes.set( 'esc', closeDropdown );
+
+		return super.init();
+	}
+
+	/**
+	 * Focuses the {@link #buttonView}.
+	 */
+	focus() {
+		this.buttonView.focus();
+	}
 }

+ 137 - 0
packages/ckeditor5-ui/tests/dropdown/dropdownview.js

@@ -4,6 +4,9 @@
  */
 
 import DropdownView from 'ckeditor5-ui/src/dropdown/dropdownview';
+import KeystrokeHandler from 'ckeditor5-utils/src/keystrokehandler';
+import FocusTracker from 'ckeditor5-utils/src/focustracker';
+import { keyCodes } from 'ckeditor5-utils/src/keyboard';
 import ButtonView from 'ckeditor5-ui/src/button/buttonview';
 import DropdownPanelView from 'ckeditor5-ui/src/dropdown/dropdownpanelview';
 
@@ -36,6 +39,14 @@ describe( 'DropdownView', () => {
 			expect( view.isOpen ).to.be.false;
 		} );
 
+		it( 'creates #focusTracker instance', () => {
+			expect( view.focusTracker ).to.be.instanceOf( FocusTracker );
+		} );
+
+		it( 'creates #keystrokeHandler instance', () => {
+			expect( view.keystrokes ).to.be.instanceOf( KeystrokeHandler );
+		} );
+
 		it( 'creates #element from template', () => {
 			expect( view.element.classList.contains( 'ck-dropdown' ) ).to.be.true;
 			expect( view.element.firstChild ).to.equal( buttonView.element );
@@ -80,4 +91,130 @@ describe( 'DropdownView', () => {
 			} );
 		} );
 	} );
+
+	describe( 'init()', () => {
+		it( 'starts listening for #keystrokes coming from #element', () => {
+			view = new DropdownView( locale,
+				new ButtonView( locale ),
+				new DropdownPanelView( locale ) );
+
+			const spy = sinon.spy( view.keystrokes, 'listenTo' );
+
+			return view.init().then( () => {
+				sinon.assert.calledOnce( spy );
+				sinon.assert.calledWithExactly( spy, view.element );
+			} );
+		} );
+
+		it( 'adds #element to #focusTracker', () => {
+			view = new DropdownView( locale,
+				new ButtonView( locale ),
+				new DropdownPanelView( locale ) );
+
+			const spy = sinon.spy( view.focusTracker, 'add' );
+
+			return view.init().then( () => {
+				sinon.assert.calledOnce( spy );
+				sinon.assert.calledWithExactly( spy, view.element );
+			} );
+		} );
+
+		describe( 'activates keyboard navigation for the dropdown', () => {
+			it( 'so "arrowdown" opens the #panelView', () => {
+				const keyEvtData = {
+					keyCode: keyCodes.arrowdown,
+					preventDefault: sinon.spy(),
+					stopPropagation: sinon.spy()
+				};
+
+				view.isOpen = true;
+				view.keystrokes.press( keyEvtData );
+				sinon.assert.notCalled( keyEvtData.preventDefault );
+				sinon.assert.notCalled( keyEvtData.stopPropagation );
+				expect( view.isOpen ).to.be.true;
+
+				view.isOpen = false;
+				view.keystrokes.press( keyEvtData );
+				sinon.assert.calledOnce( keyEvtData.preventDefault );
+				sinon.assert.calledOnce( keyEvtData.stopPropagation );
+				expect( view.isOpen ).to.be.true;
+			} );
+
+			it( 'so "arrowright" is blocked', () => {
+				const keyEvtData = {
+					keyCode: keyCodes.arrowright,
+					preventDefault: sinon.spy(),
+					stopPropagation: sinon.spy()
+				};
+
+				view.false = true;
+				view.keystrokes.press( keyEvtData );
+				sinon.assert.notCalled( keyEvtData.preventDefault );
+				sinon.assert.notCalled( keyEvtData.stopPropagation );
+				expect( view.isOpen ).to.be.false;
+
+				view.isOpen = true;
+				view.keystrokes.press( keyEvtData );
+				sinon.assert.calledOnce( keyEvtData.preventDefault );
+				sinon.assert.calledOnce( keyEvtData.stopPropagation );
+				expect( view.isOpen ).to.be.true;
+			} );
+
+			it( 'so "arrowleft" closes the #panelView', () => {
+				const keyEvtData = {
+					keyCode: keyCodes.arrowleft,
+					preventDefault: sinon.spy(),
+					stopPropagation: sinon.spy()
+				};
+				const spy = sinon.spy( view.buttonView, 'focus' );
+
+				view.isOpen = false;
+				view.keystrokes.press( keyEvtData );
+				sinon.assert.notCalled( keyEvtData.preventDefault );
+				sinon.assert.notCalled( keyEvtData.stopPropagation );
+				sinon.assert.notCalled( spy );
+				expect( view.isOpen ).to.be.false;
+
+				view.isOpen = true;
+				view.keystrokes.press( keyEvtData );
+				sinon.assert.calledOnce( keyEvtData.preventDefault );
+				sinon.assert.calledOnce( keyEvtData.stopPropagation );
+				sinon.assert.calledOnce( spy );
+				expect( view.isOpen ).to.be.false;
+			} );
+
+			it( 'so "esc" closes the #panelView', () => {
+				const keyEvtData = {
+					keyCode: keyCodes.esc,
+					preventDefault: sinon.spy(),
+					stopPropagation: sinon.spy()
+				};
+				const spy = sinon.spy( view.buttonView, 'focus' );
+
+				view.isOpen = false;
+				view.keystrokes.press( keyEvtData );
+				sinon.assert.notCalled( keyEvtData.preventDefault );
+				sinon.assert.notCalled( keyEvtData.stopPropagation );
+				sinon.assert.notCalled( spy );
+				expect( view.isOpen ).to.be.false;
+
+				view.isOpen = true;
+				view.keystrokes.press( keyEvtData );
+				sinon.assert.calledOnce( keyEvtData.preventDefault );
+				sinon.assert.calledOnce( keyEvtData.stopPropagation );
+				sinon.assert.calledOnce( spy );
+				expect( view.isOpen ).to.be.false;
+			} );
+		} );
+	} );
+
+	describe( 'focus()', () => {
+		it( 'focuses the #buttonView in DOM', () => {
+			const spy = sinon.spy( view.buttonView, 'focus' );
+
+			view.focus();
+
+			sinon.assert.calledOnce( spy );
+		} );
+	} );
 } );