Mobile Organized Content #79

Merged
w25tran merged 53 commits from feat/organized-content into main 2021-08-27 15:18:56 -04:00
7 changed files with 277 additions and 16 deletions

View File

@ -49,7 +49,7 @@
}
.selected {
background-color: var(--primary-accent-dim);
background-color: var(--primary-accent-lightest);
color: var(--primary-accent);
font-weight: 700;
}
@ -124,7 +124,48 @@
text-decoration: none;
}
.burger {
display: none;
w25tran marked this conversation as resolved Outdated

You should use transform: translateY instead of changing the bottom property because it more performant. https://stackoverflow.com/questions/7108941/css-transform-vs-position/53892597

You should use `transform: translateY` instead of changing the bottom property because it more performant. https://stackoverflow.com/questions/7108941/css-transform-vs-position/53892597
}
.mobileNavTitle {
display: none;
}
@media only screen and (max-width: calc(768rem / 16)) {
.burger {
w25tran marked this conversation as resolved
Review

0.3s instead? 0.6 feels very slow

0.3s instead? 0.6 feels very slow
display: flex;
position: fixed;
border: none;
bottom: calc(32rem / 16);
left: calc(16rem / 16);
width: calc(62rem / 16);
height: calc(57rem / 16);
cursor: pointer;
z-index: 9;
background: var(--primary-accent-light);
border-radius: calc(5rem / 16);
transition: transform 0.3s ease-in-out;
transform: translateY(calc(94rem / 16));
padding: calc(11rem / 16) calc(9rem / 16);
}
.burgerVisible {
transform: translateY(0);
}
.burger > svg {
width: 100%;
height: 100%;
stroke: var(--primary-accent);
}
.navItem {
width: auto;
padding: 0 calc(16rem / 16);
}
.content h1 {
font-size: calc(18rem / 16);
}
@ -138,6 +179,52 @@
}
.nav {
display: none;
position: fixed;
top: 0;
left: 0;
overflow-y: auto;
z-index: 30;
margin: 0;
background: var(--primary-accent-lighter);
width: calc(288rem / 16);
transform: translateX(-100vw);
transition: 0.5s;
}
.navMobileBackground {
position: fixed;
visibility: hidden;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 20;
background-color: var(--navbar-page-overlay);
opacity: 0;
transition: 0.5s;
}
.mobileNavOpen {
transform: translateX(0);
}
.mobileNavTitle {
display: flex;
font-size: calc(24rem / 16);
font-weight: 700;
margin: calc(14rem / 16);
margin-top: calc(39rem / 16);
}
.show.navMobileBackground {
visibility: visible;
opacity: 100%;
}
}

View File

