diff --git a/CHANGELOG.md b/CHANGELOG.md index 171dfa9c05..5268f4e641 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ ## Draft +## 5.2.0 (02-22-2021) +- Fixed cut off on Cart button when Zooming up to 400%. [#1988](https://github.com/bigcommerce/cornerstone/pull/1988) +- Category pages are creating alt attribute within the span tag. [1987](https://github.com/bigcommerce/cornerstone/pull/1987) +- Add alt attribute for no image placeholders. [#1984](https://github.com/bigcommerce/cornerstone/pull/1984) +- For non-required options, when the "None" option displays on the storefront it should be deselected when another option is chosen. [#1980](https://github.com/bigcommerce/cornerstone/pull/1980) +- Error message not announced automatically. [#1983](https://github.com/bigcommerce/cornerstone/pull/1983) +- Fixed Discount Banner update on adding item to Cart from PDP [#1974](https://github.com/bigcommerce/cornerstone/pull/1974) +- Make every product option group id unique. [#1979](https://github.com/bigcommerce/cornerstone/pull/1979) +- Fixed required checkbox message displaying. [1963](https://github.com/bigcommerce/cornerstone/pull/1963) +- Provided sufficient & informative text along with the color swatches [#1976](https://github.com/bigcommerce/cornerstone/pull/1976) +- If multiple Pick List Options are applied, customers cannot select "none" on both. [#1975](https://github.com/bigcommerce/cornerstone/pull/1975) +- Moved phrase from compare.html to en.json for increasing localization. [#1972](https://github.com/bigcommerce/cornerstone/pull/1972) +- Fixed focus for sort by dropdown on reloading page. [#1964](https://github.com/bigcommerce/cornerstone/pull/1964) +- Fixed filtered selection not announced by screen reader. [#1966](https://github.com/bigcommerce/cornerstone/pull/1966) +- Integrate accessibility scripts to product images slider on PDP. [#1965](https://github.com/bigcommerce/cornerstone/pull/1965) +- Move focus on filter items Modal after show more button was clicked and accessibility refactoring. [#1977](https://github.com/bigcommerce/cornerstone/pull/1977) + ## 5.1.0 (01-26-2021) - Updated Cornerstone theme and respected variants to meet the verticals/industries documented in BCTHEME-387 - Fixed selecting certain option types which pushes window view to the bottom of the page. [#1959](https://github.com/bigcommerce/cornerstone/pull/1959) @@ -27,6 +44,7 @@ - Cornerstone - Image Zoom Does Not Work on Internet Explorer. [#1923](https://github.com/bigcommerce/cornerstone/pull/1923) - Fixed input placeholder color contrast according to AA standard. [#1933](https://github.com/bigcommerce/cornerstone/pull/1933) - Bump stencil utils to 6.8.0. [#1945](https://github.com/bigcommerce/cornerstone/pull/1945) +- Bump stencil utils to 6.8.1. and removed changes from #1910 [#1945](https://github.com/bigcommerce/cornerstone/pull/1945) ## 5.0.0 (12-14-2020) - Parse HTML entities in jsContext. [#1917](https://github.com/bigcommerce/cornerstone/pull/1917) @@ -98,7 +116,7 @@ ## 4.11.0 (10-07-2020) - Add Info and Add Coupon forms on Cart Page tabbable when hidden. [#1820](https://github.com/bigcommerce/cornerstone/pull/1820) - Fixed outline styles for breadcrumbs on focus state. [#1824](https://github.com/bigcommerce/cornerstone/pull/1824) -- Fixed review rating icons focus border. [#1818](https://github.com/bigcommerce/cornerstone/pull/1818) +- Fixed review rating icons focus border. [#1819](https://github.com/bigcommerce/cornerstone/pull/1819) - Fixed announcement of SortBy Select to listbox from button via screen reader. [#1837](https://github.com/bigcommerce/cornerstone/pull/1837) - Fixed missing top outline on chosen filter's focused. [#1829](https://github.com/bigcommerce/cornerstone/pull/1829) - Added immediate keyboard focus on write-a-review modal. [#1835](https://github.com/bigcommerce/cornerstone/pull/1835) diff --git a/assets/js/theme/account.js b/assets/js/theme/account.js index 35665c6676..a4bc2f13c4 100644 --- a/assets/js/theme/account.js +++ b/assets/js/theme/account.js @@ -4,7 +4,13 @@ import nod from './common/nod'; import Wishlist from './wishlist'; import validation from './common/form-validation'; import stateCountry from './common/state-country'; -import { classifyForm, Validators, insertStateHiddenField, createPasswordValidationErrorTextObject } from './common/utils/form-utils'; +import { + classifyForm, + Validators, + announceInputErrorMessage, + insertStateHiddenField, + createPasswordValidationErrorTextObject, +} from './common/utils/form-utils'; import { createTranslationDictionary } from './common/utils/translations-utils'; import { creditCardType, storeInstrument, Validators as CCValidators, Formatters as CCFormatters } from './common/payment-method'; import swal from './global/sweet-alert'; @@ -139,6 +145,7 @@ export default class Account extends PageManager { const $stateElement = $(stateSelector); const addressValidator = nod({ submit: 'form[data-address-form] input[type="submit"]', + tap: announceInputErrorMessage, }); addressValidator.add(validationModel); @@ -226,7 +233,10 @@ export default class Account extends PageManager { const validationModel = validation($paymentMethodForm, this.context); const paymentMethodSelector = 'form[data-payment-method-form]'; - const paymentMethodValidator = nod({ submit: `${paymentMethodSelector} input[type="submit"]` }); + const paymentMethodValidator = nod({ + submit: `${paymentMethodSelector} input[type="submit"]`, + tap: announceInputErrorMessage, + }); const $stateElement = $(`${paymentMethodSelector} [data-field-type="State"]`); let $last; @@ -317,6 +327,7 @@ export default class Account extends PageManager { const formEditSelector = 'form[data-edit-account-form]'; const editValidator = nod({ submit: '${formEditSelector} input[type="submit"]', + tap: announceInputErrorMessage, }); const emailSelector = `${formEditSelector} [data-field-type="EmailAddress"]`; const $emailElement = $(emailSelector); @@ -400,6 +411,7 @@ export default class Account extends PageManager { registerInboxValidation($inboxForm) { const inboxValidator = nod({ submit: 'form[data-inbox-form] input[type="submit"]', + tap: announceInputErrorMessage, }); inboxValidator.add([ diff --git a/assets/js/theme/auth.js b/assets/js/theme/auth.js index 9aaa227a84..6c52255e43 100644 --- a/assets/js/theme/auth.js +++ b/assets/js/theme/auth.js @@ -3,7 +3,12 @@ import stateCountry from './common/state-country'; import nod from './common/nod'; import validation from './common/form-validation'; import forms from './common/models/forms'; -import { classifyForm, Validators, createPasswordValidationErrorTextObject } from './common/utils/form-utils'; +import { + classifyForm, + Validators, + createPasswordValidationErrorTextObject, + announceInputErrorMessage, +} from './common/utils/form-utils'; import { createTranslationDictionary } from './common/utils/translations-utils'; export default class Auth extends PageManager { @@ -19,6 +24,7 @@ export default class Auth extends PageManager { this.loginValidator = nod({ submit: '.login-form input[type="submit"]', + tap: announceInputErrorMessage, }); this.loginValidator.add([ @@ -56,6 +62,7 @@ export default class Auth extends PageManager { registerForgotPasswordValidation($forgotPasswordForm) { this.forgotPasswordValidator = nod({ submit: '.forgot-password-form input[type="submit"]', + tap: announceInputErrorMessage, }); this.forgotPasswordValidator.add([ @@ -86,6 +93,7 @@ export default class Auth extends PageManager { const newPasswordForm = '.new-password-form'; const newPasswordValidator = nod({ submit: $(`${newPasswordForm} input[type="submit"]`), + tap: announceInputErrorMessage, }); const passwordSelector = $(`${newPasswordForm} input[name="password"]`); const password2Selector = $(`${newPasswordForm} input[name="password_confirm"]`); @@ -103,6 +111,7 @@ export default class Auth extends PageManager { const validationModel = validation($createAccountForm, this.context); const createAccountValidator = nod({ submit: `${this.formCreateSelector} input[type='submit']`, + tap: announceInputErrorMessage, }); const $stateElement = $('[data-field-type="State"]'); const emailSelector = `${this.formCreateSelector} [data-field-type='EmailAddress']`; diff --git a/assets/js/theme/cart/shipping-estimator.js b/assets/js/theme/cart/shipping-estimator.js index 6a30260f1d..8be324d460 100644 --- a/assets/js/theme/cart/shipping-estimator.js +++ b/assets/js/theme/cart/shipping-estimator.js @@ -1,7 +1,7 @@ import stateCountry from '../common/state-country'; import nod from '../common/nod'; import utils from '@bigcommerce/stencil-utils'; -import { Validators } from '../common/utils/form-utils'; +import { Validators, announceInputErrorMessage } from '../common/utils/form-utils'; import collapsibleFactory from '../common/collapsible'; import swal from '../global/sweet-alert'; @@ -22,6 +22,7 @@ export default class ShippingEstimator { this.shippingEstimator = 'form[data-shipping-estimator]'; this.shippingValidator = nod({ submit: `${this.shippingEstimator} .shipping-estimate-submit`, + tap: announceInputErrorMessage, }); $('.shipping-estimate-submit', this.$element).on('click', event => { diff --git a/assets/js/theme/catalog.js b/assets/js/theme/catalog.js index be42a30d5e..8286054cbf 100644 --- a/assets/js/theme/catalog.js +++ b/assets/js/theme/catalog.js @@ -3,6 +3,25 @@ import urlUtils from './common/utils/url-utils'; import Url from 'url'; export default class CatalogPage extends PageManager { + constructor(context) { + super(context); + + window.addEventListener('beforeunload', () => { + if (document.activeElement.id === 'sort') { + window.localStorage.setItem('sortByStatus', 'selected'); + } + }); + } + + arrangeFocusOnSortBy() { + const $sortBySelector = $('[data-sort-by="product"] #sort'); + + if (window.localStorage.getItem('sortByStatus')) { + $sortBySelector.focus(); + window.localStorage.removeItem('sortByStatus'); + } + } + onSortBySubmit(event, currentTarget) { const url = Url.parse(window.location.href, true); const queryParams = $(currentTarget).serialize().split('='); diff --git a/assets/js/theme/category.js b/assets/js/theme/category.js index 42a1df4a2b..345a0a3617 100644 --- a/assets/js/theme/category.js +++ b/assets/js/theme/category.js @@ -11,13 +11,29 @@ export default class Category extends CatalogPage { this.validationDictionary = createTranslationDictionary(context); } - onReady() { - $('[data-button-type="add-cart"]').on('click', (e) => { - $(e.currentTarget).next().attr({ - role: 'status', - 'aria-live': 'polite', - }); + setLiveRegionAttributes($element, roleType, ariaLiveStatus) { + $element.attr({ + role: roleType, + 'aria-live': ariaLiveStatus, }); + } + + makeShopByPriceFilterAccessible() { + if (!$('[data-shop-by-price]').length) return; + + if ($('.navList-action').hasClass('is-active')) { + $('a.navList-action.is-active').focus(); + } + + $('a.navList-action').on('click', () => this.setLiveRegionAttributes($('span.price-filter-message'), 'status', 'assertive')); + } + + onReady() { + this.arrangeFocusOnSortBy(); + + $('[data-button-type="add-cart"]').on('click', (e) => this.setLiveRegionAttributes($(e.currentTarget).next(), 'status', 'polite')); + + this.makeShopByPriceFilterAccessible(); compareProducts(this.context.urls); @@ -28,12 +44,7 @@ export default class Category extends CatalogPage { hooks.on('sortBy-submitted', this.onSortBySubmit); } - $('a.reset-btn').on('click', () => { - $('span.reset-message').attr({ - role: 'status', - 'aria-live': 'polite', - }); - }); + $('a.reset-btn').on('click', () => this.setLiveRegionsAttributes($('span.reset-message'), 'status', 'polite')); this.ariaNotifyNoProducts(); this.initCategoryButton(); diff --git a/assets/js/theme/common/carousel/index.js b/assets/js/theme/common/carousel/index.js index 2a6abe3c05..3b975e65db 100644 --- a/assets/js/theme/common/carousel/index.js +++ b/assets/js/theme/common/carousel/index.js @@ -9,11 +9,13 @@ import { getRealSlidesQuantityAndCurrentSlide, } from './utils'; -const onCarouselChange = (event, carousel) => { +export const onCarouselChange = (event, carousel) => { const { options: { prevArrow, nextArrow }, currentSlide, slideCount, + $prevArrow, + $nextArrow, $dots, $slider, breakpointSettings, @@ -28,13 +30,20 @@ const onCarouselChange = (event, carousel) => { $slider.data('slick').slidesToScroll, ); - const $prevArrow = $slider.find(prevArrow); - const $nextArrow = $slider.find(nextArrow); + const $prevArrowNode = $prevArrow || $slider.find(prevArrow); + const $nextArrowNode = $nextArrow || $slider.find(nextArrow); + + const dataArrowLabel = $slider.data('arrow-label'); + if (dataArrowLabel) { + $prevArrowNode.attr('aria-label', dataArrowLabel); + $nextArrowNode.attr('aria-label', dataArrowLabel); + $slider.data('arrow-label', false); + } dotsSetup($dots, actualSlide, actualSlideCount, $slider.data('dots-labels')); - setTabindexes($slider.find('.slick-slide'), $prevArrow, $nextArrow, actualSlide, actualSlideCount); - arrowAriaLabling($prevArrow, $nextArrow, actualSlide, actualSlideCount); - tooltipSetup($prevArrow, $nextArrow, $dots); + setTabindexes($slider.find('.slick-slide'), $prevArrowNode, $nextArrowNode, actualSlide, actualSlideCount); + arrowAriaLabling($prevArrowNode, $nextArrowNode, actualSlide, actualSlideCount); + tooltipSetup($prevArrowNode, $nextArrowNode, $dots); }; export default function () { @@ -46,11 +55,6 @@ export default function () { // getting element using find to pass jest test const $carousel = $(document).find(carousel); - if ($carousel.hasClass('productView-thumbnails')) { - $carousel.slick(); - return; - } - $carousel.on('init', onCarouselChange); $carousel.on('afterChange', onCarouselChange); diff --git a/assets/js/theme/common/faceted-search.js b/assets/js/theme/common/faceted-search.js index c4ef57307d..589850310a 100644 --- a/assets/js/theme/common/faceted-search.js +++ b/assets/js/theme/common/faceted-search.js @@ -2,11 +2,14 @@ import { hooks, api } from '@bigcommerce/stencil-utils'; import _ from 'lodash'; import Url from 'url'; import urlUtils from './utils/url-utils'; -import modalFactory from '../global/modal'; +import modalFactory, { modalTypes, ModalEvents } from '../global/modal'; import collapsibleFactory from './collapsible'; import { Validators } from './utils/form-utils'; import nod from './nod'; +const { SHOW_MORE_OPTIONS } = modalTypes; +const { opened } = ModalEvents; + const defaultOptions = { accordionToggleSelector: '#facetedSearch .accordion-navigation, #facetedSearch .facetedSearch-toggle', blockerSelector: '#facetedSearch .blocker', @@ -56,6 +59,15 @@ class FacetedSearch { this.collapsedFacets = []; this.collapsedFacetItems = []; + if (this.options.modal) { + this.options.modal.$modal.on(opened, event => { + const $filterItems = $(event.target).find('#facetedSearch-filterItems'); + if ($filterItems.length) { + this.options.modal.setupFocusableElements(SHOW_MORE_OPTIONS); + } + }); + } + // Init collapsibles collapsibleFactory(); diff --git a/assets/js/theme/common/product-details.js b/assets/js/theme/common/product-details.js index ecfcb92113..2f7774254e 100644 --- a/assets/js/theme/common/product-details.js +++ b/assets/js/theme/common/product-details.js @@ -17,11 +17,23 @@ export default class ProductDetails extends ProductDetailsBase { this.imageGallery = new ImageGallery($('[data-image-gallery]', this.$scope)); this.imageGallery.init(); this.listenQuantityChange(); + this.$swatchOptionMessage = $('.swatch-option-message'); + this.swatchOptionMessageInitText = this.$swatchOptionMessage.text(); const $form = $('form[data-cart-item-add]', $scope); const $productOptionsElement = $('[data-product-option-change]', $form); const hasOptions = $productOptionsElement.html().trim().length; const hasDefaultOptions = $productOptionsElement.find('[data-default]').length; + const $productSwatchGroup = $('[id*="attribute_swatch"]', $form); + + if (context.showSwatchNames) { + this.$swatchOptionMessage.removeClass('u-hidden'); + $productSwatchGroup.on('change', ({ target }) => this.showSwatchNameOnOption($(target))); + + $.each($productSwatchGroup, (_, element) => { + if ($(element).is(':checked')) this.showSwatchNameOnOption($(element)); + }); + } $('[data-product-attribute]').each((__, value) => { const type = value.getAttribute('data-product-attribute'); @@ -193,6 +205,25 @@ export default class ProductDetails extends ProductDetailsBase { }); } + /** + * if this setting is enabled in Page Builder + * show name for swatch option + */ + showSwatchNameOnOption($swatch) { + const swatchName = $swatch.attr('aria-label'); + + $('[data-product-attribute="swatch"] [data-option-value]').text(swatchName); + this.$swatchOptionMessage.text(`${this.swatchOptionMessageInitText} ${swatchName}`); + this.setLiveRegionAttributes(this.$swatchOptionMessage, 'status', 'assertive'); + } + + setLiveRegionAttributes($element, roleType, ariaLiveStatus) { + $element.attr({ + role: roleType, + 'aria-live': ariaLiveStatus, + }); + } + showProductImage(image) { if (isPlainObject(image)) { const zoomImageUrl = utils.tools.imageSrcset.getSrcset( @@ -344,10 +375,7 @@ export default class ProductDetails extends ProductDetailsBase { } }); - $addToCartBtn.next().attr({ - role: 'status', - 'aria-live': 'polite', - }); + this.setLiveRegionAttributes($addToCartBtn.next(), 'status', 'polite'); } /** @@ -407,6 +435,16 @@ export default class ProductDetails extends ProductDetailsBase { const $cartQuantity = $('[data-cart-quantity]', modal.$content); const $cartCounter = $('.navUser-action .cart-count'); const quantity = $cartQuantity.data('cartQuantity') || 0; + const $promotionBanner = $('[data-promotion-banner]'); + const $backToShopppingBtn = $('.previewCartCheckout > [data-reveal-close]'); + const $modalCloseBtn = $('#previewModal > .modal-close'); + const bannerUpdateHandler = () => { + const $productContainer = $('#main-content > .container'); + + $productContainer.append('
'); + $('.loadingOverlay.pdp-update', $productContainer).show(); + window.location.reload(); + }; $cartCounter.addClass('cart-count--positive'); $body.trigger('cart-quantity-update', quantity); @@ -414,6 +452,11 @@ export default class ProductDetails extends ProductDetailsBase { if (onComplete) { onComplete(response); } + + if ($promotionBanner.length && $backToShopppingBtn.length) { + $backToShopppingBtn.on('click', bannerUpdateHandler); + $modalCloseBtn.on('click', bannerUpdateHandler); + } }); } diff --git a/assets/js/theme/common/utils/form-utils.js b/assets/js/theme/common/utils/form-utils.js index 5000a5e4ef..f750b66431 100644 --- a/assets/js/theme/common/utils/form-utils.js +++ b/assets/js/theme/common/utils/form-utils.js @@ -124,6 +124,29 @@ function insertStateHiddenField($stateField) { $stateField.after($('', stateFieldAttrs)); } +/** + * Announce form input error message by screen reader + * @param {params.element} dom input element where checking is happened + * @param {params.result} result of validation check + */ +function announceInputErrorMessage({ element, result }) { + if (result) { + return; + } + 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'); + + if (errorMessage.length) { + const $errMessage = $(errorMessage[0]); + + if (!$errMessage.attr('role')) { + $errMessage.attr('role', 'alert'); + } + } +} + const Validators = { /** * Sets up a new validation when the form is dirty @@ -310,4 +333,4 @@ const Validators = { }, }; -export { Validators, insertStateHiddenField }; +export { Validators, insertStateHiddenField, announceInputErrorMessage }; diff --git a/assets/js/theme/contact-us.js b/assets/js/theme/contact-us.js index c32d4c1b0f..1235b7000b 100644 --- a/assets/js/theme/contact-us.js +++ b/assets/js/theme/contact-us.js @@ -1,6 +1,7 @@ import PageManager from './page-manager'; import nod from './common/nod'; import forms from './common/models/forms'; +import { announceInputErrorMessage } from './common/utils/form-utils'; export default class ContactUs extends PageManager { onReady() { @@ -11,6 +12,7 @@ export default class ContactUs extends PageManager { const formSelector = 'form[data-contact-form]'; const contactUsValidator = nod({ submit: `${formSelector} input[type="submit"]`, + tap: announceInputErrorMessage, }); const $contactForm = $(formSelector); diff --git a/assets/js/theme/gift-certificate.js b/assets/js/theme/gift-certificate.js index e9dd44c492..f4cba12f97 100644 --- a/assets/js/theme/gift-certificate.js +++ b/assets/js/theme/gift-certificate.js @@ -3,6 +3,7 @@ import nod from './common/nod'; import giftCertChecker from './common/gift-certificate-validator'; import formModel from './common/models/forms'; import { createTranslationDictionary } from './common/utils/translations-utils'; +import { announceInputErrorMessage } from './common/utils/form-utils'; import { api } from '@bigcommerce/stencil-utils'; import { defaultModal } from './global/modal'; @@ -48,6 +49,7 @@ export default class GiftCertificate extends PageManager { const purchaseValidator = nod({ submit: '#gift-certificate-form input[type="submit"]', delay: 300, + tap: announceInputErrorMessage, }); if ($customAmounts.length) { @@ -196,6 +198,7 @@ export default class GiftCertificate extends PageManager { checkCertBalanceValidator($balanceForm) { const balanceValidator = nod({ submit: $balanceForm.find('input[type="submit"]'), + tap: announceInputErrorMessage, }); balanceValidator.add({ diff --git a/assets/js/theme/global/modal.js b/assets/js/theme/global/modal.js index 14f4fc0014..067be798ef 100644 --- a/assets/js/theme/global/modal.js +++ b/assets/js/theme/global/modal.js @@ -22,24 +22,26 @@ export const modalTypes = { PRODUCT_DETAILS: 'forProductDetails', CART_CHANGE_PRODUCT: 'forCartChangeProduct', WRITE_REVIEW: 'forWriteReview', + SHOW_MORE_OPTIONS: 'forShowMore', }; +const findRootModalTabbableElements = () => ( + $('#modal.open') + .find(allTabbableElementsSelector) + .not('#modal-review-form *') + .not('#previewModal *') +); + const focusableElements = { - [modalTypes.QUICK_VIEW]: () => ( - $('#modal') - .find(allTabbableElementsSelector) - .not('#modal-review-form *') - .not('#previewModal *') - ), + [modalTypes.QUICK_VIEW]: findRootModalTabbableElements, [modalTypes.PRODUCT_DETAILS]: () => ( - $('#previewModal').find(allTabbableElementsSelector) - ), - [modalTypes.CART_CHANGE_PRODUCT]: () => ( - $('#modal').find(allTabbableElementsSelector) + $('#previewModal.open').find(allTabbableElementsSelector) ), + [modalTypes.CART_CHANGE_PRODUCT]: findRootModalTabbableElements, [modalTypes.WRITE_REVIEW]: () => ( - $('#modal-review-form').find(allTabbableElementsSelector) + $('#modal-review-form.open').find(allTabbableElementsSelector) ), + [modalTypes.SHOW_MORE_OPTIONS]: findRootModalTabbableElements, }; export const ModalEvents = { @@ -78,7 +80,12 @@ function wrapModalBody(content) { } function restrainContentHeight($content) { + if ($content.length === 0) return; + const $body = $(`.${modalBodyClass}`, $content); + + if ($body.length === 0) return; + const bodyHeight = $body.outerHeight(); const contentHeight = $content.outerHeight(); const viewportHeight = getViewportHeight(0.9); @@ -180,13 +187,6 @@ export class Modal { this.$modal.on(ModalEvents.opened, this.onModalOpened); } - unbindEvents() { - this.$modal.off(ModalEvents.close, this.onModalClose); - this.$modal.off(ModalEvents.closed, this.onModalClosed); - this.$modal.off(ModalEvents.open, this.onModalOpen); - this.$modal.off(ModalEvents.opened, this.onModalOpened); - } - open({ size, pending = true, @@ -264,7 +264,7 @@ export class Modal { } else if ($element.is($lastTabbable)) { $element.addClass(lastTabbableClass).removeClass(firstTabbableClass); } else { - $element.removeClass(firstTabbableClass, lastTabbableClass); + $element.removeClass(firstTabbableClass).removeClass(lastTabbableClass); } }); @@ -275,13 +275,13 @@ export class Modal { if (direction === 'forwards') { const isLastActive = $activeElement.hasClass(lastTabbableClass); if (isLastActive) { - $modalTabbableCollection.get(0).focus(); + $firstTabbable.focus(); event.preventDefault(); } } else if (direction === 'backwards') { const isFirstActive = $activeElement.hasClass(firstTabbableClass); if (isFirstActive) { - $modalTabbableCollection.get(lastCollectionIdx).focus(); + $lastTabbable.focus(); event.preventDefault(); } } @@ -294,8 +294,8 @@ export class Modal { onModalClosed() { this.size = this.defaultSize; if (this.$preModalFocusedEl) this.$preModalFocusedEl.focus(); + this.$preModalFocusedEl = null; this.$modal.off('keydown'); - this.unbindEvents(); } onModalOpen() { diff --git a/assets/js/theme/global/quick-view.js b/assets/js/theme/global/quick-view.js index 35bb20f689..dc671bc087 100644 --- a/assets/js/theme/global/quick-view.js +++ b/assets/js/theme/global/quick-view.js @@ -4,6 +4,7 @@ import utils from '@bigcommerce/stencil-utils'; import ProductDetails from '../common/product-details'; import { defaultModal, modalTypes } from './modal'; import 'slick-carousel'; +import { onCarouselChange } from '../common/carousel'; export default function (context) { const modal = defaultModal(); @@ -20,7 +21,14 @@ export default function (context) { modal.$content.find('.productView').addClass('productView--quickView'); - modal.$content.find('[data-slick]').slick(); + const $carousel = modal.$content.find('[data-slick]'); + + if ($carousel.length) { + $carousel.on('init', onCarouselChange); + $carousel.on('afterChange', onCarouselChange); + + $carousel.slick(); + } modal.setupFocusableElements(modalTypes.QUICK_VIEW); diff --git a/assets/js/theme/product/reviews.js b/assets/js/theme/product/reviews.js index cabfc05ebf..d6aba7c5e3 100644 --- a/assets/js/theme/product/reviews.js +++ b/assets/js/theme/product/reviews.js @@ -2,11 +2,13 @@ import nod from '../common/nod'; import { CollapsibleEvents } from '../common/collapsible'; import forms from '../common/models/forms'; import { safeString } from '../common/utils/safe-string'; +import { announceInputErrorMessage } from '../common/utils/form-utils'; export default class { constructor($reviewForm) { this.validator = nod({ submit: $reviewForm.find('input[type="submit"]'), + tap: announceInputErrorMessage, }); this.$reviewsContent = $('#product-reviews'); diff --git a/assets/js/theme/search.js b/assets/js/theme/search.js index d9ddaadcbf..f022231cdb 100644 --- a/assets/js/theme/search.js +++ b/assets/js/theme/search.js @@ -1,6 +1,7 @@ import { hooks } from '@bigcommerce/stencil-utils'; import CatalogPage from './catalog'; import FacetedSearch from './common/faceted-search'; +import { announceInputErrorMessage } from './common/utils/form-utils'; import compareProducts from './global/compare-products'; import urlUtils from './common/utils/url-utils'; import Url from 'url'; @@ -132,37 +133,9 @@ export default class Search extends CatalogPage { $($tabsCollection.get(nextTabIdx)).focus().trigger('click'); } - getUrlParameter(queryParam) { - const regex = new RegExp(`[\\?&]${queryParam}=([^]*)`); - const results = regex.exec(window.location.search); - return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' ')); - } - - setupSortByQuerySearchParam() { - const searchQuery = this.getUrlParameter('search_query'); - - if (searchQuery.length === 0) return; - - const $baseInput = $('').attr('type', 'hidden'); - - $('[data-sort-by]').each((idx, form) => { - const $form = $(form); - $form.append( - $baseInput.clone().attr({ - name: 'search_query', - value: searchQuery, - }), - $baseInput.clone().attr({ - name: 'section', - value: $form.data('sort-by'), - }), - ); - }); - } - onReady() { compareProducts(this.context.urls); - this.setupSortByQuerySearchParam(); + this.arrangeFocusOnSortBy(); const $searchForm = $('[data-advanced-search-form]'); const $categoryTreeContainer = $searchForm.find('[data-search-category-tree]'); @@ -346,6 +319,7 @@ export default class Search extends CatalogPage { this.$form = $form; this.validator = nod({ submit: $form, + tap: announceInputErrorMessage, }); return this; diff --git a/assets/js/theme/wishlist.js b/assets/js/theme/wishlist.js index a32ab012e4..c41feacff2 100644 --- a/assets/js/theme/wishlist.js +++ b/assets/js/theme/wishlist.js @@ -3,6 +3,7 @@ import 'foundation-sites/js/foundation/foundation.reveal'; import nod from './common/nod'; import PageManager from './page-manager'; import { wishlistPaginatorHelper } from './common/utils/pagination-utils'; +import { announceInputErrorMessage } from './common/utils/form-utils'; export default class WishList extends PageManager { constructor(context) { @@ -33,6 +34,7 @@ export default class WishList extends PageManager { registerAddWishListValidation($addWishlistForm) { this.addWishlistValidator = nod({ submit: '.wishlist-form input[type="submit"]', + tap: announceInputErrorMessage, }); this.addWishlistValidator.add([ diff --git a/assets/scss/components/stencil/cart/_cart.scss b/assets/scss/components/stencil/cart/_cart.scss index 83fcacedad..a119cf829b 100644 --- a/assets/scss/components/stencil/cart/_cart.scss +++ b/assets/scss/components/stencil/cart/_cart.scss @@ -608,6 +608,16 @@ $card-preview-zoom-bottom-offset: 6rem; } @include lazy-loaded-padding('productthumb_size'); + + &:after { + @include breakpoint("xxsmall") { + padding-bottom: 75%; + } + + @include breakpoint("xsmall") { + padding-bottom: 100%; + } + } } .previewCartItem-content { @@ -640,7 +650,14 @@ $card-preview-zoom-bottom-offset: 6rem; @include grid-row; border-top: container("border"); display: block; - padding: spacing("single") spacing("half"); + + @include breakpoint("xxsmall") { + padding: spacing("half") spacing("quarter"); + } + + @include breakpoint("xsmall") { + padding: spacing("single") spacing("half"); + } .button { margin: 0; diff --git a/assets/scss/components/stencil/heroCarousel/_heroCarousel.scss b/assets/scss/components/stencil/heroCarousel/_heroCarousel.scss index 9c5dafd474..c4461ac9e6 100644 --- a/assets/scss/components/stencil/heroCarousel/_heroCarousel.scss +++ b/assets/scss/components/stencil/heroCarousel/_heroCarousel.scss @@ -249,8 +249,8 @@ left: 15px; bottom: spacing("third"); height: 32px; - min-width: 80px; - max-width: 90px; + min-width: 60px; + max-width: 60px; font-size: 14px; line-height: 1.25; font-weight: 700; @@ -263,6 +263,11 @@ border: 1px solid $slick-play-pause-button-borderColor; @include carouselOpaqueBackgrounds($slick-play-pause-button-bgColor); + @media (min-width: 375px) { + min-width: 80px; + max-width: 90px; + } + @include breakpoint("small") { max-width: 150px; font-size: 18px; diff --git a/assets/scss/components/stencil/productCarousel/_productCarousel.scss b/assets/scss/components/stencil/productCarousel/_productCarousel.scss index b34989f4c5..9b40a59938 100644 --- a/assets/scss/components/stencil/productCarousel/_productCarousel.scss +++ b/assets/scss/components/stencil/productCarousel/_productCarousel.scss @@ -11,6 +11,7 @@ .productCarousel { // 1 width: 1px; min-width: 100%; + position: relative; @include grid-row( $behavior: nest ); diff --git a/config.json b/config.json index d03b2b96c0..07d9534837 100644 --- a/config.json +++ b/config.json @@ -1,6 +1,6 @@ { "name": "Midwest Nice", - "version": "5.1.0", + "version": "5.2.0", "template_engine": "handlebars_v4", "meta": { "price": 0, @@ -95,6 +95,7 @@ "show_custom_fields_tabs": false, "show_product_weight": true, "show_product_dimensions": false, + "show_product_swatch_names": true, "floating_cart_button": false, "product_list_display_mode": "grid", "logo-position": "center", diff --git a/lang/en.json b/lang/en.json index 10417fb063..cadce6cb47 100755 --- a/lang/en.json +++ b/lang/en.json @@ -220,9 +220,11 @@ "label": "Categories", "shop_by_price": "Shop By Price", "shop_by_price_range_aria": "Price range from {from} to {to}", + "filter_price_range": "Price range:", "add_cart_announcement": "The item has been added", "reset": "Reset", "filter_reset_announcement": "The filter has been reset", + "filter_select_announcement": "The filter has been applied", "view_all": { "name": "All {category}" } @@ -720,6 +722,7 @@ "5": "5 stars (best)" }, "write_a_review": "Write a Review", + "no_reviews": "No Reviews", "form_write": { "name": "Name", "email": "Email", @@ -741,6 +744,7 @@ "upc": "UPC:", "condition": "Condition:", "availability": "Availability:", + "swatch_option_announcement": "Selected {swatch_name} is", "shipping": "Shipping:", "shipping_fixed": "{amount} (Fixed Shipping Cost)", "shipping_free": "Free Shipping", @@ -783,7 +787,8 @@ "percent": "and get {discount} off", "price": "and get {discount} off", "fixed": "and pay only {discount} each" - } + }, + "card_default_image_alt": "Image coming soon" }, "invoice": { "for_order": "{name} Invoice for Order #{id}", diff --git a/package-lock.json b/package-lock.json index bafdd2504a..d7ab07c8e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "bigcommerce-cornerstone", - "version": "5.1.0", + "version": "5.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "bigcommerce-cornerstone", - "version": "5.1.0", + "version": "5.2.0", "license": "MIT", "dependencies": { - "@bigcommerce/stencil-utils": "^6.8.0", - "core-js": "^3.6.5", + "@bigcommerce/stencil-utils": "^6.8.1", + "core-js": "^3.9.0", "creditcards": "^3.0.1", "easyzoom": "^2.5.3", "focus-within-polyfill": "^5.1.0", @@ -20,7 +20,7 @@ "jquery.tabbable": "^1.0.1", "jstree": "github:vakata/jstree", "lazysizes": "5.2.2", - "lodash": "^4.17.20", + "lodash": "^4.17.21", "nanobar": "^0.4.2", "nod-validate": "^2.0.12", "object-fit-images": "^3.2.4", @@ -28,23 +28,23 @@ "slick-carousel": "^1.8.1", "svg-injector": "^1.1.3", "sweetalert2": "^9.17.2", - "whatwg-fetch": "^3.4.1" + "whatwg-fetch": "^3.6.1" }, "devDependencies": { - "@babel/core": "^7.12.3", + "@babel/core": "^7.12.17", "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/preset-env": "^7.12.1", + "@babel/preset-env": "^7.12.17", "@bigcommerce/citadel": "^2.15.1", "babel-eslint": "^10.1.0", "babel-jest": "^25.5.1", - "babel-loader": "^8.1.0", + "babel-loader": "^8.2.2", "babel-plugin-lodash": "^3.3.4", "clean-webpack-plugin": "^0.1.19", "eslint": "^4.8.0", "eslint-config-airbnb": "^16.0.0", "eslint-plugin-import": "^2.22.1", "eslint-plugin-jsx-a11y": "^6.4.1", - "eslint-plugin-react": "^7.21.5", + "eslint-plugin-react": "^7.22.0", "expose-loader": "^0.7.5", "grunt": "^1.3.0", "grunt-cli": "^1.3.2", @@ -54,14 +54,14 @@ "grunt-svgstore": "^2.0.0", "imports-loader": "^0.7.1", "jest": "^25.5.4", - "lighthouse": "^6.4.1", + "lighthouse": "^6.5.0", "load-grunt-config": "^3.0.1", - "lodash-webpack-plugin": "^0.11.2", + "lodash-webpack-plugin": "^0.11.6", "npx": "^10.2.2", "time-grunt": "^1.2.2", - "webpack": "^4.44.2", + "webpack": "^4.46.0", "webpack-bundle-analyzer": "^3.9.0", - "webpack-cli": "^4.2.0", + "webpack-cli": "^4.5.0", "webpack-merge": "~4.1.2" } }, diff --git a/package.json b/package.json index cb51b6e9cb..ed62f20050 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "bigcommerce-cornerstone", "description": "The BigCommerce reference theme for the Stencil platform", - "version": "5.1.0", + "version": "5.2.0", "private": true, "author": "BigCommerce", "license": "MIT", "dependencies": { - "@bigcommerce/stencil-utils": "^6.8.0", - "core-js": "^3.6.5", + "@bigcommerce/stencil-utils": "^6.8.1", + "core-js": "^3.9.0", "creditcards": "^3.0.1", "easyzoom": "^2.5.3", "focus-within-polyfill": "^5.1.0", @@ -17,7 +17,7 @@ "jquery.tabbable": "^1.0.1", "jstree": "github:vakata/jstree", "lazysizes": "5.2.2", - "lodash": "^4.17.20", + "lodash": "^4.17.21", "nanobar": "^0.4.2", "nod-validate": "^2.0.12", "object-fit-images": "^3.2.4", @@ -25,23 +25,23 @@ "slick-carousel": "^1.8.1", "svg-injector": "^1.1.3", "sweetalert2": "^9.17.2", - "whatwg-fetch": "^3.4.1" + "whatwg-fetch": "^3.6.1" }, "devDependencies": { - "@babel/core": "^7.12.3", + "@babel/core": "^7.12.17", "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/preset-env": "^7.12.1", + "@babel/preset-env": "^7.12.17", "@bigcommerce/citadel": "^2.15.1", "babel-eslint": "^10.1.0", "babel-jest": "^25.5.1", - "babel-loader": "^8.1.0", + "babel-loader": "^8.2.2", "babel-plugin-lodash": "^3.3.4", "clean-webpack-plugin": "^0.1.19", "eslint": "^4.8.0", "eslint-config-airbnb": "^16.0.0", "eslint-plugin-import": "^2.22.1", "eslint-plugin-jsx-a11y": "^6.4.1", - "eslint-plugin-react": "^7.21.5", + "eslint-plugin-react": "^7.22.0", "expose-loader": "^0.7.5", "grunt": "^1.3.0", "grunt-cli": "^1.3.2", @@ -51,14 +51,14 @@ "grunt-svgstore": "^2.0.0", "imports-loader": "^0.7.1", "jest": "^25.5.4", - "lighthouse": "^6.4.1", + "lighthouse": "^6.5.0", "load-grunt-config": "^3.0.1", - "lodash-webpack-plugin": "^0.11.2", + "lodash-webpack-plugin": "^0.11.6", "npx": "^10.2.2", "time-grunt": "^1.2.2", - "webpack": "^4.44.2", + "webpack": "^4.46.0", "webpack-bundle-analyzer": "^3.9.0", - "webpack-cli": "^4.2.0", + "webpack-cli": "^4.5.0", "webpack-merge": "~4.1.2" }, "scripts": { diff --git a/schema.json b/schema.json index d45c28d869..427cd99fd9 100644 --- a/schema.json +++ b/schema.json @@ -1504,6 +1504,12 @@ "force_reload": true, "id": "show_product_dimensions" }, + { + "type": "checkbox", + "label": "i18n.ShowProductSwatchNames", + "force_reload": true, + "id": "show_product_swatch_names" + }, { "type": "checkbox", "label": "i18n.ShowShopByPriceIn", diff --git a/schemaTranslations.json b/schemaTranslations.json index b995c6da9a..eda897e4bf 100644 --- a/schemaTranslations.json +++ b/schemaTranslations.json @@ -1216,6 +1216,13 @@ "uk": "Показати розміри продукту", "zh": "显示产品尺寸" }, + "i18n.ShowProductSwatchNames": { + "default": "Show product swatch names", + "fr": "Show product swatch names", + "it": "Show product swatch names", + "uk": "Показати назви зразків продукту", + "zh": "Show product swatch names" + }, "i18n.ShowShopByPriceIn": { "default": "Show \"Shop by Price\" in filters", "fr": "Afficher \"Acheter par prix\" dans les filtres", diff --git a/templates/components/amp/products/options/input-checkbox.html b/templates/components/amp/products/options/input-checkbox.html index e4d4ee6a87..e14c3aab19 100644 --- a/templates/components/amp/products/options/input-checkbox.html +++ b/templates/components/amp/products/options/input-checkbox.html @@ -6,13 +6,15 @@ {{lang 'common.required'}} {{/if}} - - +