diff --git a/app/components/block_link_component.rb b/app/components/block_link_component.rb index 9878ee58641..34f25d68654 100644 --- a/app/components/block_link_component.rb +++ b/app/components/block_link_component.rb @@ -1,11 +1,11 @@ class BlockLinkComponent < BaseComponent - attr_reader :url, :action, :new_tab, :tag_options + attr_reader :url, :action, :new_tab, :tag_options, :component alias_method :new_tab?, :new_tab - def initialize(url:, action: tag.method(:a), new_tab: false, **tag_options) - @action = action + def initialize(url: '#', component: nil, new_tab: false, **tag_options) @url = url + @component = component @new_tab = new_tab @tag_options = tag_options end @@ -21,11 +21,11 @@ def target end def wrapper(&block) - wrapper = action.call(**tag_options, href: url, class: css_class, target:, &block) - if wrapper.respond_to?(:render_in) - render wrapper, &block + if component + render component.new(href: url, class: css_class), &block else - wrapper + action = tag.method(:a) + action.call(**tag_options, href: url, class: css_class, target:, &block) end end end diff --git a/app/components/form_link_component.html.erb b/app/components/form_link_component.html.erb new file mode 100644 index 00000000000..3e72713c616 --- /dev/null +++ b/app/components/form_link_component.html.erb @@ -0,0 +1,3 @@ + + <%= link_to('#', **tag_options) { content } %> + diff --git a/app/components/form_link_component.rb b/app/components/form_link_component.rb new file mode 100644 index 00000000000..a3ba4823fd3 --- /dev/null +++ b/app/components/form_link_component.rb @@ -0,0 +1,7 @@ +class FormLinkComponent < BaseComponent + attr_reader :tag_options + + def initialize(**tag_options) + @tag_options = tag_options + end +end diff --git a/app/components/form_link_component.ts b/app/components/form_link_component.ts new file mode 100644 index 00000000000..5c47ccb4c63 --- /dev/null +++ b/app/components/form_link_component.ts @@ -0,0 +1 @@ +import '@18f/identity-form-link/form-link-element'; diff --git a/app/javascript/packages/form-link/README.md b/app/javascript/packages/form-link/README.md new file mode 100644 index 00000000000..9bd81495941 --- /dev/null +++ b/app/javascript/packages/form-link/README.md @@ -0,0 +1,22 @@ +# `@18f/identity-form-link` + +Custom element for links which submit as a form, supporting non-GET navigation. + +## Usage + +### Custom Element + +Importing the element will register the `` custom element: + +```ts +import '@18f/identity-form-link/form-link-element'; +``` + +The custom element will implement the link submission behavior, but all markup must already exist. + +```html + + Submit + + +``` diff --git a/app/javascript/packages/form-link/form-link-element.spec.ts b/app/javascript/packages/form-link/form-link-element.spec.ts new file mode 100644 index 00000000000..d3b8bcbf350 --- /dev/null +++ b/app/javascript/packages/form-link/form-link-element.spec.ts @@ -0,0 +1,28 @@ +import sinon from 'sinon'; +import { getByRole, fireEvent } from '@testing-library/dom'; +import './form-link-element'; + +describe('FormLinkElement', () => { + function createElement() { + document.body.innerHTML = ` + + Submit + + + `; + + return document.body.querySelector('lg-form-link')!; + } + + it('submits form on link click', () => { + const element = createElement(); + const link = getByRole(element, 'link'); + + const onSubmit = sinon.stub(); + element.form.submit = onSubmit; + const didPreventDefault = !fireEvent.click(link); + + expect(onSubmit).to.have.been.called(); + expect(didPreventDefault).to.be.true(); + }); +}); diff --git a/app/javascript/packages/form-link/form-link-element.ts b/app/javascript/packages/form-link/form-link-element.ts new file mode 100644 index 00000000000..0e59ab55697 --- /dev/null +++ b/app/javascript/packages/form-link/form-link-element.ts @@ -0,0 +1,30 @@ +class FormLinkElement extends HTMLElement { + connectedCallback() { + this.link.addEventListener('click', this.submit); + } + + get form(): HTMLFormElement { + return this.querySelector('form')!; + } + + get link(): HTMLAnchorElement { + return this.querySelector('a')!; + } + + submit = (event: MouseEvent) => { + event.preventDefault(); + this.form.submit(); + }; +} + +declare global { + interface HTMLElementTagNameMap { + 'lg-form-link': FormLinkElement; + } +} + +if (!customElements.get('lg-form-link')) { + customElements.define('lg-form-link', FormLinkElement); +} + +export default FormLinkElement; diff --git a/app/javascript/packages/form-link/package.json b/app/javascript/packages/form-link/package.json new file mode 100644 index 00000000000..fe327bd7db0 --- /dev/null +++ b/app/javascript/packages/form-link/package.json @@ -0,0 +1,8 @@ +{ + "name": "@18f/identity-form-link", + "version": "1.0.0", + "private": true, + "sideEffects": [ + "./form-link-element.ts" + ] +} diff --git a/app/views/sign_up/emails/show.html.erb b/app/views/sign_up/emails/show.html.erb index 4948ff96636..be934f3b8d7 100644 --- a/app/views/sign_up/emails/show.html.erb +++ b/app/views/sign_up/emails/show.html.erb @@ -15,31 +15,29 @@ <%= t('notices.signed_up_but_unconfirmed.first_paragraph_end') %>

