diff --git a/app/code/Magento/Catalog/view/frontend/layout/catalog_product_view.xml b/app/code/Magento/Catalog/view/frontend/layout/catalog_product_view.xml
index 991d9a36d18e04c7e8b17744dd3b2d32981fdc18..2f8feb52db5ffc071055bebfb436b449331dbb1c 100644
--- a/app/code/Magento/Catalog/view/frontend/layout/catalog_product_view.xml
+++ b/app/code/Magento/Catalog/view/frontend/layout/catalog_product_view.xml
@@ -101,7 +101,33 @@
                 </block>
             </container>
             <container name="product.info.media" htmlTag="div" htmlClass="product media" after="product.info.main">
+                <block class="Magento\Framework\View\Element\Template" name="skip_gallery_after.target" before="skip_gallery_before.wrapper" template="Magento_Theme::html/skiptarget.phtml">
+                    <arguments>
+                        <argument name="target_id" xsi:type="string">gallery-prev-area</argument>
+                    </arguments>
+                </block>
+                <container name="skip_gallery_before.wrapper" htmlTag="div" htmlClass="action-skip-wrapper">
+                    <block class="Magento\Framework\View\Element\Template" before="product.info.media.image" name="skip_gallery_before" template="Magento_Theme::html/skip.phtml">
+                        <arguments>
+                            <argument name="target" xsi:type="string">gallery-next-area</argument>
+                            <argument name="label" translate="true" xsi:type="string">Skip to the end of the images gallery</argument>
+                        </arguments>
+                    </block>
+                </container>
                 <block class="Magento\Catalog\Block\Product\View\Gallery" name="product.info.media.image" template="product/view/gallery.phtml"/>
+                <container name="skip_gallery_after.wrapper" htmlTag="div" htmlClass="action-skip-wrapper">
+                    <block class="Magento\Framework\View\Element\Template" after="product.info.media.image" name="skip_gallery_after" template="Magento_Theme::html/skip.phtml">
+                        <arguments>
+                            <argument name="target" xsi:type="string">gallery-prev-area</argument>
+                            <argument name="label" translate="true" xsi:type="string">Skip to the beginning of the images gallery</argument>
+                        </arguments>
+                    </block>
+                </container>
+                <block class="Magento\Framework\View\Element\Template" name="skip_gallery_before.target" after="skip_gallery_after.wrapper" template="Magento_Theme::html/skiptarget.phtml">
+                    <arguments>
+                        <argument name="target_id" xsi:type="string">gallery-next-area</argument>
+                    </arguments>
+                </block>
             </container>
             <block class="Magento\Catalog\Block\Product\View\Description" name="product.info.details" template="product/view/details.phtml" after="product.info.media">
                 <block class="Magento\Catalog\Block\Product\View\Description" name="product.info.description" template="product/view/attribute.phtml" group="detailed_info">
diff --git a/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/result.phtml b/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/result.phtml
index b63acf736f99f4102c9bdd89568959cd1e36ba7f..3238f68401f178530ea3dc684d518bca619ab847 100644
--- a/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/result.phtml
+++ b/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/result.phtml
@@ -21,7 +21,7 @@
         <?php endif; ?>
     </div>
 <?php else: ?>
-    <div class="message error">
+    <div role="alert" class="message error">
         <div>
             <?php /* @escapeNotVerified */ echo __('We can\'t find any items matching these search criteria.');?> <a href="<?php /* @escapeNotVerified */ echo $block->getFormUrl(); ?>"><?php /* @escapeNotVerified */ echo __('Modify your search.'); ?></a>
         </div>
diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js
index 60691ab0e7b5a73bc4d3e58746be70f5ae5b070e..ddb62ceb37b37ef6bc7735c78f81367c5c6d4343 100644
--- a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js
+++ b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js
@@ -260,11 +260,13 @@ define(
                         this.source.trigger('shippingAddress.custom_attributes.data.validate');
                     }
 
