Skip to content

Commit

Permalink
Merge pull request #12257 from ckeditor/ck/9037-keyboard-support-for-…
Browse files Browse the repository at this point in the history
…special-characters-dropdown-grid

Fix (special-characters): Added keyboard support to the special characters dropdown. Closes #9037.
  • Loading branch information
oleq authored Aug 23, 2022
2 parents 64c47c7 + c03f252 commit 037ac54
Show file tree
Hide file tree
Showing 11 changed files with 588 additions and 20 deletions.
12 changes: 9 additions & 3 deletions packages/ckeditor5-special-characters/src/specialcharacters.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { CKEditorError } from 'ckeditor5/src/utils';
import SpecialCharactersNavigationView from './ui/specialcharactersnavigationview';
import CharacterGridView from './ui/charactergridview';
import CharacterInfoView from './ui/characterinfoview';
import SpecialCharactersView from './ui/specialcharactersview';

import specialCharactersIcon from '../theme/icons/specialcharacters.svg';
import '../theme/specialcharacters.css';
Expand Down Expand Up @@ -97,9 +98,14 @@ export default class SpecialCharacters extends Plugin {
if ( !dropdownPanelContent ) {
dropdownPanelContent = this._createDropdownPanelContent( locale, dropdownView );

dropdownView.panelView.children.add( dropdownPanelContent.navigationView );
dropdownView.panelView.children.add( dropdownPanelContent.gridView );
dropdownView.panelView.children.add( dropdownPanelContent.infoView );
const specialCharactersView = new SpecialCharactersView(
locale,
dropdownPanelContent.navigationView,
dropdownPanelContent.gridView,
dropdownPanelContent.infoView
);

dropdownView.panelView.children.add( specialCharactersView );
}

dropdownPanelContent.infoView.set( {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
* @module special-characters/ui/charactergridview
*/

import { View, ButtonView } from 'ckeditor5/src/ui';
import { View, ButtonView, addKeyboardHandlingForGrid } from 'ckeditor5/src/ui';

import { KeystrokeHandler, FocusTracker, global } from 'ckeditor5/src/utils';

import '../../theme/charactergrid.css';

Expand Down Expand Up @@ -56,6 +58,33 @@ export default class CharacterGridView extends View {
}
} );

/**
* Tracks information about the DOM focus in the grid.
*
* @readonly
* @member {module:utils/focustracker~FocusTracker}
*/
this.focusTracker = new FocusTracker();

/**
* An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
*
* @readonly
* @member {module:utils/keystrokehandler~KeystrokeHandler}
*/
this.keystrokes = new KeystrokeHandler();

addKeyboardHandlingForGrid( {
keystrokeHandler: this.keystrokes,
focusTracker: this.focusTracker,
gridItems: this.tiles,
numberOfColumns: () => global.window
.getComputedStyle( this.element.firstChild ) // Responsive .ck-character-grid__tiles
.getPropertyValue( 'grid-template-columns' )
.split( ' ' )
.length
} );

/**
* Fired when any of {@link #tiles grid tiles} is clicked.
*
Expand Down Expand Up @@ -113,4 +142,46 @@ export default class CharacterGridView extends View {

return tile;
}

/**
* @inheritDoc
*/
render() {
super.render();

for ( const item of this.tiles ) {
this.focusTracker.add( item.element );
}

this.tiles.on( 'change', ( eventInfo, { added, removed } ) => {
if ( added.length > 0 ) {
for ( const item of added ) {
this.focusTracker.add( item.element );
}
}
if ( removed.length > 0 ) {
for ( const item of removed ) {
this.focusTracker.remove( item.element );
}
}
} );

this.keystrokes.listenTo( this.element );
}

/**
* @inheritDoc
*/
destroy() {
super.destroy();

this.keystrokes.destroy();
}

/**
* Focuses the first focusable in {@link #tiles}.
*/
focus() {
this.tiles.get( 0 ).focus();
}
}
138 changes: 138 additions & 0 deletions packages/ckeditor5-special-characters/src/ui/specialcharactersview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/**
* @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

/**
* @module special-characters/ui/specialcharactersview
*/

import { View, FocusCycler } from 'ckeditor5/src/ui';
import { FocusTracker, KeystrokeHandler } from 'ckeditor5/src/utils';

