-
Notifications
You must be signed in to change notification settings - Fork 166
LG-9208: Confirm password #8161
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1f2b804
c0c69e0
314de59
a229005
4f1af11
71f861a
43b42fa
aea8907
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| <%= content_tag(:'lg-password-confirmation', **tag_options) do %> | ||
| <%= render ValidatedFieldComponent.new( | ||
| form: form, | ||
| name: :password, | ||
| type: :password, | ||
| label: default_label, | ||
| required: true, | ||
| **field_options, | ||
| input_html: field_options[:input_html].to_h.merge( | ||
| id: input_id, | ||
| class: ['password-confirmation__input1', *field_options.dig(:input_html, :class)], | ||
| ), | ||
| ) %> | ||
| <%= render ValidatedFieldComponent.new( | ||
| form: form, | ||
| name: :password_confirmation, | ||
| type: :password_confirmation, | ||
| label: confirmation_label, | ||
| required: true, | ||
| **field_options, | ||
| input_html: field_options[:input_html].to_h.merge( | ||
| id: input_confirmation_id, | ||
| class: ['password-confirmation__input2', *field_options.dig(:input_html, :class)], | ||
| ), | ||
| error_messages: { | ||
| valueMissing: t('components.password_confirmation.errors.empty'), | ||
| } | ||
| ) %> | ||
| <input | ||
| id="<%= toggle_id %>" | ||
| type="checkbox" | ||
| class="usa-checkbox__input password-toggle__toggle" | ||
| aria-controls="<%= input_id %>" | ||
| > | ||
| <label | ||
| for="<%= toggle_id %>" | ||
| class="usa-checkbox__label password-toggle__toggle-label" | ||
| > | ||
| <%= toggle_label %> | ||
| </label> | ||
| <% end %> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| class PasswordConfirmationComponent < BaseComponent | ||
| attr_reader :form, :label, :toggle_label, :field_options, :tag_options | ||
|
|
||
| def initialize( | ||
| form:, | ||
| toggle_label: t('components.password_toggle.toggle_label'), | ||
| field_options: {}, | ||
| **tag_options | ||
| ) | ||
| @form = form | ||
| @label = label | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like the assigned value here may not exist in scope. Should it be a constructor argument? |
||
| @toggle_label = toggle_label | ||
| @field_options = field_options | ||
| @tag_options = tag_options | ||
| end | ||
|
|
||
| def default_label | ||
| t('components.password_confirmation.label') | ||
| end | ||
|
|
||
| def confirmation_label | ||
| t('components.password_confirmation.confirm_label') | ||
| end | ||
|
|
||
| def toggle_id | ||
| "password-toggle-#{unique_id}" | ||
| end | ||
|
|
||
| def input_id | ||
| "password-input-#{unique_id}" | ||
| end | ||
|
|
||
| def input_confirmation_id | ||
| "password-confirmation-input-#{unique_id}" | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| import '@18f/identity-password-confirmation/password-confirmation-element'; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,13 @@ | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| "name": "@18f/identity-password-confirmation", | ||||||||||||||||||||||
| "private": true, | ||||||||||||||||||||||
| "version": "1.0.0", | ||||||||||||||||||||||
| "peerDependencies": { | ||||||||||||||||||||||
| "react": "^17.0.2" | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| "peerDependenciesMeta": { | ||||||||||||||||||||||
| "react": { | ||||||||||||||||||||||
| "optional": true | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
Comment on lines
+4
to
+12
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It doesn't appear that we're using React in the package.
Suggested change
|
||||||||||||||||||||||
| } | ||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| import userEvent from '@testing-library/user-event'; | ||
| import { getByLabelText } from '@testing-library/dom'; | ||
| import { useSandbox } from '@18f/identity-test-helpers'; | ||
| import * as analytics from '@18f/identity-analytics'; | ||
| import './password-confirmation-element'; | ||
| import type PasswordConfirmationElement from './password-confirmation-element'; | ||
|
|
||
| describe('PasswordConfirmationElement', () => { | ||
| let idCounter = 0; | ||
| const sandbox = useSandbox(); | ||
|
|
||
| function createElement() { | ||
| const element = document.createElement( | ||
| 'lg-password-confirmation', | ||
| ) as PasswordConfirmationElement; | ||
| const idSuffix = ++idCounter; | ||
| element.innerHTML = ` | ||
| <label for="input-${idSuffix}">Password</label> | ||
| <input id="input-${idSuffix}" class="password-confirmation__input1"> | ||
| <label for="input-${idSuffix}b">Confirm password</label> | ||
| <input id="input-${idSuffix}b" class="password-confirmation__input2"> | ||
| <div class="password-toggle__toggle-wrapper"> | ||
| <input | ||
| id="toggle-${idSuffix}" | ||
| type="checkbox" | ||
| class="password-toggle__toggle" | ||
| aria-controls="input-${idSuffix}" | ||
| > | ||
| <label for="toggle-${idSuffix}" class="usa-checkbox__label password-toggle__toggle-label"> | ||
| Show password | ||
| </label> | ||
| </div>`; | ||
| document.body.appendChild(element); | ||
| return element; | ||
| } | ||
|
|
||
| it('initializes input type', () => { | ||
| const element = createElement(); | ||
|
|
||
| const input = getByLabelText(element, 'Password') as HTMLInputElement; | ||
|
|
||
| expect(input.type).to.equal('password'); | ||
| }); | ||
|
|
||
| it('changes input type on toggle', async () => { | ||
| const element = createElement(); | ||
|
|
||
| const input = getByLabelText(element, 'Password') as HTMLInputElement; | ||
| const toggle = getByLabelText(element, 'Show password') as HTMLInputElement; | ||
|
|
||
| await userEvent.click(toggle); | ||
|
|
||
| expect(input.type).to.equal('text'); | ||
| }); | ||
|
|
||
| it('logs an event when clicking the Show Password button', async () => { | ||
| sandbox.stub(analytics, 'trackEvent'); | ||
| const element = createElement(); | ||
| const toggle = getByLabelText(element, 'Show password') as HTMLInputElement; | ||
|
|
||
| await userEvent.click(toggle); | ||
|
|
||
| expect(analytics.trackEvent).to.have.been.calledWith('Show Password button clicked', { | ||
| path: window.location.pathname, | ||
| }); | ||
| }); | ||
|
|
||
| it('should validate password confirmation', async () => { | ||
| const element = createElement(); | ||
| const input1 = getByLabelText(element, 'Password') as HTMLInputElement; | ||
| const input2 = getByLabelText(element, 'Confirm password') as HTMLInputElement; | ||
|
|
||
| await userEvent.type(input1, 'different_password1'); | ||
| await userEvent.type(input2, 'different_password2'); | ||
| expect(input2.validity.customError).to.be.true; | ||
|
|
||
| await userEvent.type(input1, 'matching_password!'); | ||
| await userEvent.type(input2, 'matching_password!'); | ||
| expect(input2.validity.customError).to.be.false; | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,66 @@ | ||||||||||||||||||||||||||||||||||
| import { trackEvent } from '@18f/identity-analytics'; | ||||||||||||||||||||||||||||||||||
| import { t } from '@18f/identity-i18n'; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| class PasswordConfirmationElement extends HTMLElement { | ||||||||||||||||||||||||||||||||||
| connectedCallback() { | ||||||||||||||||||||||||||||||||||
| this.toggle.addEventListener('change', () => this.setInputType()); | ||||||||||||||||||||||||||||||||||
| this.toggle.addEventListener('click', () => this.trackToggleEvent()); | ||||||||||||||||||||||||||||||||||
| this.input_confirmation.addEventListener('change', () => this.validatePassword()); | ||||||||||||||||||||||||||||||||||
| this.setInputType(); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Checkbox toggle for visibility. | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| get toggle(): HTMLInputElement { | ||||||||||||||||||||||||||||||||||
| return this.querySelector('.password-toggle__toggle')! as HTMLInputElement; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Text or password input. | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| get input(): HTMLInputElement { | ||||||||||||||||||||||||||||||||||
| return this.querySelector('.password-confirmation__input1')! as HTMLInputElement; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Text or password confirmation input. | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| get input_confirmation(): HTMLInputElement { | ||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should use camel-case in JavaScript (typically I'd expect the linter to flag this, but I don't know if it applies to class function names).
Suggested change
|
||||||||||||||||||||||||||||||||||
| return this.querySelector('.password-confirmation__input2')! as HTMLInputElement; | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+23
to
+30
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we're calling these "input" and "input confirmation" in the code, maybe we could represent the class names similarly for consistency?
Suggested change
|
||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| setInputType() { | ||||||||||||||||||||||||||||||||||
| const checked = this.toggle.checked ? 'text' : 'password'; | ||||||||||||||||||||||||||||||||||
| this.input.type = checked; | ||||||||||||||||||||||||||||||||||
| this.input_confirmation.type = checked; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| trackToggleEvent() { | ||||||||||||||||||||||||||||||||||
| trackEvent('Show Password button clicked', { path: window.location.pathname }); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| validatePassword() { | ||||||||||||||||||||||||||||||||||
| const password = this.input.value; | ||||||||||||||||||||||||||||||||||
| const confirmation = this.input_confirmation.value; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (password && password !== confirmation) { | ||||||||||||||||||||||||||||||||||
| const errorMsg = t('components.password_confirmation.errors.mismatch'); | ||||||||||||||||||||||||||||||||||
| this.input_confirmation.setCustomValidity(errorMsg); | ||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||
| this.input_confirmation.setCustomValidity(''); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| declare global { | ||||||||||||||||||||||||||||||||||
| interface HTMLElementTagNameMap { | ||||||||||||||||||||||||||||||||||
| 'lg-password-confirmation': PasswordConfirmationElement; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (!customElements.get('lg-password-confirmation')) { | ||||||||||||||||||||||||||||||||||
| customElements.define('lg-password-confirmation', PasswordConfirmationElement); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| export default PasswordConfirmationElement; | ||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this will need to be feature flagged in some manner since it will be backwards incompatible when running alongside a version that does not require password_confirmation.