mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-27 01:25:52 -05:00
Merge pull request #3251 from benjiwheeler/join-flow-highlighting-positive
Join flow: show positive blue tooltips when you focus an input that is empty
This commit is contained in:
commit
a5b9cdc410
7 changed files with 114 additions and 9 deletions
|
@ -11,6 +11,7 @@ require('./formik-input.scss');
|
||||||
const FormikInput = ({
|
const FormikInput = ({
|
||||||
className,
|
className,
|
||||||
error,
|
error,
|
||||||
|
toolTip,
|
||||||
validationClassName,
|
validationClassName,
|
||||||
wrapperClassName,
|
wrapperClassName,
|
||||||
...props
|
...props
|
||||||
|
@ -31,10 +32,17 @@ const FormikInput = ({
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
{error && (
|
{error ? (
|
||||||
<ValidationMessage
|
<ValidationMessage
|
||||||
className={validationClassName}
|
className={validationClassName}
|
||||||
message={error}
|
message={error}
|
||||||
|
mode="error"
|
||||||
|
/>
|
||||||
|
) : toolTip && (
|
||||||
|
<ValidationMessage
|
||||||
|
className={validationClassName}
|
||||||
|
message={toolTip}
|
||||||
|
mode="info"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -44,6 +52,7 @@ const FormikInput = ({
|
||||||
FormikInput.propTypes = {
|
FormikInput.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
error: PropTypes.string,
|
error: PropTypes.string,
|
||||||
|
toolTip: PropTypes.string,
|
||||||
type: PropTypes.string,
|
type: PropTypes.string,
|
||||||
validationClassName: PropTypes.string,
|
validationClassName: PropTypes.string,
|
||||||
wrapperClassName: PropTypes.string
|
wrapperClassName: PropTypes.string
|
||||||
|
|
|
@ -39,6 +39,7 @@ const FormikSelect = ({
|
||||||
<ValidationMessage
|
<ValidationMessage
|
||||||
className={validationClassName}
|
className={validationClassName}
|
||||||
message={error}
|
message={error}
|
||||||
|
mode="error"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,14 +5,24 @@ const React = require('react');
|
||||||
require('./validation-message.scss');
|
require('./validation-message.scss');
|
||||||
|
|
||||||
const ValidationMessage = props => (
|
const ValidationMessage = props => (
|
||||||
<div className={classNames(['validation-message', props.className])}>
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'validation-message',
|
||||||
|
{
|
||||||
|
'validation-error': props.mode === 'error',
|
||||||
|
'validation-info': props.mode === 'info'
|
||||||
|
},
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
{props.message}
|
{props.message}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
ValidationMessage.propTypes = {
|
ValidationMessage.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
message: PropTypes.string
|
message: PropTypes.string,
|
||||||
|
mode: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = ValidationMessage;
|
module.exports = ValidationMessage;
|
||||||
|
|
|
@ -11,7 +11,6 @@
|
||||||
margin-left: $arrow-border-width;
|
margin-left: $arrow-border-width;
|
||||||
border: 1px solid $active-gray;
|
border: 1px solid $active-gray;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background-color: $ui-orange;
|
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
max-width: 18.75rem;
|
max-width: 18.75rem;
|
||||||
min-height: 1rem;
|
min-height: 1rem;
|
||||||
|
@ -31,7 +30,6 @@
|
||||||
border-left: 1px solid $active-gray;
|
border-left: 1px solid $active-gray;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|
||||||
background-color: $ui-orange;
|
|
||||||
width: $arrow-border-width;
|
width: $arrow-border-width;
|
||||||
height: $arrow-border-width;
|
height: $arrow-border-width;
|
||||||
|
|
||||||
|
@ -52,3 +50,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.validation-error {
|
||||||
|
background-color: $ui-orange;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
background-color: $ui-orange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-info {
|
||||||
|
background-color: $ui-blue;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
background-color: $ui-blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -21,16 +21,26 @@ class UsernameStep extends React.Component {
|
||||||
super(props);
|
super(props);
|
||||||
bindAll(this, [
|
bindAll(this, [
|
||||||
'handleChangeShowPassword',
|
'handleChangeShowPassword',
|
||||||
|
'handleFocused',
|
||||||
'handleValidSubmit',
|
'handleValidSubmit',
|
||||||
'validatePasswordIfPresent',
|
'validatePasswordIfPresent',
|
||||||
'validatePasswordConfirmIfPresent',
|
'validatePasswordConfirmIfPresent',
|
||||||
'validateUsernameIfPresent',
|
'validateUsernameIfPresent',
|
||||||
'validateForm'
|
'validateForm'
|
||||||
]);
|
]);
|
||||||
|
this.state = {
|
||||||
|
focused: null,
|
||||||
|
showPassword: false
|
||||||
|
};
|
||||||
}
|
}
|
||||||
handleChangeShowPassword () {
|
handleChangeShowPassword () {
|
||||||
this.setState({showPassword: !this.state.showPassword});
|
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});
|
||||||
|
}
|
||||||
// we allow username to be empty on blur, since you might not have typed anything yet
|
// we allow username to be empty on blur, since you might not have typed anything yet
|
||||||
validateUsernameIfPresent (username) {
|
validateUsernameIfPresent (username) {
|
||||||
if (!username) return null; // skip validation if username is blank; null indicates valid
|
if (!username) return null; // skip validation if username is blank; null indicates valid
|
||||||
|
@ -109,7 +119,9 @@ class UsernameStep extends React.Component {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
setFieldError,
|
setFieldError,
|
||||||
|
setFieldTouched,
|
||||||
setFieldValue,
|
setFieldValue,
|
||||||
|
touched,
|
||||||
validateField,
|
validateField,
|
||||||
values
|
values
|
||||||
} = props;
|
} = props;
|
||||||
|
@ -135,14 +147,18 @@ class UsernameStep extends React.Component {
|
||||||
id="username"
|
id="username"
|
||||||
name="username"
|
name="username"
|
||||||
placeholder={this.props.intl.formatMessage({id: 'general.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}
|
validate={this.validateUsernameIfPresent}
|
||||||
validationClassName="validation-full-width-input"
|
validationClassName="validation-full-width-input"
|
||||||
/* eslint-disable react/jsx-no-bind */
|
/* eslint-disable react/jsx-no-bind */
|
||||||
onBlur={() => validateField('username')}
|
onBlur={() => validateField('username')}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setFieldValue('username', e.target.value);
|
setFieldValue('username', e.target.value);
|
||||||
|
setFieldTouched('username');
|
||||||
setFieldError('username', null);
|
setFieldError('username', null);
|
||||||
}}
|
}}
|
||||||
|
onFocus={() => this.handleFocused('username')}
|
||||||
/* eslint-enable react/jsx-no-bind */
|
/* eslint-enable react/jsx-no-bind */
|
||||||
/>
|
/>
|
||||||
<div className="join-flow-password-section">
|
<div className="join-flow-password-section">
|
||||||
|
@ -157,6 +173,8 @@ class UsernameStep extends React.Component {
|
||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
placeholder={this.props.intl.formatMessage({id: 'general.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'}
|
type={values.showPassword ? 'text' : 'password'}
|
||||||
/* eslint-disable react/jsx-no-bind */
|
/* eslint-disable react/jsx-no-bind */
|
||||||
validate={password => this.validatePasswordIfPresent(password, values.username)}
|
validate={password => this.validatePasswordIfPresent(password, values.username)}
|
||||||
|
@ -164,8 +182,10 @@ class UsernameStep extends React.Component {
|
||||||
onBlur={() => validateField('password')}
|
onBlur={() => validateField('password')}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setFieldValue('password', e.target.value);
|
setFieldValue('password', e.target.value);
|
||||||
|
setFieldTouched('password');
|
||||||
setFieldError('password', null);
|
setFieldError('password', null);
|
||||||
}}
|
}}
|
||||||
|
onFocus={() => this.handleFocused('password')}
|
||||||
/* eslint-enable react/jsx-no-bind */
|
/* eslint-enable react/jsx-no-bind */
|
||||||
/>
|
/>
|
||||||
<FormikInput
|
<FormikInput
|
||||||
|
@ -180,6 +200,12 @@ class UsernameStep extends React.Component {
|
||||||
placeholder={this.props.intl.formatMessage({
|
placeholder={this.props.intl.formatMessage({
|
||||||
id: 'registration.confirmPasswordInstruction'
|
id: 'registration.confirmPasswordInstruction'
|
||||||
})}
|
})}
|
||||||
|
toolTip={
|
||||||
|
this.state.focused === 'passwordConfirm' && !touched.passwordConfirm &&
|
||||||
|
this.props.intl.formatMessage({
|
||||||
|
id: 'registration.confirmPasswordInstruction'
|
||||||
|
})
|
||||||
|
}
|
||||||
type={values.showPassword ? 'text' : 'password'}
|
type={values.showPassword ? 'text' : 'password'}
|
||||||
/* eslint-disable react/jsx-no-bind */
|
/* eslint-disable react/jsx-no-bind */
|
||||||
validate={() =>
|
validate={() =>
|
||||||
|
@ -187,13 +213,13 @@ class UsernameStep extends React.Component {
|
||||||
values.passwordConfirm)
|
values.passwordConfirm)
|
||||||
}
|
}
|
||||||
validationClassName="validation-full-width-input"
|
validationClassName="validation-full-width-input"
|
||||||
onBlur={() =>
|
onBlur={() => validateField('passwordConfirm')}
|
||||||
validateField('passwordConfirm')
|
|
||||||
}
|
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setFieldValue('passwordConfirm', e.target.value);
|
setFieldValue('passwordConfirm', e.target.value);
|
||||||
|
setFieldTouched('passwordConfirm');
|
||||||
setFieldError('passwordConfirm', null);
|
setFieldError('passwordConfirm', null);
|
||||||
}}
|
}}
|
||||||
|
onFocus={() => this.handleFocused('passwordConfirm')}
|
||||||
/* eslint-enable react/jsx-no-bind */
|
/* eslint-enable react/jsx-no-bind */
|
||||||
/>
|
/>
|
||||||
<div className="join-flow-input-title">
|
<div className="join-flow-input-title">
|
||||||
|
|
|
@ -180,6 +180,7 @@
|
||||||
"registration.nextStep": "Next Step",
|
"registration.nextStep": "Next Step",
|
||||||
"registration.notYou": "Not you? Log in as another user",
|
"registration.notYou": "Not you? Log in as another user",
|
||||||
"registration.optIn": "Send me updates on using Scratch in educational settings",
|
"registration.optIn": "Send me updates on using Scratch in educational settings",
|
||||||
|
"registration.passwordAdviceShort": "Write it down so you remember. Don’t share it with others!",
|
||||||
"registration.personalStepTitle": "Personal Information",
|
"registration.personalStepTitle": "Personal Information",
|
||||||
"registration.personalStepDescription": "Your individual responses will not be displayed publicly, and will be kept confidential and secure",
|
"registration.personalStepDescription": "Your individual responses will not be displayed publicly, and will be kept confidential and secure",
|
||||||
"registration.private": "Scratch will always keep this information private.",
|
"registration.private": "Scratch will always keep this information private.",
|
||||||
|
@ -190,6 +191,7 @@
|
||||||
"registration.usernameStepDescription": "Fill in the following forms to request an account. The approval process may take up to one day.",
|
"registration.usernameStepDescription": "Fill in the following forms to request an account. The approval process may take up to one day.",
|
||||||
"registration.usernameStepDescriptionNonEducator": "Create projects, share ideas, make friends. It’s free!",
|
"registration.usernameStepDescriptionNonEducator": "Create projects, share ideas, make friends. It’s free!",
|
||||||
"registration.usernameStepRealName": "Please do not use any portion of your real name in your username.",
|
"registration.usernameStepRealName": "Please do not use any portion of your real name in your username.",
|
||||||
|
"registration.usernameAdviceShort": "Don't use your real name",
|
||||||
"registration.studentUsernameStepDescription": "You can make games, animations, and stories using Scratch. Setting up an account is easy and it's free. Fill in the form below to get started.",
|
"registration.studentUsernameStepDescription": "You can make games, animations, and stories using Scratch. Setting up an account is easy and it's free. Fill in the form below to get started.",
|
||||||
"registration.studentUsernameStepHelpText": "Already have a Scratch account?",
|
"registration.studentUsernameStepHelpText": "Already have a Scratch account?",
|
||||||
"registration.studentUsernameStepTooltip": "You'll need to create a new Scratch account to join this class.",
|
"registration.studentUsernameStepTooltip": "You'll need to create a new Scratch account to join this class.",
|
||||||
|
|
|
@ -4,15 +4,29 @@ import FormikInput from '../../../src/components/formik-forms/formik-input.jsx';
|
||||||
import {Formik} from 'formik';
|
import {Formik} from 'formik';
|
||||||
|
|
||||||
describe('FormikInput', () => {
|
describe('FormikInput', () => {
|
||||||
test('No validation message without an error', () => {
|
test('No validation message without an error or a tooltip', () => {
|
||||||
const component = mountWithIntl(
|
const component = mountWithIntl(
|
||||||
<Formik>
|
<Formik>
|
||||||
<FormikInput
|
<FormikInput
|
||||||
error=""
|
error=""
|
||||||
|
tooltip=""
|
||||||
/>
|
/>
|
||||||
</Formik>
|
</Formik>
|
||||||
);
|
);
|
||||||
expect(component.find('ValidationMessage').exists()).toEqual(false);
|
expect(component.find('ValidationMessage').exists()).toEqual(false);
|
||||||
|
expect(component.find('div.validation-error').exists()).toEqual(false);
|
||||||
|
expect(component.find('div.validation-info').exists()).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('No validation message with blank error or tooltip', () => {
|
||||||
|
const component = mountWithIntl(
|
||||||
|
<Formik>
|
||||||
|
<FormikInput />
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
expect(component.find('ValidationMessage').exists()).toEqual(false);
|
||||||
|
expect(component.find('div.validation-error').exists()).toEqual(false);
|
||||||
|
expect(component.find('div.validation-info').exists()).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Validation message shown when error given', () => {
|
test('Validation message shown when error given', () => {
|
||||||
|
@ -24,5 +38,34 @@ describe('FormikInput', () => {
|
||||||
</Formik>
|
</Formik>
|
||||||
);
|
);
|
||||||
expect(component.find('ValidationMessage').exists()).toEqual(true);
|
expect(component.find('ValidationMessage').exists()).toEqual(true);
|
||||||
|
expect(component.find('div.validation-error').exists()).toEqual(true);
|
||||||
|
expect(component.find('div.validation-info').exists()).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Tooltip shown when tooltip given', () => {
|
||||||
|
const component = mountWithIntl(
|
||||||
|
<Formik>
|
||||||
|
<FormikInput
|
||||||
|
toolTip="Have fun out there!"
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
expect(component.find('ValidationMessage').exists()).toEqual(true);
|
||||||
|
expect(component.find('div.validation-error').exists()).toEqual(false);
|
||||||
|
expect(component.find('div.validation-info').exists()).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('If both error and tooltip messages, error takes precedence', () => {
|
||||||
|
const component = mountWithIntl(
|
||||||
|
<Formik>
|
||||||
|
<FormikInput
|
||||||
|
error="There was an error"
|
||||||
|
toolTip="Have fun out there!"
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
expect(component.find('ValidationMessage').exists()).toEqual(true);
|
||||||
|
expect(component.find('div.validation-error').exists()).toEqual(true);
|
||||||
|
expect(component.find('div.validation-info').exists()).toEqual(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue