diff --git a/CHANGELOG.md b/CHANGELOG.md index a8d511af1d..0acbabaf61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Use https:// for schema markup. [#2039](https://github.com/bigcommerce/cornerstone/pull/2039) - Update focus tooltip styles contrast to achieve accessibility AA Complaince. [#2047](https://github.com/bigcommerce/cornerstone/pull/2047) - Apple pay button displaying needs to be fixed. [#2043](https://github.com/bigcommerce/cornerstone/pull/2043) +- Fixed NaN error on increase/decrease product quantity by adding field validation. [#2052](https://github.com/bigcommerce/cornerstone/pull/2052) ## 5.4.0 (04-26-2021) - Incorrect focus order for product carousels. [#2034](https://github.com/bigcommerce/cornerstone/pull/2034) diff --git a/assets/js/theme/common/models/forms.js b/assets/js/theme/common/models/forms.js index ddb7dfc846..23dc4f3829 100644 --- a/assets/js/theme/common/models/forms.js +++ b/assets/js/theme/common/models/forms.js @@ -22,6 +22,45 @@ const forms = { notEmpty(value) { return value.length > 0; }, + + /** + * validates a field like product quantity + * @param value + * @returns {boolean} + * + */ + numbersOnly(value) { + const re = /^\d+$/; + return re.test(value); + }, + + /** + * validates increase in value does not exceed max + * @param {number} value + * @param {number} max + * @returns {number} + * + */ + validateIncreaseAgainstMaxBoundary(value, max) { + const raise = value + 1; + + if (!max || raise <= max) return raise; + return value; + }, + + /** + * validates decrease in value does not fall below min + * @param {number} value + * @param {number} min + * @returns {number} + * + */ + validateDecreaseAgainstMinBoundary(value, min) { + const decline = value - 1; + + if (!min || decline >= min) return decline; + return value; + }, }; export default forms; diff --git a/assets/js/theme/common/product-details.js b/assets/js/theme/common/product-details.js index c475f6b66e..6f1a3f099b 100644 --- a/assets/js/theme/common/product-details.js +++ b/assets/js/theme/common/product-details.js @@ -5,6 +5,9 @@ import 'foundation-sites/js/foundation/foundation.reveal'; import ImageGallery from '../product/image-gallery'; import modalFactory, { alertModal, showAlertModal } from '../global/modal'; import { isEmpty, isPlainObject } from 'lodash'; +import nod from '../common/nod'; +import { announceInputErrorMessage } from '../common/utils/form-utils'; +import forms from '../common/models/forms'; import { normalizeFormData } from './utils/api'; import { isBrowserIE, convertIntoArray } from './utils/ie-helpers'; import bannerUtils from './utils/banner-utils'; @@ -23,6 +26,12 @@ export default class ProductDetails extends ProductDetailsBase { this.storeInitMessagesForSwatches(); const $form = $('form[data-cart-item-add]', $scope); + + this.addToCartValidator = nod({ + submit: $form.find('input#form-action-addToCart'), + tap: announceInputErrorMessage, + }); + const $productOptionsElement = $('[data-product-option-change]', $form); const hasOptions = $productOptionsElement.html().trim().length; const hasDefaultOptions = $productOptionsElement.find('[data-default]').length; @@ -41,7 +50,10 @@ export default class ProductDetails extends ProductDetailsBase { } }; - $(window).on('load', () => $.each($productSwatchLabels, placeSwatchLabelImage)); + $(window).on('load', () => { + this.registerAddToCartValidation(); + $.each($productSwatchLabels, placeSwatchLabelImage); + }); if (context.showSwatchNames) { this.$swatchOptionMessage.removeClass('u-hidden'); @@ -65,7 +77,11 @@ export default class ProductDetails extends ProductDetailsBase { }); $form.on('submit', event => { - this.addProductToCart(event, $form[0]); + this.addToCartValidator.performCheck(); + + if (this.addToCartValidator.areAll('valid')) { + this.addProductToCart(event, $form[0]); + } }); // Update product attributes. Also update the initial view in case items are oos @@ -85,6 +101,19 @@ export default class ProductDetails extends ProductDetailsBase { this.previewModal = modalFactory('#previewModal')[0]; } + registerAddToCartValidation() { + this.addToCartValidator.add([{ + selector: '[data-quantity-change] > .form-input--incrementTotal', + validate: (cb, val) => { + const result = forms.numbersOnly(val); + cb(result); + }, + errorMessage: this.context.productQuantityErrorMessage, + }]); + + return this.addToCartValidator; + } + storeInitMessagesForSwatches() { if (this.swatchGroupIdList.length && isEmpty(this.swatchInitMessageStorage)) { this.swatchGroupIdList.each((_, swatchGroupId) => { @@ -317,35 +346,20 @@ export default class ProductDetails extends ProductDetailsBase { const quantityMin = parseInt($input.data('quantityMin'), 10); const quantityMax = parseInt($input.data('quantityMax'), 10); - let qty = parseInt($input.val(), 10); - + let qty = forms.numbersOnly($input.val()) ? parseInt($input.val(), 10) : quantityMin; // If action is incrementing if ($target.data('action') === 'inc') { - // If quantity max option is set - if (quantityMax > 0) { - // Check quantity does not exceed max - if ((qty + 1) <= quantityMax) { - qty++; - } - } else { - qty++; - } + qty = forms.validateIncreaseAgainstMaxBoundary(qty, quantityMax); } else if (qty > 1) { - // If quantity min option is set - if (quantityMin > 0) { - // Check quantity does not fall below min - if ((qty - 1) >= quantityMin) { - qty--; - } - } else { - qty--; - } + qty = forms.validateDecreaseAgainstMinBoundary(qty, quantityMin); } // update hidden input viewModel.quantity.$input.val(qty); // update text viewModel.quantity.$text.text(qty); + // perform validation after updating product quantity + this.addToCartValidator.performCheck(); }); // Prevent triggering quantity change when pressing enter diff --git a/assets/js/theme/common/utils/form-utils.js b/assets/js/theme/common/utils/form-utils.js index f750b66431..bebb29a536 100644 --- a/assets/js/theme/common/utils/form-utils.js +++ b/assets/js/theme/common/utils/form-utils.js @@ -135,8 +135,9 @@ function announceInputErrorMessage({ element, result }) { } const activeInputContainer = $(element).parent(); // the reason for using span tag is nod-validate lib - // which does not add error message class while initialising form - const errorMessage = $(activeInputContainer).find('span'); + // which does not add error message class while initialising form. + // specific class is added since it can be multiple spans + const errorMessage = $(activeInputContainer).find('span.form-inlineMessage'); if (errorMessage.length) { const $errMessage = $(errorMessage[0]); diff --git a/assets/scss/components/citadel/forms/_forms.scss b/assets/scss/components/citadel/forms/_forms.scss index 0dd0e178fc..18ace621b1 100644 --- a/assets/scss/components/citadel/forms/_forms.scss +++ b/assets/scss/components/citadel/forms/_forms.scss @@ -269,6 +269,10 @@ text-align: center; vertical-align: middle; width: remCalc(35px); + + .form-field--success & { + float:none; + } } diff --git a/assets/scss/components/stencil/productView/_productView.scss b/assets/scss/components/stencil/productView/_productView.scss index 1d51469e38..1b9337e3d9 100644 --- a/assets/scss/components/stencil/productView/_productView.scss +++ b/assets/scss/components/stencil/productView/_productView.scss @@ -301,6 +301,10 @@ font-size: 0; // 2 margin-bottom: 2rem; + &--error > .form-inlineMessage { + font-size: 1rem; + } + // scss-lint:disable SelectorDepth, NestingDepth > .form-checkbox + .form-label { diff --git a/lang/en.json b/lang/en.json index 382b84025a..ca2b6e5090 100755 --- a/lang/en.json +++ b/lang/en.json @@ -694,6 +694,7 @@ "change_product_options": "Change options for {name}", "quantity_decrease": "Decrease Quantity of {name}", "quantity_increase": "Increase Quantity of {name}", + "quantity_error_message":"The quantity should contain only numbers", "purchase_units": "{quantity, plural, =0{0 units} one {# unit} other {# units}}", "max_purchase_quantity": "Maximum Purchase:", "min_purchase_quantity": "Minimum Purchase:", diff --git a/templates/components/amp/css/form.html b/templates/components/amp/css/form.html index 6ba59efca4..4ce03954b4 100644 --- a/templates/components/amp/css/form.html +++ b/templates/components/amp/css/form.html @@ -365,6 +365,9 @@ .form-field--warning .form-input { float: left } +.form-field--success[data-quantity-change] .form-input { + float:none; +} .form-field--success .form-input, .form-field--success .form-select, .form-field--success .form-checkbox + .form-label::before, diff --git a/templates/components/products/add-to-cart.html b/templates/components/products/add-to-cart.html index 5ce93c4214..37d7fa44ae 100644 --- a/templates/components/products/add-to-cart.html +++ b/templates/components/products/add-to-cart.html @@ -1,5 +1,6 @@
{{#if theme_settings.show_product_quantity_box}} + {{inject 'productQuantityErrorMessage' (lang 'products.quantity_error_message')}}