feat(client): improve navigation experience with scrolling (#41042)

This commit is contained in:
Tom
2021-02-11 12:22:04 -06:00
committed by GitHub
parent 5ac74230ae
commit 32eade41b1
9 changed files with 142 additions and 67 deletions

View File

@ -19599,6 +19599,11 @@
}
}
},
"jump.js": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/jump.js/-/jump.js-1.0.1.tgz",
"integrity": "sha1-DeKxYxupocLGuFcq0nfYd+hQNgA="
},
"just-curry-it": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/just-curry-it/-/just-curry-it-3.1.0.tgz",
@ -23347,6 +23352,15 @@
"prop-types": "^15.6.1"
}
},
"react-scrollable-anchor": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/react-scrollable-anchor/-/react-scrollable-anchor-0.6.1.tgz",
"integrity": "sha1-/W54Amx0T3ZBQFPQaQO4KtzLVNk=",
"requires": {
"jump.js": "1.0.1",
"prop-types": "^15.5.10"
}
},
"react-side-effect": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.0.tgz",

View File

@ -61,6 +61,7 @@
"react-redux": "^5.0.7",
"react-reflex": "^3.1.1",
"react-responsive": "^6.1.1",
"react-scrollable-anchor": "^0.6.1",
"react-spinkit": "^3.0.0",
"react-stripe-elements": "^2.0.3",
"react-tooltip": "^4.2.13",

View File

