Commit 8693f2fd by Diana Huang

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.
parent 96f35451
......@@ -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()
......
......@@ -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;
......
......@@ -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,
......
......@@ -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('<div class="'),
className,
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('</div>');
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')) {
......
......@@ -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 {
......
......@@ -102,7 +102,7 @@
/>
<% if ( type === 'checkbox' ) { %>
<label for="<%- form %>-<%- name %>">
<span class="label-text"><%- label %></span>
<span class="label-text"><%= HtmlUtils.HTML(label) %></span>
<% if ( required && type !== 'hidden' ) { %>
<span id="<%- form %>-<%- name %>-required-label"
class="label-required <% if ( !requiredStr ) { %>hidden<% } %>">
......
......@@ -6,8 +6,6 @@
<a href="#login" class="form-toggle" data-type="login"><%- gettext("Sign in.") %></a>
</div>
<h2><%- gettext('Create an Account')%></h2>
<form id="register" class="register-form" autocomplete="off" tabindex="-1" method="POST">
<% if (!context.currentProvider) { %>
......@@ -51,6 +49,16 @@
<%= context.fields %>
<div class="form-field checkbox-optional_fields_toggle">
<input type="checkbox" id="toggle_optional_fields" class="input-block checkbox"">
<label for="toggle_optional_fields">
<span class="label-text">
<%- gettext("Support education research by providing additional information") %>
</span>
</label>
</div>
<button type="submit" class="action action-primary action-update js-register register-button">
<% if ( context.registerFormSubmitButtonText ) { %><%- context.registerFormSubmitButtonText %><% } else { %><%- gettext("Create Account") %><% } %>
</button>
......
......@@ -9,6 +9,7 @@ from django_countries import countries
import accounts
import third_party_auth
from edxmako.shortcuts import marketing_link
from openedx.core.djangolib.markup import HTML, Text
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.user_api.helpers import FormDescription
from openedx.features.enterprise_support.api import enterprise_customer_for_request
......@@ -277,10 +278,6 @@ class RegistrationFormFactory(object):
# meant to hold the user's email address.
email_label = _(u"Email")
# Translators: This example email address is used as a placeholder in
# a field on the registration form meant to hold the user's email address.
email_placeholder = _(u"username@domain.com")
# Translators: These instructions appear on the registration form, immediately
# below a field meant to hold the user's email address.
email_instructions = _(u"This is what you will use to login.")
......@@ -289,7 +286,6 @@ class RegistrationFormFactory(object):
"email",
field_type="email",
label=email_label,
placeholder=email_placeholder,
instructions=email_instructions,
restrictions={
"min_length": accounts.EMAIL_MIN_LENGTH,
......@@ -308,6 +304,7 @@ class RegistrationFormFactory(object):
# Translators: This label appears above a field on the registration form
# meant to confirm the user's email address.
email_label = _(u"Confirm Email")
error_msg = accounts.REQUIRED_FIELD_CONFIRM_EMAIL_MSG
form_desc.add_field(
......@@ -330,10 +327,6 @@ class RegistrationFormFactory(object):
# meant to hold the user's full name.
name_label = _(u"Full Name")
# Translators: This example name is used as a placeholder in
# a field on the registration form meant to hold the user's name.
name_placeholder = _(u"Jane Q. Learner")
# Translators: These instructions appear on the registration form, immediately
# below a field meant to hold the user's full name.
name_instructions = _(u"This name will be used on any certificates that you earn.")
......@@ -341,7 +334,6 @@ class RegistrationFormFactory(object):
form_desc.add_field(
"name",
label=name_label,
placeholder=name_placeholder,
instructions=name_instructions,
restrictions={
"max_length": accounts.NAME_MAX_LENGTH,
......@@ -366,16 +358,10 @@ class RegistrationFormFactory(object):
u"The name that will identify you in your courses. "
u"It cannot be changed later."
)
# Translators: This example username is used as a placeholder in
# a field on the registration form meant to hold the user's username.
username_placeholder = _(u"Jane_Q_Learner")
form_desc.add_field(
"username",
label=username_label,
instructions=username_instructions,
placeholder=username_placeholder,
restrictions={
"min_length": accounts.USERNAME_MIN_LENGTH,
"max_length": accounts.USERNAME_MAX_LENGTH,
......@@ -711,16 +697,17 @@ class RegistrationFormFactory(object):
# form used to select the country in which the user lives.
country_label = _(u"Country or Region of Residence")
error_msg = accounts.REQUIRED_FIELD_COUNTRY_MSG
# If we set a country code, make sure it's uppercase for the sake of the form.
# pylint: disable=protected-access
default_country = form_desc._field_overrides.get('country', {}).get('defaultValue')
country_instructions = _(
# Translators: These instructions appear on the registration form, immediately
# below a field meant to hold the user's country.
u"The country or region where you live."
)
error_msg = accounts.REQUIRED_FIELD_COUNTRY_MSG
# If we set a country code, make sure it's uppercase for the sake of the form.
default_country = form_desc._field_overrides.get('country', {}).get('defaultValue')
if default_country:
form_desc.override_field_properties(
'country',
......@@ -751,7 +738,6 @@ class RegistrationFormFactory(object):
if self._is_field_visible("terms_of_service"):
terms_label = _(u"Honor Code")
terms_link = marketing_link("HONOR")
terms_text = _(u"Review the Honor Code")
# Combine terms of service and honor code checkboxes
else:
......@@ -759,13 +745,16 @@ class RegistrationFormFactory(object):
# in order to register a new account.
terms_label = _(u"Terms of Service and Honor Code")
terms_link = marketing_link("HONOR")
terms_text = _(u"Review the Terms of Service and Honor Code")
# Translators: "Terms of Service" is a legal document users must agree to
# in order to register a new account.
label = _(u"I agree to the {platform_name} {terms_of_service}").format(
label = Text(_(
u"I agree to the {platform_name} {terms_of_service_link_start}{terms_of_service}{terms_of_service_link_end}"
)).format(
platform_name=configuration_helpers.get_value("PLATFORM_NAME", settings.PLATFORM_NAME),
terms_of_service=terms_label
terms_of_service=terms_label,
terms_of_service_link_start=HTML("<a href='{terms_link}' target='_blank'>").format(terms_link=terms_link),
terms_of_service_link_end=HTML("</a>"),
)
# Translators: "Terms of Service" is a legal document users must agree to
......@@ -784,8 +773,6 @@ class RegistrationFormFactory(object):
error_messages={
"required": error_msg
},
supplementalLink=terms_link,
supplementalText=terms_text
)
def _add_terms_of_service_field(self, form_desc, required=True):
......
......@@ -972,6 +972,7 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
"default": False
}
]
link_template = "<a href='/honor' target='_blank'>{link_label}</a>"
def setUp(self):
super(RegistrationViewTest, self).setUp()
......@@ -1006,7 +1007,6 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
u"type": u"email",
u"required": True,
u"label": u"Email",
u"placeholder": u"username@domain.com",
u"instructions": u"This is what you will use to login.",
u"restrictions": {
"min_length": EMAIL_MIN_LENGTH,
......@@ -1022,7 +1022,6 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
u"type": u"text",
u"required": True,
u"label": u"Full Name",
u"placeholder": u"Jane Q. Learner",
u"instructions": u"This name will be used on any certificates that you earn.",
u"restrictions": {
"max_length": 255
......@@ -1037,7 +1036,6 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
u"type": u"text",
u"required": True,
u"label": u"Public Username",
u"placeholder": u"Jane_Q_Learner",
u"instructions": u"The name that will identify you in your courses. "
u"It cannot be changed later.",
u"restrictions": {
......@@ -1074,7 +1072,6 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
u"type": u"email",
u"required": True,
u"label": u"Email",
u"placeholder": u"username@domain.com",
u"instructions": u"This is what you will use to login.",
u"restrictions": {
"min_length": EMAIL_MIN_LENGTH,
......@@ -1171,7 +1168,6 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
u"type": u"email",
u"required": True,
u"label": u"Email",
u"placeholder": u"username@domain.com",
u"instructions": u"This is what you will use to login.",
u"restrictions": {
"min_length": EMAIL_MIN_LENGTH,
......@@ -1189,7 +1185,6 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
u"type": u"text",
u"required": True,
u"label": u"Full Name",
u"placeholder": u"Jane Q. Learner",
u"instructions": u"This name will be used on any certificates that you earn.",
u"restrictions": {
"max_length": NAME_MAX_LENGTH,
......@@ -1206,7 +1201,6 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
u"type": u"text",
u"required": True,
u"label": u"Public Username",
u"placeholder": u"Jane_Q_Learner",
u"instructions": u"The name that will identify you in your courses. "
u"It cannot be changed later.",
u"restrictions": {
......@@ -1520,13 +1514,14 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
)
@mock.patch.dict(settings.FEATURES, {"ENABLE_MKTG_SITE": True})
def test_registration_honor_code_mktg_site_enabled(self):
link_label = 'Terms of Service and Honor Code'
link_template = "<a href='https://www.test.com/honor' target='_blank'>{link_label}</a>"
link_label = "Terms of Service and Honor Code"
self._assert_reg_field(
{"honor_code": "required"},
{
"label": u"I agree to the {platform_name} {link_label}".format(
platform_name=settings.PLATFORM_NAME,
link_label=link_label
link_label=link_template.format(link_label=link_label)
),
"name": "honor_code",
"defaultValue": False,
......@@ -1544,13 +1539,13 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
@override_settings(MKTG_URLS_LINK_MAP={"HONOR": "honor"})
@mock.patch.dict(settings.FEATURES, {"ENABLE_MKTG_SITE": False})
def test_registration_honor_code_mktg_site_disabled(self):
link_label = 'Terms of Service and Honor Code'
link_label = "Terms of Service and Honor Code"
self._assert_reg_field(
{"honor_code": "required"},
{
"label": u"I agree to the {platform_name} {link_label}".format(
platform_name=settings.PLATFORM_NAME,
link_label=link_label
link_label=self.link_template.format(link_label=link_label)
),
"name": "honor_code",
"defaultValue": False,
......@@ -1575,12 +1570,13 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
# Honor code field should say ONLY honor code,
# not "terms of service and honor code"
link_label = 'Honor Code'
link_template = "<a href='https://www.test.com/honor' target='_blank'>{link_label}</a>"
self._assert_reg_field(
{"honor_code": "required", "terms_of_service": "required"},
{
"label": u"I agree to the {platform_name} {link_label}".format(
platform_name=settings.PLATFORM_NAME,
link_label=link_label
link_label=link_template.format(link_label=link_label)
),
"name": "honor_code",
"defaultValue": False,
......@@ -1596,7 +1592,7 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
)
# Terms of service field should also be present
link_label = 'Terms of Service'
link_label = "Terms of Service"
self._assert_reg_field(
{"honor_code": "required", "terms_of_service": "required"},
{
......@@ -1622,11 +1618,13 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
def test_registration_separate_terms_of_service_mktg_site_disabled(self):
# Honor code field should say ONLY honor code,
# not "terms of service and honor code"
link_label = 'Honor Code'
self._assert_reg_field(
{"honor_code": "required", "terms_of_service": "required"},
{
"label": u"I agree to the {platform_name} Honor Code".format(
platform_name=settings.PLATFORM_NAME
"label": u"I agree to the {platform_name} {link_label}".format(
platform_name=settings.PLATFORM_NAME,
link_label=self.link_template.format(link_label=link_label)
),
"name": "honor_code",
"defaultValue": False,
......@@ -1640,12 +1638,14 @@ class RegistrationViewTest(ThirdPartyAuthTestMixin, UserAPITestCase):
}
)
link_label = 'Terms of Service'
# Terms of service field should also be present
self._assert_reg_field(
{"honor_code": "required", "terms_of_service": "required"},
{
"label": u"I agree to the {platform_name} Terms of Service".format(
platform_name=settings.PLATFORM_NAME
"label": u"I agree to the {platform_name} {link_label}".format(
platform_name=settings.PLATFORM_NAME,
link_label=link_label
),
"name": "terms_of_service",
"defaultValue": False,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment