Skip to content

Commit

Permalink
Improves ARIA support of the toolbars and buttons
Browse files Browse the repository at this point in the history
  • Loading branch information
ipeychev committed Oct 28, 2014
1 parent 258ce16 commit 663db28
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 20 deletions.
42 changes: 34 additions & 8 deletions src/core/uicore.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@
* - selectionData - The data, returned from {{#crossLink "CKEDITOR.plugins.selectionregion/getSelectionData:method"}}{{/crossLink}}
*/

/**
* Fired by UI elements like Toolbars or Buttons when their state changes. The listener updates the live region with the provided data.
*
* @event ariaUpdate
* @param {Object} data An object which contains the following properties:
* - message - The provided message from the UI element.
*/

/**
* If set to true, the editor will still fire {{#crossLink "CKEDITOR.plugins.uicore/editorInteraction:event"}}{{/crossLink}} event,
* if user presses Esc key.
Expand Down Expand Up @@ -57,19 +65,19 @@
* @param {Object} editor The current CKEditor instance.
*/
init: function(editor) {
var handleUI,
handleAria,
var ariaElement,
ariaState = [],
ariaStatusElement,
handleAria,
handleUI,
uiTasksTimeout;

ariaStatusElement = this._createAriaStatusElement(editor.id);
ariaElement = this._createAriaElement(editor.id);

uiTasksTimeout = editor.config.uicore ? editor.config.uicore.timeout : 50;

handleAria = CKEDITOR.tools.debounce(
function(event) {
ariaStatusElement.innerHTML = ariaState.join('. ');
ariaElement.innerHTML = ariaState.join('. ');
},
uiTasksTimeout
);
Expand All @@ -89,7 +97,14 @@
);

editor.on('ariaUpdate', function(event) {
ariaState.push(event.data.msg);
// handleAria is debounced function, so if it is being called multiple times, it will
// be canceled until some time passes.
// For that reason here we explicitly append the current message to the list of messages
// and call handleAria. Since it is debounced, when some timeout passes,
// all the messages will be applied to the live region and not only the last one.

ariaState.push(event.data.message);

handleAria();
});

Expand All @@ -101,13 +116,24 @@
});
},

