';
html += '
';
html += settings.loadingMsg;
diff --git a/core/modules/edit/js/views/AppView.js b/core/modules/edit/js/views/AppView.js
index 6e1ed41..87aca18 100644
--- a/core/modules/edit/js/views/AppView.js
+++ b/core/modules/edit/js/views/AppView.js
@@ -80,6 +80,12 @@ Drupal.edit.AppView = Backbone.View.extend({
entityModel.set('state', 'opening');
});
break;
+ case 'opened':
+ // Constrain the tabbing context.
+ if (!app.tabbingContext) {
+ app.tabbingContext = Drupal.tabbingManager.constrain($('.edit-editable, #edit-entity-toolbar .edit-toolbar button'), false);
+ }
+ break;
case 'closed':
entityToolbarView = entityModel.toolbarView;
// First, tear down the in-place editors.
@@ -91,6 +97,11 @@ Drupal.edit.AppView = Backbone.View.extend({
entityToolbarView.remove();
delete entityModel.toolbarView;
}
+ // Release the tabbing context.
+ if (app.tabbingContext) {
+ app.tabbingContext.release();
+ app.tabbingContext = null;
+ }
// A page reload may be necessary to re-instate the original HTML of the
// edited fields.
if (reload) {
@@ -252,11 +263,17 @@ Drupal.edit.AppView = Backbone.View.extend({
var editorName = fieldModel.get('metadata').editor;
var editorModel = new Drupal.edit.EditorModel();
var editorView = new Drupal.edit.editors[editorName]({
- el: $(fieldModel.get('el')),
+ el: fieldModel.get('el'),
model: editorModel,
fieldModel: fieldModel
});
+ // Create the in-place editor's aural view — for screen reader support.
+ var fieldAuralView = new Drupal.edit.FieldAuralView({
+ el: fieldModel.get('el'),
+ model: fieldModel
+ });
+
// Create in-place editor's toolbar — positions appropriately above the
// edited element.
var toolbarView = new Drupal.edit.FieldToolbarView({
@@ -280,6 +297,7 @@ Drupal.edit.AppView = Backbone.View.extend({
fieldModel.editorView = editorView;
fieldModel.toolbarView = toolbarView;
fieldModel.decorationView = decorationView;
+ fieldModel.fieldAuralView = fieldAuralView;
},
/**
@@ -309,6 +327,10 @@ Drupal.edit.AppView = Backbone.View.extend({
// because that would remove the field itself.
fieldModel.editorView.remove();
delete fieldModel.editorView;
+
+ // Unbind event handlers; delete aural view.
+ fieldModel.fieldAuralView.remove();
+ delete fieldModel.fieldAuralView;
},
/**
diff --git a/core/modules/edit/js/views/EditorDecorationView.js b/core/modules/edit/js/views/EditorDecorationView.js
index 1e76ad3..ca78a52 100644
--- a/core/modules/edit/js/views/EditorDecorationView.js
+++ b/core/modules/edit/js/views/EditorDecorationView.js
@@ -11,11 +11,12 @@ Drupal.edit.EditorDecorationView = Backbone.View.extend({
_widthAttributeIsEmpty: null,
events: {
- 'mouseenter.edit' : 'onMouseEnter',
- 'mouseleave.edit' : 'onMouseLeave',
- 'click': 'onClick',
- 'tabIn.edit': 'onMouseEnter',
- 'tabOut.edit': 'onMouseLeave'
+ 'mouseenter.edit' : 'highlight',
+ 'mouseleave.edit' : 'dehighlight',
+ 'click': 'activate',
+ 'keypress': 'activate',
+ 'focus': 'highlight',
+ 'blur': 'dehighlight'
},
/**
@@ -30,6 +31,9 @@ Drupal.edit.EditorDecorationView = Backbone.View.extend({
this.model.on('change:state', this.stateChange, this);
this.model.on('change:isChanged change:inTempStore', this.renderChanged, this);
+
+ // Manage keyboard clicks.
+ //this.$el.on('keypress.edit', this.onKeyPress.bind(this));
},
/**
@@ -105,11 +109,11 @@ Drupal.edit.EditorDecorationView = Backbone.View.extend({
},
/**
- * Starts hover; transitions to 'highlight' state.
+ * Transitions to 'highlight' state.
*
* @param jQuery event
*/
- onMouseEnter: function (event) {
+ highlight: function (event) {
var that = this;
that.model.set('state', 'highlighted');
event.stopPropagation();
@@ -120,7 +124,7 @@ Drupal.edit.EditorDecorationView = Backbone.View.extend({
*
* @param jQuery event
*/
- onMouseLeave: function (event) {
+ dehighlight: function (event) {
var that = this;
that.model.set('state', 'candidate', { reason: 'mouseleave' });
event.stopPropagation();
@@ -131,7 +135,7 @@ Drupal.edit.EditorDecorationView = Backbone.View.extend({
*
* @param jQuery event
*/
- onClick: function (event) {
+ activate: function (event) {
this.model.set('state', 'activating');
event.preventDefault();
event.stopPropagation();
@@ -141,14 +145,26 @@ Drupal.edit.EditorDecorationView = Backbone.View.extend({
* Adds classes used to indicate an elements editable state.
*/
decorate: function () {
- this.$el.addClass('edit-candidate edit-editable');
+ this.$el
+ // Mark the view's element as editable.
+ .addClass('edit-candidate edit-editable')
+ // Mark the element as tabbable.
+ .prop({'tabIndex': '0'})
+ .attr({
+ 'role': 'button',
+ 'aria-label': Drupal.t('Quick edit field @label', {'@label': this.model.get('metadata').label})
+ });
},
/**
* Removes classes used to indicate an elements editable state.
*/
undecorate: function () {
- this.$el.removeClass('edit-candidate edit-editable edit-highlighted edit-editing');
+ this.$el
+ .removeClass('edit-candidate edit-editable edit-highlighted edit-editing')
+ // Remove the tabindex to make the element untabbable.
+ .prop('tabIndex', '-1')
+ .removeAttr('role aria-label');
},
/**
diff --git a/core/modules/edit/js/views/EntityToolbarView.js b/core/modules/edit/js/views/EntityToolbarView.js
index a2998db..628fe69 100644
--- a/core/modules/edit/js/views/EntityToolbarView.js
+++ b/core/modules/edit/js/views/EntityToolbarView.js
@@ -65,8 +65,8 @@ Drupal.edit.EntityToolbarView = Backbone.View.extend({
if ($body.children('#edit-entity-toolbar').length === 0) {
$body.append(this.$el);
}
- // The fence will define a area on the screen that the entity toolbar
- // will be position within.
+ // The fence will define an area on the screen that the entity toolbar
+ // will be positioned within.
if ($body.children('#edit-toolbar-fence').length === 0) {
this.$fence = $(Drupal.theme('editEntityToolbarFence'))
.css(Drupal.displace())
@@ -325,12 +325,17 @@ Drupal.edit.EntityToolbarView = Backbone.View.extend({
type: 'submit',
classes: 'action-save edit-button icon',
attributes: {
- 'aria-hidden': true
+ 'aria-hidden': true,
+ 'tabindex': '0'
}
},
{
label: Drupal.t('Close'),
- classes: 'action-cancel edit-button icon icon-close icon-only'
+ classes: 'action-cancel edit-button icon icon-close icon-only',
+ attributes: {
+ 'tabindex': '0',
+ 'aria-label': Drupal.t('Cancel in-place editing')
+ }
}
]
}));
@@ -387,9 +392,17 @@ Drupal.edit.EntityToolbarView = Backbone.View.extend({
label = entityLabel;
}
+ // Label the toolbar.
this.$el
+ .attr('aria-label', Drupal.t('Quick edit controls for @entity', {'@entity': entityLabel}))
.find('.edit-toolbar-label')
.html(label);
+
+ // Label the save button so that it has context.
+ var changeFieldsCount = this.model.get('fields').where({isChanged: true}).length;
+ this.$el
+ .find('.edit-toolbar-entity [type="submit"]')
+ .attr('aria-label', Drupal.t('Save changes to @fields', {'@fields': Drupal.formatPlural(changeFieldsCount, '@count field', '@count fields')}));
},
/**
diff --git a/core/modules/edit/js/views/EntityView.js b/core/modules/edit/js/views/EntityView.js
index a2e771a..308701b 100644
--- a/core/modules/edit/js/views/EntityView.js
+++ b/core/modules/edit/js/views/EntityView.js
@@ -3,6 +3,7 @@
"use strict";
Drupal.edit.EntityView = Backbone.View.extend({
+
/**
* {@inheritdoc}
*
@@ -16,15 +17,62 @@ Drupal.edit.EntityView = Backbone.View.extend({
* {@inheritdoc}
*/
render: function () {
- this.$el.toggleClass('edit-entity-active', this.model.get('isActive'));
+ var isActive = this.model.get('isActive');
+ this.$el.toggleClass('edit-entity-active', isActive);
+
+ // If the entity has a role, remember it and change the role to form.
+ if (isActive && !this.attrs) {
+ this.attrs = {
+ 'role': this.$el.attr('role'),
+ 'aria-label': this.$el.attr('aria-label'),
+ 'aria-owns': this.$el.attr('aria-owns')
+ };
+ this.$el.attr({
+ 'role': 'form',
+ 'aria-label': Drupal.t('@entity', {'@entity': this.model.get('label')}),
+ 'aria-owns': 'edit-entity-toolbar' // @todo need to add the field edit form when one is created.
+ });
+ }
+
+ // Revert to the original role if the entity has been deactivated.
+ if (!isActive && this.model.previous('isActive')) {
+ this.revertAttrs();
+ }
},
/**
* {@inheritdoc}
*/
remove: function () {
+ this.revertAttrs();
+ this.$el.off('.edit');
+
+ // The element must be set to null or the entity will be removed from the
+ // DOM.
this.setElement(null);
Backbone.View.prototype.remove.call(this);
+ },
+
+ /**
+ * Reverts the role attribute of the entity element to the original value.
+ */
+ revertAttrs: function () {
+ // Replace any attributes that might have been changed.
+ if (this.attrs) {
+ for (var name in this.attrs) {
+ if (this.attrs.hasOwnProperty(name)) {
+ if (this.attrs[name]) {
+ this.$el.attr(name, this.attrs[name]);
+ }
+ // If the element did not have this attribute originally, then just
+ // delete it.
+ else {
+ this.$el.removeAttr(name);
+ }
+ }
+ }
+ this.attrs = null;
+ }
}
});
diff --git a/core/modules/edit/js/views/FieldAuralView.js b/core/modules/edit/js/views/FieldAuralView.js
new file mode 100644
index 0000000..ae896c4
--- /dev/null
+++ b/core/modules/edit/js/views/FieldAuralView.js
@@ -0,0 +1,72 @@
+/**
+ * @file
+ * A Backbone View that adds screen reader support to in-place editors.
+ */
+(function (Backbone, Drupal) {
+
+"use strict";
+
+/**
+ * Reacts to field model changes by announcing the changes in a way that screen
+ * reading user agents will convey.
+ */
+Drupal.edit.FieldAuralView = Backbone.View.extend({
+
+ /**
+ * {@inheritdoc}
+ */
+ initialize: function () {
+ this.model.on('change:state', this.stateChange, this);
+ },
+
+ /**
+ * {@inheritdoc}
+ */
+ remove: function () {
+ // The el property is the field, which should not be removed. Remove the
+ // pointer to it, then call Backbone.View.prototype.remove().
+ this.setElement();
+ Backbone.View.prototype.remove.call(this);
+ },
+
+ /**
+ * Determines the actions to take given a change of state.
+ *
+ * @param Drupal.edit.FieldModel fieldModel
+ * @param String state
+ * The state of the associated field. One of Drupal.edit.FieldModel.states.
+ */
+ stateChange: function (fieldModel, state) {
+ var to = state;
+ switch (to) {
+ case 'active':
+ // The user can now actually use the in-place editor.
+ this.announceActiveEditor();
+ break;
+ case 'invalid':
+ // The modified field value was attempted to be saved, but there were
+ // validation errors.
+ this.announceValidationErrors();
+ break;
+ }
+ },
+
+ /**
+ * Announces details of the field being edited in place.
+ */
+ announceActiveEditor: function () {
+ Drupal.announce(Drupal.t('Editing @field', {'@field': this.model.get('metadata').aria}), 'assertive');
+ },
+
+ /**
+ * Announces validation error messages to a screen reading user agent.
+ */
+ announceValidationErrors: function () {
+ var errors = this.model.get('validationErrors');
+ // @todo, announce the validation errors. And mark them correctly with
+ // aria-invalid=true
+ }
+
+});
+
+}(Backbone, Drupal));