-                    if (this.source.get('params.invalid') ||
+                    if (emailValidationResult &&
+                        this.source.get('params.invalid') ||
                         !quote.shippingMethod().method_code ||
-                        !quote.shippingMethod().carrier_code ||
-                        !emailValidationResult
+                        !quote.shippingMethod().carrier_code
                     ) {
+                        this.focusInvalid();
+
                         return false;
                     }
 
diff --git a/app/code/Magento/Checkout/view/frontend/web/template/shipping.html b/app/code/Magento/Checkout/view/frontend/web/template/shipping.html
index 9078731a8558f6fd2e1fde6a9201fbffc0d1fce0..5049aac2bbc9b056be66344e8b21542a2323dd68 100644
--- a/app/code/Magento/Checkout/view/frontend/web/template/shipping.html
+++ b/app/code/Magento/Checkout/view/frontend/web/template/shipping.html
@@ -120,7 +120,7 @@
                         <!-- ko if:  method.error_message -->
                         <tr class="row row-error">
                             <td class="col col-error" colspan="4">
-                                <div class="message error">
+                                <div role="alert" class="message error">
                                     <div data-bind="text: method.error_message"></div>
                                 </div>
                                 <span class="no-display">
@@ -141,7 +141,7 @@
                     <!-- /ko -->
                 </div>
                 <!-- ko if: errorValidationMessage().length > 0 -->
-                <div class="message notice">
+                <div role="alert" class="message notice">
                     <span><!-- ko text: errorValidationMessage()--><!-- /ko --></span>
                 </div>
                 <!-- /ko -->
diff --git a/app/code/Magento/Cookie/view/frontend/templates/html/notices.phtml b/app/code/Magento/Cookie/view/frontend/templates/html/notices.phtml
index cbf1d3ad2b231633b142127bdcf90df83d9f7bcf..f7aaf538ae96c210ae4aa7fdff0c84412207f948 100644
--- a/app/code/Magento/Cookie/view/frontend/templates/html/notices.phtml
+++ b/app/code/Magento/Cookie/view/frontend/templates/html/notices.phtml
@@ -9,12 +9,17 @@
 /** @var \Magento\Cookie\Block\Html\Notices $block */
 ?>
 <?php if ($this->helper(\Magento\Cookie\Helper\Cookie::class)->isUserNotAllowSaveCookie()): ?>
-    <div class="message global cookie" id="notice-cookie-block" style="display: none">
-        <div class="content">
+    <div role="alertdialog"
+         tabindex="-1"
+         class="message global cookie"
+         id="notice-cookie-block"
+         style="display: none;">
+        <div role="document" class="content" tabindex="0">
             <p>
                 <strong><?php /* @escapeNotVerified */ echo __('We use cookies to make your experience better.') ?></strong>
                 <span><?php /* @escapeNotVerified */ echo __('To comply with the new e-Privacy directive, we need to ask for your consent to set the cookies.') ?></span>
-                <?php /* @escapeNotVerified */ echo __('<a href="%1">Learn more</a>.', $block->getPrivacyPolicyLink()) ?></p>
+                <?php /* @escapeNotVerified */ echo __('<a href="%1">Learn more</a>.', $block->getPrivacyPolicyLink()) ?>
+            </p>
             <div class="actions">
                 <button id="btn-cookie-allow" class="action allow primary">
                     <span><?php /* @escapeNotVerified */ echo __('Allow Cookies');?></span>
diff --git a/app/code/Magento/ImportExport/view/adminhtml/templates/busy.phtml b/app/code/Magento/ImportExport/view/adminhtml/templates/busy.phtml
index cd77de6c78d861c318077a6a258f81fdada7ea63..5e6e0b8e9abf4e44aed7a92a58d603b5bde0d3f6 100644
--- a/app/code/Magento/ImportExport/view/adminhtml/templates/busy.phtml
+++ b/app/code/Magento/ImportExport/view/adminhtml/templates/busy.phtml
@@ -10,7 +10,7 @@
     </div><br>
     <div class="messages">
         <div class="message message-success success">
-            <div><?php /* @escapeNotVerified */ echo $block->getStatusMessage(); ?>hh</div>
+            <div><?php /* @escapeNotVerified */ echo $block->getStatusMessage(); ?></div>
         </div>
     </div>
 </div>
diff --git a/app/code/Magento/Swatches/view/frontend/templates/product/layered/renderer.phtml b/app/code/Magento/Swatches/view/frontend/templates/product/layered/renderer.phtml
index 19b4002efb2a71289961b139cf41517c0c78e8ef..c8842b5b5be3a31f206e4cd9d98e0938a5342c2c 100644
--- a/app/code/Magento/Swatches/view/frontend/templates/product/layered/renderer.phtml
+++ b/app/code/Magento/Swatches/view/frontend/templates/product/layered/renderer.phtml
@@ -10,15 +10,19 @@
 ?>
 <?php $swatchData = $block->getSwatchData(); ?>
 <div class="swatch-attribute swatch-layered <?php /* @escapeNotVerified */ echo $swatchData['attribute_code'] ?>"
-     attribute-code="<?php /* @escapeNotVerified */ echo $swatchData['attribute_code'] ?>" attribute-id="<?php /* @escapeNotVerified */ echo $swatchData['attribute_id'] ?>">
+     attribute-code="<?php /* @escapeNotVerified */ echo $swatchData['attribute_code'] ?>"
+     attribute-id="<?php /* @escapeNotVerified */ echo $swatchData['attribute_id'] ?>">
     <div class="swatch-attribute-options clearfix">
         <?php foreach ($swatchData['options'] as $option => $label): ?>
-            <a href="<?php /* @escapeNotVerified */ echo $label['link'] ?>" class="swatch-option-link-layered">
+            <a href="<?php /* @escapeNotVerified */ echo $label['link'] ?>"
+               aria-label="<?php /* @escapeNotVerified */ echo $label['label'] ?>"
+               class="swatch-option-link-layered">
                 <?php if (isset($swatchData['swatches'][$option]['type'])) { ?>
                     <?php switch ($swatchData['swatches'][$option]['type']) {
                         case '3':
                             ?>
                             <div class="swatch-option <?php /* @escapeNotVerified */ echo $label['custom_style'] ?>"
+                                 tabindex="-1"
                                  option-type="3"
                                  option-id="<?php /* @escapeNotVerified */ echo $option ?>"
                                  option-label="<?php /* @escapeNotVerified */ echo $label['label'] ?>"
@@ -33,6 +37,7 @@
                             <?php $swatchImagePath = $block->getSwatchPath('swatch_image',
                             $swatchData['swatches'][$option]['value']); ?>
                             <div class="swatch-option image <?php /* @escapeNotVerified */ echo $label['custom_style'] ?>"
+                                 tabindex="-1"
                                  option-type="2"
                                  option-id="<?php /* @escapeNotVerified */ echo $option ?>"
                                  option-label="<?php /* @escapeNotVerified */ echo $label['label'] ?>"
@@ -43,6 +48,7 @@
                         case '1':
                             ?>
                             <div class="swatch-option color <?php /* @escapeNotVerified */ echo $label['custom_style'] ?>"
+                                 tabindex="-1"
                                  option-type="1"
                                  option-id="<?php /* @escapeNotVerified */ echo $option ?>"
                                  option-label="<?php /* @escapeNotVerified */ echo $label['label'] ?>"
@@ -54,6 +60,7 @@
                         default:
                             ?>
                                 <div class="swatch-option text <?php /* @escapeNotVerified */ echo $label['custom_style'] ?>"
+                                     tabindex="-1"
                                      option-type="0"
                                      option-id="<?php /* @escapeNotVerified */ echo $option ?>"
                                      option-label="<?php /* @escapeNotVerified */ echo $label['label'] ?>"
diff --git a/app/code/Magento/Swatches/view/frontend/web/css/swatches.css b/app/code/Magento/Swatches/view/frontend/web/css/swatches.css
index 8204fc7fc8f630f64ace7caa37567550354f62cf..3899a663788b0f308ce5bf4c7b0f8da4389c4245 100644
--- a/app/code/Magento/Swatches/view/frontend/web/css/swatches.css
+++ b/app/code/Magento/Swatches/view/frontend/web/css/swatches.css
@@ -210,6 +210,10 @@
     padding: 0 !important;
 }
 
+.swatch-option-link-layered:focus > div {
+    box-shadow: 0 0 3px 1px #68a8e0;
+}
+
 .swatch-option-tooltip-layered {
     width: 140px;
     position: absolute;
@@ -276,3 +280,9 @@
 .swatch-option-loading {
     content: url("../images/loader-2.gif");
 }
+
+.swatch-input {
+    left: -1000px;
+    position: absolute;
+    visibility: hidden;
+}
diff --git a/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js b/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js
index dfd4b2bb2da0ffcc19286dafd125be59d980f804..4e79c42dd6adbf4eb1f079f569ac3aeb77799241 100644
--- a/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js
+++ b/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js
@@ -6,11 +6,57 @@
 define([
     'jquery',
     'underscore',
+    'mage/smart-keyboard-handler',
     'jquery/ui',
-    'jquery/jquery.parsequery'
-], function ($, _) {
+    'jquery/jquery.parsequery',
+    'mage/validation/validation'
+], function ($, _, keyboardHandler) {
     'use strict';
 
+    /**
+     * Extend form validation to support swatch accessibility
+     */
+    $.widget('mage.validation', $.mage.validation, {
+        /**
+         * Handle form with swatches validation. Focus on first invalid swatch block.
+         *
+         * @param {jQuery.Event} event
+         * @param {Object} validation
+         */
+        listenFormValidateHandler: function (event, validation) {
+            var swatchWrapper, firstActive, swatches, swatch, successList, errorList, firstSwatch;
+
+            this._superApply(arguments);
+
+            swatchWrapper = '.swatch-attribute-options';
+            swatches = $(event.target).find(swatchWrapper);
+
+            if (!swatches.length) {
+                return;
+            }
+
+            swatch = '.swatch-attribute';
+            firstActive = $(validation.errorList[0].element || []);
+            successList = validation.successList;
+            errorList = validation.errorList;
+            firstSwatch = $(firstActive).parent(swatch).find(swatchWrapper);
+
+            keyboardHandler.focus(swatches);
+
+            $.each(successList, function (index, item) {
+                $(item).parent(swatch).find(swatchWrapper).attr('aria-invalid', false);
+            });
+
+            $.each(errorList, function (index, item) {
+                $(item.element).parent(swatch).find(swatchWrapper).attr('aria-invalid', true);
+            });
+
+            if (firstSwatch.length) {
+                $(firstSwatch).focus();
+            }
+        }
+    });
+
     /**
      * Render tooltips by attributes (only to up).
      * Required element attributes:
@@ -117,6 +163,7 @@ define([
                 $element.hide();
                 clearTimeout(timer);
             });
+
             $(document).on('tap', function () {
                 $element.hide();
                 clearTimeout(timer);
@@ -169,6 +216,9 @@ define([
             //selector of product images gallery wrapper
             mediaGallerySelector: '[data-gallery-role=gallery-placeholder]',
 
+            // selector of category product tile wrapper
+            selectorProductTile: '.product-item',
+
             // number of controls to show (false or zero = show all)
             numberToShow: false,
 
@@ -178,6 +228,9 @@ define([
             // enable label for control
             enableControlLabel: true,
 
+            // control label id
+            controlLabelId: '',
+
             // text for more button
             moreButtonText: 'More',
 
@@ -191,7 +244,10 @@ define([
             mediaGalleryInitial: [{}],
 
             //
-            onlyMainImg: false
+            onlyMainImg: false,
+
+            // whether swatches are rendered in product list or on product page
+            inProductList: false
         },
 
         /**
@@ -246,7 +302,9 @@ define([
                     'img': $main.find('.product-image-photo').attr('src')
                 }];
             }
-            this.productForm = this.element.parents(this.options.selectorProduct).find('form:first');
+
+            this.productForm = this.element.parents(this.options.selectorProductTile).find('form:first');
+            this.inProductList = this.productForm.length > 0;
         },
 
         /**
@@ -264,9 +322,11 @@ define([
 
             $.each(this.options.jsonConfig.attributes, function () {
                 var item = this,
-                    options = $widget._RenderSwatchOptions(item),
+                    controlLabelId = 'option-label-' + item.code + '-' + item.id,
+                    options = $widget._RenderSwatchOptions(item, controlLabelId),
                     select = $widget._RenderSwatchSelect(item, chooseText),
                     input = $widget._RenderFormInput(item),
+                    listLabel = '',
                     label = '';
 
                 // Show only swatch controls
@@ -276,22 +336,32 @@ define([
 
                 if ($widget.options.enableControlLabel) {
                     label +=
-                        '<span class="' + classes.attributeLabelClass + '">' + item.label + '</span>' +
+                        '<span id="' + controlLabelId + '" class="' + classes.attributeLabelClass + '">' +
+                            item.label +
+                        '</span>' +
                         '<span class="' + classes.attributeSelectedOptionLabelClass + '"></span>';
                 }
 
-                if ($widget.productForm) {
+                if ($widget.inProductList) {
                     $widget.productForm.append(input);
                     input = '';
+                    listLabel = 'aria-label="' + item.label + '"';
+                } else {
+                    listLabel = 'aria-labelledby="' + controlLabelId + '"';
                 }
 
                 // Create new control
                 container.append(
-                    '<div class="' + classes.attributeClass + ' ' + item.code +
-                        '" attribute-code="' + item.code +
-                        '" attribute-id="' + item.id + '">' +
-                            label +
-                        '<div class="' + classes.attributeOptionsWrapper + ' clearfix">' +
+                    '<div class="' + classes.attributeClass + ' ' + item.code + '" ' +
+                         'attribute-code="' + item.code + '" ' +
+                         'attribute-id="' + item.id + '">' +
+                        label +
+                        '<div aria-activedescendant="" ' +
+                             'tabindex="0" ' +
+                             'aria-invalid="false" ' +
+                             'aria-required="true" ' +
+                             'role="listbox" ' + listLabel +
+                             'class="' + classes.attributeOptionsWrapper + ' clearfix">' +
                             options + select +
                         '</div>' + input +
                     '</div>'
@@ -336,10 +406,11 @@ define([
          * Render swatch options by part of config
          *
          * @param {Object} config
+         * @param {String} controlId
          * @returns {String}
          * @private
          */
-        _RenderSwatchOptions: function (config) {
+        _RenderSwatchOptions: function (config, controlId) {
             var optionConfig = this.options.jsonSwatchConfig[config.id],
                 optionClass = this.options.classes.optionClass,
                 moreLimit = parseInt(this.options.numberToShow, 10),
@@ -375,11 +446,17 @@ define([
                 thumb = optionConfig[id].hasOwnProperty('thumb') ? optionConfig[id].thumb : '';
                 label = this.label ? this.label : '';
                 attr =
+                    ' id="' + controlId + '-item-' + id + '"' +
+                    ' aria-checked="false"' +
+                    ' aria-describedby="' + controlId + '"' +
+                    ' tabindex="0"' +
                     ' option-type="' + type + '"' +
                     ' option-id="' + id + '"' +
                     ' option-label="' + label + '"' +
+                    ' aria-label="' + label + '"' +
                     ' option-tooltip-thumb="' + thumb + '"' +
-                    ' option-tooltip-value="' + value + '"';
+                    ' option-tooltip-value="' + value + '"' +
+                    ' role="option"';
 
                 if (!this.hasOwnProperty('products') || this.products.length <= 0) {
                     attr += ' option-empty="true"';
@@ -392,19 +469,19 @@ define([
                 } else if (type === 1) {
                     // Color
                     html += '<div class="' + optionClass + ' color" ' + attr +
-                        '" style="background: ' + value +
+                        ' style="background: ' + value +
                         ' no-repeat center; background-size: initial;">' + '' +
                         '</div>';
                 } else if (type === 2) {
                     // Image
                     html += '<div class="' + optionClass + ' image" ' + attr +
-                        '" style="background: url(' + value + ') no-repeat center; background-size: initial;">' + '' +
+                        ' style="background: url(' + value + ') no-repeat center; background-size: initial;">' + '' +
                         '</div>';
                 } else if (type === 3) {
                     // Clear
                     html += '<div class="' + optionClass + '" ' + attr + '></div>';
                 } else {
-                    // Defaualt
+                    // Default
                     html += '<div class="' + optionClass + '" ' + attr + '>' + label + '</div>';
                 }
             });
@@ -460,10 +537,9 @@ define([
                 'type="text" ' +
                 'value="" ' +
                 'data-selector="super_attribute[' + config.id + ']" ' +
-                'data-validate="{required:true}" ' +
+                'data-validate="{required: true}" ' +
                 'aria-required="true" ' +
-                'aria-invalid="true" ' +
-                'style="visibility: hidden; position:absolute; left:-1000px">';
+                'aria-invalid="false">';
         },
 
         /**
@@ -472,22 +548,39 @@ define([
          * @private
          */
         _EventListener: function () {
+            var $widget = this,
+                options = this.options.classes,
+                target;
 
-            var $widget = this;
-
-            $widget.element.on('click', '.' + this.options.classes.optionClass, function () {
+            $widget.element.on('click', '.' + options.optionClass, function () {
                 return $widget._OnClick($(this), $widget);
             });
 
-            $widget.element.on('change', '.' + this.options.classes.selectClass, function () {
+            $widget.element.on('change', '.' + options.selectClass, function () {
                 return $widget._OnChange($(this), $widget);
             });
 
-            $widget.element.on('click', '.' + this.options.classes.moreButton, function (e) {
+            $widget.element.on('click', '.' + options.moreButton, function (e) {
                 e.preventDefault();
 
                 return $widget._OnMoreClick($(this));
             });
+
+            $widget.element.on('keydown', function (e) {
+                if (e.which === 13) {
+                    target = $(e.target);
+
+                    if (target.is('.' + options.optionClass)) {
+                        return $widget._OnClick(target, $widget);
+                    } else if (target.is('.' + options.selectClass)) {
+                        return $widget._OnChange(target, $widget);
+                    } else if (target.is('.' + options.moreButton)) {
+                        e.preventDefault();
+
+                        return $widget._OnMoreClick(target);
+                    }
+                }
+            });
         },
 
         /**
@@ -498,13 +591,17 @@ define([
          * @private
          */
         _OnClick: function ($this, $widget) {
-
             var $parent = $this.parents('.' + $widget.options.classes.attributeClass),
+                $wrapper = $this.parents('.' + $widget.options.classes.attributeOptionsWrapper),
                 $label = $parent.find('.' + $widget.options.classes.attributeSelectedOptionLabelClass),
                 attributeId = $parent.attr('attribute-id'),
+                $input = $parent.find('.' + $widget.options.classes.attributeInput);
+
+            if ($widget.inProductList) {
                 $input = $widget.productForm.find(
                     '.' + $widget.options.classes.attributeInput + '[name="super_attribute[' + attributeId + ']"]'
                 );
+            }
 
             if ($this.hasClass('disabled')) {
                 return;
@@ -514,11 +611,13 @@ define([
                 $parent.removeAttr('option-selected').find('.selected').removeClass('selected');
                 $input.val('');
                 $label.text('');
+                $this.attr('aria-checked', false);
             } else {
                 $parent.attr('option-selected', $this.attr('option-id')).find('.selected').removeClass('selected');
                 $label.text($this.attr('option-label'));
                 $input.val($this.attr('option-id'));
                 $this.addClass('selected');
+                $widget._toggleCheckedAttributes($this, $wrapper);
             }
 
             $widget._Rebuild();
@@ -533,6 +632,19 @@ define([
             $input.trigger('change');
         },
 
+        /**
+         * Toggle accessibility attributes
+         *
+         * @param {Object} $this
+         * @param {Object} $wrapper
+         * @private
+         */
+        _toggleCheckedAttributes: function ($this, $wrapper) {
+            $wrapper.attr('aria-activedescendant', $this.attr('id'))
+                    .find('.' + this.options.classes.optionClass).attr('aria-checked', false);
+            $this.attr('aria-checked', true);
+        },
+
         /**
          * Event for select
          *
@@ -543,9 +655,13 @@ define([
         _OnChange: function ($this, $widget) {
             var $parent = $this.parents('.' + $widget.options.classes.attributeClass),
                 attributeId = $parent.attr('attribute-id'),
+                $input = $parent.find('.' + $widget.options.classes.attributeInput);
+
+            if ($widget.productForm.length > 0) {
                 $input = $widget.productForm.find(
                     '.' + $widget.options.classes.attributeInput + '[name="super_attribute[' + attributeId + ']"]'
                 );
+            }
 
             if ($this.val() > 0) {
                 $parent.attr('option-selected', $this.val());
@@ -687,7 +803,6 @@ define([
                     'prices': $widget._getPrices(result, $productPrice.priceBox('option').prices)
                 }
             );
-
         },
 
         /**
diff --git a/app/code/Magento/Theme/view/frontend/templates/messages.phtml b/app/code/Magento/Theme/view/frontend/templates/messages.phtml
index 2bd2357a27e1adbf79a9dbf7c34cc9c6d2d0da29..7ef534ab5d65c5b1df7014509e4ab7b3b41c3884 100644
--- a/app/code/Magento/Theme/view/frontend/templates/messages.phtml
+++ b/app/code/Magento/Theme/view/frontend/templates/messages.phtml
@@ -5,7 +5,8 @@
  */
 ?>
 <div data-bind="scope: 'messages'">
-    <div data-bind="foreach: { data: cookieMessages, as: 'message' }" class="messages">
+    <!-- ko if: cookieMessages && cookieMessages.length > 0 -->
+    <div role="alert" data-bind="foreach: { data: cookieMessages, as: 'message' }" class="messages">
         <div data-bind="attr: {
             class: 'message-' + message.type + ' ' + message.type + ' message',
             'data-ui-id': 'message-' + message.type
@@ -13,7 +14,9 @@
             <div data-bind="html: message.text"></div>
         </div>
     </div>
-    <div data-bind="foreach: { data: messages().messages, as: 'message' }" class="messages">
+    <!-- /ko -->
+    <!-- ko if: messages().messages && messages().messages.length > 0 -->
+    <div role="alert" data-bind="foreach: { data: messages().messages, as: 'message' }" class="messages">
         <div data-bind="attr: {
             class: 'message-' + message.type + ' ' + message.type + ' message',
             'data-ui-id': 'message-' + message.type
@@ -21,6 +24,7 @@
             <div data-bind="html: message.text"></div>
         </div>
     </div>
+    <!-- /ko -->
 </div>
 <script type="text/x-magento-init">
     {
diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/abstract.js b/app/code/Magento/Ui/view/base/web/js/form/element/abstract.js
index 4596569a7b6ac4a98bd727e8a7c1ad273a8889cc..151219b87abc5d540a35cc4e0198688bb6f6aca8 100755
--- a/app/code/Magento/Ui/view/base/web/js/form/element/abstract.js
+++ b/app/code/Magento/Ui/view/base/web/js/form/element/abstract.js
@@ -24,7 +24,7 @@ define([
             tooltipTpl: 'ui/form/element/helper/tooltip',
             fallbackResetTpl: 'ui/form/element/helper/fallback-reset',
             'input_type': 'input',
-            placeholder: '',
+            placeholder: false,
             description: '',
             labelVisible: true,
             label: '',
@@ -74,6 +74,15 @@ define([
             return this;
         },
 
+        /**
+         * Checks if component has error.
+         *
+         * @returns {Object}
+         */
+        checkInvalid: function () {
+            return this.error() && this.error().length ? this : null;
+        },
+
         /**
          * Initializes observable properties of instance
          *
@@ -84,7 +93,7 @@ define([
 
             this._super();
 
-            this.observe('error disabled focused preview visible value warn isDifferedFromDefault')
+            this.observe('error disabled focused preview visible value warn notice isDifferedFromDefault')
                 .observe('isUseDefault')
                 .observe({
                     'required': !!rules['required-entry']
@@ -114,6 +123,7 @@ define([
             _.extend(this, {
                 uid: uid,
                 noticeId: 'notice-' + uid,
+                errorId: 'error-' + uid,
                 inputName: utils.serializeName(name.join('.')),
                 valueUpdate: valueUpdate
             });
@@ -440,6 +450,23 @@ define([
          */
         userChanges: function () {
             this.valueChangedByUser = true;
+        },
+
+        /**
+         * Returns correct id for 'aria-describedby' accessibility attribute
+         *
+         * @returns {Boolean|String}
+         */
+        getDescriptionId: function () {
+            var id = false;
+
+            if (this.error()) {
+                id = this.errorId;
+            } else if (this.notice()) {
+                id = this.noticeId;
+            }
+
+            return id;
         }
     });
 });
diff --git a/app/code/Magento/Ui/view/base/web/js/form/form.js b/app/code/Magento/Ui/view/base/web/js/form/form.js
index 672af8a7ed845a12efe19f0801325e23c06932a6..0a3d6ee5850c425b861cb084761cde1cf49df2fb 100644
--- a/app/code/Magento/Ui/view/base/web/js/form/form.js
+++ b/app/code/Magento/Ui/view/base/web/js/form/form.js
@@ -248,19 +248,31 @@ define([
          * @param {Object} data
          */
         save: function (redirect, data) {
-            var scrollTop;
-
             this.validate();
 
             if (!this.additionalInvalid && !this.source.get('params.invalid')) {
                 this.setAdditionalData(data)
                     .submit(redirect);
             } else {
-                scrollTop = $(this.errorClass).offset().top - window.innerHeight / 2;
-                window.scrollTo(0, scrollTop);
+                this.focusInvalid();
             }
         },
 
+        /**
+         * Tries to set focus on first invalid form field.
+         *
+         * @returns {Object}
+         */
+        focusInvalid: function () {
+            var invalidField = _.find(this.delegate('checkInvalid'));
+
+            if (!_.isUndefined(invalidField) && _.isFunction(invalidField.focused)) {
+                invalidField.focused(true);
+            }
+
+            return this;
+        },
+
         /**
          * Set additional data to source before form submit and after validation.
          *
diff --git a/app/code/Magento/Ui/view/base/web/js/grid/editing/editor.js b/app/code/Magento/Ui/view/base/web/js/grid/editing/editor.js
index b39fb1a371801f7f4a06c786341943ee0d245c41..0fc16010af36321c382d906a3312207d53d5e135 100644
--- a/app/code/Magento/Ui/view/base/web/js/grid/editing/editor.js
+++ b/app/code/Magento/Ui/view/base/web/js/grid/editing/editor.js
@@ -486,7 +486,7 @@ define([
         },
 
         /**
-         * Counts number of invalid fields accros all active records.
+         * Counts number of invalid fields across all active records.
          *
          * @returns {Number}
          */
@@ -502,6 +502,16 @@ define([
             return errorsCount;
         },
 
+        /**
+         * Translatable error message text.
+         *
+         * @returns {String}
+         */
+        countErrorsMessage: function () {
+            return $t('There are {placeholder} messages requires your attention.')
+                .replace('{placeholder}', this.countErrors());
+        },
+
         /**
          * Checks if editor has any errors.
          *
diff --git a/app/code/Magento/Ui/view/base/web/templates/form/field.html b/app/code/Magento/Ui/view/base/web/templates/form/field.html
index adc9814a91d0ee3cbe5a1c955b532d385536fd8f..4a9d8f8c75a77d266297dfec620d548a7a12ee76 100644
--- a/app/code/Magento/Ui/view/base/web/templates/form/field.html
+++ b/app/code/Magento/Ui/view/base/web/templates/form/field.html
@@ -40,4 +40,4 @@
 
         <render args="$data.service.template" if="$data.hasService()"/>
     </div>
-</div>
\ No newline at end of file
+</div>
diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/editing/header-buttons.html b/app/code/Magento/Ui/view/base/web/templates/grid/editing/header-buttons.html
index ab0131ed667cfefb4074e47c5aa69727e039d16f..fd4e61108aae0ef8e34e6a713207cb94a4560049 100644
--- a/app/code/Magento/Ui/view/base/web/templates/grid/editing/header-buttons.html
+++ b/app/code/Magento/Ui/view/base/web/templates/grid/editing/header-buttons.html
@@ -4,19 +4,21 @@
  * See COPYING.txt for license details.
  */
 -->
-<div class="data-grid-info-panel" visible="isMultiEditing || (hasActive() && (hasMessages() || hasErrors() ))">
-    <div class="messages" visible="hasMessages() || hasErrors()">
-        <div class="message message-warning" visible="hasErrors()">
-            <strong>There are <text args="countErrors()"/> messages requires your attention.</strong>
-            Please make corrections to the errors in the table below and re-submit.
+<div if="isMultiEditing || (hasActive() && (hasMessages() || hasErrors() ))"
+     attr="role: (isMultiEditing && multiEditingButtons) ? 'alertdialog' : 'alert'"
+     class="data-grid-info-panel">
+    <div if="hasMessages() || hasErrors()" class="messages">
+        <div if="hasErrors()" class="message message-warning">
+            <strong><text args="countErrorsMessage()"/></strong>
+            <span translate="'Please make corrections to the errors in the table below and re-submit.'"/>
         </div>
         <div class="message" outereach="messages" text="message"
-            css="
-                'message-warning': type === 'warning',
-                'message-error': type === 'error',
-                'message-success': type === 'success'"/>
+             css="
+                 'message-warning': type === 'warning',
+                 'message-error': type === 'error',
+                 'message-success': type === 'success'"/>
     </div>
-    <div class="data-grid-info-panel-actions" visible="isMultiEditing && multiEditingButtons">
+    <div if="isMultiEditing && multiEditingButtons" class="data-grid-info-panel-actions">
         <button class="action-tertiary" type="button" click="cancel">
             <span translate="'Cancel'"/>
         </button>
diff --git a/app/code/Magento/Ui/view/frontend/web/template/messages.html b/app/code/Magento/Ui/view/frontend/web/template/messages.html
index 5966523ec89206d829f2da18784ea756d83e9492..f0f03ec2e53257d2965a3acbed96d95faa1ba860 100644
--- a/app/code/Magento/Ui/view/frontend/web/template/messages.html
+++ b/app/code/Magento/Ui/view/frontend/web/template/messages.html
@@ -6,13 +6,13 @@
 -->
 <div data-role="checkout-messages" class="messages" data-bind="visible: isVisible(), click: removeAll">
     <!-- ko foreach: messageContainer.getErrorMessages() -->
-    <div class="message message-error error">
+    <div role="alert" class="message message-error error">
         <div data-ui-id="checkout-cart-validationmessages-message-error" data-bind="text: $data"></div>
     </div>
     <!--/ko-->
     <!-- ko foreach: messageContainer.getSuccessMessages() -->
-    <div class="message message-success success">
+    <div role="alert" class="message message-success success">
         <div data-ui-id="checkout-cart-validationmessages-message-success" data-bind="text: $data"></div>
     </div>
     <!--/ko-->
-</div>
\ No newline at end of file
+</div>
diff --git a/app/code/Magento/Ui/view/frontend/web/templates/form/element/checkbox.html b/app/code/Magento/Ui/view/frontend/web/templates/form/element/checkbox.html
index 50a27e6902528938e66ab2e632aa98ef23993701..c22df869a0013a9dd8960cada1ab7fb1b2601587 100644
--- a/app/code/Magento/Ui/view/frontend/web/templates/form/element/checkbox.html
+++ b/app/code/Magento/Ui/view/frontend/web/templates/form/element/checkbox.html
@@ -5,7 +5,19 @@
  */
 -->
 <div class="choice field">
-    <input type="checkbox" class="checkbox" data-bind="checked: value, attr: { id: uid, disabled: disabled, name: inputName }, hasFocus: focused">
+    <input type="checkbox"
+           class="checkbox"
+           data-bind="
+           checked: value,
+           attr: {
+                id: uid,
+                disabled: disabled,
+                name: inputName,
+                'aria-describedby': getDescriptionId(),
+                'aria-required': required,
+                'aria-invalid': error() ? true : 'false'
+                },
+           hasFocus: focused">
 
     <label class="label" data-bind="checked: value, attr: { for: uid }">
         <span data-bind="text: description || label"></span>
diff --git a/app/code/Magento/Ui/view/frontend/web/templates/form/element/date.html b/app/code/Magento/Ui/view/frontend/web/templates/form/element/date.html
index be2034a6a0b9710b0bc044a2334bf0744c5c1781..48faf4b73a5f5e4a4987bbc9c4b33595e74bed3c 100644
--- a/app/code/Magento/Ui/view/frontend/web/templates/form/element/date.html
+++ b/app/code/Magento/Ui/view/frontend/web/templates/form/element/date.html
@@ -11,6 +11,8 @@
         value: value,
         name: inputName,
         placeholder: placeholder,
-        'aria-describedby': noticeId,
+        'aria-describedby': getDescriptionId(),
+        'aria-required': required,
+        'aria-invalid': error() ? true : 'false',
         disabled: disabled
     }" />
diff --git a/app/code/Magento/Ui/view/frontend/web/templates/form/element/email.html b/app/code/Magento/Ui/view/frontend/web/templates/form/element/email.html
index dfd75630e08bc086d7cf87dedf55000cb9ffc10c..57f94670323ad25c2cd5b8038eee5a7d4f984c3f 100644
--- a/app/code/Magento/Ui/view/frontend/web/templates/form/element/email.html
+++ b/app/code/Magento/Ui/view/frontend/web/templates/form/element/email.html
@@ -10,7 +10,9 @@
     attr: {
         name: inputName,
         placeholder: placeholder,
-        'aria-describedby': noticeId,
+        'aria-describedby': getDescriptionId(),
+        'aria-required': required,
+        'aria-invalid': error() ? true : 'false',
         id: uid,
         disabled: disabled
     }"/>
diff --git a/app/code/Magento/Ui/view/frontend/web/templates/form/element/input.html b/app/code/Magento/Ui/view/frontend/web/templates/form/element/input.html
index 01234333d5816573d1c2ae291446229e8a317e9b..a09e0383ef81d65103bbc3c53182c6740b7d3587 100644
--- a/app/code/Magento/Ui/view/frontend/web/templates/form/element/input.html
+++ b/app/code/Magento/Ui/view/frontend/web/templates/form/element/input.html
@@ -11,7 +11,9 @@
     attr: {
         name: inputName,
         placeholder: placeholder,
-        'aria-describedby': noticeId,
+        'aria-describedby': getDescriptionId(),
+        'aria-required': required,
+        'aria-invalid': error() ? true : 'false',
         id: uid,
         disabled: disabled
     }" />
diff --git a/app/code/Magento/Ui/view/frontend/web/templates/form/element/password.html b/app/code/Magento/Ui/view/frontend/web/templates/form/element/password.html
index 825a9869ec2efbf9be3ad4492b972cb30946ec9f..0d93d48aa96f6d7d10a7b02160aded2a489cf442 100644
--- a/app/code/Magento/Ui/view/frontend/web/templates/form/element/password.html
+++ b/app/code/Magento/Ui/view/frontend/web/templates/form/element/password.html
@@ -10,7 +10,9 @@
     attr: {
         name: inputName,
         placeholder: placeholder,
-        'aria-describedby': noticeId,
+        'aria-describedby': getDescriptionId(),
+        'aria-required': required,
+        'aria-invalid': error() ? true : 'false',
         id: uid,
         disabled: disabled
     }"/>
diff --git a/app/code/Magento/Ui/view/frontend/web/templates/form/element/select.html b/app/code/Magento/Ui/view/frontend/web/templates/form/element/select.html
index edd1395c5a719a0050b93afdcc16628b3df40f97..37d87d183e023574f4a1485649fd5046aa43f8fd 100644
--- a/app/code/Magento/Ui/view/frontend/web/templates/form/element/select.html
+++ b/app/code/Magento/Ui/view/frontend/web/templates/form/element/select.html
@@ -9,7 +9,9 @@
         name: inputName,
         id: uid,
         disabled: disabled,
-        'aria-describedby': noticeId,
+        'aria-describedby': getDescriptionId(),
+        'aria-required': required,
+        'aria-invalid': error() ? true : 'false',
         placeholder: placeholder
     },
     hasFocus: focused,
diff --git a/app/code/Magento/Ui/view/frontend/web/templates/form/field.html b/app/code/Magento/Ui/view/frontend/web/templates/form/field.html
index f3be62f116b8358e9424934951dc9c40a4a2d22a..d0bc45c74dbb41173454e0fff3f315b06841505c 100644
--- a/app/code/Magento/Ui/view/frontend/web/templates/form/field.html
+++ b/app/code/Magento/Ui/view/frontend/web/templates/form/field.html
@@ -36,16 +36,22 @@
         <!-- /ko -->
 
         <!-- ko if: element.notice -->
-            <div class="field-note" data-bind="attr: { id: element.noticeId }"><span data-bind="text: element.notice"></span></div>
+            <div class="field-note" data-bind="attr: { id: element.noticeId }">
+                <span data-bind="text: element.notice"/>
+            </div>
         <!-- /ko -->
 
         <!-- ko if: element.error() -->
-            <div class="mage-error" data-bind="attr: { for: element.uid }, text: element.error" generated="true"></div>
+            <div class="field-error" data-bind="attr: { id: element.errorId }" generated="true">
+                <span data-bind="text: element.error"/>
+            </div>
         <!-- /ko -->
 
         <!-- ko if: element.warn() -->
-            <div class="message warning" generated="true"><span data-bind="text: element.warn"></span></div>
+            <div role="alert" class="message warning" data-bind="attr: { id: element.warningId }" generated="true">
+                <span data-bind="text: element.warn"/>
+            </div>
         <!-- /ko -->
     </div>
 </div>
-<!-- /ko -->
\ No newline at end of file
+<!-- /ko -->
diff --git a/app/design/frontend/Magento/blank/Magento_Theme/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Theme/web/css/source/_module.less
index 3b8e84da841069ed41c651329f48ef91a0e10133..7fb7985ada12a8f932134c8afeee765a260f6083 100644
--- a/app/design/frontend/Magento/blank/Magento_Theme/web/css/source/_module.less
+++ b/app/design/frontend/Magento/blank/Magento_Theme/web/css/source/_module.less
@@ -112,6 +112,11 @@
         }
     }
 
+    .action-skip-wrapper {
+        height: 0;
+        position: relative;
+    }
+
     //
     //  Global notice
     //  ---------------------------------------------
diff --git a/app/design/frontend/Magento/blank/web/css/source/_forms.less b/app/design/frontend/Magento/blank/web/css/source/_forms.less
index 72e014b8305aaf86e0405df59654c8faf1297834..8c7258c390ab723e4f55492f42cffccfc8f60a09 100644
--- a/app/design/frontend/Magento/blank/web/css/source/_forms.less
+++ b/app/design/frontend/Magento/blank/web/css/source/_forms.less
@@ -92,10 +92,15 @@
         }
     }
 
+    .field-error,
     div.mage-error[generated] {
         margin-top: 7px;
     }
 
+    .field-error {
+        .lib-form-validation-note();
+    }
+
     .field .tooltip {
         .lib-tooltip(right);
         .tooltip-content {
diff --git a/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/_module.less
index 5581eb7fb05212c4c24967baf6aab30398bd8072..12ef890f4305539539582af433c1b7961a15043a 100644
--- a/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/_module.less
+++ b/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/_module.less
@@ -168,6 +168,11 @@
         }
     }
 
+    .action-skip-wrapper {
+        height: 0;
+        position: relative;
+    }
+
     //
     //  Global notice
     //  ---------------------------------------------
diff --git a/app/design/frontend/Magento/luma/web/css/source/_forms.less b/app/design/frontend/Magento/luma/web/css/source/_forms.less
index 8d22eba9ca51f892425a60b73a43cedc6a149fa1..d29e53d7fefe68622af55b4004c3742df2be93d5 100644
--- a/app/design/frontend/Magento/luma/web/css/source/_forms.less
+++ b/app/design/frontend/Magento/luma/web/css/source/_forms.less
@@ -113,10 +113,15 @@
         .select-styling();
     }
 
+    .field-error,
     div.mage-error[generated] {
         margin-top: 7px;
     }
 
+    .field-error {
+        .lib-form-validation-note();
+    }
+
     //  TEMP
 
     .field .tooltip {
diff --git a/composer.json b/composer.json
index 8053b76a01a2283f5f1ad39ec8028123bca9bb14..a0813c8462066d0eabcacb4a21cdb7b5a09dbd13 100644
--- a/composer.json
+++ b/composer.json
@@ -64,6 +64,7 @@
         "ext-mbstring": "*",
         "ext-openssl": "*",
         "ext-zip": "*",
+        "ext-pdo_mysql": "*",
         "sjparkinson/static-review": "~4.1",
         "ramsey/uuid": "3.4"
     },
diff --git a/composer.lock b/composer.lock
index 6fd73ef405c822bcb049709ec1150c6e51254e35..4d1f23dad9b512b00d28341d28d1db5ac05899bb 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,8 +4,8 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "hash": "ad3de2234f78fd4b353ae6a1b22401fc",
-    "content-hash": "25dcf96ed1d8b12a25111e8b3af61317",
+    "hash": "c8f9e4332a46ace884514d9843021898",
+    "content-hash": "fa9bd2b88f3c2e1bf562c439cecca917",
     "packages": [
         {
             "name": "braintree/braintree_php",
@@ -4597,7 +4597,8 @@
         "ext-xsl": "*",
         "ext-mbstring": "*",
         "ext-openssl": "*",
-        "ext-zip": "*"
+        "ext-zip": "*",
+        "ext-pdo_mysql": "*"
     },
     "platform-dev": []
 }
diff --git a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/View.php b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/View.php
index 9b57ce34f4eea73928920c87dfdd1ebab23c81d1..3134fda0d2b2b3f98b3e9d0ef5c835e8cb1aad53 100644
--- a/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/View.php
+++ b/dev/tests/functional/tests/app/Magento/Catalog/Test/Block/Product/View.php
@@ -567,12 +567,19 @@ class View extends AbstractConfigureBlock
      */
     public function closeFullImage()
     {
-        $element = $this->browser->find($this->fullImageClose, Locator::SELECTOR_CSS);
-        if (!$element->isVisible()) {
-            $element->hover();
-            $this->waitForElementVisible($this->fullImageClose);
-        }
-        $element->click();
+        $this->_rootElement->waitUntil(
+            function () {
+                $this->browser->find($this->fullImage)->hover();
+
+                if ($this->browser->find($this->fullImageClose)->isVisible()) {
+                    $this->browser->find($this->fullImageClose)->click();
+
+                    return true;
+                }
+
+                return null;
+            }
+        );
     }
 
     /**
diff --git a/lib/web/mage/smart-keyboard-handler.js b/lib/web/mage/smart-keyboard-handler.js
index 859ab69372e09515f967a10a42c421ea7b750341..b6a65f2ee5d43b43ae91722b43507c39febd1941 100644
--- a/lib/web/mage/smart-keyboard-handler.js
+++ b/lib/web/mage/smart-keyboard-handler.js
@@ -16,7 +16,8 @@ define([
             CODE_TAB = 9;
 
         return {
-            apply: smartKeyboardFocus
+            apply: smartKeyboardFocus,
+            focus: handleFocus
         };
 
         /**
@@ -49,22 +50,39 @@ define([
          * Handle logic, when onTabKeyPress fired at first.
          * Then it changes state.
          */
-        function onFocusInHandler () {
+        function onFocusInHandler() {
             focusState = true;
-            $('body').addClass(tabFocusClass)
+            body.addClass(tabFocusClass)
                 .off('focusin.keyboardHandler', onFocusInHandler);
         }
 
         /**
          * Handle logic to remove state after onTabKeyPress to normal.
-         * @param {Event} event
          */
-        function onClickHandler(event) {
-            focusState  = false;
-            $('body').removeClass(tabFocusClass)
+        function onClickHandler() {
+            focusState = false;
+            body.removeClass(tabFocusClass)
                 .off('click', onClickHandler);
         }
+
+        /**
+         * Attach smart focus on specific element.
+         * @param {jQuery} element
+         */
+        function handleFocus(element) {
+            element.on('focusin.emulateTabFocus', function () {
+                focusState = true;
+                body.addClass(tabFocusClass);
+                element.off();
+            });
+
+            element.on('focusout.emulateTabFocus', function () {
+                focusState = false;
+                body.removeClass(tabFocusClass);
+                element.off();
+            });
+        }
     }
 
     return new KeyboardHandler;
-});
\ No newline at end of file
+});
diff --git a/lib/web/mage/validation.js b/lib/web/mage/validation.js
index 6c8deba8164112a31939b11d1ed79b5c23dd7bc0..df2cd0288648e4a33b6e724c4141baf31d3a4d17 100644
--- a/lib/web/mage/validation.js
+++ b/lib/web/mage/validation.js
@@ -1543,6 +1543,7 @@
                 this.validate.resetForm();
             }
         },
+
         /**
          * Validation creation
          * @protected
@@ -1559,36 +1560,46 @@
 
             this._listenFormValidate();
         },
+
         /**
          * Validation listening
          * @protected
          */
         _listenFormValidate: function () {
-            $('form').on('invalid-form.validate', function (event, validation) {
-                var firstActive = $(validation.errorList[0].element || []),
-                    lastActive = $(validation.findLastActive() || validation.errorList.length && validation.errorList[0].element || []);
-
-                if (lastActive.is(':hidden')) {
-                    var parent = lastActive.parent();
-                    var windowHeight = $(window).height();
-                    $('html, body').animate({
-                        scrollTop: parent.offset().top - windowHeight / 2
-                    });
-                }
-
-                // ARIA (removing aria attributes if success)
-                var successList = validation.successList;
-                if (successList.length) {
-                    $.each(successList, function () {
-                        $(this)
-                            .removeAttr('aria-describedby')
-                            .removeAttr('aria-invalid');
-                    })
-                }
-                if (firstActive.length) {
-                    firstActive.focus();
-                }
-            });
+            $('form').on('invalid-form.validate', this.listenFormValidateHandler);
+        },
+
+        /**
+         * Handle form validation. Focus on first invalid form field.
+         *
+         * @param {jQuery.Event} event
+         * @param {Object} validation
+         */
+        listenFormValidateHandler:  function (event, validation) {
+            var firstActive = $(validation.errorList[0].element || []),
+                lastActive = $(validation.findLastActive() || validation.errorList.length && validation.errorList[0].element || []);
+
+            if (lastActive.is(':hidden')) {
+                var parent = lastActive.parent();
+                var windowHeight = $(window).height();
+                $('html, body').animate({
+                    scrollTop: parent.offset().top - windowHeight / 2
+                });
+            }
+
+            // ARIA (removing aria attributes if success)
+            var successList = validation.successList;
+            if (successList.length) {
+                $.each(successList, function () {
+                    $(this)
+                        .removeAttr('aria-describedby')
+                        .removeAttr('aria-invalid');
+                });
+            }
+
+            if (firstActive.length) {
+                firstActive.focus();
+            }
         }
     });
 
diff --git a/setup/src/Magento/Setup/Model/PhpReadinessCheck.php b/setup/src/Magento/Setup/Model/PhpReadinessCheck.php
index 06e8b34471dada4264aef7be5dcba41febc14dd8..74b2afd1419c440977f2b10418964286eb738b39 100644
--- a/setup/src/Magento/Setup/Model/PhpReadinessCheck.php
+++ b/setup/src/Magento/Setup/Model/PhpReadinessCheck.php
@@ -102,7 +102,8 @@ class PhpReadinessCheck
 
         $settings = array_merge(
             $this->checkXDebugNestedLevel(),
-            $this->checkPopulateRawPostSetting()
+            $this->checkPopulateRawPostSetting(),
+            $this->checkFunctionsExistence()
         );
 
         foreach ($settings as $setting) {
@@ -316,6 +317,33 @@ class PhpReadinessCheck
         return $data;
     }
 
+    /**
+     * Check whether all special functions exists
+     *
+     * @return array
+     */
+    private function checkFunctionsExistence()
+    {
+        $data = [];
+        $requiredFunctions = [
+            [
+                'name' => 'imagecreatefromjpeg',
+                'message' => 'You must have installed GD library with --with-jpeg-dir=DIR option.',
+                'helpUrl' => 'http://php.net/manual/en/image.installation.php',
+            ],
+        ];
+
+        foreach ($requiredFunctions as $function) {
+            $data['missed_function_' . $function['name']] = [
+                'message' => $function['message'],
+                'helpUrl' => $function['helpUrl'],
+                'error' => !function_exists($function['name']),
+            ];
+        }
+
+        return $data;
+    }
+
     /**
      * Normalize PHP Version
      *
diff --git a/setup/src/Magento/Setup/Test/Unit/Model/PhpReadinessCheckTest.php b/setup/src/Magento/Setup/Test/Unit/Model/PhpReadinessCheckTest.php
index c24e1792331ef09c20ac5549fc9d7edb84c8a3c1..a77bd46c79f25495570070968ae62ebb0762ef34 100644
--- a/setup/src/Magento/Setup/Test/Unit/Model/PhpReadinessCheckTest.php
+++ b/setup/src/Magento/Setup/Test/Unit/Model/PhpReadinessCheckTest.php
@@ -222,7 +222,12 @@ class PhpReadinessCheckTest extends \PHPUnit_Framework_TestCase
                     'message' => $xdebugMessage,
                     'error' => false,
                 ],
-            ]
+                'missed_function_imagecreatefromjpeg' => [
+                    'message' => 'You must have installed GD library with --with-jpeg-dir=DIR option.',
+                    'helpUrl' => 'http://php.net/manual/en/image.installation.php',
+                    'error' => false,
+                ],
+            ],
         ];
         if (!$this->isPhp7OrHhvm()) {
             $this->setUpNoPrettyVersionParser();
@@ -261,8 +266,13 @@ class PhpReadinessCheckTest extends \PHPUnit_Framework_TestCase
                 'xdebug_max_nesting_level' => [
                     'message' => $xdebugMessage,
                     'error' => true,
-                ]
-            ]
+                ],
+                'missed_function_imagecreatefromjpeg' => [
+                    'message' => 'You must have installed GD library with --with-jpeg-dir=DIR option.',
+                    'helpUrl' => 'http://php.net/manual/en/image.installation.php',
+                    'error' => false,
+                ],
+            ],
         ];
         if (!$this->isPhp7OrHhvm()) {
             $this->setUpNoPrettyVersionParser();
@@ -301,6 +311,13 @@ class PhpReadinessCheckTest extends \PHPUnit_Framework_TestCase
                 ]
             ];
         }
+
+        $expected['data']['missed_function_imagecreatefromjpeg'] = [
+            'message' => 'You must have installed GD library with --with-jpeg-dir=DIR option.',
+            'helpUrl' => 'http://php.net/manual/en/image.installation.php',
+            'error' => false,
+        ];
+
         $this->assertEquals($expected, $this->phpReadinessCheck->checkPhpSettings());
     }