diff --git a/app/assets/stylesheets/preact/onboarding-modal.scss b/app/assets/stylesheets/preact/onboarding-modal.scss index 3ba8a75d3..00e538484 100644 --- a/app/assets/stylesheets/preact/onboarding-modal.scss +++ b/app/assets/stylesheets/preact/onboarding-modal.scss @@ -126,18 +126,41 @@ @extend .crayons-btn; } + .back-button-container, + .next-button { + // Explicitly set width to avoid next-button re-sizing with text, + // and to ensure that back-button-container takes up the same amount of space. + width: $su-10; + } + .back-button { @extend .crayons-btn; @extend .crayons-btn--ghost; @extend .crayons-btn--icon; + $button-size: 40px; // Explicitly set size for accessibility. + + border-radius: 100%; + min-width: $button-size; + height: $button-size; align-items: center; display: flex; + + &:hover, + &:active { + background: var(--base-10); + } + } + + .skip-for-now { + @extend .crayons-btn; + @extend .crayons-btn--secondary; } .stepper { display: flex; align-items: center; + justify-content: center; } .dot { diff --git a/app/javascript/onboarding/__tests__/Onboarding.test.jsx b/app/javascript/onboarding/__tests__/Onboarding.test.jsx index b5aac2d5d..1f5546b10 100644 --- a/app/javascript/onboarding/__tests__/Onboarding.test.jsx +++ b/app/javascript/onboarding/__tests__/Onboarding.test.jsx @@ -194,9 +194,17 @@ describe('', () => { .find('.onboarding-tags__button') .first(); + expect(onboardingSlides.find('.next-button').text()).toContain( + 'Skip for now', + ); + firstButton.simulate('click'); expect(followTags.state('selectedTags').length).toBe(1); + expect(onboardingSlides.find('.next-button').text()).toContain( + 'Continue', + ); + onboardingSlides.find('.next-button').simulate('click'); fetch.once(fakeUsersResponse); await flushPromises(); @@ -243,20 +251,31 @@ describe('', () => { target: { value: 'my employer name', name: 'employer_name' }, }; + expect(onboardingSlides.find('.next-button').text()).toContain( + 'Skip for now', + ); + onboardingSlides.find('textarea').simulate('change', summaryEvent); onboardingSlides.find('#location').simulate('change', locationEvent); onboardingSlides.find('#employment_title').simulate('change', titleEvent); onboardingSlides.find('#employer_name').simulate('change', employerEvent); - - expect(profileForm.state('summary')).toBe(summaryEvent.target.value); - expect(profileForm.state('location')).toBe(locationEvent.target.value); - expect(profileForm.state('employment_title')).toBe( + expect(profileForm.state().formValues.summary).toBe( + summaryEvent.target.value, + ); + expect(profileForm.state().formValues.location).toBe( + locationEvent.target.value, + ); + expect(profileForm.state().formValues.employment_title).toBe( titleEvent.target.value, ); - expect(profileForm.state('employer_name')).toBe( + expect(profileForm.state().formValues.employer_name).toBe( employerEvent.target.value, ); + expect(onboardingSlides.find('.next-button').text()).toContain( + 'Continue', + ); + profileForm.find('.next-button').simulate('click'); fetch.once(fakeTagsResponse); await flushPromises(); @@ -300,6 +319,10 @@ describe('', () => { fetch.once({}); const followUsers = onboardingSlides.find(); + expect(onboardingSlides.find('.next-button').text()).toContain( + 'Skip for now', + ); + onboardingSlides.find('.user').first().simulate('click'); expect(onboardingSlides.find('p').last().text()).toBe( "You're following 1 person", @@ -308,6 +331,11 @@ describe('', () => { expect(onboardingSlides.find('p').last().text()).toBe( "You're following 2 people", ); + + expect(onboardingSlides.find('.next-button').text()).toContain( + 'Continue', + ); + expect(followUsers.state('selectedUsers').length).toBe(2); onboardingSlides.find('.next-button').simulate('click'); await flushPromises(); diff --git a/app/javascript/onboarding/__tests__/__snapshots__/Onboarding.test.jsx.snap b/app/javascript/onboarding/__tests__/__snapshots__/Onboarding.test.jsx.snap index 2e76eaa11..ffea96897 100644 --- a/app/javascript/onboarding/__tests__/__snapshots__/Onboarding.test.jsx.snap +++ b/app/javascript/onboarding/__tests__/__snapshots__/Onboarding.test.jsx.snap @@ -7,21 +7,23 @@ preact-render-spy (1 nodes)
@@ -89,21 +91,23 @@ preact-render-spy (1 nodes)
@@ -195,21 +199,23 @@ preact-render-spy (1 nodes)
@@ -412,21 +418,23 @@ preact-render-spy (1 nodes)
diff --git a/app/javascript/onboarding/components/FollowTags.jsx b/app/javascript/onboarding/components/FollowTags.jsx index 47d7e39de..3a2b00677 100644 --- a/app/javascript/onboarding/components/FollowTags.jsx +++ b/app/javascript/onboarding/components/FollowTags.jsx @@ -97,11 +97,14 @@ class FollowTags extends Component { render() { const { prev, currentSlideIndex, slidesCount } = this.props; const { selectedTags, allTags } = this.state; + const canSkip = selectedTags.length === 0; + return (
diff --git a/app/javascript/onboarding/components/FollowUsers.jsx b/app/javascript/onboarding/components/FollowUsers.jsx index 04f4b2cc7..604106d09 100644 --- a/app/javascript/onboarding/components/FollowUsers.jsx +++ b/app/javascript/onboarding/components/FollowUsers.jsx @@ -130,12 +130,14 @@ class FollowUsers extends Component { render() { const { users, selectedUsers } = this.state; const { prev, slidesCount, currentSlideIndex } = this.props; + const canSkip = selectedUsers.length === 0; return (
diff --git a/app/javascript/onboarding/components/Navigation.jsx b/app/javascript/onboarding/components/Navigation.jsx index f74576038..c175d7ed4 100644 --- a/app/javascript/onboarding/components/Navigation.jsx +++ b/app/javascript/onboarding/components/Navigation.jsx @@ -27,8 +27,36 @@ class Navigation extends Component { return
{stepsList}
; } + /** + * A function to render the text for the "next-button" within the `Navigation` component. + * By default, it renders "Continue" for every slide. + * If the slide can be skipped, it renders "Skip for now". + * On the final slide, it renders "Finish". + * + * @returns {String} The HTML markup for the stepper. + */ + buttonText() { + const { canSkip, currentSlideIndex, slidesCount } = this.props; + if (slidesCount - 1 === currentSlideIndex) { + return 'Finish'; + } + if (canSkip) { + return 'Skip for now'; + } + + return 'Continue'; + } + render() { - const { next, prev, hideNext, hidePrev, disabled, className } = this.props; + const { + next, + prev, + hideNext, + hidePrev, + disabled, + canSkip, + className, + } = this.props; return (
@@ -74,6 +104,7 @@ class Navigation extends Component { Navigation.propTypes = { disabled: PropTypes.bool, + canSkip: PropTypes.bool, className: PropTypes.string, prev: PropTypes.func.isRequired, next: PropTypes.string.isRequired, @@ -85,6 +116,7 @@ Navigation.propTypes = { Navigation.defaultProps = { disabled: false, + canSkip: false, hideNext: false, hidePrev: false, className: '', diff --git a/app/javascript/onboarding/components/ProfileForm.jsx b/app/javascript/onboarding/components/ProfileForm.jsx index 78015117b..7b023b923 100644 --- a/app/javascript/onboarding/components/ProfileForm.jsx +++ b/app/javascript/onboarding/components/ProfileForm.jsx @@ -14,27 +14,31 @@ class ProfileForm extends Component { this.user = userData(); this.state = { - summary: '', - location: '', - employment_title: '', - employer_name: '', + formValues: { + summary: '', + location: '', + employment_title: '', + employer_name: '', + }, last_onboarding_page: 'v2: personal info form', + canSkip: true, }; } componentDidMount() { - updateOnboarding('bio form'); + updateOnboarding('v2: personal info form'); } onSubmit() { const csrfToken = getContentOfToken('csrf-token'); + const { formValues, last_onboarding_page } = this.state; fetch('/onboarding_update', { method: 'PATCH', headers: { 'X-CSRF-Token': csrfToken, 'Content-Type': 'application/json', }, - body: JSON.stringify({ user: { ...this.state } }), + body: JSON.stringify({ user: { ...formValues, last_onboarding_page } }), credentials: 'same-origin', }).then((response) => { if (response.ok) { @@ -45,21 +49,31 @@ class ProfileForm extends Component { } handleChange(e) { + const { formValues } = { ...this.state }; + const currentFormState = formValues; const { name, value } = e.target; - this.setState({ - [name]: value, - }); + currentFormState[name] = value; + + // Once we've derived the new form values, check if the form is empty + // and use that value to set the `canSkip` property on the state. + const formIsEmpty = + Object.values(currentFormState).filter((v) => v.length > 0).length === 0; + + this.setState({ formValues: currentFormState, canSkip: formIsEmpty }); } render() { const { prev, slidesCount, currentSlideIndex } = this.props; const { profile_image_90, username, name } = this.user; + const { canSkip } = this.state; + return (