@ckeditor/ckeditor5-core
Version:
The core architecture of CKEditor 5 – the best browser-based rich text editor.
330 lines (329 loc) • 12.1 kB
JavaScript
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module core/accessibility
*/
import { CKEditorError } from '@ckeditor/ckeditor5-utils';
const DEFAULT_CATEGORY_ID = 'contentEditing';
export const DEFAULT_GROUP_ID = 'common';
/**
* A common namespace for various accessibility features of the editor.
*
* **Information about editor keystrokes**
*
* * The information about keystrokes available in the editor is stored in the {@link #keystrokeInfos} property.
* * New info entries can be added using the {@link #addKeystrokeInfoCategory}, {@link #addKeystrokeInfoGroup},
* and {@link #addKeystrokeInfos} methods.
*/
export default class Accessibility {
/**
* Stores information about keystrokes brought by editor features for the users to interact with the editor, mainly
* keystroke combinations and their accessible labels.
*
* This information is particularly useful for screen reader and other assistive technology users. It gets displayed
* by the {@link module:ui/editorui/accessibilityhelp/accessibilityhelp~AccessibilityHelp Accessibility help} dialog.
*
* Keystrokes are organized in categories and groups. They can be added using ({@link #addKeystrokeInfoCategory},
* {@link #addKeystrokeInfoGroup}, and {@link #addKeystrokeInfos}) methods.
*
* Please note that:
* * two categories are always available:
* * `'contentEditing'` for keystrokes related to content creation,
* * `'navigation'` for keystrokes related to navigation in the UI and the content.
* * unless specified otherwise, new keystrokes are added into the `'contentEditing'` category and the `'common'`
* keystroke group within that category while using the {@link #addKeystrokeInfos} method.
*/
keystrokeInfos = new Map();
/**
* The editor instance.
*/
_editor;
/**
* @inheritDoc
*/
constructor(editor) {
this._editor = editor;
const isMenuBarVisible = editor.config.get('menuBar.isVisible');
const t = editor.locale.t;
this.addKeystrokeInfoCategory({
id: DEFAULT_CATEGORY_ID,
label: t('Content editing keystrokes'),
description: t('These keyboard shortcuts allow for quick access to content editing features.')
});
const navigationKeystrokes = [
{
label: t('Close contextual balloons, dropdowns, and dialogs'),
keystroke: 'Esc'
},
{
label: t('Open the accessibility help dialog'),
keystroke: 'Alt+0'
},
{
label: t('Move focus between form fields (inputs, buttons, etc.)'),
keystroke: [['Tab'], ['Shift+Tab']]
},
{
label: t('Move focus to the toolbar, navigate between toolbars'),
keystroke: 'Alt+F10',
mayRequireFn: true
},
{
label: t('Navigate through the toolbar or menu bar'),
keystroke: [['arrowup'], ['arrowright'], ['arrowdown'], ['arrowleft']]
},
{
// eslint-disable-next-line max-len
label: t('Execute the currently focused button. Executing buttons that interact with the editor content moves the focus back to the content.'),
keystroke: [['Enter'], ['Space']]
}
];
if (isMenuBarVisible) {
navigationKeystrokes.push({
label: t('Move focus to the menu bar, navigate between menu bars'),
keystroke: 'Alt+F9',
mayRequireFn: true
});
}
this.addKeystrokeInfoCategory({
id: 'navigation',
label: t('User interface and content navigation keystrokes'),
description: t('Use the following keystrokes for more efficient navigation in the CKEditor 5 user interface.'),
groups: [
{
id: 'common',
keystrokes: navigationKeystrokes
}
]
});
}
/**
* Adds a top-level category in the {@link #keystrokeInfos keystroke information database} with a label and optional description.
*
* Categories organize keystrokes and help users to find the right keystroke. Each category can have multiple groups
* of keystrokes that narrow down the context in which the keystrokes are available. Every keystroke category comes
* with a `'common'` group by default.
*
* By default, two categories are available:
* * `'contentEditing'` for keystrokes related to content creation,
* * `'navigation'` for keystrokes related to navigation in the UI and the content.
*
* To create a new keystroke category with new groups, use the following code:
*
* ```js
* class MyPlugin extends Plugin {
* // ...
* init() {
* const editor = this.editor;
* const t = editor.t;
*
* // ...
*
* editor.accessibility.addKeystrokeInfoCategory( {
* id: 'myCategory',
* label: t( 'My category' ),
* description: t( 'My category description.' ),
* groups: [
* {
* id: 'myGroup',
* label: t( 'My keystroke group' ),
* keystrokes: [
* {
* label: t( 'Keystroke label 1' ),
* keystroke: 'Ctrl+Shift+N'
* },
* {
* label: t( 'Keystroke label 2' ),
* keystroke: 'Ctrl+Shift+M'
* }
* ]
* }
* ]
* };
* }
* }
* ```
*
* See {@link #keystrokeInfos}, {@link #addKeystrokeInfoGroup}, and {@link #addKeystrokeInfos}.
*/
addKeystrokeInfoCategory({ id, label, description, groups }) {
this.keystrokeInfos.set(id, {
id,
label,
description,
groups: new Map()
});
this.addKeystrokeInfoGroup({
categoryId: id,
id: DEFAULT_GROUP_ID
});
if (groups) {
groups.forEach(group => {
this.addKeystrokeInfoGroup({
categoryId: id,
...group
});
});
}
}
/**
* Adds a group of keystrokes in a specific category to the {@link #keystrokeInfos keystroke information database}.
*
* Groups narrow down the context in which the keystrokes are available. When `categoryId` is not specified,
* the group goes to the `'contentEditing'` category (default).
*
* To create a new group within an existing category, use the following code:
*
* ```js
* class MyPlugin extends Plugin {
* // ...
* init() {
* const editor = this.editor;
* const t = editor.t;
*
* // ...
*
* editor.accessibility.addKeystrokeInfoGroup( {
* id: 'myGroup',
* categoryId: 'navigation',
* label: t( 'My keystroke group' ),
* keystrokes: [
* {
* label: t( 'Keystroke label 1' ),
* keystroke: 'Ctrl+Shift+N'
* },
* {
* label: t( 'Keystroke label 2' ),
* keystroke: 'Ctrl+Shift+M'
* }
* ]
* } );
* }
* }
* ```
*
* See {@link #keystrokeInfos}, {@link #addKeystrokeInfoCategory}, and {@link #addKeystrokeInfos}.
*/
addKeystrokeInfoGroup({ categoryId = DEFAULT_CATEGORY_ID, id, label, keystrokes }) {
const category = this.keystrokeInfos.get(categoryId);
if (!category) {
throw new CKEditorError('accessibility-unknown-keystroke-info-category', this._editor, { groupId: id, categoryId });
}
category.groups.set(id, {
id,
label,
keystrokes: keystrokes || []
});
}
/**
* Adds information about keystrokes to the {@link #keystrokeInfos keystroke information database}.
*
* Keystrokes without specified `groupId` or `categoryId` go to the `'common'` group in the `'contentEditing'` category (default).
*
* To add a keystroke brought by your plugin (using default group and category), use the following code:
*
* ```js
* class MyPlugin extends Plugin {
* // ...
* init() {
* const editor = this.editor;
* const t = editor.t;
*
* // ...
*
* editor.accessibility.addKeystrokeInfos( {
* keystrokes: [
* {
* label: t( 'Keystroke label' ),
* keystroke: 'CTRL+B'
* }
* ]
* } );
* }
* }
* ```
* To add a keystroke in a specific existing `'widget'` group in the default `'contentEditing'` category:
*
* ```js
* class MyPlugin extends Plugin {
* // ...
* init() {
* const editor = this.editor;
* const t = editor.t;
*
* // ...
*
* editor.accessibility.addKeystrokeInfos( {
* // Add a keystroke to the existing "widget" group.
* groupId: 'widget',
* keystrokes: [
* {
* label: t( 'A an action on a selected widget' ),
* keystroke: 'Ctrl+D',
* }
* ]
* } );
* }
* }
* ```
*
* To add a keystroke to another existing category (using default group):
*
* ```js
* class MyPlugin extends Plugin {
* // ...
* init() {
* const editor = this.editor;
* const t = editor.t;
*
* // ...
*
* editor.accessibility.addKeystrokeInfos( {
* // Add keystrokes to the "navigation" category (one of defaults).
* categoryId: 'navigation',
* keystrokes: [
* {
* label: t( 'Keystroke label' ),
* keystroke: 'CTRL+B'
* }
* ]
* } );
* }
* }
* ```
*
* See {@link #keystrokeInfos}, {@link #addKeystrokeInfoGroup}, and {@link #addKeystrokeInfoCategory}.
*/
addKeystrokeInfos({ categoryId = DEFAULT_CATEGORY_ID, groupId = DEFAULT_GROUP_ID, keystrokes }) {
if (!this.keystrokeInfos.has(categoryId)) {
/**
* Cannot add keystrokes in an unknown category. Use
* {@link module:core/accessibility~Accessibility#addKeystrokeInfoCategory}
* to add a new category or make sure the specified category exists.
*
* @error accessibility-unknown-keystroke-info-category
* @param {string} categoryId The id of the unknown keystroke category.
* @param {module:core/accessibility~AddKeystrokeInfosData#keystrokes} keystrokes Keystroke definitions about to be added.
*/
throw new CKEditorError('accessibility-unknown-keystroke-info-category', this._editor, { categoryId, keystrokes });
}
const category = this.keystrokeInfos.get(categoryId);
if (!category.groups.has(groupId)) {
/**
* Cannot add keystrokes to an unknown group.
*
* Use {@link module:core/accessibility~Accessibility#addKeystrokeInfoGroup}
* to add a new group or make sure the specified group exists.
*
* @error accessibility-unknown-keystroke-info-group
* @param {string} groupId The id of the unknown keystroke group.
* @param {string} categoryId The id of category the unknown group should belong to.
* @param {module:core/accessibility~AddKeystrokeInfosData#keystrokes} keystrokes Keystroke definitions about to be added.
*/
throw new CKEditorError('accessibility-unknown-keystroke-info-group', this._editor, { groupId, categoryId, keystrokes });
}
category.groups.get(groupId).keystrokes.push(...keystrokes);
}
}