From 8693f2fd38289f39f3f603508a77f6e6d1ebb07d Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Thu, 9 Nov 2017 12:04:18 -0500 Subject: [PATCH] UX updates to registration form. - hide optional fields by default. - instructions only shown when focused in. - removed header - Terms of service link and checkbox on same line. --- .../pages/lms/login_and_register.py | 4 +- .../js/spec/student_account/register_spec.js | 13 ++ .../js/student_account/views/FormView.js | 2 +- .../js/student_account/views/RegisterView.js | 150 ++++++++++++++++++ lms/static/sass/views/_login-register.scss | 20 +++ .../student_account/form_field.underscore | 2 +- .../student_account/register.underscore | 12 +- openedx/core/djangoapps/user_api/api.py | 41 ++--- .../djangoapps/user_api/tests/test_views.py | 34 ++-- 9 files changed, 229 insertions(+), 49 deletions(-) diff --git a/common/test/acceptance/pages/lms/login_and_register.py b/common/test/acceptance/pages/lms/login_and_register.py index e4f400cb80..e14d7cdbb1 100644 --- a/common/test/acceptance/pages/lms/login_and_register.py +++ b/common/test/acceptance/pages/lms/login_and_register.py @@ -208,7 +208,7 @@ class CombinedLoginAndRegisterPage(PageObject): """ # Fill in the form - self.wait_for_element_visibility('#register-email', 'Email field is shown') + self.wait_for_element_visibility('#register-honor_code', 'Honor code field is shown') if email: self.q(css="#register-email").fill(email) if full_name: @@ -223,6 +223,8 @@ class CombinedLoginAndRegisterPage(PageObject): self.q(css="#register-favorite_movie").fill(favorite_movie) if terms_of_service: self.q(css="label[for='register-honor_code']").click() + self.q(css="#register-honor_code").click() + EmptyPromise(lambda: self.q(css='#register-honor_code:checked'), 'Honor code field is checked').fulfill() # Submit it self.q(css=".register-button").click() diff --git a/lms/static/js/spec/student_account/register_spec.js b/lms/static/js/spec/student_account/register_spec.js index b50e82c8f8..8d38c54520 100644 --- a/lms/static/js/spec/student_account/register_spec.js +++ b/lms/static/js/spec/student_account/register_spec.js @@ -279,6 +279,8 @@ // Create a fake click event var clickEvent = $.Event('click'); + $('#toggle_optional_fields').click(); + // Simulate manual entry of registration form data fillData(); @@ -481,6 +483,17 @@ expect(view.$submitButton).toHaveAttr('disabled'); }); + it('hides optional fields by default', function() { + createRegisterView(this); + expect(view.$('.optional-fields')).toHaveClass('hidden'); + }); + + it('displays optional fields when checkbox is selected', function() { + createRegisterView(this); + $('#toggle_optional_fields').click(); + expect(view.$('.optional-fields')).not.toHaveClass('hidden'); + }); + it('displays a modal with the terms of service', function() { var $modal, $content; diff --git a/lms/static/js/student_account/views/FormView.js b/lms/static/js/student_account/views/FormView.js index 412ecf6b10..f01861fd28 100644 --- a/lms/static/js/student_account/views/FormView.js +++ b/lms/static/js/student_account/views/FormView.js @@ -84,7 +84,7 @@ data[i].errorMessages = this.escapeStrings(data[i].errorMessages); } - html.push(_.template(fieldTpl)($.extend(data[i], { + html.push(HtmlUtils.template(fieldTpl)($.extend(data[i], { form: this.formType, requiredStr: this.requiredStr, optionalStr: this.optionalStr, diff --git a/lms/static/js/student_account/views/RegisterView.js b/lms/static/js/student_account/views/RegisterView.js index 57b5bca19a..bb64ba04fd 100644 --- a/lms/static/js/student_account/views/RegisterView.js +++ b/lms/static/js/student_account/views/RegisterView.js @@ -5,12 +5,14 @@ 'underscore', 'gettext', 'edx-ui-toolkit/js/utils/string-utils', + 'edx-ui-toolkit/js/utils/html-utils', 'js/student_account/views/FormView', 'text!templates/student_account/form_status.underscore' ], function( $, _, gettext, StringUtils, + HtmlUtils, FormView, formStatusTpl ) { @@ -65,6 +67,59 @@ this.listenTo(this.model, 'validation', this.renderLiveValidations); }, + + renderFields: function(fields, className) { + var html = [], + i, + fieldTpl = this.fieldTpl; + + html.push(HtmlUtils.joinHtml( + HtmlUtils.HTML('
') + )); + for (i = 0; i < fields.length; i++) { + html.push(HtmlUtils.template(fieldTpl)($.extend(fields[i], { + form: this.formType, + requiredStr: this.requiredStr, + optionalStr: this.optionalStr, + supplementalText: fields[i].supplementalText || '', + supplementalLink: fields[i].supplementalLink || '' + }))); + } + html.push('
'); + return html; + }, + + buildForm: function(data) { + var html = [], + i, + len = data.length, + requiredFields = [], + optionalFields = []; + + this.fields = data; + + for (i = 0; i < len; i++) { + if (data[i].errorMessages) { + // eslint-disable-next-line no-param-reassign + data[i].errorMessages = this.escapeStrings(data[i].errorMessages); + } + + if (data[i].required) { + requiredFields.push(data[i]); + } else { + optionalFields.push(data[i]); + } + } + + html = this.renderFields(requiredFields, 'required-fields'); + + html.push.apply(html, this.renderFields(optionalFields, 'optional-fields')); + + this.render(html.join('')); + }, + render: function(html) { var fields = html || '', formErrorsTitle = gettext('An error occurred.'); @@ -102,6 +157,101 @@ return this; }, + postRender: function() { + var inputs = [ + this.$('#register-name'), + this.$('#register-email'), + this.$('#register-username'), + this.$('#register-password'), + this.$('#register-country') + ], + inputTipSelectors = ['tip error', 'tip tip-input'], + inputTipSelectorsHidden = ['tip error hidden', 'tip tip-input hidden'], + onInputFocus = function() { + // Apply on focus styles to input + $(this).prev('label').addClass('focus-in') + .removeClass('focus-out'); + + // Show each input tip + $(this).siblings().each(function() { + if (inputTipSelectorsHidden.includes($(this).attr('class'))) { + $(this).removeClass('hidden'); + } + }); + }, + onInputFocusOut = function() { + // If input has no text apply focus out styles + if ($(this).val().length === 0) { + $(this).prev('label').addClass('focus-out') + .removeClass('focus-in'); + } + + // Hide each input tip + $(this).siblings().each(function() { + if (inputTipSelectors.includes($(this).attr('class'))) { + $(this).addClass('hidden'); + } + }); + }, + handleInputBehavior = function(input) { + // Initially put label in input + if (input.val().length === 0) { + input.prev('label').addClass('focus-out') + .removeClass('focus-in'); + } + + // Initially hide each input tip + input.siblings().each(function() { + if (inputTipSelectors.includes($(this).attr('class'))) { + $(this).addClass('hidden'); + } + }); + + input.focusin(onInputFocus); + input.focusout(onInputFocusOut); + }, + handleAutocomplete = function() { + inputs.forEach(function(input) { + if (input.val().length === 0 && !input.is(':-webkit-autofill')) { + input.prev('label').addClass('focus-out') + .removeClass('focus-in'); + } else { + input.prev('label').addClass('focus-in') + .removeClass('focus-out'); + } + }); + }; + + FormView.prototype.postRender.call(this); + $('.optional-fields').addClass('hidden'); + $('#toggle_optional_fields').change(function() { + window.analytics.track('edx.bi.user.register.optional_fields_selected'); + $('.optional-fields').toggleClass('hidden'); + }); + + // We are swapping the order of these elements here because the honor code agreement + // is a required checkbox field and the optional fields toggle is a cosmetic + // improvement so that we don't have to show all the optional fields. + // xss-lint: disable=javascript-jquery-insert-into-target + $('.checkbox-optional_fields_toggle').insertBefore('.optional-fields'); + // xss-lint: disable=javascript-jquery-insert-into-target + $('.checkbox-honor_code').insertAfter('.optional-fields'); + + // Clicking on links inside a label should open that link. + $('label a').click(function(ev) { + ev.stopPropagation(); + ev.preventDefault(); + window.open($(this).attr('href'), $(this).attr('target')); + }); + $('#register-country option:first').html(''); + inputs.forEach(function(input) { + if (input.length > 0) { + handleInputBehavior(input); + } + }); + setTimeout(handleAutocomplete, 1000); + }, + hideRequiredMessageExceptOnError: function($el) { // We only handle blur if not in an error state. if (!$el.hasClass('error')) { diff --git a/lms/static/sass/views/_login-register.scss b/lms/static/sass/views/_login-register.scss index 91b300c651..bebaba5e68 100644 --- a/lms/static/sass/views/_login-register.scss +++ b/lms/static/sass/views/_login-register.scss @@ -292,6 +292,7 @@ width: 100%; margin: ($baseline/2) 0 0 0; + &.select-year_of_birth { @include margin-left(15px); } @@ -313,6 +314,25 @@ font-family: $font-family-sans-serif; font-style: normal; font-weight: 500; + + &.focus-in { + position: relative; + padding-top: 0; + padding-left: 0; + opacity: 1; + } + + &.focus-out { + position: absolute; + padding-top: 5px; + padding-left: 9px; + opacity: 0.75; + z-index: 1; + } + + a { + z-index: 1; + } } #login-remember { diff --git a/lms/templates/student_account/form_field.underscore b/lms/templates/student_account/form_field.underscore index 05b3692b3b..4cba9a5cd1 100644 --- a/lms/templates/student_account/form_field.underscore +++ b/lms/templates/student_account/form_field.underscore @@ -102,7 +102,7 @@ /> <% if ( type === 'checkbox' ) { %>