diff --git a/CHANGELOG.md b/CHANGELOG.md index 5268f4e641..aa3986802f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## Draft +- Fixed cut off on Cart button when Zooming up to 400%. [#1998](https://github.com/bigcommerce/cornerstone/pull/1998) ## 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) diff --git a/assets/js/theme/cart.js b/assets/js/theme/cart.js index ca438890f6..01268687e9 100644 --- a/assets/js/theme/cart.js +++ b/assets/js/theme/cart.js @@ -3,7 +3,7 @@ import { bind, debounce } from 'lodash'; import giftCertCheck from './common/gift-certificate-validator'; import utils from '@bigcommerce/stencil-utils'; import ShippingEstimator from './cart/shipping-estimator'; -import { defaultModal, modalTypes } from './global/modal'; +import { defaultModal } from './global/modal'; import swal from './global/sweet-alert'; import CartItemDetails from './common/cart-item-details'; @@ -157,7 +157,7 @@ export default class Cart extends PageManager { this.bindGiftWrappingForm(); - modal.setupFocusableElements(modalTypes.CART_CHANGE_PRODUCT); + modal.setupFocusTrap(); }); utils.hooks.on('product-option-change', (event, currentTarget) => { diff --git a/assets/js/theme/common/faceted-search.js b/assets/js/theme/common/faceted-search.js index 589850310a..78d2b18b4c 100644 --- a/assets/js/theme/common/faceted-search.js +++ b/assets/js/theme/common/faceted-search.js @@ -2,13 +2,11 @@ import { hooks, api } from '@bigcommerce/stencil-utils'; import _ from 'lodash'; import Url from 'url'; import urlUtils from './utils/url-utils'; -import modalFactory, { modalTypes, ModalEvents } from '../global/modal'; +import modalFactory, { 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', @@ -60,10 +58,10 @@ class FacetedSearch { this.collapsedFacetItems = []; if (this.options.modal) { - this.options.modal.$modal.on(opened, event => { + this.options.modal.$modal.on(ModalEvents.opened, event => { const $filterItems = $(event.target).find('#facetedSearch-filterItems'); if ($filterItems.length) { - this.options.modal.setupFocusableElements(SHOW_MORE_OPTIONS); + this.options.modal.setupFocusTrap(); } }); } diff --git a/assets/js/theme/common/product-details.js b/assets/js/theme/common/product-details.js index 7708e88fad..e9f3174fec 100644 --- a/assets/js/theme/common/product-details.js +++ b/assets/js/theme/common/product-details.js @@ -3,7 +3,7 @@ import ProductDetailsBase, { optionChangeDecorator } from './product-details-bas import 'foundation-sites/js/foundation/foundation'; import 'foundation-sites/js/foundation/foundation.reveal'; import ImageGallery from '../product/image-gallery'; -import modalFactory, { showAlertModal, modalTypes } from '../global/modal'; +import modalFactory, { showAlertModal } from '../global/modal'; import { isEmpty, isPlainObject } from 'lodash'; import { normalizeFormData } from './utils/api'; import { isBrowserIE, convertIntoArray } from './utils/ie-helpers'; @@ -361,7 +361,8 @@ export default class ProductDetails extends ProductDetailsBase { if (this.previewModal) { this.previewModal.open(); - this.updateCartContent(this.previewModal, response.data.cart_item.id, () => this.previewModal.setupFocusableElements(modalTypes.PRODUCT_DETAILS)); + if ($addToCartBtn.parents('.quickView').length === 0) this.previewModal.$preModalFocusedEl = $addToCartBtn; + this.updateCartContent(this.previewModal, response.data.cart_item.id, () => this.previewModal.setupFocusTrap()); } else { this.$overlay.show(); // if no modal, redirect to the cart page diff --git a/assets/js/theme/global/modal.js b/assets/js/theme/global/modal.js index 58f9b70de3..ab3d8ec40f 100644 --- a/assets/js/theme/global/modal.js +++ b/assets/js/theme/global/modal.js @@ -1,49 +1,17 @@ -import 'jquery.tabbable'; import foundation from './foundation'; +import * as focusTrap from 'focus-trap'; const bodyActiveClass = 'has-activeModal'; const loadingOverlayClass = 'loadingOverlay'; const modalBodyClass = 'modal-body'; const modalContentClass = 'modal-content'; -const allTabbableElementsSelector = ':tabbable'; -const tabKeyCode = 9; -const firstTabbableClass = 'first-tabbable'; -const lastTabbableClass = 'last-tabbable'; - const SizeClasses = { small: 'modal--small', large: 'modal--large', normal: '', }; -export const modalTypes = { - QUICK_VIEW: 'forQuickView', - 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]: findRootModalTabbableElements, - [modalTypes.PRODUCT_DETAILS]: () => ( - $('#previewModal.open').find(allTabbableElementsSelector) - ), - [modalTypes.CART_CHANGE_PRODUCT]: findRootModalTabbableElements, - [modalTypes.WRITE_REVIEW]: () => ( - $('#modal-review-form.open').find(allTabbableElementsSelector) - ), - [modalTypes.SHOW_MORE_OPTIONS]: findRootModalTabbableElements, -}; - export const ModalEvents = { close: 'close.fndtn.reveal', closed: 'closed.fndtn.reveal', @@ -139,6 +107,7 @@ export class Modal { this.size = this.defaultSize; this.pending = false; this.$preModalFocusedEl = null; + this.focusTrap = null; this.onModalOpen = this.onModalOpen.bind(this); this.onModalOpened = this.onModalOpened.bind(this); @@ -228,64 +197,18 @@ export class Modal { this.$content.html(''); } - setupFocusableElements(modalType) { - this.$preModalFocusedEl = $(document.activeElement); - const $modalTabbableCollection = focusableElements[modalType](); - - const elementToFocus = $modalTabbableCollection.get(0); - if (elementToFocus) elementToFocus.focus(); - - this.$modal.on('keydown', event => this.onTabbing(event, modalType)); - } - - onTabbing(event, modalType) { - const isTab = event.which === tabKeyCode; - - if (!isTab) return; - - const $modalTabbableCollection = focusableElements[modalType](); - const modalTabbableCollectionLength = $modalTabbableCollection.length; + setupFocusTrap() { + if (!this.$preModalFocusedEl) this.$preModalFocusedEl = $(document.activeElement); - if (modalTabbableCollectionLength < 1) return; - - const lastCollectionIdx = modalTabbableCollectionLength - 1; - const $firstTabbable = $modalTabbableCollection.get(0); - const $lastTabbable = $modalTabbableCollection.get(lastCollectionIdx); - - $modalTabbableCollection.each((index, element) => { - const $element = $(element); - - if (modalTabbableCollectionLength === 1) { - $element.addClass(`${firstTabbableClass} ${lastTabbableClass}`); - return false; - } - - if ($element.is($firstTabbable)) { - $element.addClass(firstTabbableClass).removeClass(lastTabbableClass); - } else if ($element.is($lastTabbable)) { - $element.addClass(lastTabbableClass).removeClass(firstTabbableClass); - } else { - $element.removeClass(firstTabbableClass).removeClass(lastTabbableClass); - } - }); - - const direction = (isTab && event.shiftKey) ? 'backwards' : 'forwards'; - - const $activeElement = $(document.activeElement); - - if (direction === 'forwards') { - const isLastActive = $activeElement.hasClass(lastTabbableClass); - if (isLastActive) { - $firstTabbable.focus(); - event.preventDefault(); - } - } else if (direction === 'backwards') { - const isFirstActive = $activeElement.hasClass(firstTabbableClass); - if (isFirstActive) { - $lastTabbable.focus(); - event.preventDefault(); - } + if (!this.focusTrap) { + this.focusTrap = focusTrap.createFocusTrap(this.$modal[0], { + escapeDeactivates: false, + returnFocusOnDeactivate: false, + }); } + + this.focusTrap.deactivate(); + this.focusTrap.activate(); } onModalClose() { @@ -294,9 +217,12 @@ export class Modal { onModalClosed() { this.size = this.defaultSize; + + if(this.focusTrap) this.focusTrap.deactivate(); + if (this.$preModalFocusedEl) this.$preModalFocusedEl.focus(); + this.$preModalFocusedEl = null; - this.$modal.off('keydown'); } onModalOpen() { diff --git a/assets/js/theme/global/quick-view.js b/assets/js/theme/global/quick-view.js index dc671bc087..196866b8cc 100644 --- a/assets/js/theme/global/quick-view.js +++ b/assets/js/theme/global/quick-view.js @@ -2,7 +2,7 @@ import 'foundation-sites/js/foundation/foundation'; import 'foundation-sites/js/foundation/foundation.dropdown'; import utils from '@bigcommerce/stencil-utils'; import ProductDetails from '../common/product-details'; -import { defaultModal, modalTypes } from './modal'; +import { defaultModal } from './modal'; import 'slick-carousel'; import { onCarouselChange } from '../common/carousel'; @@ -30,7 +30,7 @@ export default function (context) { $carousel.slick(); } - modal.setupFocusableElements(modalTypes.QUICK_VIEW); + modal.setupFocusTrap(); return new ProductDetails(modal.$content.find('.quickView'), context); }); diff --git a/assets/js/theme/product.js b/assets/js/theme/product.js index 900dfd8d9a..4a1df68d1d 100644 --- a/assets/js/theme/product.js +++ b/assets/js/theme/product.js @@ -7,9 +7,7 @@ import collapsibleFactory from './common/collapsible'; import ProductDetails from './common/product-details'; import videoGallery from './product/video-gallery'; import { classifyForm } from './common/utils/form-utils'; -import modalFactory, { modalTypes } from './global/modal'; - -const { WRITE_REVIEW } = modalTypes; +import modalFactory, { ModalEvents } from './global/modal'; export default class Product extends PageManager { constructor(context) { @@ -46,7 +44,7 @@ export default class Product extends PageManager { const review = new Review($reviewForm); - $(document).on('opened.fndtn.reveal', '#modal-review-form', () => this.reviewModal.setupFocusableElements(WRITE_REVIEW)); + $(document).on(ModalEvents.opened, '#modal-review-form', () => this.reviewModal.setupFocusTrap()); $('body').on('click', '[data-reveal-id="modal-review-form"]', () => { validator = review.registerValidation(this.context); diff --git a/package.json b/package.json index ae3eb5db82..5330dfde7e 100644 --- a/package.json +++ b/package.json @@ -10,11 +10,11 @@ "core-js": "^3.9.0", "creditcards": "^3.0.1", "easyzoom": "^2.5.3", + "focus-trap": "^6.3.0", "focus-within-polyfill": "^5.1.0", "formdata-polyfill": "^3.0.20", "foundation-sites": "^5.5.3", "jquery": "^3.5.1", - "jquery.tabbable": "^1.0.1", "jstree": "github:vakata/jstree", "lazysizes": "5.2.2", "lodash": "^4.17.21",