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:
parent
2a74f6f811
commit
e19eb60d1f
7 changed files with 201 additions and 91 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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: '',
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue