Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/assets/javascripts/application.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
//= require i18n-strings
//= require assets
//= require local-time
13 changes: 13 additions & 0 deletions app/assets/javascripts/assets.js.erb
Original file line number Diff line number Diff line change
@@ -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 %>
15 changes: 14 additions & 1 deletion app/assets/javascripts/i18n-strings.js.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
11 changes: 9 additions & 2 deletions app/javascript/app/components/accordion.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ class Accordion extends Events {
}

setup() {
this.bindEvents();
this.onInitialize();
if (!this.isInitialized()) {
this.bindEvents();
this.onInitialize();
}
}

bindEvents() {
Expand All @@ -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() {
Expand Down
80 changes: 80 additions & 0 deletions app/javascript/app/document-capture/components/accordion.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div ref={elementRef} className="accordion mb4 col-12 fs-16p">
<div aria-describedby={contentId} className="accordion-header">
<div
aria-controls={contentId}
aria-expanded="false"
role="button"
tabIndex={0}
className="accordion-header-controls py1 px2 mt-tiny mb-tiny"
>
<span className="mb0 mr2">{title}</span>
<Image
assetPath="plus.svg"
alt={t('image_description.accordian_plus_buttom')}
width={16}
className="plus-icon display-none"
/>
<Image
assetPath="minus.svg"
alt={t('image_description.accordian_minus_buttom')}
width={16}
className="minus-icon display-none"
/>
</div>
</div>
<div
id={contentId}
className="accordion-content clearfix pt1"
role="region"
aria-hidden="true"
>
<div className="px2">{children}</div>
<div
className="py1 accordion-footer"
aria-controls={contentId}
role="button"
tabIndex={0}
>
<div className="pb-tiny pt-tiny">
<Image
assetPath="up-carat-thin.svg"
alt=""
width={14}
className="mr1"
/>
{t('users.personal_key.close')}
</div>
</div>
</div>
</div>
);
}

Accordion.instances = 0;

Accordion.propTypes = {
title: PropTypes.node.isRequired,
children: PropTypes.node.isRequired,
};

export default Accordion;
Original file line number Diff line number Diff line change
@@ -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 = (
<Image
assetPath="state-id-sample-front.jpg"
alt="Sample front of state issued ID"
width={450}
height={338}
/>
);

return (
<>
<h2>{t('doc_auth.headings.welcome')}</h2>
<DocumentTips sample={sample} />
<AcuantCapture />
</>
);
Expand Down
41 changes: 41 additions & 0 deletions app/javascript/app/document-capture/components/document-tips.jsx
Original file line number Diff line number Diff line change
@@ -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 = (
<>
<strong>{t('doc_auth.tips.title')}</strong>
{` ${t('doc_auth.tips.title_more')}`}
</>
);

return (
<Accordion title={title}>
<strong>{t('doc_auth.tips.header_text')}</strong>
<ul>
<li>{t('doc_auth.tips.text1')}</li>
<li>{t('doc_auth.tips.text2')}</li>
<li>{t('doc_auth.tips.text3')}</li>
<li>{t('doc_auth.tips.text4')}</li>
<li>{t('doc_auth.tips.text5')}</li>
<li>{t('doc_auth.tips.text6')}</li>
<li>{t('doc_auth.tips.text7')}</li>
</ul>
{!!sample && <div className="center">{sample}</div>}
</Accordion>
);
}

DocumentTips.propTypes = {
sample: PropTypes.node,
};

DocumentTips.defaultProps = {
sample: null,
};

export default DocumentTips;
27 changes: 27 additions & 0 deletions app/javascript/app/document-capture/components/image.jsx
Original file line number Diff line number Diff line change
@@ -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 `<img />` 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 <img src={src} alt={alt} {...imgProps} />;
}

Image.propTypes = {
assetPath: PropTypes.string.isRequired,
alt: PropTypes.string.isRequired,
};

export default Image;
5 changes: 5 additions & 0 deletions app/javascript/app/document-capture/context/asset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createContext } from 'react';

const AssetContext = createContext({});

export default AssetContext;
7 changes: 5 additions & 2 deletions app/javascript/packs/document-capture.jsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -18,7 +19,9 @@ render(
endpoint={getMetaContent('acuant-sdk-initialization-endpoint')}
>
<I18nContext.Provider value={i18n.strings[i18n.currentLocale()]}>
<DocumentCapture />
<AssetContext.Provider value={assets}>
<DocumentCapture />
</AssetContext.Provider>
</I18nContext.Provider>
</AcuantProvider>,
appRoot,
Expand Down
2 changes: 2 additions & 0 deletions config/locales/doc_auth/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<b>Don't take the photo on a white surface!</b> &nbsp;&nbsp; See
more tips..."
title_more: See more tips…
titles:
doc_auth: Document Authentication
2 changes: 2 additions & 0 deletions config/locales/doc_auth/es.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<b>¡No tome la foto en una superficie blanca!</b> Ver más..."
title_more: Ver más…
titles:
doc_auth: Autenticación de documentos
2 changes: 2 additions & 0 deletions config/locales/doc_auth/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<b>Ne prenez pas la photo sur une surface blanche!</b> Voir plus..."
title_more: Voir plus…
titles:
doc_auth: Authentification de document
Original file line number Diff line number Diff line change
@@ -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(
<>
<Accordion title="Title">Content</Accordion>
<Accordion title="Title">Content</Accordion>
</>,
);

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);
});
});
38 changes: 38 additions & 0 deletions spec/javascripts/app/document-capture/components/image-spec.jsx
Original file line number Diff line number Diff line change
@@ -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(
<Image assetPath="unknown.png" alt="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(
<AssetContext.Provider value={{ 'icon.png': 'icon-12345.png' }}>
<Image assetPath="icon.png" alt="icon" />
</AssetContext.Provider>,
);

const img = getByAltText('icon');

expect(img.src).to.equal('icon-12345.png');
});

it('renders with given props', () => {
const { getByAltText } = render(
<Image assetPath="icon.png" alt="icon" width={50} />,
);

const img = getByAltText('icon');

expect(img.width).to.equal(50);
});
});
13 changes: 13 additions & 0 deletions spec/javascripts/app/document-capture/context/asset-spec.jsx
Original file line number Diff line number Diff line change
@@ -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(<ContextValue />);

expect(container.textContent).to.equal('{}');
});
});