Browse Source

fix: reset showHint to false when click Next Challenge (#4948)

* fix: reset showHint to false when click Next Challenge

* Split Challenges into two components

* clean code, add  to flag whether the activeChallenge has changed to avoid scrolling to Chalenges when page mounted

* Simplify

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
main
Guihua Ran 3 years ago
committed by GitHub
parent
commit
2d42098bc8
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 130
      beta/src/components/MDX/Challenges/Challenge.tsx
  2. 162
      beta/src/components/MDX/Challenges/Challenges.tsx
  3. 17
      beta/src/components/MDX/Challenges/Navigation.tsx

130
beta/src/components/MDX/Challenges/Challenge.tsx

@ -0,0 +1,130 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
import * as React from 'react';
import cn from 'classnames';
import {Button} from 'components/Button';
import {ChallengeContents} from './Challenges';
import {IconHint} from '../../Icon/IconHint';
import {IconSolution} from '../../Icon/IconSolution';
import {IconArrowSmall} from '../../Icon/IconArrowSmall';
interface ChallengeProps {
isRecipes?: boolean;
totalChallenges: number;
currentChallenge: ChallengeContents;
hasNextChallenge: boolean;
handleClickNextChallenge: () => void;
}
export function Challenge({
isRecipes,
totalChallenges,
currentChallenge,
hasNextChallenge,
handleClickNextChallenge,
}: ChallengeProps) {
const [showHint, setShowHint] = React.useState(false);
const [showSolution, setShowSolution] = React.useState(false);
const toggleHint = () => {
if (showSolution && !showHint) {
setShowSolution(false);
}
setShowHint((hint) => !hint);
};
const toggleSolution = () => {
if (showHint && !showSolution) {
setShowHint(false);
}
setShowSolution((solution) => !solution);
};
return (
<div className="p-5 sm:py-8 sm:px-8">
<div>
<h3 className="text-xl text-primary dark:text-primary-dark mb-2">
<div className="font-bold block md:inline">
{isRecipes ? 'Example' : 'Challenge'} {currentChallenge.order} of{' '}
{totalChallenges}
<span className="text-primary dark:text-primary-dark">: </span>
</div>
{currentChallenge.name}
</h3>
{currentChallenge.content}
</div>
<div className="flex justify-between items-center mt-4">
{currentChallenge.hint ? (
<div>
<Button className="mr-2" onClick={toggleHint} active={showHint}>
<IconHint className="mr-1.5" />{' '}
{showHint ? 'Hide hint' : 'Show hint'}
</Button>
<Button
className="mr-2"
onClick={toggleSolution}
active={showSolution}>
<IconSolution className="mr-1.5" />{' '}
{showSolution ? 'Hide solution' : 'Show solution'}
</Button>
</div>
) : (
!isRecipes && (
<Button
className="mr-2"
onClick={toggleSolution}
active={showSolution}>
<IconSolution className="mr-1.5" />{' '}
{showSolution ? 'Hide solution' : 'Show solution'}
</Button>
)
)}
{hasNextChallenge && (
<Button
className={cn(
isRecipes
? 'bg-purple-50 border-purple-50 hover:bg-purple-50 focus:bg-purple-50 active:bg-purple-50'
: 'bg-link dark:bg-link-dark'
)}
onClick={handleClickNextChallenge}
active>
Next {isRecipes ? 'Example' : 'Challenge'}
<IconArrowSmall displayDirection="right" className="block ml-1.5" />
</Button>
)}
</div>
{showHint && currentChallenge.hint}
{showSolution && (
<div className="mt-6">
<h3 className="text-2xl font-bold text-primary dark:text-primary-dark">
Solution
</h3>
{currentChallenge.solution}
<div className="flex justify-between items-center mt-4">
<Button onClick={() => setShowSolution(false)}>
Close solution
</Button>
{hasNextChallenge && (
<Button
className={cn(
isRecipes ? 'bg-purple-50' : 'bg-link dark:bg-link-dark'
)}
onClick={handleClickNextChallenge}
active>
Next Challenge
<IconArrowSmall
displayDirection="right"
className="block ml-1.5"
/>
</Button>
)}
</div>
</div>
)}
</div>
);
}

162
beta/src/components/MDX/Challenges/Challenges.tsx

@ -4,13 +4,10 @@
import * as React from 'react'; import * as React from 'react';
import cn from 'classnames'; import cn from 'classnames';
import {Button} from 'components/Button';
import {H2} from 'components/MDX/Heading'; import {H2} from 'components/MDX/Heading';
import {H4} from 'components/MDX/Heading'; import {H4} from 'components/MDX/Heading';
import {Challenge} from './Challenge';
import {Navigation} from './Navigation'; import {Navigation} from './Navigation';
import {IconHint} from '../../Icon/IconHint';
import {IconSolution} from '../../Icon/IconSolution';
import {IconArrowSmall} from '../../Icon/IconArrowSmall';
interface ChallengesProps { interface ChallengesProps {
children: React.ReactElement[]; children: React.ReactElement[];
@ -76,42 +73,26 @@ export function Challenges({
titleId = isRecipes ? 'examples' : 'challenges', titleId = isRecipes ? 'examples' : 'challenges',
}: ChallengesProps) { }: ChallengesProps) {
const challenges = parseChallengeContents(children); const challenges = parseChallengeContents(children);
const totalChallenges = challenges.length;
const scrollAnchorRef = React.useRef<HTMLDivElement>(null); const scrollAnchorRef = React.useRef<HTMLDivElement>(null);
const queuedScrollRef = React.useRef<boolean>(false);
const [showHint, setShowHint] = React.useState(false); const [activeIndex, setActiveIndex] = React.useState(0);
const [showSolution, setShowSolution] = React.useState(false); const currentChallenge = challenges[activeIndex];
const [activeChallenge, setActiveChallenge] = React.useState(
challenges[0].id React.useEffect(() => {
); if (queuedScrollRef.current === true) {
queuedScrollRef.current = false;
const handleChallengeChange = (challengeId: string) => { scrollAnchorRef.current!.scrollIntoView({
setShowHint(false); block: 'start',
setShowSolution(false); behavior: 'smooth',
setActiveChallenge(challengeId); });
};
const toggleHint = () => {
if (showSolution && !showHint) {
setShowSolution(false);
} }
setShowHint((hint) => !hint); });
};
const toggleSolution = () => { const handleChallengeChange = (index: number) => {
if (showHint && !showSolution) { setActiveIndex(index);
setShowHint(false);
}
setShowSolution((solution) => !solution);
}; };
const currentChallenge = challenges.find(({id}) => id === activeChallenge);
if (currentChallenge === undefined) {
throw new TypeError('currentChallenge should always exist');
}
const nextChallenge = challenges.find(({order}) => {
return order === currentChallenge.order + 1;
});
const Heading = isRecipes ? H4 : H2; const Heading = isRecipes ? H4 : H2;
return ( return (
<div className="max-w-7xl mx-auto py-4"> <div className="max-w-7xl mx-auto py-4">
@ -130,7 +111,7 @@ export function Challenges({
)}> )}>
{titleText} {titleText}
</Heading> </Heading>
{challenges.length > 1 && ( {totalChallenges > 1 && (
<Navigation <Navigation
currentChallenge={currentChallenge} currentChallenge={currentChallenge}
challenges={challenges} challenges={challenges}
@ -139,104 +120,17 @@ export function Challenges({
/> />
)} )}
</div> </div>
<div className="p-5 sm:py-8 sm:px-8"> <Challenge
<div key={activeChallenge}> key={currentChallenge.id}
<h3 className="text-xl text-primary dark:text-primary-dark mb-2"> isRecipes={isRecipes}
<div className="font-bold block md:inline"> currentChallenge={currentChallenge}
{isRecipes ? 'Example' : 'Challenge'} {currentChallenge.order}{' '} totalChallenges={totalChallenges}
of {challenges.length} hasNextChallenge={activeIndex < totalChallenges - 1}
<span className="text-primary dark:text-primary-dark">: </span> handleClickNextChallenge={() => {
</div> setActiveIndex((i) => i + 1);
{currentChallenge.name} queuedScrollRef.current = true;
</h3> }}
<>{currentChallenge.content}</> />
</div>
<div className="flex justify-between items-center mt-4">
{currentChallenge.hint ? (
<div>
<Button className="mr-2" onClick={toggleHint} active={showHint}>
<IconHint className="mr-1.5" />{' '}
{showHint ? 'Hide hint' : 'Show hint'}
</Button>
<Button
className="mr-2"
onClick={toggleSolution}
active={showSolution}>
<IconSolution className="mr-1.5" />{' '}
{showSolution ? 'Hide solution' : 'Show solution'}
</Button>
</div>
) : (
!isRecipes && (
<Button
className="mr-2"
onClick={toggleSolution}
active={showSolution}>
<IconSolution className="mr-1.5" />{' '}
{showSolution ? 'Hide solution' : 'Show solution'}
</Button>
)
)}
{nextChallenge && (
<Button
className={cn(
isRecipes
? 'bg-purple-50 border-purple-50 hover:bg-purple-50 focus:bg-purple-50 active:bg-purple-50'
: 'bg-link dark:bg-link-dark'
)}
onClick={() => {
setActiveChallenge(nextChallenge.id);
setShowSolution(false);
}}
active>
Next {isRecipes ? 'Example' : 'Challenge'}
<IconArrowSmall
displayDirection="right"
className="block ml-1.5"
/>
</Button>
)}
</div>
{showHint && currentChallenge.hint}
{showSolution && (
<div className="mt-6">
<h3 className="text-2xl font-bold text-primary dark:text-primary-dark">
Solution
</h3>
{currentChallenge.solution}
<div className="flex justify-between items-center mt-4">
<Button onClick={() => setShowSolution(false)}>
Close solution
</Button>
{nextChallenge && (
<Button
className={cn(
isRecipes ? 'bg-purple-50' : 'bg-link dark:bg-link-dark'
)}
onClick={() => {
setActiveChallenge(nextChallenge.id);
setShowSolution(false);
if (scrollAnchorRef.current) {
scrollAnchorRef.current.scrollIntoView({
block: 'start',
behavior: 'smooth',
});
}
}}
active>
Next Challenge
<IconArrowSmall
displayDirection="right"
className="block ml-1.5"
/>
</Button>
)}
</div>
</div>
)}
</div>
</div> </div>
</div> </div>
); );

17
beta/src/components/MDX/Challenges/Navigation.tsx

@ -15,7 +15,7 @@ export function Navigation({
isRecipes, isRecipes,
}: { }: {
challenges: ChallengeContents[]; challenges: ChallengeContents[];
handleChange: (id: string) => void; handleChange: (index: number) => void;
currentChallenge: ChallengeContents; currentChallenge: ChallengeContents;
isRecipes?: boolean; isRecipes?: boolean;
}) { }) {
@ -36,7 +36,7 @@ export function Navigation({
if (containerRef.current) { if (containerRef.current) {
containerRef.current.scrollLeft = currentNavRef.offsetLeft; containerRef.current.scrollLeft = currentNavRef.offsetLeft;
} }
handleChange(challenges[scrollPos + 1].id); handleChange(scrollPos + 1);
} }
}; };
@ -49,19 +49,16 @@ export function Navigation({
if (containerRef.current) { if (containerRef.current) {
containerRef.current.scrollLeft = currentNavRef.offsetLeft; containerRef.current.scrollLeft = currentNavRef.offsetLeft;
} }
handleChange(challenges[scrollPos - 1].id); handleChange(scrollPos - 1);
} }
}; };
const handleSelectNav = (id: string) => { const handleSelectNav = (index: number) => {
const selectedChallenge = challenges.findIndex( const currentNavRef = challengesNavRef.current[index].current;
(challenge) => challenge.id === id
);
const currentNavRef = challengesNavRef.current[selectedChallenge].current;
if (containerRef.current) { if (containerRef.current) {
containerRef.current.scrollLeft = currentNavRef?.offsetLeft || 0; containerRef.current.scrollLeft = currentNavRef?.offsetLeft || 0;
} }
handleChange(id); handleChange(index);
}; };
const handleResize = React.useCallback(() => { const handleResize = React.useCallback(() => {
@ -97,7 +94,7 @@ export function Navigation({
currentChallenge.id === id && currentChallenge.id === id &&
'text-link border-link hover:text-link dark:text-link-dark dark:border-link-dark dark:hover:text-link-dark' 'text-link border-link hover:text-link dark:text-link-dark dark:border-link-dark dark:hover:text-link-dark'
)} )}
onClick={() => handleSelectNav(id)} onClick={() => handleSelectNav(index)}
key={`button-${id}`} key={`button-${id}`}
ref={challengesNavRef.current[index]}> ref={challengesNavRef.current[index]}>
{order}. {name} {order}. {name}

Loading…
Cancel
Save