diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index be141f9340b..d60736db56a 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -1,2 +1,3 @@ //= require i18n-strings +//= require assets //= require local-time diff --git a/app/assets/javascripts/assets.js.erb b/app/assets/javascripts/assets.js.erb new file mode 100644 index 00000000000..f4c170af449 --- /dev/null +++ b/app/assets/javascripts/assets.js.erb @@ -0,0 +1,13 @@ +window.LoginGov = window.LoginGov || {}; +window.LoginGov.assets = {}; + +<% keys = [ + 'state-id-sample-front.jpg', + 'plus.svg', + 'minus.svg', + 'up-carat-thin.svg' +] %> + +<% keys.each do |key| %> + window.LoginGov.assets['<%= ActionController::Base.helpers.j key %>'] = '<%= ActionController::Base.helpers.j ActionController::Base.helpers.asset_path key %>'; +<% end %> diff --git a/app/assets/javascripts/i18n-strings.js.erb b/app/assets/javascripts/i18n-strings.js.erb index fefe0ae54a8..f19f3e3e55d 100644 --- a/app/assets/javascripts/i18n-strings.js.erb +++ b/app/assets/javascripts/i18n-strings.js.erb @@ -47,7 +47,20 @@ window.LoginGov = window.LoginGov || {}; 'zxcvbn.feedback.for_a_stronger_password_use_a_few_words_separated_by_spaces_but_avoid_common_phrases', 'zxcvbn.feedback.use_a_longer_keyboard_pattern_with_more_turns', 'doc_auth.buttons.take_picture', - 'doc_auth.headings.welcome' + 'doc_auth.headings.welcome', + 'image_description.accordian_plus_buttom', + 'image_description.accordian_minus_buttom', + 'users.personal_key.close', + 'doc_auth.tips.title', + 'doc_auth.tips.title_more', + 'doc_auth.tips.header_text', + 'doc_auth.tips.text1', + 'doc_auth.tips.text2', + 'doc_auth.tips.text3', + 'doc_auth.tips.text4', + 'doc_auth.tips.text5', + 'doc_auth.tips.text6', + 'doc_auth.tips.text7' ] %> window.LoginGov.I18n = { diff --git a/app/javascript/app/components/accordion.js b/app/javascript/app/components/accordion.js index 14f5da6239d..186ddd19985 100644 --- a/app/javascript/app/components/accordion.js +++ b/app/javascript/app/components/accordion.js @@ -17,8 +17,10 @@ class Accordion extends Events { } setup() { - this.bindEvents(); - this.onInitialize(); + if (!this.isInitialized()) { + this.bindEvents(); + this.onInitialize(); + } } bindEvents() { @@ -41,6 +43,11 @@ class Accordion extends Events { onInitialize() { this.setExpanded(false); this.collapsedIcon.classList.remove('display-none'); + this.el.setAttribute('data-initialized', ''); + } + + isInitialized() { + return this.el.hasAttribute('data-initialized'); } handleClick() { diff --git a/app/javascript/app/document-capture/components/accordion.jsx b/app/javascript/app/document-capture/components/accordion.jsx new file mode 100644 index 00000000000..76974a1c0ef --- /dev/null +++ b/app/javascript/app/document-capture/components/accordion.jsx @@ -0,0 +1,80 @@ +import React, { useEffect, useRef, useMemo } from 'react'; +import PropTypes from 'prop-types'; +import BaseAccordion from '../../components/accordion'; +import useI18n from '../hooks/use-i18n'; +import Image from './image'; + +function Accordion({ title, children }) { + const elementRef = useRef(null); + const instanceId = useMemo(() => { + Accordion.instances += 1; + return Accordion.instances; + }, []); + const t = useI18n(); + useEffect(() => { + new BaseAccordion(elementRef.current).setup(); + }, []); + + const contentId = `accordion-content-${instanceId}`; + + return ( +
+
+ +
+ +
+ ); +} + +Accordion.instances = 0; + +Accordion.propTypes = { + title: PropTypes.node.isRequired, + children: PropTypes.node.isRequired, +}; + +export default Accordion; diff --git a/app/javascript/app/document-capture/components/document-capture.jsx b/app/javascript/app/document-capture/components/document-capture.jsx index 154b3d1d9b0..8da4b8ee12d 100644 --- a/app/javascript/app/document-capture/components/document-capture.jsx +++ b/app/javascript/app/document-capture/components/document-capture.jsx @@ -1,13 +1,25 @@ import React from 'react'; import AcuantCapture from './acuant-capture'; +import DocumentTips from './document-tips'; +import Image from './image'; import useI18n from '../hooks/use-i18n'; function DocumentCapture() { const t = useI18n(); + const sample = ( + Sample front of state issued ID + ); + return ( <>

{t('doc_auth.headings.welcome')}

+ ); diff --git a/app/javascript/app/document-capture/components/document-tips.jsx b/app/javascript/app/document-capture/components/document-tips.jsx new file mode 100644 index 00000000000..07f0fe5c684 --- /dev/null +++ b/app/javascript/app/document-capture/components/document-tips.jsx @@ -0,0 +1,41 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Accordion from './accordion'; +import useI18n from '../hooks/use-i18n'; + +function DocumentTips({ sample }) { + const t = useI18n(); + + const title = ( + <> + {t('doc_auth.tips.title')} + {` ${t('doc_auth.tips.title_more')}`} + + ); + + return ( + + {t('doc_auth.tips.header_text')} +
    +
  • {t('doc_auth.tips.text1')}
  • +
  • {t('doc_auth.tips.text2')}
  • +
  • {t('doc_auth.tips.text3')}
  • +
  • {t('doc_auth.tips.text4')}
  • +
  • {t('doc_auth.tips.text5')}
  • +
  • {t('doc_auth.tips.text6')}
  • +
  • {t('doc_auth.tips.text7')}
  • +
+ {!!sample &&
{sample}
} +
+ ); +} + +DocumentTips.propTypes = { + sample: PropTypes.node, +}; + +DocumentTips.defaultProps = { + sample: null, +}; + +export default DocumentTips; diff --git a/app/javascript/app/document-capture/components/image.jsx b/app/javascript/app/document-capture/components/image.jsx new file mode 100644 index 00000000000..26a73bdba3c --- /dev/null +++ b/app/javascript/app/document-capture/components/image.jsx @@ -0,0 +1,27 @@ +import React, { useContext } from 'react'; +import PropTypes from 'prop-types'; +import AssetContext from '../context/asset'; + +function Image({ assetPath, alt, ...imgProps }) { + const assets = useContext(AssetContext); + + const src = Object.prototype.hasOwnProperty.call(assets, assetPath) + ? assets[assetPath] + : assetPath; + + // Disable reason: While props spreading can introduce confusion to what is + // being passed down, in this case the component is intended to represent a + // pass-through to a base `` element, with handling for asset paths. + // + // Seee: https://github.com/airbnb/javascript/tree/master/react#props + + // eslint-disable-next-line react/jsx-props-no-spreading + return {alt}; +} + +Image.propTypes = { + assetPath: PropTypes.string.isRequired, + alt: PropTypes.string.isRequired, +}; + +export default Image; diff --git a/app/javascript/app/document-capture/context/asset.js b/app/javascript/app/document-capture/context/asset.js new file mode 100644 index 00000000000..91173cbe6d9 --- /dev/null +++ b/app/javascript/app/document-capture/context/asset.js @@ -0,0 +1,5 @@ +import { createContext } from 'react'; + +const AssetContext = createContext({}); + +export default AssetContext; diff --git a/app/javascript/packs/document-capture.jsx b/app/javascript/packs/document-capture.jsx index 4ee038f0a6a..ef61ee13f84 100644 --- a/app/javascript/packs/document-capture.jsx +++ b/app/javascript/packs/document-capture.jsx @@ -1,10 +1,11 @@ import React from 'react'; import { render } from 'react-dom'; import DocumentCapture from '../app/document-capture/components/document-capture'; +import AssetContext from '../app/document-capture/context/asset'; import I18nContext from '../app/document-capture/context/i18n'; import { Provider as AcuantProvider } from '../app/document-capture/context/acuant'; -const { I18n: i18n } = window.LoginGov; +const { I18n: i18n, assets } = window.LoginGov; function getMetaContent(name) { return document.querySelector(`meta[name="${name}"]`)?.content ?? null; @@ -18,7 +19,9 @@ render( endpoint={getMetaContent('acuant-sdk-initialization-endpoint')} > - + + + , appRoot, diff --git a/config/locales/doc_auth/en.yml b/config/locales/doc_auth/en.yml index 29f25e08c0a..8682fd90f0e 100644 --- a/config/locales/doc_auth/en.yml +++ b/config/locales/doc_auth/en.yml @@ -92,7 +92,9 @@ en: information. text7: Use a high-resolution camera. A good mobile phone or tablet camera will work. + title: Don't take the photo on a white surface! title_html: "Don't take the photo on a white surface!    See more tips..." + title_more: See more tips… titles: doc_auth: Document Authentication diff --git a/config/locales/doc_auth/es.yml b/config/locales/doc_auth/es.yml index fa31b60c9bf..8f4b8363858 100644 --- a/config/locales/doc_auth/es.yml +++ b/config/locales/doc_auth/es.yml @@ -99,6 +99,8 @@ es: la información. text7: Utilice una cámara de alta resolución. La cámara de un buen teléfono móvil o tableta funcionará. + title: "¡No tome la foto en una superficie blanca!" title_html: "¡No tome la foto en una superficie blanca! Ver más..." + title_more: Ver más… titles: doc_auth: Autenticación de documentos diff --git a/config/locales/doc_auth/fr.yml b/config/locales/doc_auth/fr.yml index f4c68e77c47..5f5e92e403b 100644 --- a/config/locales/doc_auth/fr.yml +++ b/config/locales/doc_auth/fr.yml @@ -107,6 +107,8 @@ fr: de lire toutes les informations. text7: Utilisez une caméra haute résolution. La caméra d'un bon téléphone mobile ou d'une tablette fonctionnera. + title: Ne prenez pas la photo sur une surface blanche! title_html: "Ne prenez pas la photo sur une surface blanche! Voir plus..." + title_more: Voir plus… titles: doc_auth: Authentification de document diff --git a/spec/javascripts/app/document-capture/components/accordion-spec.jsx b/spec/javascripts/app/document-capture/components/accordion-spec.jsx new file mode 100644 index 00000000000..e184731e8cd --- /dev/null +++ b/spec/javascripts/app/document-capture/components/accordion-spec.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import Accordion from '../../../../../app/javascript/app/document-capture/components/accordion'; + +describe('document-capture/components/accordion', () => { + it('renders with a unique ID', () => { + const { container } = render( + <> + Content + Content + , + ); + + const contents = container.querySelectorAll('[id^="accordion-content-"]'); + + expect(contents).to.have.lengthOf(2); + expect(contents[0].id).to.be.ok(); + expect(contents[1].id).to.be.ok(); + expect(contents[0].id).not.to.equal(contents[1].id); + }); +}); diff --git a/spec/javascripts/app/document-capture/components/image-spec.jsx b/spec/javascripts/app/document-capture/components/image-spec.jsx new file mode 100644 index 00000000000..79251481273 --- /dev/null +++ b/spec/javascripts/app/document-capture/components/image-spec.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import Image from '../../../../../app/javascript/app/document-capture/components/image'; +import AssetContext from '../../../../../app/javascript/app/document-capture/context/asset'; + +describe('document-capture/components/image', () => { + it('renders the given assetPath as src if the asset is not known', () => { + const { getByAltText } = render( + unknown, + ); + + const img = getByAltText('unknown'); + + expect(img.src).to.equal('unknown.png'); + }); + + it('renders an img at mapped src if known by context', () => { + const { getByAltText } = render( + + icon + , + ); + + const img = getByAltText('icon'); + + expect(img.src).to.equal('icon-12345.png'); + }); + + it('renders with given props', () => { + const { getByAltText } = render( + icon, + ); + + const img = getByAltText('icon'); + + expect(img.width).to.equal(50); + }); +}); diff --git a/spec/javascripts/app/document-capture/context/asset-spec.jsx b/spec/javascripts/app/document-capture/context/asset-spec.jsx new file mode 100644 index 00000000000..5986fa6ca69 --- /dev/null +++ b/spec/javascripts/app/document-capture/context/asset-spec.jsx @@ -0,0 +1,13 @@ +import React, { useContext } from 'react'; +import { render } from '@testing-library/react'; +import AssetContext from '../../../../../app/javascript/app/document-capture/context/asset'; + +describe('document-capture/context/asset', () => { + const ContextValue = () => JSON.stringify(useContext(AssetContext)); + + it('defaults to empty object', () => { + const { container } = render(); + + expect(container.textContent).to.equal('{}'); + }); +});