beta/src/components/MDX/Challenges/Navigation.tsx (123 lines of code) (raw):
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
import React, {createRef} from 'react';
import cn from 'classnames';
import {IconChevron} from 'components/Icon/IconChevron';
import {ChallengeContents} from './Challenges';
const debounce = require('debounce');
export function Navigation({
challenges,
handleChange,
currentChallenge,
isRecipes,
}: {
challenges: ChallengeContents[];
handleChange: (id: string) => void;
currentChallenge: ChallengeContents;
isRecipes?: boolean;
}) {
const containerRef = React.useRef<HTMLDivElement>(null);
const challengesNavRef = React.useRef(
challenges.map(() => createRef<HTMLButtonElement>())
);
const scrollPos = currentChallenge.order - 1;
const canScrollLeft = scrollPos > 0;
const canScrollRight = scrollPos < challenges.length - 1;
const handleScrollRight = () => {
if (scrollPos < challenges.length - 1) {
const currentNavRef = challengesNavRef.current[scrollPos + 1].current;
if (!currentNavRef) {
return;
}
if (containerRef.current) {
containerRef.current.scrollLeft = currentNavRef.offsetLeft;
}
handleChange(challenges[scrollPos + 1].id);
}
};
const handleScrollLeft = () => {
if (scrollPos > 0) {
const currentNavRef = challengesNavRef.current[scrollPos - 1].current;
if (!currentNavRef) {
return;
}
if (containerRef.current) {
containerRef.current.scrollLeft = currentNavRef.offsetLeft;
}
handleChange(challenges[scrollPos - 1].id);
}
};
const handleSelectNav = (id: string) => {
const selectedChallenge = challenges.findIndex(
(challenge) => challenge.id === id
);
const currentNavRef = challengesNavRef.current[selectedChallenge].current;
if (containerRef.current) {
containerRef.current.scrollLeft = currentNavRef?.offsetLeft || 0;
}
handleChange(id);
};
const handleResize = React.useCallback(() => {
if (containerRef.current) {
const el = containerRef.current;
el.scrollLeft =
challengesNavRef.current[scrollPos].current?.offsetLeft || 0;
}
}, [containerRef, challengesNavRef, scrollPos]);
React.useEffect(() => {
handleResize();
window.addEventListener('resize', debounce(handleResize, 200));
return () => {
window.removeEventListener('resize', handleResize);
};
}, [handleResize]);
return (
<div className="flex items-center justify-between">
<div className="overflow-hidden">
<div
ref={containerRef}
className="flex relative transition-transform content-box overflow-x-auto">
{challenges.map(({name, id, order}, index) => (
<button
className={cn(
'py-2 mr-4 text-base border-b-4 duration-100 ease-in transition whitespace-nowrap text-ellipsis',
isRecipes &&
currentChallenge.id === id &&
'text-purple-50 border-purple-50 hover:text-purple-50 dark:text-purple-30 dark:border-purple-30 dark:hover:text-purple-30',
!isRecipes &&
currentChallenge.id === id &&
'text-link border-link hover:text-link dark:text-link-dark dark:border-link-dark dark:hover:text-link-dark'
)}
onClick={() => handleSelectNav(id)}
key={`button-${id}`}
ref={challengesNavRef.current[index]}>
{order}. {name}
</button>
))}
</div>
</div>
<div className="flex z-10 pb-2 pl-2">
<button
onClick={handleScrollLeft}
aria-label="Scroll left"
className={cn(
'bg-secondary-button dark:bg-secondary-button-dark h-8 px-2 rounded-l border-gray-20 border-r',
{
'text-primary dark:text-primary-dark': canScrollLeft,
'text-gray-30': !canScrollLeft,
}
)}>
<IconChevron displayDirection="left" />
</button>
<button
onClick={handleScrollRight}
aria-label="Scroll right"
className={cn(
'bg-secondary-button dark:bg-secondary-button-dark h-8 px-2 rounded-r-lg',
{
'text-primary dark:text-primary-dark': canScrollRight,
'text-gray-30': !canScrollRight,
}
)}>
<IconChevron displayDirection="right" />
</button>
</div>
</div>
);
}