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": {
|
"just-curry-it": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/just-curry-it/-/just-curry-it-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/just-curry-it/-/just-curry-it-3.1.0.tgz",
|
||||||
@ -23347,6 +23352,15 @@
|
|||||||
"prop-types": "^15.6.1"
|
"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": {
|
"react-side-effect": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.0.tgz",
|
"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-redux": "^5.0.7",
|
||||||
"react-reflex": "^3.1.1",
|
"react-reflex": "^3.1.1",
|
||||||
"react-responsive": "^6.1.1",
|
"react-responsive": "^6.1.1",
|
||||||
|
"react-scrollable-anchor": "^0.6.1",
|
||||||
"react-spinkit": "^3.0.0",
|
"react-spinkit": "^3.0.0",
|
||||||
"react-stripe-elements": "^2.0.3",
|
"react-stripe-elements": "^2.0.3",
|
||||||
"react-tooltip": "^4.2.13",
|
"react-tooltip": "^4.2.13",
|
||||||
|
@ -26,6 +26,7 @@ function ChallengeTitle({ block, children, isCompleted, superBlock }) {
|
|||||||
<div className='challenge-title-breadcrumbs'>
|
<div className='challenge-title-breadcrumbs'>
|
||||||
<Link
|
<Link
|
||||||
className='breadcrumb-left'
|
className='breadcrumb-left'
|
||||||
|
state={{ breadcrumbBlockClick: block }}
|
||||||
to={`/learn/${dasherize(superBlock)}`}
|
to={`/learn/${dasherize(superBlock)}`}
|
||||||
>
|
>
|
||||||
<span className='ellipsis'>
|
<span className='ellipsis'>
|
||||||
@ -36,7 +37,7 @@ function ChallengeTitle({ block, children, isCompleted, superBlock }) {
|
|||||||
<Link
|
<Link
|
||||||
className='breadcrumb-right'
|
className='breadcrumb-right'
|
||||||
state={{ breadcrumbBlockClick: block }}
|
state={{ breadcrumbBlockClick: block }}
|
||||||
to={`/learn/${dasherize(superBlock)}`}
|
to={`/learn/${dasherize(superBlock)}/#${dasherize(block)}`}
|
||||||
>
|
>
|
||||||
{i18next.t(
|
{i18next.t(
|
||||||
`intro:${dasherize(superBlock)}.blocks.${dasherize(block)}.title`
|
`intro:${dasherize(superBlock)}.blocks.${dasherize(block)}.title`
|
||||||
|
@ -6,8 +6,10 @@ import renderer from 'react-test-renderer';
|
|||||||
import ChallengeTitle from './Challenge-Title';
|
import ChallengeTitle from './Challenge-Title';
|
||||||
|
|
||||||
const baseProps = {
|
const baseProps = {
|
||||||
|
block: 'fake block',
|
||||||
children: 'title text',
|
children: 'title text',
|
||||||
isCompleted: true
|
isCompleted: true,
|
||||||
|
superBlock: 'fake superblock'
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('<ChallengeTitle/>', () => {
|
describe('<ChallengeTitle/>', () => {
|
||||||
|
@ -15,7 +15,12 @@ exports[`<ChallengeTitle/> renders correctly 1`] = `
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
className="breadcrumb-left"
|
className="breadcrumb-left"
|
||||||
href="/learn/undefined"
|
href="/learn/fake-superblock"
|
||||||
|
state={
|
||||||
|
Object {
|
||||||
|
"breadcrumbBlockClick": "fake block",
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="ellipsis"
|
className="ellipsis"
|
||||||
@ -26,10 +31,10 @@ exports[`<ChallengeTitle/> renders correctly 1`] = `
|
|||||||
/>
|
/>
|
||||||
<a
|
<a
|
||||||
className="breadcrumb-right"
|
className="breadcrumb-right"
|
||||||
href="/learn/undefined"
|
href="/learn/fake-superblock/#fake-block"
|
||||||
state={
|
state={
|
||||||
Object {
|
Object {
|
||||||
"breadcrumbBlockClick": undefined,
|
"breadcrumbBlockClick": "fake block",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -8,6 +8,7 @@ import { createSelector } from 'reselect';
|
|||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { withTranslation } from 'react-i18next';
|
import { withTranslation } from 'react-i18next';
|
||||||
import { Grid, Row, Col } from '@freecodecamp/react-bootstrap';
|
import { Grid, Row, Col } from '@freecodecamp/react-bootstrap';
|
||||||
|
import { configureAnchors } from 'react-scrollable-anchor';
|
||||||
|
|
||||||
import Login from '../../components/Header/components/Login';
|
import Login from '../../components/Header/components/Login';
|
||||||
import Map from '../../components/Map';
|
import Map from '../../components/Map';
|
||||||
@ -40,6 +41,7 @@ const propTypes = {
|
|||||||
}),
|
}),
|
||||||
isSignedIn: PropTypes.bool,
|
isSignedIn: PropTypes.bool,
|
||||||
location: PropTypes.shape({
|
location: PropTypes.shape({
|
||||||
|
hash: PropTypes.string,
|
||||||
state: PropTypes.shape({
|
state: PropTypes.shape({
|
||||||
breadcrumbBlockClick: PropTypes.string
|
breadcrumbBlockClick: PropTypes.string
|
||||||
})
|
})
|
||||||
@ -49,6 +51,8 @@ const propTypes = {
|
|||||||
toggleBlock: PropTypes.func
|
toggleBlock: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
|
configureAnchors({ offset: -40, scrollDuration: 0 });
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
currentChallengeIdSelector,
|
currentChallengeIdSelector,
|
||||||
@ -71,6 +75,14 @@ const mapDispatchToProps = dispatch =>
|
|||||||
export class SuperBlockIntroductionPage extends Component {
|
export class SuperBlockIntroductionPage extends Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.initializeExpandedState();
|
this.initializeExpandedState();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
configureAnchors({ offset: -40, scrollDuration: 400 });
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
configureAnchors({ offset: -40, scrollDuration: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
getChosenBlock() {
|
getChosenBlock() {
|
||||||
@ -84,8 +96,15 @@ export class SuperBlockIntroductionPage extends Component {
|
|||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
// if coming from breadcrumb click
|
// if coming from breadcrumb click
|
||||||
if (location.state && location.state.breadcrumbBlockClick)
|
if (location.state && location.state.breadcrumbBlockClick) {
|
||||||
return dasherize(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];
|
let edge = edges[0];
|
||||||
|
|
||||||
@ -146,7 +165,7 @@ export class SuperBlockIntroductionPage extends Component {
|
|||||||
<Spacer />
|
<Spacer />
|
||||||
<div className='block-ui'>
|
<div className='block-ui'>
|
||||||
{blockDashedNames.map(blockDashedName => (
|
{blockDashedNames.map(blockDashedName => (
|
||||||
<div key={blockDashedName}>
|
<>
|
||||||
<Block
|
<Block
|
||||||
blockDashedName={blockDashedName}
|
blockDashedName={blockDashedName}
|
||||||
challenges={nodesForSuperBlock.filter(
|
challenges={nodesForSuperBlock.filter(
|
||||||
@ -154,10 +173,8 @@ export class SuperBlockIntroductionPage extends Component {
|
|||||||
)}
|
)}
|
||||||
superBlockDashedName={superBlockDashedName}
|
superBlockDashedName={superBlockDashedName}
|
||||||
/>
|
/>
|
||||||
{blockDashedName !== 'project-euler' ? (
|
{blockDashedName !== 'project-euler' ? <Spacer /> : null}
|
||||||
<Spacer size={2} />
|
</>
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
{superBlock !== 'Coding Interview Prep' && (
|
{superBlock !== 'Coding Interview Prep' && (
|
||||||
<div>
|
<div>
|
||||||
|
@ -4,6 +4,7 @@ import { bindActionCreators } from 'redux';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { withTranslation } from 'react-i18next';
|
import { withTranslation } from 'react-i18next';
|
||||||
|
import ScrollableAnchor from 'react-scrollable-anchor';
|
||||||
|
|
||||||
import { makeExpandedBlockSelector, toggleBlock } from '../redux';
|
import { makeExpandedBlockSelector, toggleBlock } from '../redux';
|
||||||
import { completedChallengesSelector, executeGA } from '../../../redux';
|
import { completedChallengesSelector, executeGA } from '../../../redux';
|
||||||
@ -132,9 +133,15 @@ export class Block extends Component {
|
|||||||
} = t('intro:misc-text');
|
} = t('intro:misc-text');
|
||||||
|
|
||||||
return isProjectBlock ? (
|
return isProjectBlock ? (
|
||||||
|
<ScrollableAnchor id={blockDashedName}>
|
||||||
<div className='block'>
|
<div className='block'>
|
||||||
<div className='block-title-wrapper'>
|
<div className='block-title-wrapper'>
|
||||||
<h3 className='big-block-title'>{blockTitle}</h3>
|
<a className='block-link' href={`#${blockDashedName}`}>
|
||||||
|
<h3 className='big-block-title'>
|
||||||
|
{blockTitle}
|
||||||
|
<span className='block-link-icon'>#</span>
|
||||||
|
</h3>
|
||||||
|
</a>
|
||||||
{!isAuditedCert(curriculumLocale, superBlockDashedName) && (
|
{!isAuditedCert(curriculumLocale, superBlockDashedName) && (
|
||||||
<div className='block-cta-wrapper'>
|
<div className='block-cta-wrapper'>
|
||||||
<Link
|
<Link
|
||||||
@ -152,10 +159,17 @@ export class Block extends Component {
|
|||||||
isProjectBlock={isProjectBlock}
|
isProjectBlock={isProjectBlock}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</ScrollableAnchor>
|
||||||
) : (
|
) : (
|
||||||
|
<ScrollableAnchor id={blockDashedName}>
|
||||||
<div className={`block ${isExpanded ? 'open' : ''}`}>
|
<div className={`block ${isExpanded ? 'open' : ''}`}>
|
||||||
<div className='block-title-wrapper'>
|
<div className='block-title-wrapper'>
|
||||||
<h3 className='big-block-title'>{blockTitle}</h3>
|
<a className='block-link' href={`#${blockDashedName}`}>
|
||||||
|
<h3 className='big-block-title'>
|
||||||
|
{blockTitle}
|
||||||
|
<span className='block-link-icon'>#</span>
|
||||||
|
</h3>
|
||||||
|
</a>
|
||||||
{!isAuditedCert(curriculumLocale, superBlockDashedName) && (
|
{!isAuditedCert(curriculumLocale, superBlockDashedName) && (
|
||||||
<div className='block-cta-wrapper'>
|
<div className='block-cta-wrapper'>
|
||||||
<Link
|
<Link
|
||||||
@ -193,6 +207,7 @@ export class Block extends Component {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</ScrollableAnchor>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,25 @@
|
|||||||
flex-direction: row;
|
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 {
|
.big-block-title {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
@ -185,6 +204,7 @@ button.map-title {
|
|||||||
|
|
||||||
.map-challenges-ul {
|
.map-challenges-ul {
|
||||||
padding-inline-start: 0;
|
padding-inline-start: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-challenge-title {
|
.map-challenge-title {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/* global cy */
|
/* global cy */
|
||||||
|
|
||||||
const selectors = {
|
const selectors = {
|
||||||
firstBlock: ':nth-child(1) > .block > .map-title'
|
firstBlock: '.block-ui > .block:nth-child(1) > .map-title'
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Certificate intro page', () => {
|
describe('Certificate intro page', () => {
|
||||||
|
Reference in New Issue
Block a user