From de34d0a35f1613a23061d5dba173e7b8e4174512 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=2E=20Rene=CC=81e=20Beach?= <splendidnoise@gmail.com>
Date: Fri, 9 Aug 2013 01:13:51 -0400
Subject: [PATCH] Issue #1844220 by jessebeach: Make in-place editing keyboard
 and aurally accessible
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

commit dc4204b68e60c261c8aa613bd94f84451b9104da
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Fri Aug 9 01:12:24 2013 -0400

    bad reference

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

commit 1e294e2a8095febf2d78ac27be732adc0659a346
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Fri Aug 9 01:10:57 2013 -0400

    Clean up fieldAuralView

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

commit cc7e5a030f458f3e206b83b469a74a46d83c894d
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Fri Aug 9 00:46:10 2013 -0400

    alert dialog.

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

commit e00aeac54820d7cbe0b84de4a57bfcd5029d0233
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Fri Aug 9 00:29:34 2013 -0400

    Focus in the dialog.

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

commit d0cbe8cb7244cd2a675cd80c57b32b84a31009d3
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Fri Aug 9 00:24:26 2013 -0400

    aria-owns

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

commit 693ac446f30a0a837a43bf906d3ff2a91fa141eb
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Thu Aug 8 16:34:06 2013 -0400

    add aria label and owns to the entity

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

commit d1d95027b2aa32439a62c3ffa28c4a13a28393ce
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Thu Aug 8 16:16:15 2013 -0400

    Labels for fields

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

commit eb99b4425e9b933ba0b3261f3c1a8336d4c1c332
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Thu Aug 8 16:01:41 2013 -0400

    aria label for edit form

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

commit f4a42cc14e9095b6f00660937e7bd2eaa75b3d14
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Thu Aug 8 15:49:37 2013 -0400

    better save button

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

commit ba6a8e1086ea173c4b801595950f6ed0da51886d
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Thu Aug 8 14:54:20 2013 -0400

    Tabbing manager fix

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

commit d0ea0e8d7b972a07678dbdb8b38041bf333548d5
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Wed Aug 7 19:24:38 2013 -0400

    getting closer to the fields announcing what they are

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

commit 5bea773b52c1412db9515d773e345dd46fe3c214
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Wed Aug 7 19:04:03 2013 -0400

    close button text

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

commit 36282c768d35ae6c06fc10069816ac007018b979
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Wed Aug 7 18:56:48 2013 -0400

    entity

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

commit 9d43fbed467963e4a81a42abd959d659f7ce6894
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Wed Aug 7 18:04:30 2013 -0400

    labels on the fields

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

commit f86dc6a1843fe2ef64d25eb1960306a1b104dbe0
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Wed Aug 7 17:17:50 2013 -0400

    Announce that an editor is active.

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

commit f0427f5b4092818a83cdd71a9fb65966dc5d5053
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Wed Aug 7 17:04:41 2013 -0400

    announcing changes.

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

commit 803b1b5cd2bf70b195cc9521cfd2e0cce679cf7b
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Wed Aug 7 16:06:43 2013 -0400

    Tabbing to fields.

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

commit bfe7b0194a610f33865fb3e21d32c479f2065e62
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Wed Aug 7 15:55:31 2013 -0400

    keypresses

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

commit 36d9716b3610ee5c471967e9d977e626ac575bcb
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Wed Aug 7 15:18:05 2013 -0400

    in place editing is now tabbable.

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>
---
 core/misc/tabbingmanager.js                        | 16 ++++-
 core/modules/contextual/contextual.toolbar.js      | 13 ++--
 core/modules/edit/edit.module                      |  2 +
 core/modules/edit/js/editors/formEditor.js         | 23 ++++++-
 core/modules/edit/js/theme.js                      |  8 +--
 core/modules/edit/js/views/AppView.js              | 30 +++++++++-
 core/modules/edit/js/views/EditorDecorationView.js | 40 +++++++++----
 core/modules/edit/js/views/EntityToolbarView.js    | 24 ++++++--
 core/modules/edit/js/views/EntityView.js           | 49 ++++++++++++++-
 core/modules/edit/js/views/FieldAuralView.js       | 70 ++++++++++++++++++++++
 10 files changed, 245 insertions(+), 30 deletions(-)
 create mode 100644 core/modules/edit/js/views/FieldAuralView.js