_createAriaStatusElement: function(id) {
/**
* Creates and applies an HTML element to the body of the document which will contain ARIA messages.
*
* @method _createAriaElement
* @protected
* @param {String} id The provided id of the element. It will be used as prefix for the final element Id.
* @return {HTMLElement} The created and applied to DOM element.
*/
_createAriaElement: function(id) {
var statusElement;

statusElement = document.createElement('div');

statusElement.className = 'sr-only';

statusElement.setAttribute('aria-live', 'polite');
statusElement.setAttribute('role', 'status');
statusElement.setAttribute('id', id + '_aria');
statusElement.setAttribute('id', id + 'LiveRegion');

document.body.appendChild(statusElement);

Expand Down
9 changes: 6 additions & 3 deletions src/ui/yui/src/buttons/button-a.js
Original file line number Diff line number Diff line change
Expand Up @@ -699,10 +699,12 @@ YUI.add('button-a', function(Y) {
'</span>' +
'</div>' +
'<div class="pull-left btn-group input-close-container">' +
'<button aria-label="{confirm}" class="alloy-editor-button btn btn-default close-link"><i class="alloy-editor-icon-ok"></i></button>' +
'<button aria-label="{confirm}" class="alloy-editor-button btn btn-default close-link">' +
'<i class="alloy-editor-icon-ok"></i></button>' +
'</div>' +
'<div class="pull-right btn-group show-buttons-container">' +
'<button aria-label="{back}" class="alloy-editor-button btn btn-default switch-to-edit"><i class="alloy-editor-icon-remove"></i></button>' +
'<button aria-label="{back}" class="alloy-editor-button btn btn-default switch-to-edit">' +
'<i class="alloy-editor-icon-remove"></i></button>' +
'</div>' +
'</div>'
}, {
Expand Down Expand Up @@ -731,7 +733,8 @@ YUI.add('button-a', function(Y) {
* - Button actions (back, clear and confirm)
*
* @attribute strings
* @default {back: 'Back', clear: 'Clear', confirm: 'Confirm', label: 'Link', placeholder: 'Type or paste link here'}
* @default {back: 'Back', clear: 'Clear', confirm: 'Confirm', label: 'Link', placeholder:
* 'Type or paste link here'}
* @type Object
*/
strings: {
Expand Down
59 changes: 51 additions & 8 deletions src/ui/yui/src/toolbars/toolbar-add.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ YUI.add('toolbar-add', function(Y) {
*
* @class ToolbarAdd
*/
ToolbarAdd = Y.Base.create('toolbaradd', Y.Widget, [Y.ToolbarBase, Y.ToolbarPosition, Y.WidgetPosition, Y.WidgetPositionConstrain], {
ToolbarAdd = Y.Base.create('toolbaradd', Y.Widget, [Y.ToolbarBase, Y.ToolbarPosition, Y.WidgetPosition,
Y.WidgetPositionConstrain], {
/**
* Initializer lifecycle implementation for the ToolbarAdd class.
*
Expand Down Expand Up @@ -49,6 +50,7 @@ YUI.add('toolbar-add', function(Y) {
editor = this.get('editor');

this._triggerButton.on('click', this._showToolbarAddContent, this);

this.on('visibleChange', this._onVisibleChange, this);
editor.on('toolbarsReady', this._onToolbarsReady, this);
editor.on('toolbarsHide', this._onToolbarsHide, this);
Expand Down Expand Up @@ -279,6 +281,7 @@ YUI.add('toolbar-add', function(Y) {

triggerButtonContainer = YNode.create(Lang.sub(
this.TPL_TRIGGER, {
addContent: this.get('strings').addContent,
content: this.TPL_TRIGGER_CONTENT
}));

Expand Down Expand Up @@ -330,28 +333,42 @@ YUI.add('toolbar-add', function(Y) {
* @param {Number} top The top offset in page coordinates where Trigger should be shown.
*/
_showTriggerAtPoint: function(left, top) {
var triggerButtonContainer,
var strings,
triggerButtonContainer,
triggerGutter;

if (!this._trigger.get('visible')) {
this._trigger.show();

strings = this.get('strings');

this.get('editor').fire('ariaUpdate', {
message: Lang.sub(strings.state, {
focus: strings.focus,
name: this.name,
state: strings.visible
})
});
}

triggerButtonContainer = this._triggerButtonContainer.getDOMNode();

triggerGutter = this.get('triggerGutter');

this._trigger.set('xy', this.getConstrainedXY([left - triggerButtonContainer.offsetWidth - triggerGutter.left, top - triggerGutter.top - triggerButtonContainer.offsetHeight / 2]));
this._trigger.set('xy', this.getConstrainedXY([left - triggerButtonContainer.offsetWidth -
triggerGutter.left, top - triggerGutter.top - triggerButtonContainer.offsetHeight / 2]));
},

BOUNDING_TEMPLATE: '<div class="alloy-editor-toolbar alloy-editor-toolbar-add alloy-editor-arrow-box"></div>',
BOUNDING_TEMPLATE: '<div class="alloy-editor-toolbar alloy-editor-toolbar-add alloy-editor-arrow-box">' +
'</div>',

CONTENT_TEMPLATE: '<div class="alloy-editor-toolbar-content btn-toolbar"></div>',

TPL_BUTTONS_CONTAINER: '<div class="alloy-editor-toolbar-buttons btn-group"></div>',

TPL_TRIGGER: '<div class="alloy-editor-toolbar-buttons btn-group">' +
'<button type="button" class="alloy-editor-button btn btn-add">{content}</button>' +
'<button aria-label="{addContent}" class="alloy-editor-button btn btn-add" type="button">{content}' +
'</button>' +
'</div>',

TPL_TRIGGER_CONTENT: '<i class="alloy-editor-icon-add"></i>'
Expand Down Expand Up @@ -399,6 +416,31 @@ YUI.add('toolbar-add', function(Y) {
value: true
},

/**
* Collection of strings used to label elements of the toolbar's UI.
* ToolbarBase provides string properties to specify the messages for:
* - How to focus on the toolbar
* - Possible toolbar states (hidden and visible)
* - Current toolbar state. This works as a template. It's possible to
* use the placeholders {name}, {state} and {focus} to inject messages
* into the generated string.
*
* @attribute strings
* @default {addContent: 'Add content', focus: 'Press Alt + F10 to focus on the toolbar.',
* hidden: 'hidden', state: 'Toolbar {name} is now {state}. {focus}', visible: 'visible'}
* @type Object
*/
strings: {
validator: Lang.isObject,
value: {
addContent: 'Add content',
focus: 'Press Alt + F10 to focus on the toolbar.',
hidden: 'hidden',
state: 'Toolbar {name} is now {state}. {focus}',
visible: 'visible'
}
},

/**
* Specifies the gutter of the trigger button. The gutter object contains the top
* and left offsets from the point, where the trigger is supposed to appear.
Expand All @@ -423,8 +465,8 @@ YUI.add('toolbar-add', function(Y) {
Y.ToolbarAdd = ToolbarAdd;

/**
* The ToolbarAddTrigger class hosts controls for showing the toolbar with the add controls. This class is intended to be
* used internally by {{#crossLink "ToolbarAdd"}}{{/crossLink}} class.
* The ToolbarAddTrigger class hosts controls for showing the toolbar with the add controls. This class is intended
* to be used internally by {{#crossLink "ToolbarAdd"}}{{/crossLink}} class.
*
* @class ToolbarAddTrigger
*/
Expand All @@ -436,5 +478,6 @@ YUI.add('toolbar-add', function(Y) {

});
}, '0.1', {
requires: ['widget-base', 'widget-position', 'widget-position-constrain', 'widget-position-align', 'toolbar-base', 'toolbar-position']
requires: ['widget-base', 'widget-position', 'widget-position-constrain', 'widget-position-align', 'toolbar-base',
'toolbar-position']
});
11 changes: 10 additions & 1 deletion src/ui/yui/src/toolbars/toolbar-base.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,20 @@ YUI.add('toolbar-base', function(Y) {
buttonsContainer.on('keydown', this._onKeyDown, this);
},

/**
* Handles showing or hiding of the toolbar.
* Fires {{#crossLink "CKEDITOR.plugins.uicore/ariaUpdate:event"}}{{/crossLink}} event with the status changes
* of the toolbar.
*
* @method _afterVisibleChange
* @protected
* @param {EventFacade} event Event that triggered the toolbar has been made visible or hidden.
*/
_afterVisibleChange: function(event) {
var strings = this.get('strings');

this.get('editor').fire('ariaUpdate', {
msg: Lang.sub(strings.state, {
message: Lang.sub(strings.state, {
focus: (event.newVal ? strings.focus : ''),
name: this.name,
state: (event.newVal ? strings.visible : strings.hidden)
Expand Down

0 comments on commit 663db28

Please sign in to comment.