';
html += '
';
html += settings.loadingMsg;
diff --git a/core/modules/quickedit/js/views/AppView.js b/core/modules/quickedit/js/views/AppView.js
index cd3212f..35333cc 100644
--- a/core/modules/quickedit/js/views/AppView.js
+++ b/core/modules/quickedit/js/views/AppView.js
@@ -91,6 +91,13 @@
});
break;
+ case 'opened':
+ // Constrain the tabbing context.
+ if (!app.tabbingContext) {
+ app.tabbingContext = Drupal.tabbingManager.constrain('.quickedit-editable, #quickedit-entity-toolbar .quickedit-toolbar button', false);
+ }
+ break;
+
case 'closed':
entityToolbarView = entityModel.toolbarView;
// First, tear down the in-place editors.
@@ -102,6 +109,11 @@
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) {
@@ -269,6 +281,12 @@
fieldModel: fieldModel
});
+ // Create the in-place editor's aural view — for screen reader support.
+ var fieldAuralView = new Drupal.quickedit.FieldAuralView({
+ el: $(fieldModel.get('el')),
+ model: fieldModel
+ });
+
// Create in-place editor's toolbar for this field — stored inside the
// entity toolbar, the entity toolbar will position itself appropriately
// above (or below) the edited element.
@@ -293,6 +311,7 @@
fieldModel.editorView = editorView;
fieldModel.toolbarView = toolbarView;
fieldModel.decorationView = decorationView;
+ fieldModel.fieldAuralView = fieldAuralView;
},
/**
@@ -322,6 +341,10 @@
// 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/quickedit/js/views/EntityToolbarView.js b/core/modules/quickedit/js/views/EntityToolbarView.js
index f2a7f47..03d508f 100644
--- a/core/modules/quickedit/js/views/EntityToolbarView.js
+++ b/core/modules/quickedit/js/views/EntityToolbarView.js
@@ -79,8 +79,8 @@
if ($body.children('#quickedit-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('#quickedit-toolbar-fence').length === 0) {
this.$fence = $(Drupal.theme('quickeditEntityToolbarFence'))
.css(Drupal.displace())
@@ -395,12 +395,17 @@
type: 'submit',
classes: 'action-save quickedit-button icon',
attributes: {
- 'aria-hidden': true
+ 'aria-hidden': true,
+ 'tabindex': '0'
}
},
{
label: Drupal.t('Close'),
- classes: 'action-cancel quickedit-button icon icon-close icon-only'
+ classes: 'action-cancel quickedit-button icon icon-close icon-only',
+ attributes: {
+ 'tabindex': '0',
+ 'aria-label': Drupal.t('Cancel in-place editing')
+ }
}
]
}));
@@ -457,9 +462,18 @@
label = entityLabel;
}
+ // Label the toolbar.
this.$el
+ .attr('aria-label', Drupal.t('Quick edit controls for @entity', {'@entity': entityLabel}))
.find('.quickedit-toolbar-label')
.html(label);
+
+ // Label the save button so that it has context.
+ var changeFieldsCount = this.model.get('fields').where({isChanged: true}).length;
+ var labelArgs = {'@fields': Drupal.formatPlural(changeFieldsCount, '@count field', '@count fields')};
+ this.$el
+ .find('.quickedit-toolbar-entity [type="submit"]')
+ .attr('aria-label', Drupal.t('Save changes to @fields', labelArgs));
},
/**
diff --git a/core/modules/quickedit/js/views/FieldAuralView.js b/core/modules/quickedit/js/views/FieldAuralView.js
new file mode 100644
index 0000000..4331eb5
--- /dev/null
+++ b/core/modules/quickedit/js/views/FieldAuralView.js
@@ -0,0 +1,73 @@
+/**
+ * @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.quickedit.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.quickedit.FieldModel fieldModel
+ * @param String state
+ * The state of the associated field. One of Drupal.quickedit.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));
diff --git a/core/modules/quickedit/js/views/FieldDecorationView.js b/core/modules/quickedit/js/views/FieldDecorationView.js
index 8de4b16..0583431 100644
--- a/core/modules/quickedit/js/views/FieldDecorationView.js
+++ b/core/modules/quickedit/js/views/FieldDecorationView.js
@@ -40,6 +40,8 @@
this.listenTo(this.model, 'change:state', this.stateChange);
this.listenTo(this.model, 'change:isChanged change:inTempStore', this.renderChanged);
+
+ $(document).on('keypress.edit', this.onKeypress.bind(this));
},
/**
@@ -48,6 +50,7 @@
remove: function () {
// The el property is the field, which should not be removed. Remove the
// pointer to it, then call Backbone.View.prototype.remove().
+ $(document).off('keypress.edit');
this.setElement();
Backbone.View.prototype.remove.call(this);
},
@@ -147,6 +150,17 @@
},
/**
+ * Triggers a click event on fields when the enter key is pressed on them.
+ *
+ * @param jQuery event
+ */
+ onKeypress: function (event) {
+ if (event.target === this.el && event.keyCode === 13) {
+ this.onClick(event);
+ }
+ },
+
+ /**
* Transition to 'activating' stage.
*
* @param {jQuery.Event} event
@@ -162,6 +176,10 @@
*/
decorate: function () {
this.$el.addClass('quickedit-candidate quickedit-editable');
+ this.$el.attr({
+ 'role': 'button',
+ 'tabindex': '0'
+ });
},
/**
@@ -169,6 +187,7 @@
*/
undecorate: function () {
this.$el.removeClass('quickedit-candidate quickedit-editable quickedit-highlighted quickedit-editing');
+ this.$el.removeAttr('tabindex role');
},
/**
diff --git a/core/modules/quickedit/quickedit.libraries.yml b/core/modules/quickedit/quickedit.libraries.yml
index 7c81563..0718db3 100644
--- a/core/modules/quickedit/quickedit.libraries.yml
+++ b/core/modules/quickedit/quickedit.libraries.yml
@@ -18,6 +18,7 @@ quickedit:
js/views/ContextualLinkView.js: {}
js/views/FieldToolbarView.js: {}
js/views/EditorView.js: {}
+ js/views/FieldAuralView.js: {}
# Other.
js/theme.js: {}
css:
@@ -35,6 +36,7 @@ quickedit:
- core/jquery.ui.position
- core/drupal
- core/drupal.displace
+ - core/drupal.tabbingmanager
- core/drupal.form
- core/drupal.ajax
- core/drupal.debounce