mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2025-12-19 18:18:27 -05:00
fix(client): add better navigation for navigating back to blocks (#64027)
Co-authored-by: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com>
This commit is contained in:
@@ -34,6 +34,7 @@ describe('<BlockHeader />', () => {
|
|||||||
{...defaultProps}
|
{...defaultProps}
|
||||||
accordion={true}
|
accordion={true}
|
||||||
blockUrl='/learn/test-block'
|
blockUrl='/learn/test-block'
|
||||||
|
onLinkClick={() => {}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import CheckMark from './check-mark';
|
|||||||
import BlockLabel from './block-label';
|
import BlockLabel from './block-label';
|
||||||
import BlockIntros from './block-intros';
|
import BlockIntros from './block-intros';
|
||||||
|
|
||||||
interface BlockHeaderProps {
|
interface BaseBlockHeaderProps {
|
||||||
blockDashed: string;
|
blockDashed: string;
|
||||||
blockTitle: string;
|
blockTitle: string;
|
||||||
blockLabel: BlockLabelType | null;
|
blockLabel: BlockLabelType | null;
|
||||||
@@ -22,9 +22,19 @@ interface BlockHeaderProps {
|
|||||||
percentageCompleted: number;
|
percentageCompleted: number;
|
||||||
blockIntroArr?: string[];
|
blockIntroArr?: string[];
|
||||||
accordion?: boolean;
|
accordion?: boolean;
|
||||||
blockUrl?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface BlockHeaderButtonProps extends BaseBlockHeaderProps {
|
||||||
|
blockUrl?: never;
|
||||||
|
onLinkClick?: never;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BlockHeaderLinkProps extends BaseBlockHeaderProps {
|
||||||
|
blockUrl: string;
|
||||||
|
onLinkClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type BlockHeaderProps = BlockHeaderButtonProps | BlockHeaderLinkProps;
|
||||||
function BlockHeader({
|
function BlockHeader({
|
||||||
blockDashed,
|
blockDashed,
|
||||||
blockTitle,
|
blockTitle,
|
||||||
@@ -37,7 +47,8 @@ function BlockHeader({
|
|||||||
percentageCompleted,
|
percentageCompleted,
|
||||||
blockIntroArr,
|
blockIntroArr,
|
||||||
accordion,
|
accordion,
|
||||||
blockUrl
|
blockUrl,
|
||||||
|
onLinkClick
|
||||||
}: BlockHeaderProps): JSX.Element {
|
}: BlockHeaderProps): JSX.Element {
|
||||||
const InnerBlockHeader = () => (
|
const InnerBlockHeader = () => (
|
||||||
<>
|
<>
|
||||||
@@ -68,7 +79,7 @@ function BlockHeader({
|
|||||||
<>
|
<>
|
||||||
<h3 className='block-grid-title'>
|
<h3 className='block-grid-title'>
|
||||||
{accordion && blockUrl ? (
|
{accordion && blockUrl ? (
|
||||||
<Link className='block-header' to={blockUrl}>
|
<Link className='block-header' to={blockUrl} onClick={onLinkClick}>
|
||||||
<InnerBlockHeader />
|
<InnerBlockHeader />
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export class Block extends Component<BlockProps> {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.handleBlockClick = this.handleBlockClick.bind(this);
|
this.handleBlockClick = this.handleBlockClick.bind(this);
|
||||||
this.handleBlockHover = this.handleBlockHover.bind(this);
|
this.handleChallengeClick = this.handleChallengeClick.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleBlockClick = (): void => {
|
handleBlockClick = (): void => {
|
||||||
@@ -91,13 +91,8 @@ export class Block extends Component<BlockProps> {
|
|||||||
toggleBlock(block);
|
toggleBlock(block);
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
handleChallengeClick = (): void => {
|
||||||
* This function handles the block hover event.
|
|
||||||
* It also updates the URL hash to reflect the current block.
|
|
||||||
*/
|
|
||||||
handleBlockHover = (): void => {
|
|
||||||
const { block } = this.props;
|
const { block } = this.props;
|
||||||
// Convert block to dashed format
|
|
||||||
const dashedBlock = block
|
const dashedBlock = block
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/\s+/g, '-')
|
.replace(/\s+/g, '-')
|
||||||
@@ -181,11 +176,7 @@ export class Block extends Component<BlockProps> {
|
|||||||
*/
|
*/
|
||||||
const LegacyChallengeListBlock = (
|
const LegacyChallengeListBlock = (
|
||||||
<Element name={block}>
|
<Element name={block}>
|
||||||
<div
|
<div className={`block ${isExpanded ? 'open' : ''}`}>
|
||||||
className={`block ${isExpanded ? 'open' : ''}`}
|
|
||||||
onMouseOver={this.handleBlockHover}
|
|
||||||
onFocus={this.handleBlockHover}
|
|
||||||
>
|
|
||||||
<div className='block-header'>
|
<div className='block-header'>
|
||||||
<h3 className='big-block-title'>{blockTitle}</h3>
|
<h3 className='big-block-title'>{blockTitle}</h3>
|
||||||
{blockLabel && <BlockLabel blockLabel={blockLabel} />}
|
{blockLabel && <BlockLabel blockLabel={blockLabel} />}
|
||||||
@@ -224,7 +215,12 @@ export class Block extends Component<BlockProps> {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{isExpanded && <ChallengesList challenges={extendedChallenges} />}
|
{isExpanded && (
|
||||||
|
<ChallengesList
|
||||||
|
challenges={extendedChallenges}
|
||||||
|
onChallengeClick={this.handleChallengeClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Element>
|
</Element>
|
||||||
);
|
);
|
||||||
@@ -236,11 +232,7 @@ export class Block extends Component<BlockProps> {
|
|||||||
*/
|
*/
|
||||||
const ProjectListBlock = (
|
const ProjectListBlock = (
|
||||||
<Element name={block}>
|
<Element name={block}>
|
||||||
<div
|
<div className='block'>
|
||||||
className='block'
|
|
||||||
onMouseOver={this.handleBlockHover}
|
|
||||||
onFocus={this.handleBlockHover}
|
|
||||||
>
|
|
||||||
<div className='block-header'>
|
<div className='block-header'>
|
||||||
<h3 className='big-block-title'>{blockTitle}</h3>
|
<h3 className='big-block-title'>{blockTitle}</h3>
|
||||||
{blockLabel && <BlockLabel blockLabel={blockLabel} />}
|
{blockLabel && <BlockLabel blockLabel={blockLabel} />}
|
||||||
@@ -255,7 +247,10 @@ export class Block extends Component<BlockProps> {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ChallengesList challenges={extendedChallenges} />
|
<ChallengesList
|
||||||
|
challenges={extendedChallenges}
|
||||||
|
onChallengeClick={this.handleChallengeClick}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Element>
|
</Element>
|
||||||
);
|
);
|
||||||
@@ -267,11 +262,7 @@ export class Block extends Component<BlockProps> {
|
|||||||
*/
|
*/
|
||||||
const LegacyChallengeGridBlock = (
|
const LegacyChallengeGridBlock = (
|
||||||
<Element name={block}>
|
<Element name={block}>
|
||||||
<div
|
<div className={`block block-grid ${isExpanded ? 'open' : ''}`}>
|
||||||
className={`block block-grid ${isExpanded ? 'open' : ''}`}
|
|
||||||
onMouseOver={this.handleBlockHover}
|
|
||||||
onFocus={this.handleBlockHover}
|
|
||||||
>
|
|
||||||
<BlockHeader
|
<BlockHeader
|
||||||
blockDashed={block}
|
blockDashed={block}
|
||||||
blockTitle={blockTitle}
|
blockTitle={blockTitle}
|
||||||
@@ -305,6 +296,7 @@ export class Block extends Component<BlockProps> {
|
|||||||
challenges={extendedChallenges}
|
challenges={extendedChallenges}
|
||||||
isProjectBlock={isProjectBlock}
|
isProjectBlock={isProjectBlock}
|
||||||
blockTitle={blockTitle}
|
blockTitle={blockTitle}
|
||||||
|
onChallengeClick={this.handleChallengeClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -353,6 +345,7 @@ export class Block extends Component<BlockProps> {
|
|||||||
challenges={extendedChallenges}
|
challenges={extendedChallenges}
|
||||||
blockTitle={blockTitle}
|
blockTitle={blockTitle}
|
||||||
jumpLink={!accordion}
|
jumpLink={!accordion}
|
||||||
|
onChallengeClick={this.handleChallengeClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -368,11 +361,7 @@ export class Block extends Component<BlockProps> {
|
|||||||
*/
|
*/
|
||||||
const LegacyLinkBlock = (
|
const LegacyLinkBlock = (
|
||||||
<Element name={block}>
|
<Element name={block}>
|
||||||
<div
|
<div className='block block-grid grid-project-block'>
|
||||||
className='block block-grid grid-project-block'
|
|
||||||
onMouseOver={this.handleBlockHover}
|
|
||||||
onFocus={this.handleBlockHover}
|
|
||||||
>
|
|
||||||
<div className='tags-wrapper'>
|
<div className='tags-wrapper'>
|
||||||
<span className='cert-tag' aria-hidden='true'>
|
<span className='cert-tag' aria-hidden='true'>
|
||||||
{t('misc.certification-project')}
|
{t('misc.certification-project')}
|
||||||
@@ -394,9 +383,7 @@ export class Block extends Component<BlockProps> {
|
|||||||
<h3 className='block-grid-title'>
|
<h3 className='block-grid-title'>
|
||||||
<Link
|
<Link
|
||||||
className='block-header'
|
className='block-header'
|
||||||
onClick={() => {
|
onClick={this.handleChallengeClick}
|
||||||
this.handleBlockClick();
|
|
||||||
}}
|
|
||||||
to={link}
|
to={link}
|
||||||
>
|
>
|
||||||
<CheckMark isCompleted={isBlockCompleted} />
|
<CheckMark isCompleted={isBlockCompleted} />
|
||||||
@@ -423,8 +410,6 @@ export class Block extends Component<BlockProps> {
|
|||||||
</Element>
|
</Element>
|
||||||
<div
|
<div
|
||||||
className={`block block-grid block-grid-no-border challenge-grid-block ${isExpanded ? 'open' : ''}`}
|
className={`block block-grid block-grid-no-border challenge-grid-block ${isExpanded ? 'open' : ''}`}
|
||||||
onMouseOver={this.handleBlockHover}
|
|
||||||
onFocus={this.handleBlockHover}
|
|
||||||
>
|
>
|
||||||
<BlockHeader
|
<BlockHeader
|
||||||
blockDashed={block}
|
blockDashed={block}
|
||||||
@@ -461,9 +446,13 @@ export class Block extends Component<BlockProps> {
|
|||||||
blockTitle={blockTitle}
|
blockTitle={blockTitle}
|
||||||
isProjectBlock={isProjectBlock}
|
isProjectBlock={isProjectBlock}
|
||||||
jumpLink={false}
|
jumpLink={false}
|
||||||
|
onChallengeClick={this.handleChallengeClick}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ChallengesList challenges={extendedChallenges} />
|
<ChallengesList
|
||||||
|
challenges={extendedChallenges}
|
||||||
|
onChallengeClick={this.handleChallengeClick}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -477,8 +466,6 @@ export class Block extends Component<BlockProps> {
|
|||||||
</Element>
|
</Element>
|
||||||
<div
|
<div
|
||||||
className={`block block-grid challenge-grid-block ${isExpanded ? 'open' : ''}`}
|
className={`block block-grid challenge-grid-block ${isExpanded ? 'open' : ''}`}
|
||||||
onMouseOver={this.handleBlockHover}
|
|
||||||
onFocus={this.handleBlockHover}
|
|
||||||
>
|
>
|
||||||
<BlockHeader
|
<BlockHeader
|
||||||
blockDashed={block}
|
blockDashed={block}
|
||||||
@@ -516,9 +503,13 @@ export class Block extends Component<BlockProps> {
|
|||||||
challenges={extendedChallenges}
|
challenges={extendedChallenges}
|
||||||
blockTitle={blockTitle}
|
blockTitle={blockTitle}
|
||||||
isProjectBlock={isProjectBlock}
|
isProjectBlock={isProjectBlock}
|
||||||
|
onChallengeClick={this.handleChallengeClick}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ChallengesList challenges={extendedChallenges} />
|
<ChallengesList
|
||||||
|
challenges={extendedChallenges}
|
||||||
|
onChallengeClick={this.handleChallengeClick}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -539,6 +530,7 @@ export class Block extends Component<BlockProps> {
|
|||||||
completedCount={completedCount}
|
completedCount={completedCount}
|
||||||
courseCompletionStatus={courseCompletionStatus()}
|
courseCompletionStatus={courseCompletionStatus()}
|
||||||
handleClick={this.handleBlockClick}
|
handleClick={this.handleBlockClick}
|
||||||
|
onLinkClick={this.handleChallengeClick}
|
||||||
isCompleted={isBlockCompleted}
|
isCompleted={isBlockCompleted}
|
||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
percentageCompleted={percentageCompleted}
|
percentageCompleted={percentageCompleted}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ interface ChallengeInfo {
|
|||||||
|
|
||||||
interface ChallengesProps {
|
interface ChallengesProps {
|
||||||
challenges: ChallengeInfo[];
|
challenges: ChallengeInfo[];
|
||||||
|
onChallengeClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface JumpLinkProps {
|
interface JumpLinkProps {
|
||||||
@@ -37,25 +38,44 @@ interface IsProjectBlockProps {
|
|||||||
const CheckMark = ({ isCompleted }: { isCompleted: boolean }) =>
|
const CheckMark = ({ isCompleted }: { isCompleted: boolean }) =>
|
||||||
isCompleted ? <GreenPass /> : <GreenNotCompleted />;
|
isCompleted ? <GreenPass /> : <GreenNotCompleted />;
|
||||||
|
|
||||||
const ListChallenge = ({ challenge }: { challenge: ChallengeInfo }) => (
|
const ListChallenge = ({
|
||||||
<Link to={challenge.fields.slug}>
|
challenge,
|
||||||
<span>
|
onChallengeClick
|
||||||
<CheckMark isCompleted={challenge.isCompleted} />
|
}: {
|
||||||
</span>
|
challenge: ChallengeInfo;
|
||||||
{challenge.title}
|
onChallengeClick: () => void;
|
||||||
</Link>
|
}) => {
|
||||||
);
|
return (
|
||||||
|
<Link to={challenge.fields.slug} onClick={onChallengeClick}>
|
||||||
|
<span>
|
||||||
|
<CheckMark isCompleted={challenge.isCompleted} />
|
||||||
|
</span>
|
||||||
|
{challenge.title}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const CertChallenge = ({ challenge }: { challenge: ChallengeInfo }) => (
|
const CertChallenge = ({
|
||||||
<Link to={challenge.fields.slug}>
|
challenge,
|
||||||
{challenge.title}
|
onChallengeClick
|
||||||
<span className='map-project-checkmark'>
|
}: {
|
||||||
<CheckMark isCompleted={challenge.isCompleted} />
|
challenge: ChallengeInfo;
|
||||||
</span>
|
onChallengeClick: () => void;
|
||||||
</Link>
|
}) => {
|
||||||
);
|
return (
|
||||||
|
<Link to={challenge.fields.slug} onClick={onChallengeClick}>
|
||||||
|
{challenge.title}
|
||||||
|
<span className='map-project-checkmark'>
|
||||||
|
<CheckMark isCompleted={challenge.isCompleted} />
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export function ChallengesList({ challenges }: ChallengesProps): JSX.Element {
|
export function ChallengesList({
|
||||||
|
challenges,
|
||||||
|
onChallengeClick
|
||||||
|
}: ChallengesProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<ul className={`map-challenges-ul`}>
|
<ul className={`map-challenges-ul`}>
|
||||||
{challenges.map(challenge => (
|
{challenges.map(challenge => (
|
||||||
@@ -64,7 +84,10 @@ export function ChallengesList({ challenges }: ChallengesProps): JSX.Element {
|
|||||||
id={challenge.dashedName}
|
id={challenge.dashedName}
|
||||||
key={'map-challenge' + challenge.fields.slug}
|
key={'map-challenge' + challenge.fields.slug}
|
||||||
>
|
>
|
||||||
<ListChallenge challenge={challenge} />
|
<ListChallenge
|
||||||
|
challenge={challenge}
|
||||||
|
onChallengeClick={onChallengeClick}
|
||||||
|
/>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -73,16 +96,19 @@ export function ChallengesList({ challenges }: ChallengesProps): JSX.Element {
|
|||||||
// Step or Task challenge
|
// Step or Task challenge
|
||||||
const GridChallenge = ({
|
const GridChallenge = ({
|
||||||
challenge,
|
challenge,
|
||||||
isTask = false
|
isTask = false,
|
||||||
|
onChallengeClick
|
||||||
}: {
|
}: {
|
||||||
challenge: ChallengeInfo;
|
challenge: ChallengeInfo;
|
||||||
isTask?: boolean;
|
isTask?: boolean;
|
||||||
|
onChallengeClick: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
to={challenge.fields.slug}
|
to={challenge.fields.slug}
|
||||||
|
onClick={onChallengeClick}
|
||||||
className={`map-grid-item ${
|
className={`map-grid-item ${
|
||||||
challenge.isCompleted ? 'challenge-completed' : ''
|
challenge.isCompleted ? 'challenge-completed' : ''
|
||||||
}`}
|
}`}
|
||||||
@@ -100,7 +126,8 @@ const GridChallenge = ({
|
|||||||
|
|
||||||
const LinkToFirstIncompleteChallenge = ({
|
const LinkToFirstIncompleteChallenge = ({
|
||||||
challenges,
|
challenges,
|
||||||
blockTitle
|
blockTitle,
|
||||||
|
onChallengeClick
|
||||||
}: ChallengesProps & BlockTitleProps) => {
|
}: ChallengesProps & BlockTitleProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -113,7 +140,11 @@ const LinkToFirstIncompleteChallenge = ({
|
|||||||
);
|
);
|
||||||
return firstIncompleteChallenge ? (
|
return firstIncompleteChallenge ? (
|
||||||
<div className='challenge-jump-link'>
|
<div className='challenge-jump-link'>
|
||||||
<ButtonLink size='small' href={firstIncompleteChallenge.fields.slug}>
|
<ButtonLink
|
||||||
|
size='small'
|
||||||
|
href={firstIncompleteChallenge.fields.slug}
|
||||||
|
onClick={onChallengeClick}
|
||||||
|
>
|
||||||
{!isChallengeStarted
|
{!isChallengeStarted
|
||||||
? t('buttons.start-project')
|
? t('buttons.start-project')
|
||||||
: t('buttons.resume-project')}{' '}
|
: t('buttons.resume-project')}{' '}
|
||||||
@@ -127,7 +158,8 @@ export const GridMapChallenges = ({
|
|||||||
challenges,
|
challenges,
|
||||||
blockTitle,
|
blockTitle,
|
||||||
isProjectBlock,
|
isProjectBlock,
|
||||||
jumpLink
|
jumpLink,
|
||||||
|
onChallengeClick
|
||||||
}: ChallengesProps & BlockTitleProps & IsProjectBlockProps & JumpLinkProps) => {
|
}: ChallengesProps & BlockTitleProps & IsProjectBlockProps & JumpLinkProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
@@ -136,6 +168,7 @@ export const GridMapChallenges = ({
|
|||||||
<LinkToFirstIncompleteChallenge
|
<LinkToFirstIncompleteChallenge
|
||||||
challenges={challenges}
|
challenges={challenges}
|
||||||
blockTitle={blockTitle}
|
blockTitle={blockTitle}
|
||||||
|
onChallengeClick={onChallengeClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<nav aria-label={t('aria.steps-for', { blockTitle })}>
|
<nav aria-label={t('aria.steps-for', { blockTitle })}>
|
||||||
@@ -153,9 +186,15 @@ export const GridMapChallenges = ({
|
|||||||
key={`map-challenge ${challenge.fields.slug}`}
|
key={`map-challenge ${challenge.fields.slug}`}
|
||||||
>
|
>
|
||||||
{!isProjectBlock ? (
|
{!isProjectBlock ? (
|
||||||
<GridChallenge challenge={challenge} />
|
<GridChallenge
|
||||||
|
challenge={challenge}
|
||||||
|
onChallengeClick={onChallengeClick}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<CertChallenge challenge={challenge} />
|
<CertChallenge
|
||||||
|
challenge={challenge}
|
||||||
|
onChallengeClick={onChallengeClick}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -168,7 +207,8 @@ export const GridMapChallenges = ({
|
|||||||
export const ChallengesWithDialogues = ({
|
export const ChallengesWithDialogues = ({
|
||||||
challenges,
|
challenges,
|
||||||
blockTitle,
|
blockTitle,
|
||||||
jumpLink
|
jumpLink,
|
||||||
|
onChallengeClick
|
||||||
}: ChallengesProps & BlockTitleProps & JumpLinkProps) => {
|
}: ChallengesProps & BlockTitleProps & JumpLinkProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
@@ -177,6 +217,7 @@ export const ChallengesWithDialogues = ({
|
|||||||
<LinkToFirstIncompleteChallenge
|
<LinkToFirstIncompleteChallenge
|
||||||
challenges={challenges}
|
challenges={challenges}
|
||||||
blockTitle={blockTitle}
|
blockTitle={blockTitle}
|
||||||
|
onChallengeClick={onChallengeClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -193,9 +234,16 @@ export const ChallengesWithDialogues = ({
|
|||||||
key={`map-challenge ${challenge.fields.slug}`}
|
key={`map-challenge ${challenge.fields.slug}`}
|
||||||
>
|
>
|
||||||
{challenge.challengeType === challengeTypes.dialogue ? (
|
{challenge.challengeType === challengeTypes.dialogue ? (
|
||||||
<ListChallenge challenge={challenge} />
|
<ListChallenge
|
||||||
|
challenge={challenge}
|
||||||
|
onChallengeClick={onChallengeClick}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<GridChallenge challenge={challenge} isTask />
|
<GridChallenge
|
||||||
|
challenge={challenge}
|
||||||
|
isTask
|
||||||
|
onChallengeClick={onChallengeClick}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
92
e2e/block-navigation.spec.ts
Normal file
92
e2e/block-navigation.spec.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test.describe('Block Navigation - Hash Updates', () => {
|
||||||
|
test.use({ storageState: { cookies: [], origins: [] } });
|
||||||
|
|
||||||
|
test('should update URL hash when clicking on a challenge link in a grid layout', async ({
|
||||||
|
page
|
||||||
|
}) => {
|
||||||
|
await page.goto(
|
||||||
|
'/learn/javascript-algorithms-and-data-structures-v8/#learn-introductory-javascript-by-building-a-pyramid-generator'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify the hash is set correctly
|
||||||
|
await expect(page).toHaveURL(
|
||||||
|
/#learn-introductory-javascript-by-building-a-pyramid-generator$/
|
||||||
|
);
|
||||||
|
|
||||||
|
// Click on step 1 in the grid - the accessible name includes the sr-only text
|
||||||
|
const step1Link = page.getByRole('link', { name: 'Step 1 Not Passed' });
|
||||||
|
await expect(step1Link).toBeVisible();
|
||||||
|
await step1Link.click();
|
||||||
|
|
||||||
|
// Wait for navigation
|
||||||
|
await page.waitForURL(/step-1$/);
|
||||||
|
|
||||||
|
// Go back to verify the hash persists in history
|
||||||
|
await page.goBack();
|
||||||
|
await expect(page).toHaveURL(
|
||||||
|
/#learn-introductory-javascript-by-building-a-pyramid-generator$/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should update URL hash when clicking on a certification project', async ({
|
||||||
|
page
|
||||||
|
}) => {
|
||||||
|
await page.goto('/learn/javascript-algorithms-and-data-structures-v8');
|
||||||
|
|
||||||
|
// Click on the certification project link
|
||||||
|
const projectLink = page.getByRole('link', {
|
||||||
|
name: 'Build a Palindrome Checker Project Certification Project, Not completed'
|
||||||
|
});
|
||||||
|
await expect(projectLink).toBeVisible();
|
||||||
|
await projectLink.click();
|
||||||
|
|
||||||
|
// Wait for navigation
|
||||||
|
await page.waitForURL(/build-a-palindrome-checker$/);
|
||||||
|
|
||||||
|
// Go back to verify the hash persists in history
|
||||||
|
await page.goBack();
|
||||||
|
await expect(page).toHaveURL(/#build-a-palindrome-checker-project$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should update URL hash when clicking on a challenge in accordion layout (v9)', async ({
|
||||||
|
page
|
||||||
|
}) => {
|
||||||
|
await page.goto('/learn/javascript-v9');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Build a Greeting Bot' }).click();
|
||||||
|
|
||||||
|
// Click on step 1 in the accordion
|
||||||
|
const step1Link = page.getByRole('link', { name: 'Step 1 Not Passed' });
|
||||||
|
await expect(step1Link).toBeVisible();
|
||||||
|
await step1Link.click();
|
||||||
|
|
||||||
|
// Wait for navigation
|
||||||
|
await page.waitForURL(/step-1$/);
|
||||||
|
|
||||||
|
// Go back to verify the hash persists in history
|
||||||
|
await page.goBack();
|
||||||
|
await expect(page).toHaveURL(/#workshop-greeting-bot$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should update URL hash when clicking on a certification project in accordion layout (v9)', async ({
|
||||||
|
page
|
||||||
|
}) => {
|
||||||
|
await page.goto('/learn/javascript-v9');
|
||||||
|
|
||||||
|
// Click on the certification project link
|
||||||
|
const projectLink = page.getByRole('link', {
|
||||||
|
name: 'Build a Markdown to HTML Converter'
|
||||||
|
});
|
||||||
|
await expect(projectLink).toBeVisible();
|
||||||
|
await projectLink.click();
|
||||||
|
|
||||||
|
// Wait for navigation
|
||||||
|
await page.waitForURL(/build-a-markdown-to-html-converter$/);
|
||||||
|
|
||||||
|
// Go back to verify the hash persists in history
|
||||||
|
await page.goBack();
|
||||||
|
await expect(page).toHaveURL(/#lab-markdown-to-html-converter$/);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user