Feat: Sync Navbar Section Dropdown

Added section context that can be set when section heading
scrolls into browser viewport and it's value can be used
to set the current value of the navbar dropdown.

Added kebab case utility method for converting title to element
id.
This commit is contained in:
Stevan Freeborn
2023-04-27 16:03:27 -05:00
parent 7cb9cf24f7
commit dfb50ca6ba
9 changed files with 67 additions and 25 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
'use client'; 'use client';
import { Fragment } from 'react'; import { Fragment } from 'react';
import { themes, useThemeContext } from '../theme'; import { themes, useThemeContext } from '../context/theme';
import { DocSection } from '../types/types'; import { DocSection } from '../types/types';
import styles from './Main.module.css'; import styles from './Main.module.css';
import Section from './Section'; import Section from './Section';
+1 -1
View File
@@ -2,7 +2,7 @@
import Link from 'next/link'; import Link from 'next/link';
import { useState } from 'react'; import { useState } from 'react';
import { themes, useThemeContext } from '../theme'; import { themes, useThemeContext } from '../context/theme';
import { DocsStructure } from '../types/types'; import { DocsStructure } from '../types/types';
import styles from './NavBar.module.css'; import styles from './NavBar.module.css';
import SectionDropdown from './SectionDropdown'; import SectionDropdown from './SectionDropdown';
+15 -9
View File
@@ -1,7 +1,8 @@
'use client'; 'use client';
import { useEffect, useRef, useState } from 'react'; import { useEffect } from 'react';
import { themes, useThemeContext } from '../theme'; import { useSectionContext } from '../context/section';
import { themes, useThemeContext } from '../context/theme';
import { DocSection, DocsStructure } from '../types/types'; import { DocSection, DocsStructure } from '../types/types';
import Main from './Main'; import Main from './Main';
import NavBar from './NavBar'; import NavBar from './NavBar';
@@ -17,26 +18,31 @@ export default function Page({
}) { }) {
const { theme } = useThemeContext(); const { theme } = useThemeContext();
const themeStyles = themes[theme]; const themeStyles = themes[theme];
const docRef = useRef<HTMLDivElement>(null);
const [currentSection, setCurrentSection] = useState(''); const { setSection } = useSectionContext();
useEffect(() => { useEffect(() => {
const titles = const titles =
document.querySelectorAll<HTMLHeadingElement>( document.querySelectorAll<HTMLHeadingElement>(
'.section-title' 'article:nth-child(1) > h1'
); );
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
entries => { entries => {
entries.forEach(entry => { entries.forEach(entry => {
if (entry.isIntersecting) { if (entry.isIntersecting) {
setCurrentSection(entry.target.id); setSection(entry.target.id);
history.pushState(
null,
'',
`#${entry.target.id}`
);
} }
}); });
}, },
{ {
root: null, root: null,
rootMargin: '-10% 0% -80% 0%', rootMargin: '-10% 0% -70% 0%',
threshold: 1, threshold: 1,
} }
); );
@@ -46,12 +52,12 @@ export default function Page({
return () => { return () => {
titles.forEach(title => observer.unobserve(title)); titles.forEach(title => observer.unobserve(title));
}; };
}, []); }, [setSection]);
return ( return (
<div className={styles.wrapper} style={themeStyles}> <div className={styles.wrapper} style={themeStyles}>
<SideBar version={version} /> <SideBar version={version} />
<div ref={docRef} className={styles.container}> <div className={styles.container}>
<NavBar version={version} /> <NavBar version={version} />
<Main versionSections={versionSections} /> <Main versionSections={versionSections} />
</div> </div>
+8 -4
View File
@@ -1,5 +1,7 @@
import { Fragment } from 'react'; import { Fragment } from 'react';
import { useSectionContext } from '../context/section';
import { Doc, DocsStructure } from '../types/types'; import { Doc, DocsStructure } from '../types/types';
import { toKebabCase } from '../utils/stringUtils';
import styles from './SectionDropdown.module.css'; import styles from './SectionDropdown.module.css';
function SectionDropdownOption({ doc }: { doc: Doc }) { function SectionDropdownOption({ doc }: { doc: Doc }) {
@@ -9,6 +11,7 @@ function SectionDropdownOption({ doc }: { doc: Doc }) {
<option <option
className={styles.selectItem} className={styles.selectItem}
label={doc.title} label={doc.title}
value={toKebabCase(doc.title)}
> >
{doc.title} {doc.title}
</option> </option>
@@ -17,6 +20,7 @@ function SectionDropdownOption({ doc }: { doc: Doc }) {
<optgroup label={doc.title}> <optgroup label={doc.title}>
{doc.children.map(child => ( {doc.children.map(child => (
<option <option
value={toKebabCase(child.title)}
className={styles.selectItem} className={styles.selectItem}
label={child.title} label={child.title}
key={child.title} key={child.title}
@@ -35,11 +39,10 @@ export default function SectionDropdown({
}: { }: {
version: DocsStructure; version: DocsStructure;
}) { }) {
const { section, setSection } = useSectionContext();
const handleSelectChange = (e: any) => { const handleSelectChange = (e: any) => {
const selectedOption = e.target.value; setSection(e.target.value);
location.hash = selectedOption location.hash = e.target.value;
.replaceAll(' ', '-')
.toLowerCase();
}; };
return ( return (
@@ -47,6 +50,7 @@ export default function SectionDropdown({
onChange={handleSelectChange} onChange={handleSelectChange}
title="navigation dropdown" title="navigation dropdown"
className={styles.dropdownSelect} className={styles.dropdownSelect}
value={section}
> >
{version.docs.map(doc => { {version.docs.map(doc => {
return ( return (
+5 -8
View File
@@ -2,8 +2,9 @@
import Link from 'next/link.js'; import Link from 'next/link.js';
import { useState } from 'react'; import { useState } from 'react';
import { useThemeContext } from '../theme'; import { useThemeContext } from '../context/theme';
import { Doc, DocsStructure } from '../types/types.js'; import { Doc, DocsStructure } from '../types/types';
import { toKebabCase } from '../utils/stringUtils';
import styles from './SideBar.module.css'; import styles from './SideBar.module.css';
function TreeNode({ doc }: { doc: Doc }) { function TreeNode({ doc }: { doc: Doc }) {
@@ -19,9 +20,7 @@ function TreeNode({ doc }: { doc: Doc }) {
<div className={styles.expandable}> <div className={styles.expandable}>
{doc.copy ? ( {doc.copy ? (
<a <a
href={`#${doc.title href={`#${toKebabCase(doc.title)}`}
.replaceAll(' ', '-')
.toLowerCase()}`}
className={styles.link} className={styles.link}
> >
{doc.title} {doc.title}
@@ -66,9 +65,7 @@ function TreeNode({ doc }: { doc: Doc }) {
</div> </div>
) : doc.copy ? ( ) : doc.copy ? (
<a <a
href={`#${doc.title href={`#${toKebabCase(doc.title)}`}
.replaceAll(' ', '-')
.toLowerCase()}`}
className={styles.link} className={styles.link}
> >
{doc.title} {doc.title}
+29
View File
@@ -0,0 +1,29 @@
'use client';
import {
ReactNode,
createContext,
useContext,
useState,
} from 'react';
const SectionContext = createContext({});
export const SectionContextProvider = ({
children,
}: {
children: ReactNode;
}) => {
const [section, setSection] = useState('introduction');
return (
<SectionContext.Provider
value={{ section, setSection }}
>
{children}
</SectionContext.Provider>
);
};
export const useSectionContext: any = () =>
useContext(SectionContext);
+5 -2
View File
@@ -1,7 +1,8 @@
import { Analytics } from '@vercel/analytics/react'; import { Analytics } from '@vercel/analytics/react';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { SectionContextProvider } from './context/section';
import { ThemeContextProvider } from './context/theme';
import './globals.css'; import './globals.css';
import { ThemeContextProvider } from './theme';
export const metadata = { export const metadata = {
title: 'Onspring API Docs', title: 'Onspring API Docs',
@@ -30,7 +31,9 @@ export default function RootLayout({
<html lang="en"> <html lang="en">
<body> <body>
<ThemeContextProvider> <ThemeContextProvider>
{children} <SectionContextProvider>
{children}
</SectionContextProvider>
</ThemeContextProvider> </ThemeContextProvider>
<Analytics /> <Analytics />
</body> </body>
+3
View File
@@ -0,0 +1,3 @@
export function toKebabCase(str: string): string {
return str.replaceAll(' ', '-').toLowerCase();
}