diff --git a/package-lock.json b/package-lock.json index 914aedd69..ce4127ae2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17654,14 +17654,14 @@ "dev": true }, "url-loader": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-2.0.1.tgz", - "integrity": "sha512-nd+jtHG6VgYx/NnXxXSWCJ7FtHIhuyk6Pe48HKhq29Avq3r5FSdIrenvzlbb67A3SNFaQyLk0/lMZfubj0+5ww==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-2.1.0.tgz", + "integrity": "sha512-kVrp/8VfEm5fUt+fl2E0FQyrpmOYgMEkBsv8+UDP1wFhszECq5JyGF33I7cajlVY90zRZ6MyfgKXngLvHYZX8A==", "dev": true, "requires": { - "loader-utils": "^1.1.0", + "loader-utils": "^1.2.3", "mime": "^2.4.4", - "schema-utils": "^1.0.0" + "schema-utils": "^2.0.0" }, "dependencies": { "mime": { @@ -17669,6 +17669,16 @@ "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", "dev": true + }, + "schema-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.1.0.tgz", + "integrity": "sha512-g6SViEZAfGNrToD82ZPUjq52KUPDYc+fN5+g6Euo5mLokl/9Yx14z0Cu4RR1m55HtBXejO0sBt+qw79axN+Fiw==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0" + } } } }, diff --git a/package.json b/package.json index fca7f25ed..dcce1385d 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "source-map-support": "0.3.2", "style-loader": "0.12.3", "tap": "14.2.0", - "url-loader": "2.0.1", + "url-loader": "2.1.0", "watch": "0.16.0", "webpack": "2.7.0", "webpack-dev-middleware": "2.0.4", diff --git a/src/components/extension-landing/extension-landing.scss b/src/components/extension-landing/extension-landing.scss index 41d19722c..2c550f79b 100644 --- a/src/components/extension-landing/extension-landing.scss +++ b/src/components/extension-landing/extension-landing.scss @@ -256,6 +256,10 @@ } } + div.cards + div.faq { + padding-top: 2rem; + } + .faq { p { margin-bottom: 1.25rem; diff --git a/src/components/formik-forms/formik-checkbox.jsx b/src/components/formik-forms/formik-checkbox.jsx index c9ff7e471..804c4b855 100644 --- a/src/components/formik-forms/formik-checkbox.jsx +++ b/src/components/formik-forms/formik-checkbox.jsx @@ -8,19 +8,16 @@ require('./formik-forms.scss'); require('../forms/row.scss'); const FormikCheckboxSubComponent = ({ - className, field, id, label, + labelClassName, ...props }) => (
@@ -44,7 +42,6 @@ const FormikCheckboxSubComponent = ({ ); FormikCheckboxSubComponent.propTypes = { - className: PropTypes.string, field: PropTypes.shape({ name: PropTypes.string, onBlur: PropTypes.function, @@ -52,31 +49,32 @@ FormikCheckboxSubComponent.propTypes = { value: PropTypes.bool }), id: PropTypes.string, - label: PropTypes.string + label: PropTypes.string, + labelClassName: PropTypes.string }; const FormikCheckbox = ({ - className, id, label, + labelClassName, name, ...props }) => ( ); FormikCheckbox.propTypes = { - className: PropTypes.string, id: PropTypes.string, label: PropTypes.string, + labelClassName: PropTypes.string, name: PropTypes.string }; diff --git a/src/components/formik-forms/formik-checkbox.scss b/src/components/formik-forms/formik-checkbox.scss index 66c302587..9dd11b106 100644 --- a/src/components/formik-forms/formik-checkbox.scss +++ b/src/components/formik-forms/formik-checkbox.scss @@ -1,9 +1,5 @@ @import "../../colors"; -.formik-checkbox-label { - font-weight: 300; -} - input[type="checkbox"].formik-checkbox { display: block; float: left; @@ -14,26 +10,19 @@ input[type="checkbox"].formik-checkbox { height: 1.25rem; appearance: none; - &:focus:checked { - transition: all .5s ease; + &:focus { + transition: all .25s ease; outline: none; box-shadow: 0 0 0 .25rem $ui-blue-25percent; } - &:focus:not(:checked) { - outline: none; - } - &:checked { background-color: $ui-blue; text-align: center; text-indent: .125rem; line-height: 1.25rem; font-size: .75rem; - - &:after { - color: $type-white; - content: "\2714"; - } + background-image: url("/svgs/forms/checkmark.svg"); + background-position: center; } } diff --git a/src/components/formik-forms/formik-input.jsx b/src/components/formik-forms/formik-input.jsx index 26bff6a82..e1287b2d7 100644 --- a/src/components/formik-forms/formik-input.jsx +++ b/src/components/formik-forms/formik-input.jsx @@ -11,6 +11,8 @@ require('./formik-input.scss'); const FormikInput = ({ className, error, + onSetRef, + toolTip, validationClassName, wrapperClassName, ...props @@ -29,21 +31,32 @@ const FormikInput = ({ {fail: error}, className )} + /* formik uses "innerRef" to return the actual input element */ + innerRef={onSetRef} {...props} /> - {error && ( + {error ? ( + ) : toolTip && ( + )}
); - FormikInput.propTypes = { className: PropTypes.string, - error: PropTypes.string, + // error and toolTip can be false, in which case we ignore them + error: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), + onSetRef: PropTypes.func, + toolTip: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), type: PropTypes.string, validationClassName: PropTypes.string, wrapperClassName: PropTypes.string diff --git a/src/components/formik-forms/formik-input.scss b/src/components/formik-forms/formik-input.scss index 3cf483877..adcfd2028 100644 --- a/src/components/formik-forms/formik-input.scss +++ b/src/components/formik-forms/formik-input.scss @@ -5,7 +5,7 @@ border-radius: .5rem; background-color: $ui-white; margin-bottom: .5rem; - transition: all .5s ease; + transition: all .5s ease, font-size 0s; border: 1px solid $active-gray; padding: 0 1rem; color: $type-gray; @@ -15,6 +15,7 @@ box-shadow: 0 0 0 .25rem $ui-blue-25percent; outline: none; border: 1px solid $ui-blue; + transition: all .5s ease, font-size 0s; } &.fail { @@ -25,4 +26,9 @@ outline: none; } } + + &::placeholder { + font-style: italic; + color: $type-gray-75percent; + } } diff --git a/src/components/formik-forms/formik-radio-button.jsx b/src/components/formik-forms/formik-radio-button.jsx index 70b27fb9b..b7ee7f488 100644 --- a/src/components/formik-forms/formik-radio-button.jsx +++ b/src/components/formik-forms/formik-radio-button.jsx @@ -10,24 +10,26 @@ require('./formik-radio-button.scss'); require('../forms/row.scss'); const FormikRadioButtonSubComponent = ({ - buttonValue, children, className, - field, + field, // field.value is the current selected value of the entire radio group + id, label, labelClassName, + value, ...props }) => ( {label} @@ -49,7 +51,6 @@ const FormikRadioButtonSubComponent = ({ ); FormikRadioButtonSubComponent.propTypes = { - buttonValue: PropTypes.string, children: PropTypes.node, className: PropTypes.string, field: PropTypes.shape({ @@ -58,6 +59,7 @@ FormikRadioButtonSubComponent.propTypes = { onChange: PropTypes.function, value: PropTypes.string }), + id: PropTypes.string, label: PropTypes.string, labelClassName: PropTypes.string, value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]) @@ -65,21 +67,24 @@ FormikRadioButtonSubComponent.propTypes = { const FormikRadioButton = ({ - buttonValue, className, + id, isCustomInput, label, name, onSetCustom, + onSetCustomRef, + value, ...props }) => ( {isCustomInput && ( @@ -91,18 +96,20 @@ const FormikRadioButton = ({ onChange={event => onSetCustom(event.target.value)} onFocus={event => onSetCustom(event.target.value)} /* eslint-enable react/jsx-no-bind */ + onSetRef={onSetCustomRef} /> )} ); FormikRadioButton.propTypes = { - buttonValue: PropTypes.string, className: PropTypes.string, + id: PropTypes.string, isCustomInput: PropTypes.bool, label: PropTypes.string, name: PropTypes.string, onSetCustom: PropTypes.func, + onSetCustomRef: PropTypes.func, value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]) }; diff --git a/src/components/formik-forms/formik-select.jsx b/src/components/formik-forms/formik-select.jsx index 4578d8a64..fe666a88a 100644 --- a/src/components/formik-forms/formik-select.jsx +++ b/src/components/formik-forms/formik-select.jsx @@ -1,4 +1,3 @@ -const classNames = require('classnames'); const PropTypes = require('prop-types'); const React = require('react'); import {Field} from 'formik'; @@ -7,6 +6,7 @@ const ValidationMessage = require('../forms/validation-message.jsx'); require('../forms/select.scss'); require('../forms/row.scss'); +require('./formik-select.scss'); const FormikSelect = ({ className, @@ -27,9 +27,7 @@ const FormikSelect = ({ return (
@@ -39,6 +37,7 @@ const FormikSelect = ({ )}
diff --git a/src/components/formik-forms/formik-select.scss b/src/components/formik-forms/formik-select.scss new file mode 100644 index 000000000..40ec008f9 --- /dev/null +++ b/src/components/formik-forms/formik-select.scss @@ -0,0 +1,12 @@ +@import "../../colors"; + +.select { + .fail { + border: 1px solid $ui-orange; + + &:focus { + box-shadow: 0 0 0 .25rem $ui-orange-25percent; + outline: none; + } + } +} diff --git a/src/components/formik-forms/input.scss b/src/components/formik-forms/input.scss deleted file mode 100644 index 1ac2a0b7e..000000000 --- a/src/components/formik-forms/input.scss +++ /dev/null @@ -1,7 +0,0 @@ -@import "../../colors"; -@import "../../frameless"; - -.input::placeholder { - font-style: italic; - color: $type-gray-75percent; -} diff --git a/src/components/forms/select.scss b/src/components/forms/select.scss index 4159086b7..008bf3da3 100644 --- a/src/components/forms/select.scss +++ b/src/components/forms/select.scss @@ -11,7 +11,7 @@ margin-bottom: .75rem; border: 1px solid $active-gray; border-radius: 5px; - background: $ui-light-gray url("../../../static/svgs/forms/carot.svg") no-repeat right center; + background: $ui-light-gray url("../../../static/svgs/forms/caret.svg") no-repeat right center; padding-right: 4rem; padding-left: 1rem; width: 100%; @@ -42,7 +42,7 @@ &:focus, &:hover { - background: $ui-light-gray url("../../../static/svgs/forms/carot-hover.svg") no-repeat right center; + background: $ui-light-gray url("../../../static/svgs/forms/caret-hover.svg") no-repeat right center; } > option { diff --git a/src/components/forms/validation-message.jsx b/src/components/forms/validation-message.jsx index 10c05ce13..717128e39 100644 --- a/src/components/forms/validation-message.jsx +++ b/src/components/forms/validation-message.jsx @@ -5,14 +5,24 @@ const React = require('react'); require('./validation-message.scss'); const ValidationMessage = props => ( -
+
{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 */ />
(
- {headerImgSrc && ( -
- -
- )} -
- - {title && ( - + {headerImgSrc && ( +
+ - )} - {description && ( -
- {description} - {infoMessage && ( - - )} -
- )} - {children} - -
- {footerContent && ( -
- {footerContent} +
+ )} +
+ + {title && ( + + )} + {description && ( +
+ {description} + {infoMessage && ( + + )} +
+ )} + {children} +
- )} - + {footerContent && ( +
+ {footerContent} +
+ )} + +
); 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 { , - ( - USE_SCRATCH3_REGISTRATION ? ( - - ) : ( - - ) - ),
  • - ]) : []} + ]) : [] + } + {this.props.registrationOpen && ( + this.props.useScratch3Registration ? ( + + ) : ( + + ) + )} @@ -235,7 +235,6 @@ class Navigation extends React.Component { Navigation.propTypes = { accountNavOpen: PropTypes.bool, - closeAccountMenus: PropTypes.func, getMessageCount: PropTypes.func, handleCloseAccountNav: PropTypes.func, handleLogOut: PropTypes.func, @@ -250,12 +249,14 @@ Navigation.propTypes = { educator_invitee: PropTypes.bool, student: PropTypes.bool }), + registrationOpen: PropTypes.bool, searchTerm: PropTypes.string, session: PropTypes.shape({ status: PropTypes.string }), setMessageCount: PropTypes.func, unreadMessageCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + useScratch3Registration: PropTypes.bool, user: PropTypes.shape({ classroomId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), thumbnailUrl: PropTypes.string, @@ -273,15 +274,14 @@ const mapStateToProps = state => ({ accountNavOpen: state.navigation && state.navigation.accountNavOpen, session: state.session, permissions: state.permissions, + registrationOpen: state.navigation.registrationOpen, searchTerm: state.navigation.searchTerm, unreadMessageCount: state.messageCount.messageCount, - user: state.session && state.session.session && state.session.session.user + user: state.session && state.session.session && state.session.session.user, + useScratch3Registration: state.navigation.useScratch3Registration }); const mapDispatchToProps = dispatch => ({ - closeAccountMenus: () => { - dispatch(navigationActions.closeAccountMenus()); - }, getMessageCount: username => { dispatch(messageCountActions.getCount(username)); }, diff --git a/src/components/registration/scratch3-registration.jsx b/src/components/registration/scratch3-registration.jsx index 38e7bb90a..03f53d763 100644 --- a/src/components/registration/scratch3-registration.jsx +++ b/src/components/registration/scratch3-registration.jsx @@ -28,10 +28,6 @@ Registration.propTypes = { isOpen: PropTypes.bool }; -const mapStateToProps = state => ({ - isOpen: state.navigation.registrationOpen -}); - const mapDispatchToProps = dispatch => ({ handleCloseRegistration: () => { dispatch(navigationActions.setRegistrationOpen(false)); @@ -42,6 +38,6 @@ const mapDispatchToProps = dispatch => ({ }); module.exports = connect( - mapStateToProps, + () => ({}), mapDispatchToProps )(Registration); diff --git a/src/l10n.json b/src/l10n.json index dc0d02c59..555cbbe39 100644 --- a/src/l10n.json +++ b/src/l10n.json @@ -13,6 +13,7 @@ "general.confirmEmail": "Confirm Email", "general.contactUs": "Contact Us", "general.contact": "Contact", + "general.downloadPDF": "Download PDF", "general.emailUs": "Email Us", "general.conferences": "Conferences", "general.copyright": "Scratch is a project of the Lifelong Kindergarten Group at the MIT Media Lab", @@ -62,6 +63,7 @@ "general.noDeletionTitle": "Your Account Will Not Be Deleted", "general.noDeletionDescription": "Your account was scheduled for deletion but you logged in. Your account has been reactivated. If you didn’t request for your account to be deleted, you should {resetLink} to make sure your account is secure.", "general.noDeletionLink": "change your password", + "general.nonBinary": "Non-binary", "general.notRequired": "Not Required", "general.okay": "Okay", "general.other": "Other", @@ -156,9 +158,10 @@ "registration.generalError": "Sorry, an unexpected error occurred.", "registration.classroomInviteExistingStudentStepDescription": "you have been invited to join the class:", "registration.classroomInviteNewStudentStepDescription": "Your teacher has invited you to join a class:", + "registration.confirmPasswordInstruction": "Type password again", "registration.confirmYourEmail": "Confirm Your Email", "registration.confirmYourEmailDescription": "If you haven't already, please click the link in the confirmation email sent to:", - "registration.createAccount": "Create Account", + "registration.createAccount": "Create Your Account", "registration.createUsername": "Create a username", "registration.genderStepTitle": "What's your gender?", "registration.genderStepDescription": "Scratch welcomes people of all genders. We will always keep this information private.", @@ -178,20 +181,23 @@ "registration.nextStep": "Next Step", "registration.notYou": "Not you? Log in as another user", "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.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.receiveEmails": "I'd like to receive emails from the Scratch Team about project ideas, events, and more.", "registration.selectCountry": "select country", "registration.studentPersonalStepDescription": "This information will not appear on the Scratch website.", "registration.showPassword": "Show password", "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.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.studentUsernameStepHelpText": "Already have a Scratch account?", "registration.studentUsernameStepTooltip": "You'll need to create a new Scratch account to join this class.", "registration.studentUsernameFieldHelpText": "For safety, don't use your real name!", - "registration.acceptTermsOfUse": "By creating an account, I accept and agree to the {touLink}.", + "registration.acceptTermsOfUse": "By creating an account, you accept and agree to the {touLink}.", "registration.usernameStepTitle": "Request a Teacher Account", "registration.usernameStepTitleScratcher": "Create a Scratch Account", "registration.validationMaxLength": "Sorry, you have exceeded the maximum character limit.", diff --git a/src/lib/validate.js b/src/lib/validate.js index 85fe64944..b4fd84f31 100644 --- a/src/lib/validate.js +++ b/src/lib/validate.js @@ -20,7 +20,7 @@ module.exports.validateUsernameRemotely = username => ( uri: `/accounts/checkusername/${username}/` }, (err, body, res) => { if (err || res.statusCode !== 200) { - resolve({valid: false, errMsgId: 'general.apiError'}); + resolve({valid: false, errMsgId: 'general.error'}); } switch (body.msg) { case 'valid username': diff --git a/src/redux/navigation.js b/src/redux/navigation.js index 71459af75..0e71090ae 100644 --- a/src/redux/navigation.js +++ b/src/redux/navigation.js @@ -18,6 +18,7 @@ const Types = keyMirror({ }); module.exports.getInitialState = () => ({ + useScratch3Registration: false, accountNavOpen: false, canceledDeletionOpen: false, loginError: null, @@ -96,11 +97,6 @@ module.exports.handleCompleteRegistration = () => (dispatch => { dispatch(module.exports.setRegistrationOpen(false)); }); -module.exports.closeAccountMenus = () => (dispatch => { - dispatch(module.exports.setAccountNavOpen(false)); - dispatch(module.exports.setRegistrationOpen(false)); -}); - module.exports.handleLogIn = (formData, callback) => (dispatch => { dispatch(module.exports.setLoginError(null)); formData.useMessages = true; // NOTE: this may or may not be being used anywhere else diff --git a/src/views/conference/2019/index/index.jsx b/src/views/conference/2019/index/index.jsx index 753977585..4c71f9a50 100644 --- a/src/views/conference/2019/index/index.jsx +++ b/src/views/conference/2019/index/index.jsx @@ -141,7 +141,7 @@ const ConferenceSplash = () => ( -
    +
    EU Flag (
    -
    +
    Kenya Flag (

    - +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + Calendar Icon + + + {' - '} + +
    + Map Icon + {'Nairobi, Kenya'}
    + Audience Icon +
    + Language Icon +
    + Language Icon + #scratchafrica #scratch2019nbo
    + + +
    diff --git a/src/views/conference/2019/index/index.scss b/src/views/conference/2019/index/index.scss index 341768bac..c8f4c403d 100644 --- a/src/views/conference/2019/index/index.scss +++ b/src/views/conference/2019/index/index.scss @@ -119,12 +119,6 @@ h1.title-banner-h1.mod-2019 { margin: 2rem 0; } -.mod-kenya { - .conf2019-panel-desc { - font-style: italic; - } -} - .conf2019-mailing-list { font-weight: normal; } diff --git a/src/views/conference/2019/index/l10n.json b/src/views/conference/2019/index/l10n.json index 3f3f999a2..6f40df65e 100644 --- a/src/views/conference/2019/index/l10n.json +++ b/src/views/conference/2019/index/l10n.json @@ -20,11 +20,11 @@ "conference-2019.ukDesc": "Hosted by Raspberry Pi, the 2019 Scratch Conference Europe will take place in Cambridge, UK, from Friday 23 August to Sunday 25 August. The schedule is full of exciting participatory activities led by members of the Scratch community. Participants can look forward to workshops, talks, and keynotes across a range of topics, including the new Scratch 3.0, as well as plenty of informal opportunities to chat and connect!", "conference-2019.ukAudience": "Education professionals and volunteers", - "conference-2019.kenyaTitle": "Scratch2019NBO", + "conference-2019.kenyaTitle": "Scratch Conference Africa: Scratch2019NBO", "conference-2019.kenyaSubTitle": "Waves of Innovation", "conference-2019.kenyaDesc": "In recognition of Africa's technological contributions to the world and the potential of the youth of Africa, Scratch2019NBO will be held in Nairobi, Kenya. Join educators from around the world to share lessons, empower young people, and celebrate accomplishments in creative coding.", "conference-2019.kenyaPostpone": "The Scratch2019NBO conference, originally planned for Nairobi, Kenya in July 2019, has been postponed. Information about future plans will be available later this year.", - "conference-2019.kenyaAudience": "Educators, students, and enthusiasts", + "conference-2019.kenyaAudience": "Educators", "conference-2019.chileDesc": "Scratch al Sur Conferencia Chile 2019 is an event aimed at teachers of all educational areas and levels, who seek to innovate in the classroom through creative learning, thus supporting public policies that are promoted through the National Plan of Digital Languages, launched by the Chilean government as of 2019. Various workshops, panels, experiences, stands, a presentation of the new Scratch 3.0, Makey-Makey, and much more will be offered.", "conference-2019.chileAudience": "Teachers and policy makers", diff --git a/src/views/credits/credits.jsx b/src/views/credits/credits.jsx index 838c99786..5aa272cbe 100644 --- a/src/views/credits/credits.jsx +++ b/src/views/credits/credits.jsx @@ -140,7 +140,7 @@ const Credits = () => ( {' '} Ben Berg, Amos Blanton, Karen Brennan, Juanita Buitrago, Leo Burd, Gaia Carini, Kasia Chmielinski, Michelle Chung, Shane Clements, - Hannah Cole, Sayamindu Dasgupta, Margarita Dekoli, Evelyn Eastmond, + Hannah Cole, Sayamindu Dasgupta, Margarita Dekoli, Dave Feinberg, Chris Graves, Megan Haddadi, Connor Hudson, Christina Huang, Tony Hwang, Abdulrahman Idlbi, Randy Jou, Lily Kim, Tauntaun Kim, Saskia Leggett, Tim Mickel, Amon Millner, Ricarose Roque, diff --git a/src/views/ideas/l10n.json b/src/views/ideas/l10n.json index e2defd646..ca182da98 100644 --- a/src/views/ideas/l10n.json +++ b/src/views/ideas/l10n.json @@ -21,7 +21,6 @@ "ideas.seeAllTutorials": "See All Tutorials", "ideas.cardsTitle": "Get the Entire Collection of Coding Cards", "ideas.cardsText": "With the Scratch Coding Cards, you can learn to create interactive games, stories, music, animations, and more!", - "ideas.downloadPDF": "Download PDF", "ideas.starterProjectsTitle": "Starter Projects", "ideas.starterProjectsText": "You can play with Starter Projects and remix them to make your own creations.", "ideas.starterProjectsButton": "Explore Starter Projects", diff --git a/src/views/microbit/l10n-static.json b/src/views/microbit/l10n-static.json new file mode 100644 index 000000000..079f8ef82 --- /dev/null +++ b/src/views/microbit/l10n-static.json @@ -0,0 +1,3 @@ +{ + "cards.microbit-cardsLink": "https://resources.scratch.mit.edu/www/cards/en/microbit-cards.pdf" +} diff --git a/src/views/microbit/l10n.json b/src/views/microbit/l10n.json index 8691e56a8..fd97d6ef6 100644 --- a/src/views/microbit/l10n.json +++ b/src/views/microbit/l10n.json @@ -2,7 +2,9 @@ "microbit.headerText": "{microbitLink} is a tiny circuit board designed to help kids learn to code and create with technology. It has many features including an LED display, buttons, and a motion sensor. You can connect it to Scratch and build creative projects that combine the magic of the digital and physical worlds.", "microbit.gettingStarted": "Getting Started", "microbit.installMicrobitHex": "Install Scratch micro:bit HEX", + "microbit.cardsDescription": "These cards show how to start making projects with micro:bit and Scratch.", "microbit.connectUSB": "Connect a micro:bit to your computer with a USB cable", + "microbit.downloadCardsTitle": "Download micro:bit Cards", "microbit.downloadHex": "Download the Scratch micro:bit HEX file", "microbit.dragDropHex": "Drag and drop the HEX file onto your micro:bit", "microbit.connectingMicrobit": "Connecting micro:bit to Scratch", diff --git a/src/views/microbit/microbit.jsx b/src/views/microbit/microbit.jsx index 305184fcf..92df6ea77 100644 --- a/src/views/microbit/microbit.jsx +++ b/src/views/microbit/microbit.jsx @@ -17,6 +17,7 @@ const ExtensionRequirements = require('../../components/extension-landing/extens const ExtensionSection = require('../../components/extension-landing/extension-section.jsx'); const InstallScratchLink = require('../../components/extension-landing/install-scratch-link.jsx'); const ProjectCard = require('../../components/extension-landing/project-card.jsx'); +const Button = require('../../components/forms/button.jsx'); const Steps = require('../../components/steps/steps.jsx'); const Step = require('../../components/steps/step.jsx'); @@ -269,6 +270,35 @@ class MicroBit extends ExtensionLanding { /> + + +
    + +
    +
    +

    + +

    +

    + +

    +

    + + + +

    +
    +
    +
    +

    diff --git a/src/views/microbit/microbit.scss b/src/views/microbit/microbit.scss index cc01b5375..5deb1646f 100644 --- a/src/views/microbit/microbit.scss +++ b/src/views/microbit/microbit.scss @@ -14,4 +14,60 @@ } } } + + .cards { + /* ends with
    , so no need for extra padding */ + padding-bottom: 0; + /* slightly more padding on top, since
    at bottom has its own extra padding */ + padding-top: 4.5rem; + } + + .cards-row { + flex-wrap: nowrap; + } + + .cards-image-column { + width: 50%; + } + + .cards-image { + width: calc(100% - 4rem); + margin-top: 1rem; + margin-right: 2rem; + margin-bottom: 1rem; + } + + .cards-description-column { + width: 50%; + + p { + font-size: 1.2rem; + } + } + + .download-cards-button { + min-width: 10rem; + + &:before { + display: inline-block; + background-image: url("/svgs/extensions/download-white.svg"); + background-repeat: no-repeat; + width: 1.5rem; + height: 1.5rem; + margin-right: .75rem; + vertical-align: text-top; + content: ""; + } + } +} + +@media #{$medium-and-smaller} { + .microbit { + .cards-image-column { + width: calc(100% - 2rem); + } + .cards-description-column { + width: calc(100% - 2rem); + } + } } diff --git a/src/views/preview/project-view.jsx b/src/views/preview/project-view.jsx index 240996f98..f31353bc1 100644 --- a/src/views/preview/project-view.jsx +++ b/src/views/preview/project-view.jsx @@ -18,6 +18,7 @@ const ProjectInfo = require('../../lib/project-info'); const PreviewPresentation = require('./presentation.jsx'); const projectShape = require('./projectshape.jsx').projectShape; const Registration = require('../../components/registration/registration.jsx'); +const Scratch3Registration = require('../../components/registration/scratch3-registration.jsx'); const ConnectedLogin = require('../../components/login/connected-login.jsx'); const CanceledDeletionModal = require('../../components/login/canceled-deletion-modal.jsx'); const NotAvailable = require('../../components/not-available/not-available.jsx'); @@ -751,7 +752,15 @@ class Preview extends React.Component { onUpdateProjectThumbnail={this.props.handleUpdateProjectThumbnail} onUpdateProjectTitle={this.handleUpdateProjectTitle} /> - + {this.props.registrationOpen && ( + this.props.useScratch3Registration ? ( + + ) : ( + + ) + )} } @@ -822,6 +831,7 @@ Preview.propTypes = { projectInfo: projectShape, projectNotAvailable: PropTypes.bool, projectStudios: PropTypes.arrayOf(PropTypes.object), + registrationOpen: PropTypes.bool, remixProject: PropTypes.func, remixes: PropTypes.arrayOf(PropTypes.object), replies: PropTypes.objectOf(PropTypes.array), @@ -835,6 +845,7 @@ Preview.propTypes = { shareProject: PropTypes.func.isRequired, toggleStudio: PropTypes.func.isRequired, updateProject: PropTypes.func.isRequired, + useScratch3Registration: PropTypes.bool, user: PropTypes.shape({ id: PropTypes.number, banned: PropTypes.bool, @@ -927,9 +938,11 @@ const mapStateToProps = state => { projectInfo: state.preview.projectInfo, projectNotAvailable: state.preview.projectNotAvailable, projectStudios: state.preview.projectStudios, + registrationOpen: state.navigation.registrationOpen, remixes: state.preview.remixes, replies: state.preview.replies, sessionStatus: state.session.status, // check if used + useScratch3Registration: state.navigation.useScratch3Registration, user: state.session.session.user, userOwnsProject: userOwnsProject, userPresent: userPresent, diff --git a/src/views/tips/l10n.json b/src/views/tips/l10n.json index 93324a496..5c56ff1b3 100644 --- a/src/views/tips/l10n.json +++ b/src/views/tips/l10n.json @@ -6,7 +6,6 @@ "tips.tttBody": "What do you want to make with Scratch? For each activity, you can try the Tutorial, download a set of Activity Cards, or view the Educator Guide.", "tips.cardsHeader": "Get the Entire Collection of Activity Cards", "tips.cardsBody": "With the Scratch Activity Cards, you can learn to create interactive games, stories, music, animations, and more!", - "tips.cardsDownload": "Download PDF", "tips.cardsPurchase": "Purchase Printed Set", "tips.starterProjectsHeader": "Starter Projects", "tips.starterProjectsBody": "You can play with Starter Projects to get ideas for making your own projects.", diff --git a/src/views/tips/tips.jsx b/src/views/tips/tips.jsx index b23a0d979..dccc8f014 100644 --- a/src/views/tips/tips.jsx +++ b/src/views/tips/tips.jsx @@ -134,7 +134,7 @@ class Tips extends React.Component { })} > \ No newline at end of file diff --git a/static/svgs/forms/caret.svg b/static/svgs/forms/caret.svg new file mode 100644 index 000000000..8f7415603 --- /dev/null +++ b/static/svgs/forms/caret.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/svgs/forms/carot-hover.svg b/static/svgs/forms/carot-hover.svg deleted file mode 100644 index 20e2e01f4..000000000 --- a/static/svgs/forms/carot-hover.svg +++ /dev/null @@ -1 +0,0 @@ -carot \ No newline at end of file diff --git a/static/svgs/forms/carot.svg b/static/svgs/forms/carot.svg deleted file mode 100644 index 078e08109..000000000 --- a/static/svgs/forms/carot.svg +++ /dev/null @@ -1 +0,0 @@ -carot-hover \ No newline at end of file diff --git a/static/svgs/forms/checkmark.svg b/static/svgs/forms/checkmark.svg new file mode 100644 index 000000000..cda1d56c5 --- /dev/null +++ b/static/svgs/forms/checkmark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/svgs/info-button/info-button.svg b/static/svgs/info-button/info-button.svg new file mode 100644 index 000000000..829bc9442 --- /dev/null +++ b/static/svgs/info-button/info-button.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/__mocks__/react-responsive.js b/test/__mocks__/react-responsive.js new file mode 100644 index 000000000..93bbcb263 --- /dev/null +++ b/test/__mocks__/react-responsive.js @@ -0,0 +1,5 @@ +// __mocks__/react-responsive.js + +const MediaQuery = ({children}) => children; + +export default MediaQuery; diff --git a/test/unit/components/formik-input.test.jsx b/test/unit/components/formik-input.test.jsx new file mode 100644 index 000000000..cc5113863 --- /dev/null +++ b/test/unit/components/formik-input.test.jsx @@ -0,0 +1,85 @@ +import React from 'react'; +import {mountWithIntl} from '../../helpers/intl-helpers.jsx'; +import FormikInput from '../../../src/components/formik-forms/formik-input.jsx'; +import {Formik} from 'formik'; + +describe('FormikInput', () => { + test('No validation message with empty error, empty toolTip', () => { + const component = mountWithIntl( + + + + ); + 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 false error, false toolTip', () => { + const component = mountWithIntl( + + + + ); + 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 nonexistent error or toolTip', () => { + const component = mountWithIntl( + + + + ); + 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', () => { + const component = mountWithIntl( + + + + ); + 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( + + + + ); + 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( + + + + ); + 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); + }); +}); diff --git a/test/unit/components/formik-select.test.jsx b/test/unit/components/formik-select.test.jsx new file mode 100644 index 000000000..46accb37e --- /dev/null +++ b/test/unit/components/formik-select.test.jsx @@ -0,0 +1,60 @@ +import React from 'react'; +import {mountWithIntl} from '../../helpers/intl-helpers.jsx'; +import FormikSelect from '../../../src/components/formik-forms/formik-select.jsx'; +import {Formik} from 'formik'; +import {Field} from 'formik'; + +describe('FormikSelect', () => { + test('No validation message without an error', () => { + const component = mountWithIntl( + + + + ); + + expect(component.find('ValidationMessage').exists()).toEqual(false); + expect(component.find(Field).exists()).toEqual(true); + }); + + test('Validation message shown when error present', () => { + const component = mountWithIntl( + + + + ); + expect(component.find('ValidationMessage').exists()).toEqual(true); + expect(component.find(Field).exists()).toEqual(true); + }); + + test('list of options passed to formik', () => { + const optionList = [ + { + disabled: false, + label: 'option1', + value: 'value1' + }, + { + disabled: false, + label: 'option2', + value: 'value2' + } + + ]; + const component = mountWithIntl( + + + + ); + expect(component.find(Field).exists()).toEqual(true); + expect(component.find(Field).prop('children').length).toEqual(2); + }); +}); diff --git a/test/unit/components/info-button.test.jsx b/test/unit/components/info-button.test.jsx index 777fa2f40..e7e48231d 100644 --- a/test/unit/components/info-button.test.jsx +++ b/test/unit/components/info-button.test.jsx @@ -11,6 +11,15 @@ describe('InfoButton', () => { ); expect(component.find('div.info-button-message').exists()).toEqual(false); }); + test('mouseOver on info button makes info message visible', () => { + const component = mountWithIntl( + + ); + component.find('div.info-button').simulate('mouseOver'); + expect(component.find('div.info-button-message').exists()).toEqual(true); + }); test('clicking on info button makes info message visible', () => { const component = mountWithIntl( { message="Here is some info about something!" /> ); - component.find('div.info-button').simulate('click'); + component.find('div.info-button').simulate('mouseOver'); expect(component.find('div.info-button-message').exists()).toEqual(true); component.find('div.info-button').simulate('mouseOut'); expect(component.find('div.info-button-message').exists()).toEqual(false); diff --git a/test/unit/components/join-flow-step.test.jsx b/test/unit/components/join-flow-step.test.jsx index 80518cddd..5c0f8b685 100644 --- a/test/unit/components/join-flow-step.test.jsx +++ b/test/unit/components/join-flow-step.test.jsx @@ -20,7 +20,7 @@ describe('JoinFlowStep', () => { {...props} /> ); - expect(component.find('div.join-flow-header-image').exists()).toEqual(true); + expect(component.find('img.join-flow-header-image').exists()).toEqual(true); expect(component.find({src: props.headerImgSrc}).exists()).toEqual(true); expect(component.find('.join-flow-inner-content').exists()).toEqual(true); expect(component.find('.join-flow-title').exists()).toEqual(true); @@ -39,7 +39,7 @@ describe('JoinFlowStep', () => { ); - expect(component.find('div.join-flow-header-image').exists()).toEqual(false); + expect(component.find('img.join-flow-header-image').exists()).toEqual(false); expect(component.find('.join-flow-inner-content').exists()).toEqual(true); expect(component.find('.join-flow-title').exists()).toEqual(false); expect(component.find('div.join-flow-description').exists()).toEqual(false); diff --git a/test/unit/redux/navigation.test.js b/test/unit/redux/navigation.test.js new file mode 100644 index 000000000..277b2d574 --- /dev/null +++ b/test/unit/redux/navigation.test.js @@ -0,0 +1,241 @@ +const { + handleToggleAccountNav, + navigationReducer, + setAccountNavOpen, + setCanceledDeletionOpen, + setLoginError, + setLoginOpen, + setRegistrationOpen, + setSearchTerm, + toggleLoginOpen +} = require('../../../src/redux/navigation'); + + +describe('unit test lib/validate.js', () => { + test('initialState', () => { + let defaultState; + /* navigationReducer(state, action) */ + expect(navigationReducer(defaultState, {type: 'anything'})).toBeDefined(); + expect(navigationReducer(defaultState, {type: 'anything'}).accountNavOpen).toBe(false); + expect(navigationReducer(defaultState, {type: 'anything'}).canceledDeletionOpen).toBe(false); + expect(navigationReducer(defaultState, {type: 'anything'}).loginError).toBe(null); + expect(navigationReducer(defaultState, {type: 'anything'}).loginOpen).toBe(false); + expect(navigationReducer(defaultState, {type: 'anything'}).registrationOpen).toBe(false); + expect(navigationReducer(defaultState, {type: 'anything'}).searchTerm).toBe(''); + expect(navigationReducer(defaultState, {type: 'anything'}).useScratch3Registration).toBe(false); + }); + + // handleToggleAccountNav + + test('handleToggleAccountNav can toggle on', () => { + const initialState = { + accountNavOpen: false + }; + const action = handleToggleAccountNav(); + const resultState = navigationReducer(initialState, action); + expect(resultState.accountNavOpen).toBe(true); + }); + + test('handleToggleAccountNav can toggle off', () => { + const initialState = { + accountNavOpen: true + }; + const action = handleToggleAccountNav(); + const resultState = navigationReducer(initialState, action); + expect(resultState.accountNavOpen).toBe(false); + }); + + // setAccountNavOpen + + test('setAccountNavOpen opens account nav, if it is closed', () => { + const initialState = { + accountNavOpen: false + }; + const action = setAccountNavOpen(true); + const resultState = navigationReducer(initialState, action); + expect(resultState.accountNavOpen).toBe(true); + }); + + test('setAccountNavOpen leaves account nav open, if it is already open', () => { + const initialState = { + accountNavOpen: true + }; + const action = setAccountNavOpen(true); + const resultState = navigationReducer(initialState, action); + expect(resultState.accountNavOpen).toBe(true); + }); + + test('setAccountNavOpen closes account nav, if it is open', () => { + const initialState = { + accountNavOpen: true + }; + const action = setAccountNavOpen(false); + const resultState = navigationReducer(initialState, action); + expect(resultState.accountNavOpen).toBe(false); + }); + + test('setAccountNavOpen leaves account nav closed, if it is already closed', () => { + const initialState = { + accountNavOpen: false + }; + const action = setAccountNavOpen(false); + const resultState = navigationReducer(initialState, action); + expect(resultState.accountNavOpen).toBe(false); + }); + + // setCanceledDeletionOpen + + test('setCanceledDeletionOpen opens account nav, if it is closed', () => { + const initialState = { + canceledDeletionOpen: false + }; + const action = setCanceledDeletionOpen(true); + const resultState = navigationReducer(initialState, action); + expect(resultState.canceledDeletionOpen).toBe(true); + }); + + test('setCanceledDeletionOpen leaves account nav open, if it is already open', () => { + const initialState = { + canceledDeletionOpen: true + }; + const action = setCanceledDeletionOpen(true); + const resultState = navigationReducer(initialState, action); + expect(resultState.canceledDeletionOpen).toBe(true); + }); + + test('setCanceledDeletionOpen closes account nav, if it is open', () => { + const initialState = { + canceledDeletionOpen: true + }; + const action = setCanceledDeletionOpen(false); + const resultState = navigationReducer(initialState, action); + expect(resultState.canceledDeletionOpen).toBe(false); + }); + + test('setCanceledDeletionOpen leaves account nav closed, if it is already closed', () => { + const initialState = { + canceledDeletionOpen: false + }; + const action = setCanceledDeletionOpen(false); + const resultState = navigationReducer(initialState, action); + expect(resultState.canceledDeletionOpen).toBe(false); + }); + + // setLoginError + + test('setLoginError sets login error', () => { + const initialState = { + loginError: null + }; + const action = setLoginError('Danger! Error! Mistake!'); + const resultState = navigationReducer(initialState, action); + expect(resultState.loginError).toBe('Danger! Error! Mistake!'); + }); + + // setLoginOpen + + test('setLoginOpen opens account nav, if it is closed', () => { + const initialState = { + loginOpen: false + }; + const action = setLoginOpen(true); + const resultState = navigationReducer(initialState, action); + expect(resultState.loginOpen).toBe(true); + }); + + test('setLoginOpen leaves account nav open, if it is already open', () => { + const initialState = { + loginOpen: true + }; + const action = setLoginOpen(true); + const resultState = navigationReducer(initialState, action); + expect(resultState.loginOpen).toBe(true); + }); + + test('setLoginOpen closes account nav, if it is open', () => { + const initialState = { + loginOpen: true + }; + const action = setLoginOpen(false); + const resultState = navigationReducer(initialState, action); + expect(resultState.loginOpen).toBe(false); + }); + + test('setLoginOpen leaves account nav closed, if it is already closed', () => { + const initialState = { + loginOpen: false + }; + const action = setLoginOpen(false); + const resultState = navigationReducer(initialState, action); + expect(resultState.loginOpen).toBe(false); + }); + + // setRegistrationOpen + + test('setRegistrationOpen opens account nav, if it is closed', () => { + const initialState = { + registrationOpen: false + }; + const action = setRegistrationOpen(true); + const resultState = navigationReducer(initialState, action); + expect(resultState.registrationOpen).toBe(true); + }); + + test('setRegistrationOpen leaves account nav open, if it is already open', () => { + const initialState = { + registrationOpen: true + }; + const action = setRegistrationOpen(true); + const resultState = navigationReducer(initialState, action); + expect(resultState.registrationOpen).toBe(true); + }); + + test('setRegistrationOpen closes account nav, if it is open', () => { + const initialState = { + registrationOpen: true + }; + const action = setRegistrationOpen(false); + const resultState = navigationReducer(initialState, action); + expect(resultState.registrationOpen).toBe(false); + }); + + test('setRegistrationOpen leaves account nav closed, if it is already closed', () => { + const initialState = { + registrationOpen: false + }; + const action = setRegistrationOpen(false); + const resultState = navigationReducer(initialState, action); + expect(resultState.registrationOpen).toBe(false); + }); + + // setSearchTerm + + test('setSearchTerm sets search term', () => { + const initialState = { + searchTerm: null + }; + const action = setSearchTerm('outer space'); + const resultState = navigationReducer(initialState, action); + expect(resultState.searchTerm).toBe('outer space'); + }); + + // toggleLoginOpen + + test('toggleLoginOpen can toggle on', () => { + const initialState = { + loginOpen: false + }; + const action = toggleLoginOpen(); + const resultState = navigationReducer(initialState, action); + expect(resultState.loginOpen).toBe(true); + }); + + test('toggleLoginOpen can toggle off', () => { + const initialState = { + loginOpen: true + }; + const action = toggleLoginOpen(); + const resultState = navigationReducer(initialState, action); + expect(resultState.loginOpen).toBe(false); + }); +});