diff --git a/app/javascript/packages/components/package.json b/app/javascript/packages/components/package.json index c3d276925b1..400ac567672 100644 --- a/app/javascript/packages/components/package.json +++ b/app/javascript/packages/components/package.json @@ -3,6 +3,6 @@ "private": true, "version": "1.0.0", "dependencies": { - "react": "^17.0.1" + "react": "^17.0.2" } } diff --git a/app/javascript/packages/compose-components/README.md b/app/javascript/packages/compose-components/README.md new file mode 100644 index 00000000000..fec7f76731e --- /dev/null +++ b/app/javascript/packages/compose-components/README.md @@ -0,0 +1,17 @@ +# `@18f/identity-compose-components` + +A utility function to compose a set of React components and their props to a single component. + +Convenient for flattening a deeply-nested arrangement of context providers, for example. + +## Example + +```jsx +const App = composeComponents( + [FirstContext.Provider, { value: 1 }], + [SecondContext.Provider, { value: 2 }], + AppRoot, +); + +render(App, document.getElementById('app-root')); +``` diff --git a/app/javascript/packages/compose-components/index.js b/app/javascript/packages/compose-components/index.js new file mode 100644 index 00000000000..d152e389d7f --- /dev/null +++ b/app/javascript/packages/compose-components/index.js @@ -0,0 +1,51 @@ +import { createElement } from 'react'; + +/** @typedef {import('react').ComponentType

} ComponentType @template P */ + +/** + * @typedef {[ComponentType

, P]} NormalizedComponentPair + * + * @template P + */ + +/** + * @typedef {[ComponentType

, P]|[ComponentType

]|ComponentType

} ComponentPair + * + * @template P + */ + +/** + * A utility function to compose a set of React components and their props to a single component. + * + * Convenient for flattening a deeply-nested arrangement of context providers, for example. + * + * @example + * ```jsx + * const App = composeComponents( + * [FirstContext.Provider, { value: 1 }], + * [SecondContext.Provider, { value: 2 }], + * AppRoot, + * ); + * + * render(App, document.getElementById('app-root')); + * ``` + * + * @param {...ComponentPair<*>} components + * + * @return {ComponentType<*>} + */ +export function composeComponents(...components) { + return function ComposedComponent() { + /** @type {JSX.Element?} */ + let element = null; + for (let i = components.length - 1; i >= 0; i--) { + const componentPair = /** @type {NormalizedComponentPair<*>} */ (Array.isArray(components[i]) + ? components[i] + : [components[i]]); + const [ComponentType, props] = componentPair; + element = createElement(ComponentType, props, element); + } + + return element; + }; +} diff --git a/app/javascript/packages/compose-components/index.spec.jsx b/app/javascript/packages/compose-components/index.spec.jsx new file mode 100644 index 00000000000..7e1e296e7c0 --- /dev/null +++ b/app/javascript/packages/compose-components/index.spec.jsx @@ -0,0 +1,27 @@ +import { createContext, useContext } from 'react'; +import { render } from '@testing-library/react'; +import { composeComponents } from './index.js'; + +describe('composeComponents', () => { + it('composes components', () => { + const FirstContext = createContext(null); + const SecondContext = createContext(null); + const AppRoot = () => ( + <> + {useContext(FirstContext)} + {useContext(SecondContext)} + + ); + + const ComposedComponent = composeComponents( + [FirstContext.Provider, { value: 1 }], + [SecondContext.Provider, { value: 2 }], + [({ children }) => <>{children}3], + AppRoot, + ); + + const { getByText } = render(); + + expect(getByText('123')).to.be.ok(); + }); +}); diff --git a/app/javascript/packages/compose-components/package.json b/app/javascript/packages/compose-components/package.json new file mode 100644 index 00000000000..8ec82d11652 --- /dev/null +++ b/app/javascript/packages/compose-components/package.json @@ -0,0 +1,8 @@ +{ + "name": "@18f/identity-compose-components", + "private": true, + "version": "1.0.0", + "dependencies": { + "react": "^17.0.2" + } +} diff --git a/app/javascript/packages/document-capture/package.json b/app/javascript/packages/document-capture/package.json index 7c522b3f53a..393b64ed7ed 100644 --- a/app/javascript/packages/document-capture/package.json +++ b/app/javascript/packages/document-capture/package.json @@ -4,7 +4,7 @@ "version": "1.0.0", "dependencies": { "focus-trap": "^6.2.3", - "react": "^17.0.1", - "react-dom": "^17.0.1" + "react": "^17.0.2", + "react-dom": "^17.0.2" } } diff --git a/app/javascript/packages/react-i18n/package.json b/app/javascript/packages/react-i18n/package.json index 0906ab47a9f..75371b668c6 100644 --- a/app/javascript/packages/react-i18n/package.json +++ b/app/javascript/packages/react-i18n/package.json @@ -3,6 +3,6 @@ "private": true, "version": "1.0.0", "dependencies": { - "react": "^17.0.1" + "react": "^17.0.2" } } diff --git a/app/javascript/packs/document-capture.jsx b/app/javascript/packs/document-capture.jsx index d2d16028432..31fb9617b91 100644 --- a/app/javascript/packs/document-capture.jsx +++ b/app/javascript/packs/document-capture.jsx @@ -1,4 +1,5 @@ import { render } from 'react-dom'; +import { composeComponents } from '@18f/identity-compose-components'; import { AppContext, DocumentCapture, @@ -138,41 +139,39 @@ loadPolyfills(['fetch', 'crypto', 'url']).then(async () => { appName: /** @type string */ (appRoot.dataset.appName), }; - render( - - - - - - - - - - - - - - - - - , - appRoot, + const App = composeComponents( + [AppContext.Provider, { value: appContext }], + [DeviceContext.Provider, { value: device }], + [AnalyticsContext.Provider, { value: { addPageAction, noticeError } }], + [ + AcuantContextProvider, + { + credentials: getMetaContent('acuant-sdk-initialization-creds'), + endpoint: getMetaContent('acuant-sdk-initialization-endpoint'), + glareThreshold, + sharpnessThreshold, + }, + ], + [ + UploadContextProvider, + { + endpoint: String(appRoot.getAttribute('data-endpoint')), + statusEndpoint: String(appRoot.getAttribute('data-status-endpoint')), + statusPollInterval: + Number(appRoot.getAttribute('data-status-poll-interval-ms')) || undefined, + method: isAsyncForm ? 'PUT' : 'POST', + csrf, + isMockClient, + backgroundUploadURLs, + backgroundUploadEncryptKey, + formData, + }, + ], + [I18nContext.Provider, { value: i18n.strings }], + [ServiceProviderContextProvider, { value: getServiceProvider() }], + [AssetContext.Provider, { value: assets }], + [DocumentCapture, { isAsyncForm, onStepChange: keepAlive }], ); + + render(, appRoot); }); diff --git a/package.json b/package.json index 09a647e654e..29095cb14e8 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,8 @@ "identity-style-guide": "^6.2.0", "intl-tel-input": "^17.0.8", "postcss-clean": "^1.1.0", - "react": "^17.0.1", - "react-dom": "^17.0.1", + "react": "^17.0.2", + "react-dom": "^17.0.2", "source-map-loader": "^1.1.3", "zxcvbn": "4.4.2" }, @@ -57,7 +57,7 @@ "mocha": "^8.2.1", "mq-polyfill": "^1.1.8", "prettier": "^2.2.1", - "react-test-renderer": "^17.0.1", + "react-test-renderer": "^17.0.2", "sinon": "^9.2.2", "sinon-chai": "^3.5.0", "svgo": "^1.3.2", diff --git a/yarn.lock b/yarn.lock index e2424b4ee88..80049cda8ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7642,19 +7642,19 @@ raw-body@2.4.0: iconv-lite "0.4.24" unpipe "1.0.0" -react-dom@^17.0.1: - version "17.0.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.1.tgz#1de2560474ec9f0e334285662ede52dbc5426fc6" - integrity sha512-6eV150oJZ9U2t9svnsspTMrWNyHc6chX0KzDeAOXftRa8bNeOKTTfCJ7KorIwenkHd2xqVTBTCZd79yk/lx/Ug== +react-dom@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" + integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" - scheduler "^0.20.1" + scheduler "^0.20.2" -"react-is@^16.12.0 || ^17.0.0", react-is@^17.0.1: - version "17.0.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" - integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== +"react-is@^16.12.0 || ^17.0.0", react-is@^17.0.1, react-is@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== react-is@^16.8.1: version "16.13.1" @@ -7669,20 +7669,20 @@ react-shallow-renderer@^16.13.1: object-assign "^4.1.1" react-is "^16.12.0 || ^17.0.0" -react-test-renderer@^17.0.1: - version "17.0.1" - resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-17.0.1.tgz#3187e636c3063e6ae498aedf21ecf972721574c7" - integrity sha512-/dRae3mj6aObwkjCcxZPlxDFh73XZLgvwhhyON2haZGUEhiaY5EjfAdw+d/rQmlcFwdTpMXCSGVk374QbCTlrA== +react-test-renderer@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-17.0.2.tgz#4cd4ae5ef1ad5670fc0ef776e8cc7e1231d9866c" + integrity sha512-yaQ9cB89c17PUb0x6UfWRs7kQCorVdHlutU1boVPEsB8IDZH6n9tHxMacc3y0JoXOJUsZb/t/Mb8FUWMKaM7iQ== dependencies: object-assign "^4.1.1" - react-is "^17.0.1" + react-is "^17.0.2" react-shallow-renderer "^16.13.1" - scheduler "^0.20.1" + scheduler "^0.20.2" -react@^17.0.1: - version "17.0.1" - resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127" - integrity sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w== +react@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" + integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" @@ -8067,10 +8067,10 @@ saxes@^5.0.0: dependencies: xmlchars "^2.2.0" -scheduler@^0.20.1: - version "0.20.1" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.1.tgz#da0b907e24026b01181ecbc75efdc7f27b5a000c" - integrity sha512-LKTe+2xNJBNxu/QhHvDR14wUXHRQbVY5ZOYpOGWRzhydZUqrLb2JBvLPY7cAqFmqrWuDED0Mjk7013SZiOz6Bw== +scheduler@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" + integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1"