diff --git a/app/javascript/app/components/focus-trap-proxy.js b/app/javascript/app/components/focus-trap-proxy.js deleted file mode 100644 index 8c65257ec47..00000000000 --- a/app/javascript/app/components/focus-trap-proxy.js +++ /dev/null @@ -1,51 +0,0 @@ -import createFocusTrap from 'focus-trap'; - -function FocusTrapProxy() { - const focusables = []; - let activated = []; - - return function makeTrap(el, options = {}) { - const ownTrap = createFocusTrap(el, options); - - focusables.push(ownTrap); - - return { - activate() { - focusables.forEach((trap) => trap.deactivate()); - - activated.push(ownTrap); - - ownTrap.activate(); - - return ownTrap; - }, - - deactivate(opts = {}) { - const deactivatedTrap = ownTrap.deactivate(opts); - - // `deactivate` will return a valid trap object if it is available to be - // deactivated. If not, it returns a falsey value. If nothing was deactivated, - // bail out. - if (!deactivatedTrap) { - return false; - } - - activated = activated.filter((activatedTrap) => activatedTrap !== ownTrap); - - if (activated.length) { - activated[activated.length - 1].activate(); - } - - return deactivatedTrap; - }, - - pause() { - ownTrap.pause(); - }, - }; - }; -} - -const focusTrapProxy = FocusTrapProxy.call(FocusTrapProxy); - -export default focusTrapProxy; diff --git a/app/javascript/app/components/index.js b/app/javascript/app/components/index.js index 69c77cfcefb..c4a4bc5facb 100644 --- a/app/javascript/app/components/index.js +++ b/app/javascript/app/components/index.js @@ -1,10 +1,4 @@ -import focusTrapProxy from './focus-trap-proxy'; -import modal from './modal'; +import Modal from './modal'; window.LoginGov = window.LoginGov || {}; -const { LoginGov } = window; -const trapModal = modal(focusTrapProxy); - -LoginGov.Modal = trapModal; - -export { trapModal as Modal }; +window.LoginGov.Modal = Modal; diff --git a/app/javascript/app/components/modal.js b/app/javascript/app/components/modal.js index 6a335cd6ba9..2676f67bb52 100644 --- a/app/javascript/app/components/modal.js +++ b/app/javascript/app/components/modal.js @@ -1,4 +1,5 @@ import 'classlist.js'; +import { createFocusTrap } from 'focus-trap'; import Events from '../utils/events'; const STATES = { @@ -6,43 +7,41 @@ const STATES = { SHOW: 'show', }; -function modal(focusTrap) { - return class extends Events { - constructor(options) { - super(); +class Modal extends Events { + constructor(options) { + super(); - this.el = document.querySelector(options.el); - this.shown = false; - this.trap = focusTrap(this.el, { escapeDeactivates: false }); - } - - toggle() { - if (this.shown) { - this.hide(); - } else { - this.show(); - } - } - - show(target) { - this.setElementVisibility(target, true); - this.emit(STATES.SHOW); - } - - hide(target) { - this.setElementVisibility(target, false); - this.emit(STATES.HIDE); - } - - setElementVisibility(target = null, showing) { - const el = target || this.el; + this.el = document.querySelector(options.el); + this.shown = false; + this.trap = createFocusTrap(this.el, { escapeDeactivates: false }); + } - this.shown = showing; - el.classList[showing ? 'remove' : 'add']('display-none'); - document.body.classList[showing ? 'add' : 'remove']('modal-open'); - this.trap[showing ? 'activate' : 'deactivate'](); + toggle() { + if (this.shown) { + this.hide(); + } else { + this.show(); } - }; + } + + show(target) { + this.setElementVisibility(target, true); + this.emit(STATES.SHOW); + } + + hide(target) { + this.setElementVisibility(target, false); + this.emit(STATES.HIDE); + } + + setElementVisibility(target = null, showing) { + const el = target || this.el; + + this.shown = showing; + el.classList[showing ? 'remove' : 'add']('display-none'); + document.body.classList[showing ? 'add' : 'remove']('modal-open'); + this.trap[showing ? 'activate' : 'deactivate'](); + } } -export default modal; +export default Modal; diff --git a/app/javascript/packages/document-capture/components/full-screen.jsx b/app/javascript/packages/document-capture/components/full-screen.jsx index 992000313fc..87d743c2fdb 100644 --- a/app/javascript/packages/document-capture/components/full-screen.jsx +++ b/app/javascript/packages/document-capture/components/full-screen.jsx @@ -1,5 +1,5 @@ import React, { useRef, useEffect, useCallback } from 'react'; -import createFocusTrap from 'focus-trap'; +import { createFocusTrap } from 'focus-trap'; import useI18n from '../hooks/use-i18n'; import useAsset from '../hooks/use-asset'; diff --git a/app/javascript/packages/document-capture/package.json b/app/javascript/packages/document-capture/package.json index fa7a47b7eab..a8940e7472d 100644 --- a/app/javascript/packages/document-capture/package.json +++ b/app/javascript/packages/document-capture/package.json @@ -3,7 +3,7 @@ "private": true, "version": "1.0.0", "dependencies": { - "focus-trap": "^2.3.0", + "focus-trap": "^6.0.1", "react": "^16.13.1" } } diff --git a/package.json b/package.json index 9cd5dde57d1..259e9c53b75 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "classlist.js": "^1.1.20150312", "cleave.js": "^1.5.3", "clipboard": "^1.6.1", - "focus-trap": "^2.3.0", + "focus-trap": "^6.0.1", "hint.css": "^2.3.2", "identity-style-guide": "^2.1.5", "intl-tel-input": "^16.0.7", diff --git a/spec/javascripts/app/components/focus-trap-proxy_spec.js b/spec/javascripts/app/components/focus-trap-proxy_spec.js deleted file mode 100644 index a7d9b3be977..00000000000 --- a/spec/javascripts/app/components/focus-trap-proxy_spec.js +++ /dev/null @@ -1,37 +0,0 @@ -const proxyquire = require('proxyquire'); -const sinon = require('sinon'); - -const { stub } = sinon; - -describe('focusTrap', () => { - let proxy; - - beforeEach(() => { - proxy = proxyquire('../../../../app/javascript/app/components/focus-trap-proxy', { - // Mock external focus-trap library - 'focus-trap': () => ({ - deactivate: stub(), - activate: stub(), - }), - }).default; - }); - - context('#deactivate', () => { - it('proxies to `deactivate` and reactivates the last active trap', () => { - const trapA = proxy('foo1'); - const trapB = proxy('foo2'); - - const aFocusTrap = trapA.activate(); - const bFocusTrap = trapB.activate(); - - bFocusTrap.deactivate.returns(bFocusTrap); - - trapB.deactivate(); - - expect(aFocusTrap.activate.callCount).to.be.equal(2); - expect(aFocusTrap.deactivate.callCount).to.be.equal(2); - expect(bFocusTrap.activate.callCount).to.be.equal(1); - expect(bFocusTrap.deactivate.callCount).to.equal(3); - }); - }); -}); diff --git a/spec/javascripts/app/components/modal-spec.js b/spec/javascripts/app/components/modal-spec.js new file mode 100644 index 00000000000..2f8ed284750 --- /dev/null +++ b/spec/javascripts/app/components/modal-spec.js @@ -0,0 +1,90 @@ +import { waitFor } from '@testing-library/dom'; +import BaseModal from '../../../../app/javascript/app/components/modal'; +import { useCleanDOM } from '../../support/dom'; +import { useSandbox } from '../../support/sinon'; + +describe('components/modal', () => { + useCleanDOM(); + const sandbox = useSandbox(); + + class Modal extends BaseModal { + static instances = []; + + constructor(...args) { + super(...args); + Modal.instances.push(this); + } + } + + function createModalContainer(id = 'modal') { + const container = document.createElement('div'); + container.id = id; + container.className = 'modal display-none'; + container.innerHTML = ` +