@ -26,6 +26,7 @@ function ChallengeTitle({ block, children, isCompleted, superBlock }) {
<div className='challenge-title-breadcrumbs'>
<Link
className='breadcrumb-left'
state={{ breadcrumbBlockClick: block }}
to={`/learn/${dasherize(superBlock)}`}
>
<span className='ellipsis'>
@ -36,7 +37,7 @@ function ChallengeTitle({ block, children, isCompleted, superBlock }) {
<Link
className='breadcrumb-right'
state={{ breadcrumbBlockClick: block }}
to={`/learn/${dasherize(superBlock)}`}
to={`/learn/${dasherize(superBlock)}/#${dasherize(block)}`}
>
{i18next.t(
`intro:${dasherize(superBlock)}.blocks.${dasherize(block)}.title`

View File

@ -6,8 +6,10 @@ import renderer from 'react-test-renderer';
import ChallengeTitle from './Challenge-Title';
const baseProps = {
block: 'fake block',
children: 'title text',
isCompleted: true
isCompleted: true,
superBlock: 'fake superblock'
};
describe('<ChallengeTitle/>', () => {

View File

@ -15,7 +15,12 @@ exports[`<ChallengeTitle/> renders correctly 1`] = `
>
<a
className="breadcrumb-left"
href="/learn/undefined"
href="/learn/fake-superblock"
state={
Object {
"breadcrumbBlockClick": "fake block",
}
}
>
<span
className="ellipsis"
@ -26,10 +31,10 @@ exports[`<ChallengeTitle/> renders correctly 1`] = `
/>
<a
className="breadcrumb-right"
href="/learn/undefined"
href="/learn/fake-superblock/#fake-block"
state={
Object {
"breadcrumbBlockClick": undefined,
"breadcrumbBlockClick": "fake block",
}
}
/>

View File

@ -8,6 +8,7 @@ import { createSelector } from 'reselect';
import { bindActionCreators } from 'redux';
import { withTranslation } from 'react-i18next';
import { Grid, Row, Col } from '@freecodecamp/react-bootstrap';
import { configureAnchors } from 'react-scrollable-anchor';
import Login from '../../components/Header/components/Login';
import Map from '../../components/Map';
@ -40,6 +41,7 @@ const propTypes = {
}),
isSignedIn: PropTypes.bool,
location: PropTypes.shape({
hash: PropTypes.string,
state: PropTypes.shape({
breadcrumbBlockClick: PropTypes.string
})
@ -49,6 +51,8 @@ const propTypes = {
toggleBlock: PropTypes.func
};
configureAnchors({ offset: -40, scrollDuration: 0 });
const mapStateToProps = state => {
return createSelector(
currentChallengeIdSelector,
@ -71,6 +75,14 @@ const mapDispatchToProps = dispatch =>
export class SuperBlockIntroductionPage extends Component {
componentDidMount() {
this.initializeExpandedState();
setTimeout(() => {
configureAnchors({ offset: -40, scrollDuration: 400 });
}, 0);
}
componentWillUnmount() {
configureAnchors({ offset: -40, scrollDuration: 0 });
}
getChosenBlock() {
@ -84,8 +96,15 @@ export class SuperBlockIntroductionPage extends Component {
} = this.props;
// if coming from breadcrumb click
if (location.state && location.state.breadcrumbBlockClick)
if (location.state && location.state.breadcrumbBlockClick) {
return dasherize(location.state.breadcrumbBlockClick);
}
// if the URL includes a hash
if (location.hash) {
const dashedBlock = location.hash.replace('#', '').replace('/', '');
return dashedBlock;
}
let edge = edges[0];
@ -146,7 +165,7 @@ export class SuperBlockIntroductionPage extends Component {
<Spacer />
<div className='block-ui'>
{blockDashedNames.map(blockDashedName => (
<div key={blockDashedName}>
<>
<Block
blockDashedName={blockDashedName}
challenges={nodesForSuperBlock.filter(
@ -154,10 +173,8 @@ export class SuperBlockIntroductionPage extends Component {
)}
superBlockDashedName={superBlockDashedName}
/>
{blockDashedName !== 'project-euler' ? (
<Spacer size={2} />
) : null}
</div>
{blockDashedName !== 'project-euler' ? <Spacer /> : null}
</>
))}
{superBlock !== 'Coding Interview Prep' && (
<div>

View File

@ -4,6 +4,7 @@ import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { withTranslation } from 'react-i18next';
import ScrollableAnchor from 'react-scrollable-anchor';
import { makeExpandedBlockSelector, toggleBlock } from '../redux';
import { completedChallengesSelector, executeGA } from '../../../redux';
@ -132,67 +133,81 @@ export class Block extends Component {
} = t('intro:misc-text');
return isProjectBlock ? (
<div className='block'>
<div className='block-title-wrapper'>
<h3 className='big-block-title'>{blockTitle}</h3>
{!isAuditedCert(curriculumLocale, superBlockDashedName) && (
<div className='block-cta-wrapper'>
<Link
className='block-title-translation-cta'
to='https://contribute.freecodecamp.org/#/how-to-translate-files'
>
{t('misc.translation-pending')}
</Link>
</div>
)}
</div>
{this.renderBlockIntros(blockIntroArr)}
<Challenges
challengesWithCompleted={challengesWithCompleted}
isProjectBlock={isProjectBlock}
/>
</div>
) : (
<div className={`block ${isExpanded ? 'open' : ''}`}>
<div className='block-title-wrapper'>
<h3 className='big-block-title'>{blockTitle}</h3>
{!isAuditedCert(curriculumLocale, superBlockDashedName) && (
<div className='block-cta-wrapper'>
<Link
className='block-title-translation-cta'
to='https://contribute.freecodecamp.org/#/how-to-translate-files'
>
{t('misc.translation-pending')}
</Link>
</div>
)}
</div>
{this.renderBlockIntros(blockIntroArr)}
<button
aria-expanded={isExpanded}
className='map-title'
onClick={this.handleBlockClick}
>
<Caret />
<h4 className='course-title'>
{`${
isExpanded ? collapseText : expandText
} ${coursesText.toLowerCase()}`}
</h4>
<div className='map-title-completed course-title'>
{this.renderCheckMark(
completedCount === challengesWithCompleted.length
<ScrollableAnchor id={blockDashedName}>
<div className='block'>
<div className='block-title-wrapper'>
<a className='block-link' href={`#${blockDashedName}`}>
<h3 className='big-block-title'>
{blockTitle}
<span className='block-link-icon'>#</span>
</h3>
</a>
{!isAuditedCert(curriculumLocale, superBlockDashedName) && (
<div className='block-cta-wrapper'>
<Link
className='block-title-translation-cta'
to='https://contribute.freecodecamp.org/#/how-to-translate-files'
>
{t('misc.translation-pending')}
</Link>
</div>
)}
<span className='map-completed-count'>{`${completedCount}/${challengesWithCompleted.length}`}</span>
</div>
</button>
{isExpanded && (
{this.renderBlockIntros(blockIntroArr)}
<Challenges
challengesWithCompleted={challengesWithCompleted}
isProjectBlock={isProjectBlock}
/>
)}
</div>
</div>
</ScrollableAnchor>
) : (
<ScrollableAnchor id={blockDashedName}>
<div className={`block ${isExpanded ? 'open' : ''}`}>
<div className='block-title-wrapper'>
<a className='block-link' href={`#${blockDashedName}`}>
<h3 className='big-block-title'>
{blockTitle}
<span className='block-link-icon'>#</span>
</h3>
</a>
{!isAuditedCert(curriculumLocale, superBlockDashedName) && (
<div className='block-cta-wrapper'>
<Link
className='block-title-translation-cta'
to='https://contribute.freecodecamp.org/#/how-to-translate-files'
>
{t('misc.translation-pending')}
</Link>
</div>
)}
</div>
{this.renderBlockIntros(blockIntroArr)}
<button
aria-expanded={isExpanded}
className='map-title'
onClick={this.handleBlockClick}
>
<Caret />
<h4 className='course-title'>
{`${
isExpanded ? collapseText : expandText
} ${coursesText.toLowerCase()}`}
</h4>
<div className='map-title-completed course-title'>
{this.renderCheckMark(
completedCount === challengesWithCompleted.length
)}
<span className='map-completed-count'>{`${completedCount}/${challengesWithCompleted.length}`}</span>
</div>
</button>
{isExpanded && (
<Challenges
challengesWithCompleted={challengesWithCompleted}
isProjectBlock={isProjectBlock}
/>
)}
</div>
</ScrollableAnchor>
);
}
}

View File

@ -14,6 +14,25 @@
flex-direction: row;
}
.block-link:hover,
.block-link:focus,
.block-link:active {
background-color: var(--primary-background);
}
.block-link {
cursor: pointer;
}
.block-link:hover .block-link-icon {
display: inline-block;
}
.block-link-icon {
display: none;
margin-left: 15px;
}
.big-block-title {
font-size: 1.5rem;
overflow-wrap: break-word;
@ -185,6 +204,7 @@ button.map-title {
.map-challenges-ul {
padding-inline-start: 0;
margin-bottom: 0;
}
.map-challenge-title {

View File

@ -1,7 +1,7 @@
/* global cy */
const selectors = {
firstBlock: ':nth-child(1) > .block > .map-title'
firstBlock: '.block-ui > .block:nth-child(1) > .map-title'
};
describe('Certificate intro page', () => {