From b8db75b7a93a21ca909f7bec1faf9657bc23c54e Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 15 Apr 2022 10:52:05 -0400 Subject: [PATCH] LG-6066: Implement print button on FSMv2 Personal Key step (#6205) * LG-6066: Implement print button on FSMv2 Personal Key step **Why**: - So that we can retain feature parity with the existing screen. - To reduce size and scope of common application bundle - To create a more rigid connection between print JavaScript functionality and server-side render logic - To improve test coverage for existing print button behavior changelog: Upcoming Features, Identity Verification, Add personal key step screen * Create README.md --- app/components/print_button_component.rb | 15 ++++++++ app/components/print_button_component.ts | 1 + app/javascript/app/print-personal-key.js | 15 -------- .../packages/print-button/README.md | 35 +++++++++++++++++++ app/javascript/packages/print-button/index.ts | 3 ++ .../packages/print-button/package.json | 13 +++++++ .../print-button/print-button-element.spec.ts | 25 +++++++++++++ .../print-button/print-button-element.ts | 17 +++++++++ .../print-button/print-button.spec.tsx | 35 +++++++++++++++++++ .../packages/print-button/print-button.tsx | 24 +++++++++++++ .../steps/personal-key/personal-key-step.tsx | 5 ++- app/javascript/packs/application.js | 1 - app/views/shared/_personal_key.html.erb | 5 ++- .../users/backup_code_setup/create.html.erb | 5 ++- config/locales/components/en.yml | 2 ++ config/locales/components/es.yml | 2 ++ config/locales/components/fr.yml | 2 ++ config/locales/forms/en.yml | 1 - config/locales/forms/es.yml | 1 - config/locales/forms/fr.yml | 1 - config/locales/users/en.yml | 1 - config/locales/users/es.yml | 1 - config/locales/users/fr.yml | 1 - .../components/print_button_component_spec.rb | 26 ++++++++++++++ 24 files changed, 206 insertions(+), 31 deletions(-) create mode 100644 app/components/print_button_component.rb create mode 100644 app/components/print_button_component.ts delete mode 100644 app/javascript/app/print-personal-key.js create mode 100644 app/javascript/packages/print-button/README.md create mode 100644 app/javascript/packages/print-button/index.ts create mode 100644 app/javascript/packages/print-button/package.json create mode 100644 app/javascript/packages/print-button/print-button-element.spec.ts create mode 100644 app/javascript/packages/print-button/print-button-element.ts create mode 100644 app/javascript/packages/print-button/print-button.spec.tsx create mode 100644 app/javascript/packages/print-button/print-button.tsx create mode 100644 spec/components/print_button_component_spec.rb diff --git a/app/components/print_button_component.rb b/app/components/print_button_component.rb new file mode 100644 index 00000000000..63e53509a89 --- /dev/null +++ b/app/components/print_button_component.rb @@ -0,0 +1,15 @@ +class PrintButtonComponent < ButtonComponent + attr_reader :tag_options + + def initialize(**tag_options) + super(**tag_options, type: :button, icon: :print) + end + + def call + content_tag(:'lg-print-button', super) + end + + def content + t('components.print_button.label') + end +end diff --git a/app/components/print_button_component.ts b/app/components/print_button_component.ts new file mode 100644 index 00000000000..e4215f01c5f --- /dev/null +++ b/app/components/print_button_component.ts @@ -0,0 +1 @@ +import '@18f/identity-print-button'; diff --git a/app/javascript/app/print-personal-key.js b/app/javascript/app/print-personal-key.js deleted file mode 100644 index a34d641c607..00000000000 --- a/app/javascript/app/print-personal-key.js +++ /dev/null @@ -1,15 +0,0 @@ -const openSystemPrintDialog = (event) => { - event.preventDefault(); - window.print(); -}; - -const enablePersonalKeyPrintButton = () => { - const buttonNodes = document.querySelectorAll('[data-print]'); - const buttons = [].slice.call(buttonNodes); - - buttons.forEach((button) => { - button.addEventListener('click', openSystemPrintDialog); - }); -}; - -document.addEventListener('DOMContentLoaded', enablePersonalKeyPrintButton); diff --git a/app/javascript/packages/print-button/README.md b/app/javascript/packages/print-button/README.md new file mode 100644 index 00000000000..681cbab1494 --- /dev/null +++ b/app/javascript/packages/print-button/README.md @@ -0,0 +1,35 @@ +# `@18f/identity-print-button` + +Custom element and React implementation for a print button component. + +## Usage + +### Custom Element + +Importing the package will register the `` custom element: + +```ts +import '@18f/identity-print-button'; +``` + +The custom element will implement the behavior to show a print dialog upon click, but all markup must already exist, rendered server-side or by the included React component. + +```html + + + +``` + +### React + +The package exports a `PrintButton` component, which extends the `Button` component from `@18f/identity-components`. + +```tsx +import { PrintButton } from '@18f/identity-print-button'; + +export function Example() { + return ( + + ); +} +``` diff --git a/app/javascript/packages/print-button/index.ts b/app/javascript/packages/print-button/index.ts new file mode 100644 index 00000000000..f28a5ae942a --- /dev/null +++ b/app/javascript/packages/print-button/index.ts @@ -0,0 +1,3 @@ +import './print-button-element'; + +export { default as PrintButton } from './print-button'; diff --git a/app/javascript/packages/print-button/package.json b/app/javascript/packages/print-button/package.json new file mode 100644 index 00000000000..2ba4b26c81f --- /dev/null +++ b/app/javascript/packages/print-button/package.json @@ -0,0 +1,13 @@ +{ + "name": "@18f/identity-print-button", + "version": "1.0.0", + "private": true, + "peerDependencies": { + "react": "*" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } +} diff --git a/app/javascript/packages/print-button/print-button-element.spec.ts b/app/javascript/packages/print-button/print-button-element.spec.ts new file mode 100644 index 00000000000..34aafcb88c9 --- /dev/null +++ b/app/javascript/packages/print-button/print-button-element.spec.ts @@ -0,0 +1,25 @@ +import sinon from 'sinon'; +import { screen } from '@testing-library/dom'; +import userEvent from '@testing-library/user-event'; +import './print-button-element'; + +describe('PrintButtonElement', () => { + const sandbox = sinon.createSandbox(); + + beforeEach(() => { + sandbox.stub(window, 'print'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('prints when clicked', () => { + document.body.innerHTML = ``; + const button = screen.getByRole('button'); + + userEvent.click(button); + + expect(window.print).to.have.been.called(); + }); +}); diff --git a/app/javascript/packages/print-button/print-button-element.ts b/app/javascript/packages/print-button/print-button-element.ts new file mode 100644 index 00000000000..886f6d22bf4 --- /dev/null +++ b/app/javascript/packages/print-button/print-button-element.ts @@ -0,0 +1,17 @@ +class PrintButtonElement extends HTMLElement { + connectedCallback() { + this.addEventListener('click', () => window.print()); + } +} + +declare global { + interface HTMLElementTagNameMap { + 'lg-print-button': PrintButtonElement; + } +} + +if (!customElements.get('lg-print-button')) { + customElements.define('lg-print-button', PrintButtonElement); +} + +export default PrintButtonElement; diff --git a/app/javascript/packages/print-button/print-button.spec.tsx b/app/javascript/packages/print-button/print-button.spec.tsx new file mode 100644 index 00000000000..aeb85206a28 --- /dev/null +++ b/app/javascript/packages/print-button/print-button.spec.tsx @@ -0,0 +1,35 @@ +import sinon from 'sinon'; +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import PrintButton from './print-button'; + +describe('PrintButton', () => { + const sandbox = sinon.createSandbox(); + + beforeEach(() => { + sandbox.stub(window, 'print'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('renders a button that prints when clicked', () => { + const { getByRole } = render(); + + const button = getByRole('button', { name: 'components.print_button.label' }); + + userEvent.click(button); + + expect(window.print).to.have.been.called(); + }); + + it('forwards all other props to the button child', () => { + const { getByRole } = render(); + + const button = getByRole('button', { name: 'components.print_button.label' }); + + expect(button.closest('lg-print-button')).to.exist(); + expect(button.classList.contains('usa-button--outline')).to.be.true(); + }); +}); diff --git a/app/javascript/packages/print-button/print-button.tsx b/app/javascript/packages/print-button/print-button.tsx new file mode 100644 index 00000000000..889cbefa332 --- /dev/null +++ b/app/javascript/packages/print-button/print-button.tsx @@ -0,0 +1,24 @@ +import type { HTMLAttributes } from 'react'; +import { t } from '@18f/identity-i18n'; +import { Button } from '@18f/identity-components'; +import type { ButtonProps } from '@18f/identity-components'; +import type PrintButtonElement from './print-button-element'; +import './print-button-element'; + +declare global { + namespace JSX { + interface IntrinsicElements { + 'lg-print-button': HTMLAttributes & { class?: string }; + } + } +} + +function PrintButton(buttonProps: ButtonProps) { + return ( + + + + ); +} + +export default PrintButton; diff --git a/app/javascript/packages/verify-flow/steps/personal-key/personal-key-step.tsx b/app/javascript/packages/verify-flow/steps/personal-key/personal-key-step.tsx index 54735216197..196e40b8f5d 100644 --- a/app/javascript/packages/verify-flow/steps/personal-key/personal-key-step.tsx +++ b/app/javascript/packages/verify-flow/steps/personal-key/personal-key-step.tsx @@ -1,5 +1,6 @@ import { PageHeading, Button } from '@18f/identity-components'; import { ClipboardButton } from '@18f/identity-clipboard-button'; +import { PrintButton } from '@18f/identity-print-button'; import { t } from '@18f/identity-i18n'; import { formatHTML } from '@18f/identity-react-i18n'; import { FormStepsContinueButton } from '@18f/identity-form-steps'; @@ -42,9 +43,7 @@ function PersonalKeyStep({ value }: PersonalKeyStepProps) { - + -<%= render ButtonComponent.new( +<%= render PrintButtonComponent.new( icon: :print, outline: true, type: :button, - data: { print: '' }, class: 'margin-right-2 margin-bottom-2 tablet:margin-bottom-0', - ).with_content(t('users.personal_key.print')) %> + ) %> <%= render ClipboardButtonComponent.new( clipboard_text: code, outline: true, diff --git a/app/views/users/backup_code_setup/create.html.erb b/app/views/users/backup_code_setup/create.html.erb index 42c8b3e1428..90bff6899ca 100644 --- a/app/views/users/backup_code_setup/create.html.erb +++ b/app/views/users/backup_code_setup/create.html.erb @@ -37,13 +37,12 @@ icon: :file_download, ).with_content(t('forms.backup_code.download')) %> <% end %> - <%= render ButtonComponent.new( + <%= render PrintButtonComponent.new( icon: :print, outline: true, type: :button, - data: { print: '' }, class: 'margin-top-2 tablet:margin-top-0 tablet:margin-left-2', - ).with_content(t('forms.backup_code.print')) %> + ) %> <%= render ClipboardButtonComponent.new( clipboard_text: @codes.join(' '), outline: true, diff --git a/config/locales/components/en.yml b/config/locales/components/en.yml index 0b7a8421dd2..5feec45dedd 100644 --- a/config/locales/components/en.yml +++ b/config/locales/components/en.yml @@ -8,6 +8,8 @@ en: toggle_label: Show password phone_input: country_code_label: Country code + print_button: + label: Print status_page: icons: error: Error diff --git a/config/locales/components/es.yml b/config/locales/components/es.yml index 01ecffecca4..32a36a002f2 100644 --- a/config/locales/components/es.yml +++ b/config/locales/components/es.yml @@ -8,6 +8,8 @@ es: toggle_label: Mostrar contraseña phone_input: country_code_label: Código del país + print_button: + label: Imprima esta página status_page: icons: error: Error diff --git a/config/locales/components/fr.yml b/config/locales/components/fr.yml index 3ea9b1ec611..9c51e73f05e 100644 --- a/config/locales/components/fr.yml +++ b/config/locales/components/fr.yml @@ -8,6 +8,8 @@ fr: toggle_label: Afficher le mot de passe phone_input: country_code_label: Code pays + print_button: + label: Imprimer cette page status_page: icons: error: Erreur diff --git a/config/locales/forms/en.yml b/config/locales/forms/en.yml index 5701f924f9c..5ca92a6eaa8 100644 --- a/config/locales/forms/en.yml +++ b/config/locales/forms/en.yml @@ -18,7 +18,6 @@ en: generate: Get codes last_code: You used your last backup code. Please print, copy or download the codes below. You can use these new codes the next time you sign in. - print: Print regenerate: Get new codes subinfo_html: 'Don’t lose these codes. Download, print, or copy them. Each code can only be used once. After you’ve used all 10 codes, we’ll diff --git a/config/locales/forms/es.yml b/config/locales/forms/es.yml index 1a43385f069..1aef885a26d 100644 --- a/config/locales/forms/es.yml +++ b/config/locales/forms/es.yml @@ -20,7 +20,6 @@ es: last_code: Usted utilizó el último código de seguridad. Imprima, copie o descargue los códigos que aparecen a continuación. Puede introducir estos nuevos códigos la próxima vez que se registre. - print: Impresión regenerate: Obtener nuevos códigos subinfo_html: 'No pierdas estos códigos. Descarga, imprime, o copialos. Cada código solo se puede usar una vez. Después de que haya utilizado el diff --git a/config/locales/forms/fr.yml b/config/locales/forms/fr.yml index a81b7d5bb73..689986365c2 100644 --- a/config/locales/forms/fr.yml +++ b/config/locales/forms/fr.yml @@ -22,7 +22,6 @@ fr: last_code: Vous avez utilisé votre dernier code de sauvegarde. Veuillez imprimer, copier ou télécharger les codes ci-dessous. Vous pourrez utiliser ces nouveaux codes la prochaine fois que vous vous connecterez. - print: Impression regenerate: Obtenir de nouveaux codes subinfo_html: 'Ne pas perdre ces codes. Téléchargez, imprimez ou copiez-les. Chaque code ne peut être utilisé qu’une seule fois. Une fois diff --git a/config/locales/users/en.yml b/config/locales/users/en.yml index aefeefc1a02..378d09f4a02 100644 --- a/config/locales/users/en.yml +++ b/config/locales/users/en.yml @@ -23,7 +23,6 @@ en: confirmation_error: You’ve entered an incorrect personal key. generated_on_html: Generated on %{date} header: Your personal key - print: Print phones: error_message: You’ve added the maximum number of phone numbers. rules_of_use: diff --git a/config/locales/users/es.yml b/config/locales/users/es.yml index 1b3001054ab..f1e8262854a 100644 --- a/config/locales/users/es.yml +++ b/config/locales/users/es.yml @@ -24,7 +24,6 @@ es: confirmation_error: Ha ingresado una clave personal incorrecta. generated_on_html: Generado el %{date} header: Su clave personal - print: Imprima esta página phones: error_message: Agregó el número máximo de números de teléfono. rules_of_use: diff --git a/config/locales/users/fr.yml b/config/locales/users/fr.yml index a5777bd5c4f..1c17d049ae1 100644 --- a/config/locales/users/fr.yml +++ b/config/locales/users/fr.yml @@ -26,7 +26,6 @@ fr: confirmation_error: Vous avez entré un clé personnelle erronée. generated_on_html: Générée le %{date} header: Votre clé personnelle - print: Imprimer cette page phones: error_message: Vous avez ajouté le nombre maximum de numéros de téléphone. rules_of_use: diff --git a/spec/components/print_button_component_spec.rb b/spec/components/print_button_component_spec.rb new file mode 100644 index 00000000000..826af042dc7 --- /dev/null +++ b/spec/components/print_button_component_spec.rb @@ -0,0 +1,26 @@ +require 'rails_helper' + +RSpec.describe PrintButtonComponent, type: :component do + let(:tag_options) { {} } + + subject(:rendered) do + render_inline PrintButtonComponent.new(**tag_options) + end + + it 'renders custom element with button' do + expect(rendered).to have_css( + 'lg-print-button button[type="button"]', + text: t('components.print_button.label'), + ) + end + + context 'with tag options' do + let(:tag_options) { { outline: true, data: { foo: 'bar' } } } + + it 'renders with tag options forwarded to button' do + expect(rendered).to have_css( + 'lg-print-button button.usa-button--outline:not([outline])[data-foo="bar"]', + ) + end + end +end