Skip to content

Commit

Permalink
Merge pull request #180 from Vizzuality/SKY30-241-highlight-side-navi…
Browse files Browse the repository at this point in the history
…gation-active-section-item-on-the-home-and-about-pages

[SKY30-241] Highlight side navigation active section item on the home and about pages
  • Loading branch information
SARodrigues authored Feb 15, 2024
2 parents 2362636 + eb54adc commit 59e85ad
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 6 deletions.
72 changes: 72 additions & 0 deletions frontend/src/hooks/use-scroll-spy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { RefObject, useLayoutEffect, useState } from 'react';

type ScrollSpyItem = {
id: string;
ref: RefObject<HTMLDivElement>;
};

type ScrollSpyOptions = {
overBounds: boolean;
};

const DEFAULT_OPTIONS = {
overBounds: true,
thresholdPercentage: 20,
};

const useScrollSpy = (items: ScrollSpyItem[], options?: ScrollSpyOptions) => {
const [activeId, setActiveId] = useState<string | null>(null);

useLayoutEffect(() => {
const scrollListener = () => {
const windowHeight = window.innerHeight;
const viewingPosition = (DEFAULT_OPTIONS.thresholdPercentage * windowHeight) / 100;

const itemsPositions = items.map(({ id, ref }) => {
const element = ref?.current;
if (!element) return null;

const boundingRect = element.getBoundingClientRect();

return {
id,
top: boundingRect.top,
bottom: boundingRect.bottom,
};
});

const activeItem = itemsPositions.find(({ top: itemTop, bottom: itemBottom }) => {
return itemTop < viewingPosition && itemBottom > viewingPosition;
});

if (!activeItem?.id && (options?.overBounds || DEFAULT_OPTIONS.overBounds) === true) {
const firstItem = itemsPositions[0];
const lastItem = itemsPositions[itemsPositions.length - 1];

if (viewingPosition < firstItem.top) {
setActiveId(firstItem.id);
}

if (viewingPosition > lastItem.bottom) {
setActiveId(lastItem.id);
}
} else {
setActiveId(activeItem?.id);
}
};

scrollListener();

window.addEventListener('resize', scrollListener);
window.addEventListener('scroll', scrollListener);

return () => {
window.removeEventListener('resize', scrollListener);
window.removeEventListener('scroll', scrollListener);
};
}, [items, options]);

return activeId;
};

export default useScrollSpy;
22 changes: 18 additions & 4 deletions frontend/src/layouts/static-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ import ArrowRight from '@/styles/icons/arrow-right.svg?sprite';
type SidebarProps = {
sections: {
[key: string]: {
id: string;
name: string;
ref: MutableRefObject<HTMLDivElement>;
};
};
activeSection?: string;
arrowColor?: 'black' | 'orange' | 'purple';
};

const Sidebar: React.FC<SidebarProps> = ({ sections }) => {
const Sidebar: React.FC<SidebarProps> = ({ sections, activeSection, arrowColor = 'black' }) => {
if (!sections) return null;

const handleClick = (key) => {
Expand All @@ -28,16 +31,27 @@ const Sidebar: React.FC<SidebarProps> = ({ sections }) => {
return (
<div className="-mb-6 min-w-[200px] px-8 md:mb-0 md:px-0">
<nav className="sticky top-10 bottom-3 my-10 flex flex-col gap-3 font-mono text-sm">
{Object.entries(sections).map(([key, { name }]) => {
{Object.entries(sections).map(([key, { id, name }]) => {
return (
<button
key={key}
className="flex w-full items-center gap-3"
type="button"
onClick={() => handleClick(key)}
>
<Icon icon={ArrowRight} className="h-6 fill-black" />
<span className="pt-1">{name}</span>
{id === activeSection && (
<Icon
icon={ArrowRight}
className={cn('h-6', {
'text-black': arrowColor === 'black',
'text-orange': arrowColor === 'orange',
'text-purple-400': arrowColor === 'purple',
})}
/>
)}
<span className={cn('pt-1 hover:font-bold', { 'font-bold': id === activeSection })}>
{name}
</span>
</button>
);
})}
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/pages/about/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import HighlightedText from '@/containers/about/highlighted-text';
import Logo from '@/containers/about/logo';
import LogosGrid from '@/containers/about/logos-grid';
import QuestionsList from '@/containers/about/questions-list';
import useScrollSpy from '@/hooks/use-scroll-spy';
import Layout, { Sidebar, Content } from '@/layouts/static-page';
import {
getGetStaticIndicatorsQueryKey,
Expand Down Expand Up @@ -50,27 +51,34 @@ const About: React.FC = ({
}) => {
const sections = {
definition: {
id: 'definition',
name: 'Definition',
ref: useRef<HTMLDivElement>(null),
},
problem: {
id: 'problem',
name: 'Problem',
ref: useRef<HTMLDivElement>(null),
},
dataPartners: {
id: 'data-partners',
name: 'Data Partners',
ref: useRef<HTMLDivElement>(null),
},
futureObjectives: {
id: 'future-objectives',
name: 'Future Objectives',
ref: useRef<HTMLDivElement>(null),
},
teamAndFunders: {
id: 'teams-and-funders',
name: 'Team & Funders',
ref: useRef<HTMLDivElement>(null),
},
};

const scrollActiveId = useScrollSpy(Object.values(sections).map(({ id, ref }) => ({ id, ref })));

const handleIntroScrollClick = () => {
sections.definition?.ref?.current?.scrollIntoView({ behavior: 'smooth' });
};
Expand Down Expand Up @@ -105,7 +113,7 @@ const About: React.FC = ({
/>
}
>
<Sidebar sections={sections} />
<Sidebar sections={sections} activeSection={scrollActiveId} arrowColor="purple" />
<Content>
<Section ref={sections.definition.ref}>
<SectionTitle>What is 30x30</SectionTitle>
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import EarthSurfaceCoverage from '@/containers/homepage/earth-surface-coverage';
import InteractiveMap from '@/containers/homepage/interactive-map';
import Intro from '@/containers/homepage/intro';
import LinkCards from '@/containers/homepage/link-cards';
import useScrollSpy from '@/hooks/use-scroll-spy';
import Layout, { Content, Sidebar } from '@/layouts/static-page';
import {
getGetStaticIndicatorsQueryKey,
Expand Down Expand Up @@ -58,19 +59,24 @@ const Home: React.FC = ({
}) => {
const sections = {
services: {
id: 'services',
name: 'Services',
ref: useRef<HTMLDivElement>(null),
},
context: {
id: 'context',
name: 'Context',
ref: useRef<HTMLDivElement>(null),
},
impact: {
id: 'impact',
name: 'Impact',
ref: useRef<HTMLDivElement>(null),
},
};

const scrollActiveId = useScrollSpy(Object.values(sections).map(({ id, ref }) => ({ id, ref })));

const handleIntroScrollClick = () => {
sections.services?.ref?.current?.scrollIntoView({ behavior: 'smooth' });
};
Expand Down Expand Up @@ -106,7 +112,7 @@ const Home: React.FC = ({
/>
}
>
<Sidebar sections={sections} />
<Sidebar sections={sections} activeSection={scrollActiveId} arrowColor={'orange'} />
<Content>
<Section ref={sections.services.ref}>
<SectionTitle>An entry point for 30x30</SectionTitle>
Expand Down

0 comments on commit 59e85ad

Please sign in to comment.