diff --git a/core/misc/tabbingmanager.js b/core/misc/tabbingmanager.js
index f212a1d..29b677b 100644
--- a/core/misc/tabbingmanager.js
+++ b/core/misc/tabbingmanager.js
@@ -41,7 +41,10 @@ $.extend(TabbingManager.prototype, {
    *
    * @return TabbingContext
    */
-  constrain: function (elements) {
+  constrain: function (elements, expand) {
+    if (typeof expand === 'undefined') {
+      expand = true;
+    }
     // Deactivate all tabbingContexts to prepare for the new constraint. A
     // tabbingContext instance will only be reactivated if the stack is unwound
     // to it in the _unwindStack() method.
@@ -51,7 +54,16 @@ $.extend(TabbingManager.prototype, {
 
     // The "active tabbing set" are the elements tabbing should be constrained
     // to.
-    var $elements = $(elements).find(':tabbable').addBack(':tabbable');
+    var $elements;
+    // Find tabbable elements within the set of supplied elements if expansion
+    // is requested.
+    if (expand) {
+      $elements = $(elements).find(':tabbable').addBack(':tabbable');
+    }
+    // Assume that a list of tabbable elements is provided.
+    else {
+      $elements = $(elements);
+    }
 
     var tabbingContext = new TabbingContext({
       // The level is the current height of the stack before this new
diff --git a/core/modules/contextual/contextual.toolbar.js b/core/modules/contextual/contextual.toolbar.js
index f4a37f5..ad259c7 100644
--- a/core/modules/contextual/contextual.toolbar.js
+++ b/core/modules/contextual/contextual.toolbar.js
@@ -190,7 +190,7 @@ Drupal.contextualToolbar = {
      */
     initialize: function () {
       this.model.on('change', this.render, this);
-      this.model.on('change:isViewing', this.manageTabbing, this);
+      this.model.on('change:isViewing change:contextualCount', this.manageTabbing, this);
 
       $(document).on('keyup', _.bind(this.onKeypress, this));
     },
@@ -222,10 +222,13 @@ Drupal.contextualToolbar = {
       }
       // Create a new tabbing context when edit mode is enabled.
       if (!this.model.get('isViewing')) {
-        tabbingContext = Drupal.tabbingManager.constrain($('.contextual-toolbar-tab, .contextual'));
-        this.model.set('tabbingContext', tabbingContext);
-        this.announceTabbingConstraint();
-        this.announcedOnce = true;
+        var $tabbables = $('.contextual');
+        if ($tabbables.length) {
+          tabbingContext = Drupal.tabbingManager.constrain($('.contextual-toolbar-tab').add($tabbables));
+          this.model.set('tabbingContext', tabbingContext);
+          this.announceTabbingConstraint();
+          this.announcedOnce = true;
+        }
       }
     },
 
diff --git a/core/modules/edit/edit.module b/core/modules/edit/edit.module
index a7fbb5d..7d2bd57 100644
--- a/core/modules/edit/edit.module
+++ b/core/modules/edit/edit.module
@@ -95,6 +95,7 @@ function edit_library_info() {
       $path . '/js/views/ModalView.js' => $options,
       $path . '/js/views/FieldToolbarView.js' => $options,
       $path . '/js/views/EditorView.js' => $options,
+      $path . '/js/views/FieldAuralView.js' => $options,
       // Other.
       $path . '/js/util.js' => $options,
       $path . '/js/theme.js' => $options,
@@ -111,6 +112,7 @@ function edit_library_info() {
       array('system', 'jquery.form'),
       array('system', 'jquery.ui.position'),
       array('system', 'drupal.displace'),
+      array('system', 'drupal.tabbingmanager'),
       array('system', 'drupal.form'),
       array('system', 'drupal.ajax'),
       array('system', 'drupal.debounce'),
diff --git a/core/modules/edit/js/editors/formEditor.js b/core/modules/edit/js/editors/formEditor.js
index 27069b8..6030315 100644
--- a/core/modules/edit/js/editors/formEditor.js
+++ b/core/modules/edit/js/editors/formEditor.js
@@ -63,12 +63,17 @@ Drupal.edit.editors.form = Drupal.edit.EditorView.extend({
     // Render form container.
     var $formContainer = this.$formContainer = $(Drupal.theme('editFormContainer', {
       id: id,
+      'aria-label': Drupal.t('Edit form for @aria', {'@aria': fieldModel.get('metadata').aria}),
       loadingMsg: Drupal.t('Loading…')}
     ));
+    // Add this form's id to the list of elements that the entity owns.
+    var owns = $(this.fieldModel.get('entity').get('el')).attr('aria-owns').split(' ');
+    owns.push(id);
+    $(this.fieldModel.get('entity').get('el')).attr('aria-owns', owns.join(' '));
+    // Mark up the form with edit module state classes.
     $formContainer
       .find('.edit-form')
-      .addClass('edit-editable edit-highlighted edit-editing')
-      .attr('role', 'dialog');
+      .addClass('edit-editable edit-highlighted edit-editing');
 
     // Insert form container in DOM.
     if (this.$el.css('display') === 'inline') {
@@ -121,6 +126,9 @@ Drupal.edit.editors.form = Drupal.edit.EditorView.extend({
           }
         });
 
+      // Set focus on the dialog, so that the next tab goes to the first field
+      // in this container.
+      $formContainer[0].focus();
       // The in-place editor has loaded; change state to 'active'.
       fieldModel.set('state', 'active');
     });
@@ -142,6 +150,17 @@ Drupal.edit.editors.form = Drupal.edit.EditorView.extend({
       .off('keypress.edit', 'input')
       .remove();
     this.$formContainer = null;
+    // Remove the form id from the entity's aria-owns attribute.
+    var $entity = $(this.fieldModel.get('entity').get('el'));
+    var owns = $entity
+      .attr('aria-owns')
+      .split(' ')
+      .filter(function (id) {
+        // Remove id values that start with 'edit-form-for-'.
+        return (!(id.indexOf('edit-form-for-') === 0));
+      })
+      .join(' ');
+    $entity.attr('aria-owns', owns);
   },
 
   /**
diff --git a/core/modules/edit/js/theme.js b/core/modules/edit/js/theme.js
index 459eb3a..8034ec4 100644
--- a/core/modules/edit/js/theme.js
+++ b/core/modules/edit/js/theme.js
@@ -30,8 +30,8 @@ Drupal.theme.editBackstage = function (settings) {
 Drupal.theme.editModal = function () {
   var classes = 'edit-animate-slow edit-animate-invisible edit-animate-delay-veryfast';
   var html = '';
-  html += '<div id="edit_modal" class="' + classes + '" role="dialog">';
-  html += '  <div class="main"><p></p></div>';
+  html += '<div id="edit_modal" class="' + classes + '" role="alertdialog" aria-labelledby="edit_modal-title">';
+  html += '  <div id="edit_modal-title" class="main"><p></p></div>';
   html += '  <div class="actions"></div>';
   html += '</div>';
   return html;
@@ -48,7 +48,7 @@ Drupal.theme.editModal = function () {
  */
 Drupal.theme.editEntityToolbar = function (settings) {
   var html = '';
-  html += '<div id="' + settings.id + '" class="edit edit-toolbar-container clearfix">';
+  html += '<div id="' + settings.id + '" class="edit edit-toolbar-container clearfix" role="dialog">';
   html += '<i class="edit-toolbar-pointer"></i>';
   html += '<div class="edit-toolbar-content">';
   html += '<div class="edit-toolbar edit-toolbar-entity clearfix">';
@@ -172,7 +172,7 @@ Drupal.theme.editButtons = function (settings) {
  */
 Drupal.theme.editFormContainer = function (settings) {
   var html = '';
-  html += '<div id="' + settings.id + '" class="edit-form-container">';
+  html += '<div id="' + settings.id + '" class="edit-form-container" role="dialog" tabindex="0" aria-label="' + settings['aria-label'] + '">';
   html += '  <div class="edit-form">';
   html += '    <div class="placeholder">';
   html +=        settings.loadingMsg;
diff --git a/core/modules/edit/js/views/AppView.js b/core/modules/edit/js/views/AppView.js
index 5a5c2d3..5df2816 100644
--- a/core/modules/edit/js/views/AppView.js
+++ b/core/modules/edit/js/views/AppView.js
@@ -80,6 +80,10 @@ Drupal.edit.AppView = Backbone.View.extend({
           entityModel.set('state', 'opening');
         });
         break;
+      case 'opened':
+        // Constrain the tabbing context.
+        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 +95,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 +261,16 @@ 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
     });
 
+    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 +294,7 @@ Drupal.edit.AppView = Backbone.View.extend({
     fieldModel.editorView = editorView;
     fieldModel.toolbarView = toolbarView;
     fieldModel.decorationView = decorationView;
+    fieldModel.fieldAuralView = fieldAuralView;
   },
 
   /**
@@ -309,6 +324,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;
   },
 
   /**
@@ -339,6 +358,10 @@ Drupal.edit.AppView = Backbone.View.extend({
         callback: function (action) {
           // The active modal has been removed.
           that.model.set('activeModal', null);
+          // Release the tabbing context.
+          if (that.tabbingContext) {
+            that.tabbingContext.release();
+          }
           // If the targetState is saving, the field must be saved, then the
           // entity must be saved.
           if (action === 'save') {
@@ -360,6 +383,11 @@ Drupal.edit.AppView = Backbone.View.extend({
       // The modal will set the activeModal property on the model when rendering
       // to prevent multiple modals from being instantiated.
       modal.render();
+      // Allow the modal to render before constraining the tabbing context to
+      // it.
+      _.defer(function () {
+        that.tabbingContext = Drupal.tabbingManager.constrain(modal.el);
+      })
     }
   },
 
diff --git a/core/modules/edit/js/views/EditorDecorationView.js b/core/modules/edit/js/views/EditorDecorationView.js
index 17e5ee2..de9b5d1 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,28 @@ 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 00e0a9d..fd53bd0 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())
@@ -316,12 +316,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')
+            }
           }
         ]
       }));
@@ -378,9 +383,20 @@ 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..f994660 100644
--- a/core/modules/edit/js/views/EntityView.js
+++ b/core/modules/edit/js/views/EntityView.js
@@ -16,15 +16,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.
+    var role = this.$el.attr('role');
+    var ariaLabel = this.$el.attr('aria-label');
+    var owns = this.$el.attr('aria-owns');
+    if (isActive && !this.attrs) {
+      this.attrs = {
+        'role': role,
+        'aria-label': ariaLabel,
+        'aria-owns': 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..5f45584
--- /dev/null
+++ b/core/modules/edit/js/views/FieldAuralView.js
@@ -0,0 +1,70 @@
+(function ($, _, Backbone, Drupal) {
+
+"use strict";
+
+/**
+ * Reacts to field model changes by announces 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 that = this;
+    var from = fieldModel.previous('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 @aria', {'@aria': 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
+  }
+
+});
+
+}(jQuery, _, Backbone, Drupal));
-- 
1.8.2

