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/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/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/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 {