diff --git a/src/assets/img/okta-sign-up-steps/dots.svg b/src/assets/img/okta-sign-up-steps/dots.svg new file mode 100644 index 000000000..3b114bfed --- /dev/null +++ b/src/assets/img/okta-sign-up-steps/dots.svg @@ -0,0 +1,3 @@ + diff --git a/src/assets/img/okta-sign-up-steps/one-complete.svg b/src/assets/img/okta-sign-up-steps/one-complete.svg new file mode 100644 index 000000000..58029fd2b --- /dev/null +++ b/src/assets/img/okta-sign-up-steps/one-complete.svg @@ -0,0 +1,4 @@ + diff --git a/src/assets/img/okta-sign-up-steps/one.svg b/src/assets/img/okta-sign-up-steps/one.svg new file mode 100644 index 000000000..2cfc5f47e --- /dev/null +++ b/src/assets/img/okta-sign-up-steps/one.svg @@ -0,0 +1,6 @@ + diff --git a/src/assets/img/okta-sign-up-steps/three-complete.svg b/src/assets/img/okta-sign-up-steps/three-complete.svg new file mode 100644 index 000000000..05ca13bac --- /dev/null +++ b/src/assets/img/okta-sign-up-steps/three-complete.svg @@ -0,0 +1,4 @@ + diff --git a/src/assets/img/okta-sign-up-steps/three.svg b/src/assets/img/okta-sign-up-steps/three.svg new file mode 100644 index 000000000..8f0f1e4fb --- /dev/null +++ b/src/assets/img/okta-sign-up-steps/three.svg @@ -0,0 +1,11 @@ + diff --git a/src/assets/img/okta-sign-up-steps/two-complete.svg b/src/assets/img/okta-sign-up-steps/two-complete.svg new file mode 100644 index 000000000..804eef15e --- /dev/null +++ b/src/assets/img/okta-sign-up-steps/two-complete.svg @@ -0,0 +1,4 @@ + diff --git a/src/assets/img/okta-sign-up-steps/two.svg b/src/assets/img/okta-sign-up-steps/two.svg new file mode 100644 index 000000000..c0f86cdb9 --- /dev/null +++ b/src/assets/img/okta-sign-up-steps/two.svg @@ -0,0 +1,4 @@ + diff --git a/src/assets/scss/components/_oktaSigninWidget.scss b/src/assets/scss/components/_oktaSigninWidget.scss index 105aa21a3..eb54c9639 100644 --- a/src/assets/scss/components/_oktaSigninWidget.scss +++ b/src/assets/scss/components/_oktaSigninWidget.scss @@ -8,8 +8,8 @@ } .auth-content { - padding-top: 10px; padding-bottom: 0; + padding-top: 10px; } .okta-form-title { @@ -18,11 +18,6 @@ } sign-up-modal { - #osw-container { - display: flex; - align-items: center; - justify-content: center; - } .back-to-sign-in { margin-top: 20px; @@ -30,30 +25,259 @@ sign-up-modal { } #okta-sign-in.auth-container { - // Injected cru back button + $okta-grey: #D9D6D6; + width: 100%; + + // ============== Container ============== // + + .o-form-content { + overflow: hidden; + padding-top: 10px; + } + + &.main-container { + box-shadow: none; + } + + .o-form-fieldset-container { + border: 1px solid $okta-grey; + border-radius: 7px; + display: flex; + flex-wrap: wrap; + } + + // ============== Errors ============== // + + .o-form-explain.o-form-input-error { + background: #ffe3e3; + padding-left: 20px; + padding-top: 6px; + } + + // ============== Fields ============== // + + .o-form-fieldset { + margin: 0; + position: relative; + } + + .o-form-label { + @include transition(top 0.3s ease, font-size 0.3s ease); + padding: 0; + padding-left: 8px; + position: absolute; + top: 17px; + width: auto; + z-index: 20; + + @media (prefers-reduced-motion) { + transition: none; + } + + &.active { + top: 2px; + + label { + font-size: 12px; + } + } + + label { + color: #9C9C9C; + font-size: 16px; + font-weight: 400; + } + + .o-form-explain { + font-size: 12px; + } + } + + .o-form-select-fieldset { + .o-form-label { + top: 2px; + + label { + font-size: 12px; + } + } + + .chzn-container { + &:focus-within { + box-shadow: none; + + &>.chzn-single { + border-color: $colorCru-gold; + box-shadow: none; + } + } + + &>.chzn-single { + border: 1px solid transparent; + border-radius: 0; + height: auto; + padding-top: 12px; + + div { + top: 6px; + } + } + } + + .chzn-container-single .chzn-drop { + border-radius: 0 0 6px 6px; + } + } + + // override okta styles + .focused-input, + .link.help:focus, + input[type=radio]:focus+label, + input[type=text]:focus { + + box-shadow: none + } + + .o-form-fieldset { + border-top: 1px solid $okta-grey; + width: 100%; + + &[data-se="o-form-fieldset-userProfile.state"], + &[data-se="o-form-fieldset-userProfile.zipCode"] { + width: 50%; + } + + &[data-se="o-form-fieldset-userProfile.zipCode"] { + border-left: 1px solid $okta-grey; + } + + &:first-child { + border-top: none; + } + + &:first-of-type { + + input[type=text]:not(.chzn-search input), + input[type=password], + .chzn-container a { + border-top-left-radius: 6px; + border-top-right-radius: 6px; + } + } + + &:last-of-type { + + // We don't include password as it's the only field on it's step and has the password requirements below it. + input[type=text]:not(.chzn-search input), + .chzn-container a { + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; + } + } + + // Allow the account type dropdown to have rounded corners if the organisation field is hidden + &:has(+ .o-form-fieldset[style="display: none;"]) .chzn-container a { + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; + } + } + + .o-form-input .o-form-control { + background: none; + border: none; + border-radius: 0; + height: auto; + + // Style input fields, but not input fields in dropdowns + input[type=text]:not(.chzn-search input), + input[type=password] { + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; + border: 1px solid transparent; + height: auto; + padding-bottom: 12px; + padding-top: 20px; + + &:focus { + border-color: $colorCru-gold; + box-shadow: none; + } + } + } + + // ============== Password rules ============== // + + section[data-se="password-authenticator--rules"] { + border-radius: 0 0 6px 6px; + border-top: 1px solid $okta-grey; + padding: 10px; + width: 100%; + + .password-authenticator--list { + margin: 0; + } + } + + // ============== Injected buttons ============== // + + // cru back button .o-form-button-bar { + align-items: center; display: flex; - gap: 50px; + gap: 10px; justify-content: space-between; - align-items: center; + + #backButton, + .button.button-primary { + padding-top: 7px; + padding-bottom: 7px; + } #backButton { + color: $colorCru-gray !important; + background-color: $colorCru-white !important; + border-color: $colorCru-gray !important; width: 100%; + + &:hover, + &:focus, + &:active { + color: $colorCru-white !important; + background: $colorCru-gray !important; + } } .button.button-primary { + color: #3a3a3a; height: auto; + @extend .btn; @extend .btn-primary; } } - // Injected cru error message + + // cru error message .cru-error { - float: left + float: left; } - // Injected cru retry button + // cru retry button .cru-retry-button:hover { text-decoration: underline; } } + +// ============== Okta overrides ============== // +// select dropdown focus style +.chzn-container:focus-within { + box-shadow: none; +} + +// select dropdown border radius style +.chzn-container-single .chzn-single { + border-radius: 0; +} +#countryCodeInput_chzn.chzn-container-single .chzn-single { + border-radius: 6px 6px 0 0; +} diff --git a/src/assets/scss/components/_signUpModal.scss b/src/assets/scss/components/_signUpModal.scss index 646839a97..a4def549b 100644 --- a/src/assets/scss/components/_signUpModal.scss +++ b/src/assets/scss/components/_signUpModal.scss @@ -1,6 +1,39 @@ sign-up-modal { position: relative; + .signup-steps { + display: flex; + justify-content: center; + gap: 16px; + padding: 20px 0 0; + width: 100%; + justify-content: center; + align-items: center; + gap: 16px; + padding: 10px 0; + + .between-steps { + img { + width: 18px; + } + } + + .step { + img { + width: 55px; + opacity: 1; + + &.semi-transparent { + opacity: .5; + } + } + } + } + + .okta-signup-loading { + padding: 153px 0 153px; + } + form { margin-bottom: 40px; } diff --git a/src/common/components/signUpModal/signUpFormCustomFields.js b/src/common/components/signUpModal/signUpFormCustomFields.js index f78886a11..ec55494f3 100644 --- a/src/common/components/signUpModal/signUpFormCustomFields.js +++ b/src/common/components/signUpModal/signUpFormCustomFields.js @@ -10,7 +10,8 @@ export const customFields = { label: 'Account Type', required: true, wide: true, - value: 'Household' + value: 'Household', + className: 'o-form-select-fieldset' }, organizationName: { name: 'organizationName', @@ -33,7 +34,8 @@ export const customFields = { 'label-top': true, multirowError: true, 'data-se': 'o-form-fieldset-userProfile.countryCode', - wide: true + wide: true, + className: 'o-form-select-fieldset' }, streetAddress: { name: 'userProfile.streetAddress', @@ -110,7 +112,8 @@ export const customFields = { wide: true, showWhen: { 'userProfile.countryCode': 'US' - } + }, + className: 'o-form-select-fieldset' }, zipCode: { name: 'userProfile.zipCode', diff --git a/src/common/components/signUpModal/signUpModal.component.js b/src/common/components/signUpModal/signUpModal.component.js index 3ce31bda9..f7118a32f 100644 --- a/src/common/components/signUpModal/signUpModal.component.js +++ b/src/common/components/signUpModal/signUpModal.component.js @@ -18,8 +18,8 @@ export const countryFieldSelector = '.o-form-fieldset[data-se="o-form-fieldset-u export const regionFieldSelector = '.o-form-fieldset[data-se="o-form-fieldset-userProfile.state"]' export const inputFieldErrorSelectorPrefix = '.o-form-input-name-' const componentName = 'signUpModal' -const signUpButtonText = 'Create an Account' -const nextButtonText = 'Next' +const signUpButtonText = 'Create Account' +const nextButtonText = 'Continue' const backButtonId = 'backButton' const backButtonText = 'Back' const errorIconHtml = '' @@ -55,16 +55,17 @@ class SignUpModalController { } initializeVariables () { + this.isLoading = true this.currentStep = 1 this.donorDetails = {} this.translations = {} this.signUpErrors = [] - this.isLoading = true this.submitting = false this.countryCodeOptions = {} this.countriesData = [] this.stateOptions = {} this.selectedCountry = {} + this.floatingLabelAbortControllers = [] } loadTranslations () { @@ -126,6 +127,7 @@ class SignUpModalController { this.signIn() this.loadCountries({ initial: true }).subscribe() + this.oktaSignInWidget.on('ready', this.ready.bind(this)) this.oktaSignInWidget.on('afterRender', this.afterRender.bind(this)) this.oktaSignInWidget.on('afterError', this.afterError.bind(this)) } @@ -269,6 +271,9 @@ class SignUpModalController { return this.geographiesService.getRegions(countryData).pipe( map((data) => { + // Order regions in alphabetical order + data.sort((a, b) => a['display-name'].localeCompare(b['display-name'])) + this.stateOptions = {} data.forEach(state => { this.stateOptions[state.name] = state['display-name'] @@ -414,11 +419,23 @@ class SignUpModalController { } afterRender (context) { + // Handle inactivity error + // The Okta widget has an issue where if the page is idle for a period of time, + // the Okta interaction session will expire, causing the widget to show an error "You have been logged out due to inactivity..." + // The user then has to click "back to sign in," to rerender the Okta widget. + // To avoid the error showing, we check if the context formName is 'terminal'. If it is, we know there was an error, + // and we re-render the widget to refresh the widget. + if (context.formName === 'terminal') { + this.reRenderWidget() + return + } + this.updateSignUpButtonText() this.resetCurrentStepOnRegistrationComplete(context) this.redirectToSignInModalIfNeeded(context) this.injectErrorMessages() this.injectBackButton() + this.initializeFloatingLabels() if (this.loadingCountriesError && this.currentStep === 1) { this.injectCountryLoadError() @@ -428,6 +445,12 @@ class SignUpModalController { } } + ready () { + this.$scope.$apply(() => { + this.isLoading = false + }) + } + updateSignUpButtonText () { // Change the text of the sign up button to ensure it's clear what the user is doing const signUpButton = angular.element(document.querySelector('.o-form-button-bar input.button.button-primary')) @@ -525,6 +548,39 @@ class SignUpModalController { }) } + initializeFloatingLabels () { + // As the Label and Input fields are not directly related in the DOM, we need to manually + // add the active class to the label when the input is focused or has a value. + + // Remove any existing listeners before adding new ones + this.floatingLabelAbortControllers.forEach(controller => controller.abort()) + this.floatingLabelAbortControllers = [] + + document.querySelectorAll('.o-form-content input[type="text"], .o-form-content input[type="password"]').forEach(input => { + const label = input.labels[0]?.parentNode + if (!label) { + return + } + // if the input already has a value, mark the label as active + if (input.value.trim() !== '') { + label.classList.add('active') + } + // Create and save the controller so we can later remove the listeners. + const controller = new AbortController() + this.floatingLabelAbortControllers.push(controller) + + input.addEventListener('focus', () => { + label.classList.add('active') + }, { signal: controller.signal }) + input.addEventListener('blur', () => { + // When the input loses focus, check its value. + if (input.value.trim() === '') { + label.classList.remove('active') + } + }, { signal: controller.signal }) + }) + } + goToNextStep () { this.currentStep++ this.reRenderWidget() @@ -550,17 +606,12 @@ class SignUpModalController { ) } - signIn (retrying = false) { + signIn () { return this.oktaSignInWidget.showSignInAndRedirect({ el: '#osw-container' }).then(tokens => { this.oktaSignInWidget.authClient.handleLoginRedirect(tokens) }).catch(error => { - if (!retrying) { - // Retry once - return this.reRenderWidget().then(() => this.signIn(true)) - } - this.$log.error('Error showing Okta sign in widget.', error) }) } diff --git a/src/common/components/signUpModal/signUpModal.tpl.html b/src/common/components/signUpModal/signUpModal.tpl.html index 2ce5c7fa4..a96a2252a 100644 --- a/src/common/components/signUpModal/signUpModal.tpl.html +++ b/src/common/components/signUpModal/signUpModal.tpl.html @@ -15,6 +15,49 @@