Skip to content

Commit

Permalink
fix(storefront): BCTHEME-409 Update focus trap in Modal
Browse files Browse the repository at this point in the history
  • Loading branch information
yurytut1993 committed Feb 28, 2021
1 parent b66bd69 commit e937756
Show file tree
Hide file tree
Showing 9 changed files with 3,056 additions and 6,433 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
@@ -1,6 +1,7 @@
# Changelog

## Draft
- Update focus trap in Modal. [#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)
Expand Down
4 changes: 2 additions & 2 deletions assets/js/theme/cart.js
Expand Up @@ -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';

Expand Down Expand Up @@ -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) => {
Expand Down
8 changes: 3 additions & 5 deletions assets/js/theme/common/faceted-search.js
Expand Up @@ -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',
Expand Down Expand Up @@ -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();
}
});
}
Expand Down
5 changes: 3 additions & 2 deletions assets/js/theme/common/product-details.js
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down
106 changes: 16 additions & 90 deletions 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',
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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() {
Expand All @@ -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() {
Expand Down
4 changes: 2 additions & 2 deletions assets/js/theme/global/quick-view.js
Expand Up @@ -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';

Expand Down Expand Up @@ -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);
});
Expand Down
6 changes: 2 additions & 4 deletions assets/js/theme/product.js
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit e937756

Please sign in to comment.