+
{props.message}
);
ValidationMessage.propTypes = {
className: PropTypes.string,
- message: PropTypes.string
+ message: PropTypes.string,
+ mode: PropTypes.string
};
module.exports = ValidationMessage;
diff --git a/src/components/forms/validation-message.scss b/src/components/forms/validation-message.scss
index d10e30e27..5cbbcb12b 100644
--- a/src/components/forms/validation-message.scss
+++ b/src/components/forms/validation-message.scss
@@ -39,6 +39,24 @@
}
}
+.validation-left {
+ $arrow-border-width: 1rem;
+ left: unset;
+ right: 0;
+ margin-left: unset;
+ margin-right: $arrow-border-width;
+ transform: translate(-16rem, 0);
+
+ &:before {
+ left: unset;
+ right: -$arrow-border-width / 2;
+ border-top: 1px solid $active-gray;
+ border-right: 1px solid $active-gray;
+ border-bottom: none;
+ border-left: none;
+ }
+}
+
@media #{$intermediate-and-smaller} {
.validation-message {
position: relative;
@@ -52,3 +70,21 @@
}
}
}
+
+.validation-error {
+ background-color: $ui-orange;
+
+ &:before {
+ background-color: $ui-orange;
+ }
+}
+
+.validation-info {
+ background-color: $ui-blue;
+ box-shadow: 0 0 4px 2px rgba(0, 0, 0, .15);
+ font-weight: 500;
+
+ &:before {
+ background-color: $ui-blue;
+ }
+}
diff --git a/src/components/info-button/info-button.jsx b/src/components/info-button/info-button.jsx
index 40b488d12..f79890c05 100644
--- a/src/components/info-button/info-button.jsx
+++ b/src/components/info-button/info-button.jsx
@@ -1,6 +1,9 @@
const bindAll = require('lodash.bindall');
const PropTypes = require('prop-types');
const React = require('react');
+const MediaQuery = require('react-responsive').default;
+
+const frameless = require('../../lib/frameless');
require('./info-button.scss');
@@ -22,25 +25,38 @@ class InfoButton extends React.Component {
this.setState({visible: true});
}
render () {
- return (
-
- {this.state.visible && (
-
- {this.props.message}
-
- )}
+ const messageJsx = this.state.visible && (
+
+ {this.props.message}
);
+ return (
+
+
+
+ {messageJsx}
+
+
+ {/* for small screens, add additional position: relative element,
+ so info message can position itself relative to the width which
+ encloses info-button -- rather than relative to info-button itself */}
+
+
+ {messageJsx}
+
+
+
+ );
}
}
InfoButton.propTypes = {
- message: PropTypes.string
+ message: PropTypes.string.isRequired
};
module.exports = InfoButton;
diff --git a/src/components/info-button/info-button.scss b/src/components/info-button/info-button.scss
index 7e5dfc8c6..5daa65a26 100644
--- a/src/components/info-button/info-button.scss
+++ b/src/components/info-button/info-button.scss
@@ -9,17 +9,9 @@
margin-left: .375rem;
border-radius: 50%;
background-color: $ui-blue;
-
- &:after {
- position: absolute;
- content: "?";
- color: $ui-white;
- font-family: verdana;
- font-weight: 400;
- top: -.125rem;
- left: .325rem;
- font-size: .75rem;
- }
+ background-image: url("/svgs/info-button/info-button.svg");
+ background-size: cover;
+ top: .125rem;
}
.info-button-message {
@@ -41,7 +33,7 @@
line-height: 1.25rem;
text-align: left;
font-size: .875rem;
- z-index: 1;
+ z-index: 2;
&:before {
display: block;
@@ -65,11 +57,12 @@
@media #{$intermediate-and-smaller} {
.info-button-message {
- position: relative;
+ position: absolute;
transform: none;
- margin: inherit;
- width: 100%;
- height: inherit;
+ /* since we're positioning message relative to info-button's parent,
+ we need to center this element within its width. */
+ margin: 0 calc((100% - 16.5rem) / 2);;
+ top: .125rem;
&:before {
display: none;
diff --git a/src/components/join-flow/birthdate-step.jsx b/src/components/join-flow/birthdate-step.jsx
index d83a7814d..f722b50d6 100644
--- a/src/components/join-flow/birthdate-step.jsx
+++ b/src/components/join-flow/birthdate-step.jsx
@@ -87,8 +87,10 @@ class BirthDateStep extends React.Component {
return (
diff --git a/src/components/join-flow/country-step.jsx b/src/components/join-flow/country-step.jsx
index 227ec4f94..86eee7497 100644
--- a/src/components/join-flow/country-step.jsx
+++ b/src/components/join-flow/country-step.jsx
@@ -29,7 +29,7 @@ class CountryStep extends React.Component {
this.countryOptions = [...countryData.registrationCountryOptions];
this.countryOptions.unshift({
disabled: true,
- label: this.props.intl.formatMessage({id: 'registration.selectCountry'}),
+ label: this.props.intl.formatMessage({id: 'general.country'}),
value: 'null'
});
}
@@ -68,7 +68,9 @@ class CountryStep extends React.Component {
return (
)}
- headerImgSrc="/images/hoc/getting-started.jpg"
- innerContentClassName="modal-inner-content-email"
+ headerImgSrc="/images/join-flow/email-header.png"
+ innerClassName="join-flow-inner-email-step"
nextButton={this.props.intl.formatMessage({id: 'registration.createAccount'})}
title={this.props.intl.formatMessage({id: 'registration.emailStepTitle'})}
waiting={isSubmitting}
@@ -89,10 +101,21 @@ class EmailStep extends React.Component {
id="email"
name="email"
placeholder={this.props.intl.formatMessage({id: 'general.emailAddress'})}
- validate={this.validateEmailIfPresent}
+ validate={this.validateEmail}
validationClassName="validation-full-width-input"
- onBlur={() => validateField('email')} // eslint-disable-line react/jsx-no-bind
+ /* eslint-disable react/jsx-no-bind */
+ onBlur={() => validateField('email')}
+ onFocus={() => setFieldError('email', null)}
+ /* eslint-enable react/jsx-no-bind */
+ onSetRef={this.handleSetEmailRef}
/>
+
+
+
);
}}
diff --git a/src/components/join-flow/gender-step.jsx b/src/components/join-flow/gender-step.jsx
index ec6b23870..4d389ff79 100644
--- a/src/components/join-flow/gender-step.jsx
+++ b/src/components/join-flow/gender-step.jsx
@@ -11,10 +11,12 @@ const JoinFlowStep = require('./join-flow-step.jsx');
require('./join-flow-steps.scss');
const GenderOption = ({
+ id,
label,
onSetFieldValue,
selectedValue,
- value
+ value,
+ ...props
}) => (
);
GenderOption.propTypes = {
+ id: PropTypes.string,
label: PropTypes.string,
onSetFieldValue: PropTypes.func,
selectedValue: PropTypes.string,
@@ -50,9 +55,13 @@ class GenderStep extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
+ 'handleSetCustomRef',
'handleValidSubmit'
]);
}
+ handleSetCustomRef (customInputRef) {
+ this.customInput = customInputRef;
+ }
handleValidSubmit (formData, formikBag) {
formikBag.setSubmitting(false);
if (!formData.gender || formData.gender === 'null') {
@@ -80,25 +89,34 @@ class GenderStep extends React.Component {
} = props;
return (
+
setFieldValue('gender', values.custom, false)}
+ onClick={() => {
+ setFieldValue('gender', values.custom, false);
+ if (this.customInput) this.customInput.focus();
+ }}
/* eslint-enable react/jsx-no-bind */
>
setValues({
gender: newCustomVal,
custom: newCustomVal
})}
+ onSetCustomRef={this.handleSetCustomRef}
/* eslint-enable react/jsx-no-bind */
/>
(
);
JoinFlowStep.propTypes = {
children: PropTypes.node,
- className: PropTypes.string,
description: PropTypes.string,
+ descriptionClassName: PropTypes.string,
footerContent: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
headerImgSrc: PropTypes.string,
infoMessage: PropTypes.string,
- innerContentClassName: PropTypes.string,
+ innerClassName: PropTypes.string,
nextButton: PropTypes.node,
onSubmit: PropTypes.func,
title: PropTypes.string,
diff --git a/src/components/join-flow/join-flow-step.scss b/src/components/join-flow/join-flow-step.scss
index f16bcc51c..1b13cbec6 100644
--- a/src/components/join-flow/join-flow-step.scss
+++ b/src/components/join-flow/join-flow-step.scss
@@ -1,6 +1,24 @@
@import "../../colors";
@import "../../frameless";
+.join-flow-outer-content {
+ /* hopefully this lets text expand the height of the modal, if need be */
+ min-height: 32.5rem;
+ display: flex;
+ justify-content: space-between;
+ flex-direction: column;
+ overflow-wrap: break-word;
+}
+
+.join-flow-inner-content {
+ box-shadow: none;
+ width: calc(100% - 5.875rem);
+ /* must use padding for top, rather than margin, because margins will collapse */
+ margin: 0 auto;
+ padding: 2.3125rem 0 2.5rem;
+ font-size: .875rem;
+}
+
.join-flow-title {
color: $type-gray;
font-size: 1.875rem;
@@ -15,25 +33,21 @@
text-align: center;
}
-.join-flow-inner-content {
- box-shadow: none;
- width: calc(100% - 5.875rem);
- /* must use padding for top, rather than margin, because margins will collapse */
- margin: 0 auto;
- padding: 2.3125rem 0 2.5rem;
- font-size: .875rem;
-}
-
/* overflow will only work if this class is set on parent of img, not img itself */
-.join-flow-header-image {
+.join-flow-header-image-wrapper {
width: 100%;
- height: 7.5rem;
+ min-height: 7.5rem;
+ max-height: 8.75rem;
overflow: hidden;
margin: 0;
border-top-left-radius: 1rem;
border-top-right-radius: 1rem;
}
+.join-flow-header-image {
+ width: 27.5rem;
+}
+
.join-flow-footer-message {
width: 100%;
padding: 1.125rem 1.5rem 1.125rem;
diff --git a/src/components/join-flow/join-flow-steps.scss b/src/components/join-flow/join-flow-steps.scss
index ce124b89e..a8d9550ea 100644
--- a/src/components/join-flow/join-flow-steps.scss
+++ b/src/components/join-flow/join-flow-steps.scss
@@ -13,6 +13,10 @@
}
}
+.join-flow-input-password {
+ font-size: 1.5rem;
+}
+
.join-flow-password-confirm {
margin-bottom: .6875rem;
}
@@ -38,8 +42,13 @@
transform: translate(21.5625rem, 0);
}
-.validation-birthdate-input {
- transform: translate(8.75rem, .25rem);
+.validation-birthdate-month {
+ transform: translate(-9.25rem, 0);
+ width: 7.25rem;
+}
+
+.validation-birthdate-year {
+ transform: translate(8.75rem, 0);
width: 7.25rem;
}
@@ -55,9 +64,22 @@
}
}
+.select .join-flow-select {
+ height: 3.5rem;
+ background-color: white;
+ border-color: $box-shadow-light-gray;
+ font-size: 1rem;
+ font-weight: 500;
+ padding-right: 3.25rem;
+}
+
.select .join-flow-select-month {
- width: 9.125rem;
margin-right: .5rem;
+ width: 9.125rem;
+}
+
+.select .join-flow-select-year {
+ width: 9.125rem;
}
.select .join-flow-select-country {
@@ -74,24 +96,68 @@
margin: 0 auto;
}
-.join-flow-gender-step {
- height: 27.375rem;
+.join-flow-inner-username-step {
+ padding-top: 2.75rem;
+}
+
+.join-flow-inner-birthdate-step {
+ padding-top: 1rem;
+ padding-bottom: 2.25rem;
+}
+
+.join-flow-inner-gender-step {
+ /* need height so that flex will adjust children proportionately */
+ height: 27.25rem;
+ padding-top: 2.625rem;
+ padding-bottom: 1rem;
+}
+
+.join-flow-inner-country-step {
+ padding-top: 1rem;
+ padding-bottom: 2rem;
+}
+
+.join-flow-inner-email-step {
padding-top: 3rem;
}
+.join-flow-inner-welcome-step {
+ padding-top: 3rem;
+}
+
+.join-flow-birthdate-description {
+ margin-top: 1.25rem;
+ margin-right: -.5rem;
+ margin-bottom: 2rem;
+ margin-left: -.5rem;
+}
+
+.join-flow-gender-description {
+ margin-top: .625rem;
+ margin-bottom: 1.25rem;
+}
+
+.join-flow-country-description {
+ margin-top: 1rem;
+}
+
.gender-radio-row {
transition: all .125s ease;
width: 20.875rem;
height: 2.85rem;
background-color: $ui-gray;
border-radius: .5rem;
- margin-bottom: 0.375rem;
+ margin: 0 auto 0.375rem;
padding-left: 0.8125rem;
display: flex;
align-items: center;
}
-.gender-radio-row-selected {
+.gender-radio-row:hover {
+ background-color: $ui-blue-10percent;
+}
+
+.gender-radio-row-selected, .gender-radio-row-selected:hover {
transition: all .125s ease;
background-color: $ui-blue-25percent;
}
@@ -106,6 +172,11 @@
padding-top: 2.9rem;
}
+.join-flow-email-checkbox-row {
+ font-size: .75rem;
+ margin: .25rem .125rem;
+}
+
a.join-flow-link:link, a.join-flow-link:visited, a.join-flow-link:active {
text-decoration: underline;
}
diff --git a/src/components/join-flow/username-step.jsx b/src/components/join-flow/username-step.jsx
index 33092f57c..a1783a958 100644
--- a/src/components/join-flow/username-step.jsx
+++ b/src/components/join-flow/username-step.jsx
@@ -21,16 +21,34 @@ class UsernameStep extends React.Component {
super(props);
bindAll(this, [
'handleChangeShowPassword',
+ 'handleFocused',
+ 'handleSetUsernameRef',
'handleValidSubmit',
'validatePasswordIfPresent',
'validatePasswordConfirmIfPresent',
'validateUsernameIfPresent',
'validateForm'
]);
+ this.state = {
+ focused: null,
+ showPassword: false
+ };
+ }
+ componentDidMount () {
+ // automatically start with focus on username field
+ if (this.usernameInput) this.usernameInput.focus();
}
handleChangeShowPassword () {
this.setState({showPassword: !this.state.showPassword});
}
+ // track the currently focused input field, to determine whether each field should
+ // display a tooltip. (We only display it if a field is focused and has never been touched.)
+ handleFocused (fieldName) {
+ this.setState({focused: fieldName});
+ }
+ handleSetUsernameRef (usernameInputRef) {
+ this.usernameInput = usernameInputRef;
+ }
// we allow username to be empty on blur, since you might not have typed anything yet
validateUsernameIfPresent (username) {
if (!username) return null; // skip validation if username is blank; null indicates valid
@@ -109,7 +127,9 @@ class UsernameStep extends React.Component {
handleSubmit,
isSubmitting,
setFieldError,
+ setFieldTouched,
setFieldValue,
+ touched,
validateField,
values
} = props;
@@ -118,6 +138,7 @@ class UsernameStep extends React.Component {
description={this.props.intl.formatMessage({
id: 'registration.usernameStepDescriptionNonEducator'
})}
+ innerClassName="join-flow-inner-username-step"
title={this.props.intl.formatMessage({id: 'general.joinScratch'})}
waiting={isSubmitting}
onSubmit={handleSubmit}
@@ -133,15 +154,21 @@ class UsernameStep extends React.Component {
error={errors.username}
id="username"
name="username"
+ placeholder={this.props.intl.formatMessage({id: 'general.username'})}
+ toolTip={this.state.focused === 'username' && !touched.username &&
+ this.props.intl.formatMessage({id: 'registration.usernameAdviceShort'})}
validate={this.validateUsernameIfPresent}
validationClassName="validation-full-width-input"
/* eslint-disable react/jsx-no-bind */
onBlur={() => validateField('username')}
onChange={e => {
setFieldValue('username', e.target.value);
+ setFieldTouched('username');
setFieldError('username', null);
}}
+ onFocus={() => this.handleFocused('username')}
/* eslint-enable react/jsx-no-bind */
+ onSetRef={this.handleSetUsernameRef}
/>
@@ -149,11 +176,16 @@ class UsernameStep extends React.Component {
0}
)}
error={errors.password}
id="password"
name="password"
+ placeholder={this.props.intl.formatMessage({id: 'general.password'})}
+ toolTip={this.state.focused === 'password' && !touched.password &&
+ this.props.intl.formatMessage({id: 'registration.passwordAdviceShort'})}
type={values.showPassword ? 'text' : 'password'}
/* eslint-disable react/jsx-no-bind */
validate={password => this.validatePasswordIfPresent(password, values.username)}
@@ -161,19 +193,34 @@ class UsernameStep extends React.Component {
onBlur={() => validateField('password')}
onChange={e => {
setFieldValue('password', e.target.value);
+ setFieldTouched('password');
setFieldError('password', null);
}}
+ onFocus={() => this.handleFocused('password')}
/* eslint-enable react/jsx-no-bind */
/>
0,
+ 'fail': errors.passwordConfirm
+ }
)}
error={errors.passwordConfirm}
id="passwordConfirm"
name="passwordConfirm"
+ placeholder={this.props.intl.formatMessage({
+ id: 'registration.confirmPasswordInstruction'
+ })}
+ toolTip={
+ this.state.focused === 'passwordConfirm' && !touched.passwordConfirm &&
+ this.props.intl.formatMessage({
+ id: 'registration.confirmPasswordInstruction'
+ })
+ }
type={values.showPassword ? 'text' : 'password'}
/* eslint-disable react/jsx-no-bind */
validate={() =>
@@ -181,13 +228,13 @@ class UsernameStep extends React.Component {
values.passwordConfirm)
}
validationClassName="validation-full-width-input"
- onBlur={() =>
- validateField('passwordConfirm')
- }
+ onBlur={() => validateField('passwordConfirm')}
onChange={e => {
setFieldValue('passwordConfirm', e.target.value);
+ setFieldTouched('passwordConfirm');
setFieldError('passwordConfirm', null);
}}
+ onFocus={() => this.handleFocused('passwordConfirm')}
/* eslint-enable react/jsx-no-bind */
/>
diff --git a/src/components/join-flow/welcome-step.jsx b/src/components/join-flow/welcome-step.jsx
index def0462d3..42b9ac8b1 100644
--- a/src/components/join-flow/welcome-step.jsx
+++ b/src/components/join-flow/welcome-step.jsx
@@ -27,8 +27,6 @@ class WelcomeStep extends React.Component {
render () {
return (
diff --git a/src/components/modal/join/modal.jsx b/src/components/modal/join/modal.jsx
index c8898b4e2..d309433ee 100644
--- a/src/components/modal/join/modal.jsx
+++ b/src/components/modal/join/modal.jsx
@@ -6,15 +6,14 @@ const JoinFlow = require('../../join-flow/join-flow.jsx');
require('./modal.scss');
const JoinModal = ({
- isOpen,
onCompleteRegistration, // eslint-disable-line no-unused-vars
onRequestClose,
...modalProps
}) => (
@@ -25,7 +24,6 @@ const JoinModal = ({
);
JoinModal.propTypes = {
- isOpen: PropTypes.bool,
onCompleteRegistration: PropTypes.func,
onRequestClose: PropTypes.func
};
diff --git a/src/components/modal/ttt/modal.jsx b/src/components/modal/ttt/modal.jsx
index e54bcb085..333f8011b 100644
--- a/src/components/modal/ttt/modal.jsx
+++ b/src/components/modal/ttt/modal.jsx
@@ -59,7 +59,7 @@ const TTTModal = props => (
rel="noopener noreferrer"
target="_blank"
>
-
+
@@ -76,7 +76,7 @@ const TTTModal = props => (
rel="noopener noreferrer"
target="_blank"
>
-
+
diff --git a/src/components/navigation/www/navigation.jsx b/src/components/navigation/www/navigation.jsx
index ab5c13fef..7bc3ca663 100644
--- a/src/components/navigation/www/navigation.jsx
+++ b/src/components/navigation/www/navigation.jsx
@@ -23,8 +23,6 @@ const AccountNav = require('./accountnav.jsx');
require('./navigation.scss');
-const USE_SCRATCH3_REGISTRATION = false;
-
class Navigation extends React.Component {
constructor (props) {
super(props);
@@ -48,7 +46,7 @@ class Navigation extends React.Component {
}
componentDidUpdate (prevProps) {
if (prevProps.user !== this.props.user) {
- this.props.closeAccountMenus();
+ this.props.handleCloseAccountNav();
if (this.props.user) {
const intervalId = setInterval(() => {
this.props.getMessageCount(this.props.user.username);
@@ -198,17 +196,6 @@ class Navigation extends React.Component {