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:
Benjamin Wheeler 2019-08-21 00:15:57 +02:00 committed by GitHub
commit a5b9cdc410
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 114 additions and 9 deletions

View file

@ -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

View file

@ -39,6 +39,7 @@ const FormikSelect = ({
<ValidationMessage <ValidationMessage
className={validationClassName} className={validationClassName}
message={error} message={error}
mode="error"
/> />
)} )}
</div> </div>

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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">

View file

@ -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. Dont 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. Its free!", "registration.usernameStepDescriptionNonEducator": "Create projects, share ideas, make friends. Its 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.",

View file

@ -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);
}); });
}); });