feat(client): improve navigation experience with scrolling (#41042)
This commit is contained in:
14
client/package-lock.json
generated
14
client/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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`
|
||||
|
@ -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/>', () => {
|
||||
|
@ -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",
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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', () => {
|
||||
|
Reference in New Issue
Block a user