@ -1,5 +1,12 @@
import NextLink from "next/link";
import React, { ReactNode, ComponentType } from "react";
import React, {
ReactNode,
ComponentType,
useState,
useRef,
useEffect,
useCallback,
} from "react";
import styles from "./OrganizedContent.module.css";
@ -17,6 +24,7 @@ interface Props {
sections: Section[];
id: string;
children: ReactNode;
pageTitle: string;
link: Link;
}
@ -24,8 +32,10 @@ export function OrganizedContent({
sections,
id,
children,
pageTitle,
link: Link,
}: Props) {
const [mobileNavOpen, setMobileNavOpen] = useState(false);
const currentIndex = sections.findIndex(
({ id: sectionId }) => sectionId === id
);
@ -36,10 +46,34 @@ export function OrganizedContent({
const section = sections[currentIndex];
const isReadAll = section.id === READ_ALL_ID;
const ref = useRef<HTMLDivElement>(null);
const isVisible = useOnScreen(ref.current);
const burgerVisible = useBurger(isVisible);
useEffect(() => {
mobileNavOpen
? (document.body.style.overflow = "hidden")
: (document.body.style.overflow = "visible");
}, [mobileNavOpen]);
return (
<div className={styles.wrapper}>
<Nav sections={sections} currentIndex={currentIndex} link={Link} />
<div className={styles.wrapper} ref={ref}>
<div
className={
mobileNavOpen
? `${styles.navMobileBackground} ${styles.show}`
: styles.navMobileBackground
}
onClick={() => setMobileNavOpen(false)}
/>
<Nav
sections={sections}
currentIndex={currentIndex}
link={Link}
pageTitle={pageTitle}
mobileNavOpen={mobileNavOpen}
setMobileNavOpen={setMobileNavOpen}
/>
w25tran marked this conversation as resolved Outdated

We should not have a separate component just for mobile unless it's absolutely necessary. We can wrap the desktop Nav with something which till be much better, and allow us to reuse the same component as the desktop

We should not have a separate component just for mobile unless it's absolutely necessary. We can wrap the desktop Nav with something which till be much better, and allow us to reuse the same component as the desktop
<div className={styles.content}>
{isReadAll ? (
children
@ -57,6 +91,14 @@ export function OrganizedContent({
</>
)}
</div>
<button
className={`${styles.burger} ${
burgerVisible ? styles.burgerVisible : ""
}`}
onClick={() => setMobileNavOpen(!mobileNavOpen)}
>
<Burger />
</button>
</div>
);
w25tran marked this conversation as resolved Outdated

Can you separate it out to its own function within the same file?

Can you separate it out to its own function within the same file?
}
@ -65,11 +107,29 @@ interface NavProps {
sections: Section[];
currentIndex: number;
link: Link;
pageTitle: string;
mobileNavOpen: boolean;
setMobileNavOpen: (mobileNavOpen: boolean) => void;
}
function Nav({ sections, currentIndex, link: Link }: NavProps) {
function Nav({
sections,
currentIndex,
link: Link,
pageTitle,
mobileNavOpen,
setMobileNavOpen,
}: NavProps) {
const navStyles = mobileNavOpen
? [styles.nav, styles.mobileNavOpen]
: [styles.nav];
return (
<nav className={styles.nav}>
<nav
className={navStyles.join(" ")}
onClick={(event) => event.stopPropagation()}
>
<h1 className={styles.mobileNavTitle}>{pageTitle}</h1>
{sections.map((section, index) => {
const classNames = [styles.navItem];
@ -82,14 +142,17 @@ function Nav({ sections, currentIndex, link: Link }: NavProps) {
}
return (
<Link
className={classNames.join(" ")}
id={section.id}
<div
onClick={() => {
setMobileNavOpen(false);
}}
w25tran marked this conversation as resolved Outdated

Clicking on nav items should close the sidebar

Clicking on nav items should close the sidebar
key={section.id}
>
<span className={styles.marker} />
<div>{section.title}</div>
</Link>
<Link className={classNames.join(" ")} id={section.id}>
<span className={styles.marker} />
<div>{section.title}</div>
</Link>
</div>
);
})}
</nav>
@ -137,6 +200,20 @@ function Footer({ sections, currentIndex, link: Link }: FooterProps) {
);
}
function useDebounce(func: () => void, delay = 300) {
const timerRef = useRef<number | undefined>(undefined);
return useCallback(() => {
if (timerRef.current != null) {
return;
}
timerRef.current = window.setTimeout(() => {
func();
timerRef.current = undefined;
}, delay);
}, [func, delay]);
}
export interface SectionWithContent {
section: Section;
Content: ComponentType;
@ -216,3 +293,91 @@ function Arrow({ direction }: { direction: "left" | "right" }) {
</svg>
);
}
function useOnScreen(element: HTMLDivElement | null) {
const [isIntersecting, setIntersecting] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(([entry]) =>
setIntersecting(entry.isIntersecting)
);
if (element) {
observer.observe(element);
}
// Remove the observer as soon as the component is unmounted
return () => {
observer.disconnect();
};
}, [element]);
return isIntersecting;
}
function useBurger(componentIsVisible: boolean): boolean {
const [prevScrollPos, setPrevScrollPos] = useState(0);
const [burgerVisible, setBurgerVisible] = useState(true);
const handleScroll = useDebounce(() => {
// find current scroll position
const currentScrollPos = window.pageYOffset;
setBurgerVisible(
componentIsVisible &&
((prevScrollPos > currentScrollPos &&
prevScrollPos - currentScrollPos > 70) ||
currentScrollPos < 10)
);
// set state to new scroll position
setPrevScrollPos(currentScrollPos);
});
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, [handleScroll]);
return burgerVisible;
}
// Inlining this svg because we want to fill in colors using css variables
w25tran marked this conversation as resolved Outdated

it might be a better idea to make ref.current a dependency, instead of ref.

it might be a better idea to make ref.current a dependency, instead of ref.
function Burger() {
return (
<svg
width="30"
height="23"
viewBox="0 0 30 23"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<line
x1="28"
y1="2"
x2="2"
y2="2"
strokeWidth="4"
strokeLinecap="round"
strokeLinejoin="round"
/>
<line
x1="28"
y1="11.375"
x2="2"
y2="11.375"
strokeWidth="4"
strokeLinecap="round"
strokeLinejoin="round"
/>
<line
x1="28"
y1="20.75"
x2="2"
y2="20.75"
strokeWidth="4"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}

View File

@ -65,6 +65,7 @@ export function createReadAllPage({
readAllSection.section,
...sections.map(({ section }) => section),
]}
pageTitle={title}
link={Link}
>
<readAllSection.Content />

View File

@ -38,6 +38,7 @@ export function createSectionPage({
<OrganizedContent
sections={sections}
id={sections[current].id}
pageTitle={title}
link={Link}
>
<MDXRemote {...content} />

View File

@ -22,7 +22,8 @@ export const PALETTE_NAMES = [
"--primary-accent",
"--primary-accent-soft",
"--primary-accent-light",
"--primary-accent-dim",
"--primary-accent-lighter",
"--primary-accent-lightest",
"--secondary-accent",
"--secondary-accent-light",

View File

@ -238,7 +238,12 @@ export function OrganizedContentDemo() {
)!.Content;
return (
<OrganizedContent sections={sections} id={id} link={FakeLink}>
<OrganizedContent
sections={sections}
id={id}
link={FakeLink}
pageTitle="Playground"
>
<Content />
</OrganizedContent>
);

View File

@ -10,7 +10,8 @@ body {
--primary-accent: #1482e3;
--primary-accent-soft: #5caff9;
--primary-accent-light: #c4e0f8;
--primary-accent-dim: #f7fbff;
--primary-accent-lighter: #e1eefa;
--primary-accent-lightest: #f7fbff;
--secondary-accent: #4ed4b2;
--secondary-accent-light: #dcf6f0;