-
-
-
+

<%= t('devise.registrations.close_window') %>

-<%= simple_form_for @resend_email_confirmation_form, - html: { class: 'margin-bottom-2' }, - url: sign_up_register_path do |f| %> - <%= f.input :email, as: :hidden %> - <%= f.input :resend, as: :hidden %> -

<%= t('notices.signed_up_but_unconfirmed.no_email_sent_explanation_start') %> - <%= f.button :button, t('links.resend'), class: 'usa-button--unstyled margin-left-05' %>

- -

- <%= t( - 'notices.use_diff_email.text_html', - link_html: link_to(t('notices.use_diff_email.link'), sign_up_email_path), - ) %> -

-

<%= t('devise.registrations.close_window') %>

+<%= render TroubleshootingOptionsComponent.new do |c| %> + <% c.with_header { t('components.troubleshooting_options.default_heading') } %> + <% c.with_option(component: FormLinkComponent) do %> + <%= t('notices.signed_up_but_unconfirmed.resend_confirmation_email') %> + <%= simple_form_for @resend_email_confirmation_form, + html: { class: 'display-none' }, + url: sign_up_register_path do |f| %> + <%= f.input :email, as: :hidden %> + <%= f.input :resend, as: :hidden %> + <%= f.button :button, t('notices.signed_up_but_unconfirmed.resend_confirmation_email') %> + <% end %> + <% end %> + <% c.with_option( + url: sign_up_email_path, + ).with_content(t('notices.use_diff_email.link').upcase_first) %> +<% end %> - <% if FeatureManagement.enable_load_testing_mode? && EmailAddress.find_with_email(email) %> - <%= link_to( - 'CONFIRM NOW', - sign_up_create_email_confirmation_url(confirmation_token: EmailAddress.find_with_email(email).confirmation_token), - id: 'confirm-now', - ) %> - <% end %> +<% if FeatureManagement.enable_load_testing_mode? && EmailAddress.find_with_email(email) %> + <%= link_to( + 'CONFIRM NOW', + sign_up_create_email_confirmation_url(confirmation_token: EmailAddress.find_with_email(email).confirmation_token), + id: 'confirm-now', + ) %> <% end %> diff --git a/config/locales/notices/en.yml b/config/locales/notices/en.yml index c621d34246d..a617bb02aa9 100644 --- a/config/locales/notices/en.yml +++ b/config/locales/notices/en.yml @@ -35,7 +35,7 @@ en: first_paragraph_end: with a link to confirm your email address. Follow the link to continue creating your account. first_paragraph_start: We sent an email to - no_email_sent_explanation_start: Didn’t receive an email? + resend_confirmation_email: Resend the confirmation email timeout_warning: partially_signed_in: continue: Continue sign in diff --git a/config/locales/notices/es.yml b/config/locales/notices/es.yml index 180544da64e..3fae3ca377b 100644 --- a/config/locales/notices/es.yml +++ b/config/locales/notices/es.yml @@ -37,7 +37,7 @@ es: first_paragraph_end: con un enlace para confirmar su email. Siga el enlace para continuar creando su cuenta. first_paragraph_start: Enviamos un email a - no_email_sent_explanation_start: '¿No recibió un email?' + resend_confirmation_email: Reenviar el correo electrónico de confirmación timeout_warning: partially_signed_in: continue: Continuar el inicio de sesión diff --git a/config/locales/notices/fr.yml b/config/locales/notices/fr.yml index 0e366e5db32..9ed9eb1a3cc 100644 --- a/config/locales/notices/fr.yml +++ b/config/locales/notices/fr.yml @@ -37,7 +37,7 @@ fr: first_paragraph_end: avec un lien pour confirmer votre adresse courriel. Suivez le lien pour continuer à créer votre compte. first_paragraph_start: Nous avons envoyé un courriel à - no_email_sent_explanation_start: Vous n’avez pas reçu d’e-mail? + resend_confirmation_email: Renvoyer le courriel de confirmation timeout_warning: partially_signed_in: continue: Continuer la connexion diff --git a/spec/components/block_link_component_spec.rb b/spec/components/block_link_component_spec.rb index f3b9ece14a2..4ead0f748c6 100644 --- a/spec/components/block_link_component_spec.rb +++ b/spec/components/block_link_component_spec.rb @@ -29,26 +29,22 @@ end end - context 'with custom renderer' do - # rubocop:disable RSpec/LeakyConstantDeclaration - class ExampleBlockLinkCustomRendererComponent < BaseComponent - def initialize(href:, **) - @href = href - end - - def call - content_tag(:button, "Example #{content.strip}", data: { href: @href }) - end + context 'with a component' do + before do + stub_const( + 'TestComponent', Class.new(BaseComponent) do + def call + content_tag(:div, 'from test component', class: 'style') + end + end + ) end - # rubocop:enable RSpec/LeakyConstantDeclaration - it 'renders using the custom renderer' do - rendered = render_inline BlockLinkComponent.new( - url: '/', - action: ExampleBlockLinkCustomRendererComponent.method(:new), - ).with_content('Link Text') + it 'renders using the specified component' do + rendered = render_inline(BlockLinkComponent.new(component: TestComponent)) - expect(rendered).to have_css('button[data-href="/"]', text: 'Example Link Text') + expect(rendered).to have_css('.style') + expect(rendered).to have_text('from test component') end end end diff --git a/spec/components/form_link_component_spec.rb b/spec/components/form_link_component_spec.rb new file mode 100644 index 00000000000..f6d11807824 --- /dev/null +++ b/spec/components/form_link_component_spec.rb @@ -0,0 +1,14 @@ +require 'rails_helper' + +RSpec.describe FormLinkComponent, type: :component do + let(:options) { { href: '/', method: :post } } + let(:content) { 'Title' } + + subject(:rendered) do + render_inline(described_class.new(**options).with_content(content)) + end + + it 'renders custom element with link' do + expect(rendered).to have_css('lg-form-link a[href="/"]', text: content) + end +end diff --git a/spec/features/visitors/email_confirmation_spec.rb b/spec/features/visitors/email_confirmation_spec.rb index 9e86c14bc27..2a9ac6680ac 100644 --- a/spec/features/visitors/email_confirmation_spec.rb +++ b/spec/features/visitors/email_confirmation_spec.rb @@ -46,7 +46,7 @@ it 'sends the confirmation email again' do sign_up_with('test@example.com') - expect { click_on t('links.resend') }. + expect { click_on t('notices.signed_up_but_unconfirmed.resend_confirmation_email') }. to change { ActionMailer::Base.deliveries.count }.by(1) expect(last_email.html_part.body).to have_content( diff --git a/spec/support/features/session_helper.rb b/spec/support/features/session_helper.rb index b5754b5dc38..70f3ba3a267 100644 --- a/spec/support/features/session_helper.rb +++ b/spec/support/features/session_helper.rb @@ -490,7 +490,7 @@ def submit_form_with_valid_but_wrong_email end def click_link_to_use_a_different_email - click_link t('notices.use_diff_email.link') + click_link t('notices.use_diff_email.link').upcase_first end def submit_form_with_valid_email(email = 'test@test.com') diff --git a/spec/views/sign_up/emails/show.html.erb_spec.rb b/spec/views/sign_up/emails/show.html.erb_spec.rb index b51b5daf051..b8f2e9f22d7 100644 --- a/spec/views/sign_up/emails/show.html.erb_spec.rb +++ b/spec/views/sign_up/emails/show.html.erb_spec.rb @@ -19,10 +19,14 @@ expect(rendered).to have_selector('h1', text: t('headings.verify_email')) end - it 'contains link to resend confirmation page' do + it 'contains a form link to resend confirmation page' do render - expect(rendered).to have_button(t('links.resend')) + expect(rendered).to have_selector('lg-form-link') + expect(rendered).to have_link(href: '#', class: ['usa-link', 'block-link']) + expect(rendered). + to have_button(t('notices.signed_up_but_unconfirmed.resend_confirmation_email')) + expect(rendered).to have_css("form[action='#{sign_up_register_path}']") end context 'when enable_load_testing_mode? is true and email address found' do