Add skip/continue functionality to onboarding navigation (#7488) [deploy]

* Dynamically render forward text in Navigation component

* Render either "Skip for now" or "Continue" on the FollowTags, ProfileForm, and FollowUsers components.
* Refactor the ProfileForm component to dynamically change the button text based on the form's state.

* Render specific button text for last onboarding slide

* Fix back arrow styles for accessibility

Also set an explicit width on the containing button around the svg.

* Use min-width to specify back-button size
This commit is contained in:
Vaidehi Joshi 2020-04-23 16:30:48 -07:00 committed by GitHub
parent 2a74f6f811
commit e19eb60d1f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 201 additions and 91 deletions

View file

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

View file

@ -194,9 +194,17 @@ describe('<Onboarding />', () => {
.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('<Onboarding />', () => {
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('<Onboarding />', () => {
fetch.once({});
const followUsers = onboardingSlides.find(<FollowUsers />);
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('<Onboarding />', () => {
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();

View file

@ -7,21 +7,23 @@ preact-render-spy (1 nodes)
<div class="onboarding-main">
<nav class="onboarding-navigation">
<div class="navigation-content">
<button
onClick={[Function bound prevSlide]}
class="back-button"
type="button"
>
<svg
width="16"
height="16"
fill="none"
class="crayons-icon"
xmlns="http://www.w3.org/2000/svg"
<div class="back-button-container">
<button
onClick={[Function bound prevSlide]}
class="back-button"
type="button"
>
<path d="M3.828 7H16v2H3.828l5.364 5.364-1.414 1.414L0 8 7.778.222l1.414 1.414L3.828 7z"></path>
</svg>
</button>
<svg
width="24"
height="24"
fill="none"
class="crayons-icon"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M7.828 11H20v2H7.828l5.364 5.364-1.414 1.414L4 12l7.778-7.778 1.414 1.414L7.828 11z"></path>
</svg>
</button>
</div>
<div class="stepper">
<span class="dot active"></span>
<span class="dot active"></span>
@ -34,7 +36,7 @@ preact-render-spy (1 nodes)
class="next-button"
type="button"
>
Continue
Finish
</button>
</div>
</nav>
@ -89,21 +91,23 @@ preact-render-spy (1 nodes)
<div class="onboarding-main">
<nav class="onboarding-navigation">
<div class="navigation-content">
<button
onClick={[Function bound prevSlide]}
class="back-button"
type="button"
>
<svg
width="16"
height="16"
fill="none"
class="crayons-icon"
xmlns="http://www.w3.org/2000/svg"
<div class="back-button-container">
<button
onClick={[Function bound prevSlide]}
class="back-button"
type="button"
>
<path d="M3.828 7H16v2H3.828l5.364 5.364-1.414 1.414L0 8 7.778.222l1.414 1.414L3.828 7z"></path>
</svg>
</button>
<svg
width="24"
height="24"
fill="none"
class="crayons-icon"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M7.828 11H20v2H7.828l5.364 5.364-1.414 1.414L4 12l7.778-7.778 1.414 1.414L7.828 11z"></path>
</svg>
</button>
</div>
<div class="stepper">
<span class="dot active"></span>
<span class="dot "></span>
@ -113,10 +117,10 @@ preact-render-spy (1 nodes)
<button
disabled={false}
onClick={[Function bound handleComplete]}
class="next-button"
class="next-button skip-for-now"
type="button"
>
Continue
Skip for now
</button>
</div>
</nav>
@ -195,21 +199,23 @@ preact-render-spy (1 nodes)
<div class="onboarding-main">
<nav class="onboarding-navigation">
<div class="navigation-content">
<button
onClick={[Function bound prevSlide]}
class="back-button"
type="button"
>
<svg
width="16"
height="16"
fill="none"
class="crayons-icon"
xmlns="http://www.w3.org/2000/svg"
<div class="back-button-container">
<button
onClick={[Function bound prevSlide]}
class="back-button"
type="button"
>
<path d="M3.828 7H16v2H3.828l5.364 5.364-1.414 1.414L0 8 7.778.222l1.414 1.414L3.828 7z"></path>
</svg>
</button>
<svg
width="24"
height="24"
fill="none"
class="crayons-icon"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M7.828 11H20v2H7.828l5.364 5.364-1.414 1.414L4 12l7.778-7.778 1.414 1.414L7.828 11z"></path>
</svg>
</button>
</div>
<div class="stepper">
<span class="dot active"></span>
<span class="dot active"></span>
@ -219,10 +225,10 @@ preact-render-spy (1 nodes)
<button
disabled={false}
onClick={[Function bound handleComplete]}
class="next-button"
class="next-button skip-for-now"
type="button"
>
Continue
Skip for now
</button>
</div>
</nav>
@ -412,21 +418,23 @@ preact-render-spy (1 nodes)
<div class="onboarding-main">
<nav class="onboarding-navigation">
<div class="navigation-content">
<button
onClick={[Function bound prevSlide]}
class="back-button"
type="button"
>
<svg
width="16"
height="16"
fill="none"
class="crayons-icon"
xmlns="http://www.w3.org/2000/svg"
<div class="back-button-container">
<button
onClick={[Function bound prevSlide]}
class="back-button"
type="button"
>
<path d="M3.828 7H16v2H3.828l5.364 5.364-1.414 1.414L0 8 7.778.222l1.414 1.414L3.828 7z"></path>
</svg>
</button>
<svg
width="24"
height="24"
fill="none"
class="crayons-icon"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M7.828 11H20v2H7.828l5.364 5.364-1.414 1.414L4 12l7.778-7.778 1.414 1.414L7.828 11z"></path>
</svg>
</button>
</div>
<div class="stepper">
<span class="dot active"></span>
<span class="dot active"></span>
@ -436,10 +444,10 @@ preact-render-spy (1 nodes)
<button
disabled={false}
onClick={[Function bound onSubmit]}
class="next-button"
class="next-button skip-for-now"
type="button"
>
Continue
Skip for now
</button>
</div>
</nav>

View file

@ -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 (
<div className="onboarding-main">
<Navigation
prev={prev}
next={this.handleComplete}
canSkip={canSkip}
slidesCount={slidesCount}
currentSlideIndex={currentSlideIndex}
/>

View file

@ -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 (
<div className="onboarding-main">
<Navigation
prev={prev}
next={this.handleComplete}
canSkip={canSkip}
slidesCount={slidesCount}
currentSlideIndex={currentSlideIndex}
/>

View file

@ -27,8 +27,36 @@ class Navigation extends Component {
return <div className="stepper">{stepsList}</div>;
}
/**
* 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 (
<nav
className={`onboarding-navigation${
@ -41,17 +69,19 @@ class Navigation extends Component {
}`}
>
{!hidePrev && (
<button onClick={prev} className="back-button" type="button">
<svg
width="16"
height="16"
fill="none"
className="crayons-icon"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M3.828 7H16v2H3.828l5.364 5.364-1.414 1.414L0 8 7.778.222l1.414 1.414L3.828 7z" />
</svg>
</button>
<div className="back-button-container">
<button onClick={prev} className="back-button" type="button">
<svg
width="24"
height="24"
fill="none"
className="crayons-icon"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M7.828 11H20v2H7.828l5.364 5.364-1.414 1.414L4 12l7.778-7.778 1.414 1.414L7.828 11z" />
</svg>
</button>
</div>
)}
{this.createStepper()}
@ -60,10 +90,10 @@ class Navigation extends Component {
<button
disabled={disabled}
onClick={next}
className="next-button"
className={`next-button${canSkip ? ' skip-for-now' : ''}`}
type="button"
>
Continue
{this.buttonText()}
</button>
)}
</div>
@ -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: '',

View file

@ -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 (
<div className="onboarding-main">
<Navigation
prev={prev}
next={this.onSubmit}
canSkip={canSkip}
slidesCount={slidesCount}
currentSlideIndex={currentSlideIndex}
/>