/**
* A view that glues pieces of the special characters dropdown panel together:
*
* * the navigation view (allows selecting the category),
* * the grid view (displays characters as a grid),
* * and the info view (displays detailed info about a specific character).
*
* @extends module:ui/view~View
*/
export default class SpecialCharactersView extends View {
/**
* Creates an instance of the `SpecialCharactersView`.
*
* @param {module:utils/locale~Locale} locale The localization services instance.
* @param {module:special-characters/ui/specialcharactersnavigationview~SpecialCharactersNavigationView} navigationView
* @param {module:special-characters/ui/charactergridview~CharacterGridView} gridView
* @param {module:special-characters/ui/characterinfoview~CharacterInfoView} infoView
*/
constructor( locale, navigationView, gridView, infoView ) {
super( locale );

/**
* A collection of the focusable children of the view.
*
* @readonly
* @member {module:ui/viewcollection~ViewCollection}
*/
this.items = this.createCollection();

/**
* Tracks information about the DOM focus in the view.
*
* @readonly
* @member {module:utils/focustracker~FocusTracker}
*/
this.focusTracker = new FocusTracker();

/**
* An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
*
* @readonly
* @member {module:utils/keystrokehandler~KeystrokeHandler}
*/
this.keystrokes = new KeystrokeHandler();

/**
* Helps cycling over focusable {@link #items} in the view.
*
* @readonly
* @protected
* @member {module:ui/focuscycler~FocusCycler}
*/
this._focusCycler = new FocusCycler( {
focusables: this.items,
focusTracker: this.focusTracker,
keystrokeHandler: this.keystrokes,
actions: {
focusPrevious: 'shift + tab',
focusNext: 'tab'
}
} );

/**
* An instance of the `SpecialCharactersNavigationView`.
*
* @member {module:special-characters/ui/specialcharactersnavigationview~SpecialCharactersNavigationView}
*/
this.navigationView = navigationView;

/**
* An instance of the `CharacterGridView`.
*
* @member {module:special-characters/ui/charactergridview~CharacterGridView}
*/
this.gridView = gridView;

/**
* An instance of the `CharacterInfoView`.
*
* @member {module:special-characters/ui/characterinfoview~CharacterInfoView}
*/
this.infoView = infoView;

this.setTemplate( {
tag: 'div',
children: [
this.navigationView,
this.gridView,
this.infoView
]
} );

this.items.add( this.navigationView.groupDropdownView.buttonView );
this.items.add( this.gridView );
}

/**
* @inheritDoc
*/
render() {
super.render();

this.focusTracker.add( this.navigationView.groupDropdownView.buttonView.element );
this.focusTracker.add( this.gridView.element );

// Start listening for the keystrokes coming from #element.
this.keystrokes.listenTo( this.element );
}

/**
* @inheritDoc
*/
destroy() {
super.destroy();

this.focusTracker.destroy();
this.keystrokes.destroy();
}

/**
* Focuses the first focusable in {@link #items}.
*/
focus() {
this.navigationView.focus();
}
}
18 changes: 9 additions & 9 deletions packages/ckeditor5-special-characters/tests/specialcharacters.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,23 +72,23 @@ describe( 'SpecialCharacters', () => {
} );

it( 'has a navigation view', () => {
expect( dropdown.panelView.children.first ).to.be.instanceOf( SpecialCharactersNavigationView );
expect( dropdown.panelView.children.first.navigationView ).to.be.instanceOf( SpecialCharactersNavigationView );
} );

it( 'a navigation contains the "All" special category', () => {
const listView = dropdown.panelView.children.first.groupDropdownView.panelView.children.first;
const listView = dropdown.panelView.children.first.navigationView.groupDropdownView.panelView.children.first;

// "Mathematical" and "Arrows" are provided by other plugins. "All" is being added by SpecialCharacters itself.
expect( listView.items.length ).to.equal( 3 );
expect( listView.items.first.children.first.label ).to.equal( 'All' );
} );

it( 'has a grid view', () => {
expect( dropdown.panelView.children.get( 1 ) ).to.be.instanceOf( CharacterGridView );
expect( dropdown.panelView.children.first.gridView ).to.be.instanceOf( CharacterGridView );
} );

it( 'has a character info view', () => {
expect( dropdown.panelView.children.last ).to.be.instanceOf( CharacterInfoView );
expect( dropdown.panelView.children.first.infoView ).to.be.instanceOf( CharacterInfoView );
} );

describe( '#buttonView', () => {
Expand All @@ -108,7 +108,7 @@ describe( 'SpecialCharacters', () => {
} );

it( 'executes a command and focuses the editing view', () => {
const grid = dropdown.panelView.children.get( 1 );
const grid = dropdown.panelView.children.get( 0 ).gridView;
const executeSpy = sinon.stub( editor, 'execute' );
const focusSpy = sinon.stub( editor.editing.view, 'focus' );

Expand All @@ -125,7 +125,7 @@ describe( 'SpecialCharacters', () => {
let grid;

beforeEach( () => {
grid = dropdown.panelView.children.get( 1 );
grid = dropdown.panelView.children.get( 0 ).gridView;
} );

it( 'delegates #execute to the dropdown', () => {
Expand All @@ -142,7 +142,7 @@ describe( 'SpecialCharacters', () => {
} );

it( 'is updated when navigation view fires #execute', () => {
const navigation = dropdown.panelView.children.first;
const navigation = dropdown.panelView.children.first.navigationView;

expect( grid.tiles.get( 0 ).label ).to.equal( '<' );
navigation.groupDropdownView.fire( new EventInfo( { label: 'Arrows' }, 'execute' ) );
Expand All @@ -155,8 +155,8 @@ describe( 'SpecialCharacters', () => {
let grid, characterInfo;

beforeEach( () => {
grid = dropdown.panelView.children.get( 1 );
characterInfo = dropdown.panelView.children.last;
grid = dropdown.panelView.children.first.gridView;
characterInfo = dropdown.panelView.children.first.infoView;
} );

it( 'is empty when the dropdown was shown', () => {
Expand Down
Loading

0 comments on commit 037ac54

Please sign in to comment.