Mobile Organized Content #79
|
@ -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
|
||||
}
|
||||
|
||||
.mobileNavTitle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: calc(768rem / 16)) {
|
||||
|
||||
.burger {
|
||||
w25tran marked this conversation as resolved
a3thakra
commented
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%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
a3thakra
commented
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
a3thakra
commented
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
a3thakra
commented
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
a3thakra
commented
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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -65,6 +65,7 @@ export function createReadAllPage({
|
|||
readAllSection.section,
|
||||
...sections.map(({ section }) => section),
|
||||
]}
|
||||
pageTitle={title}
|
||||
link={Link}
|
||||
>
|
||||
<readAllSection.Content />
|
||||
|
|
|
@ -38,6 +38,7 @@ export function createSectionPage({
|
|||
<OrganizedContent
|
||||
sections={sections}
|
||||
id={sections[current].id}
|
||||
pageTitle={title}
|
||||
link={Link}
|
||||
>
|
||||
<MDXRemote {...content} />
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue
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