Skip to content

Commit

Permalink
feat: show a skeleton while menu is loading
Browse files Browse the repository at this point in the history
  • Loading branch information
isqua committed Oct 23, 2023
1 parent ae06754 commit 6f492ea
Show file tree
Hide file tree
Showing 12 changed files with 171 additions and 22 deletions.
5 changes: 5 additions & 0 deletions src/components/Error/Error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export function Error() {
return (
<p>Something went wrong, refresh the page</p>
)
}
1 change: 1 addition & 0 deletions src/components/Error/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Error } from './Error'
6 changes: 4 additions & 2 deletions src/components/Menu/Context/MenuProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ import { LocationContext, TocContext } from './contexts'
type MenuProviderProps = PropsWithChildren<{
toc: TableOfContent
url: PageURL
isLoading?: boolean
}>

export function MenuProvider({ toc, url, children }: MenuProviderProps): JSX.Element {
export function MenuProvider({ toc, url, children, isLoading = false }: MenuProviderProps): JSX.Element {
const breadcrumbs = getBreadCrumbs(toc, url)
const tocContextValue = { toc, isLoading }
const locationContextValue = { url, breadcrumbs }

return (
<TocContext.Provider value={toc}>
<TocContext.Provider value={tocContextValue}>
<LocationContext.Provider value={locationContextValue}>
{children}
</LocationContext.Provider>
Expand Down
11 changes: 10 additions & 1 deletion src/components/Menu/Context/contexts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { createContext } from 'react'
import { type PageDescriptor, type PageURL, type TableOfContent } from '../../../features/toc'

type TocContextValue = {
toc: TableOfContent
isLoading: boolean
}

type LocationContextValue = {
url: PageURL
breadcrumbs: PageDescriptor[]
Expand All @@ -14,7 +19,11 @@ const defaultToc: TableOfContent = {
const defaultUrl = '/'
const defaultBreadCrumbs: PageDescriptor[] = []

export const TocContext = createContext<TableOfContent>(defaultToc)
export const TocContext = createContext<TocContextValue>({
toc: defaultToc,
isLoading: true,
})

export const LocationContext = createContext<LocationContextValue>({
url: defaultUrl,
breadcrumbs: defaultBreadCrumbs,
Expand Down
8 changes: 7 additions & 1 deletion src/components/Menu/Context/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { buildMenu, type PageId, type SectionHighlight } from '../../../features
import { LocationContext, TocContext } from './contexts'

export const useMenuItems = (parentId: PageId = '', level: number = 0, highlight: SectionHighlight) => {
const toc = useContext(TocContext)
const { toc } = useContext(TocContext)
const currentLocation = useContext(LocationContext)

const items = buildMenu(toc, {
Expand All @@ -16,3 +16,9 @@ export const useMenuItems = (parentId: PageId = '', level: number = 0, highlight

return items
}

export const useIsLoading = () => {
const { isLoading } = useContext(TocContext)

return isLoading
}
6 changes: 6 additions & 0 deletions src/components/Menu/Item/Item.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@

.text {
position: relative;
flex-grow: 1;
}

.loader {
/* Make right side of the skeletons column more dynamic */
width: 80%;
}

.toggle {
Expand Down
26 changes: 19 additions & 7 deletions src/components/Menu/Item/Item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import { Link } from 'react-router-dom'

import { MenuItem } from '../../../features/toc'
import { Chevron } from '../../Chevron'
import { Skeleton } from '../../Skeleton'

import styles from './Item.module.css'

type BaseItemProps = {
item: MenuItem
isLoading?: boolean
}

type LeafItemProps = BaseItemProps
Expand Down Expand Up @@ -52,26 +54,36 @@ function OptionalLink({ to, className, children, onClick }: OptionalLinkProps) {
}

export function Item(props: ItemProps): JSX.Element {
const { item } = props
const { item, isLoading } = props

const linkClassName = clsx(
styles.link,
getItemHighlightStyles(item),
!isLoading && getItemHighlightStyles(item),
)

const textClassName = clsx(
styles.text,
isLoading && styles.skeleton
)

const ariaLevel = Math.min(item.level + 1, INDENT_LEVEL_LIMIT)

const onLinkClick = isSubMenuItemProps(props) && !props.open ?
const onLinkClick = isSubMenuItemProps(props) && !isLoading && !props.open ?
props.onToggle : undefined

const itemUrl = isLoading ? '' : item.url

return (
<li className={styles.item} aria-level={ariaLevel}>
<OptionalLink to={item.url} className={linkClassName} onClick={onLinkClick}>
<span className={styles.text}>
{isSubMenuItemProps(props) && (
<OptionalLink to={itemUrl} className={linkClassName} onClick={onLinkClick}>
<span className={textClassName}>
{isSubMenuItemProps(props) && !isLoading && (
<Chevron className={styles.toggle} open={props.open} onClick={props.onToggle} />
)}
{item.title}
{isLoading ?
<Skeleton className={styles.loader} /> :
item.title
}
</span>
</OptionalLink>
</li>
Expand Down
11 changes: 11 additions & 0 deletions src/components/Menu/Menu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ import { renderInApp } from '../../test'
import { Menu } from './Menu'

describe('components/Menu', () => {
it('should render skeletons while TOC is loading', async () => {
const toc: TableOfContent = tocTwoLevels
const currentUrl = '/bar.html'

act(() => {
renderInApp(<Menu isLoading toc={toc} />, { url: currentUrl })
})

expect(await screen.findByRole('navigation')).toMatchSnapshot()
})

it('should build a menu and highlight current page', async () => {
const toc: TableOfContent = tocFlat
const currentUrl = '/bar.html'
Expand Down
5 changes: 3 additions & 2 deletions src/components/Menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@ import styles from './Menu.module.css'

type MenuProps = {
toc: TableOfContent
isLoading?: boolean
}

export function Menu({ toc }: MenuProps): JSX.Element {
export function Menu({ toc, isLoading }: MenuProps): JSX.Element {
const currentUrl = useCurrentPageUrl()

return (
<nav className={styles.menu}>
<ul className={styles.list}>
<MenuProvider toc={toc} url={currentUrl}>
<MenuProvider toc={toc} url={currentUrl} isLoading={isLoading}>
<Section parentId='' level={0} />
</MenuProvider>
</ul>
Expand Down
13 changes: 8 additions & 5 deletions src/components/Menu/Section/Section.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState } from 'react'
import { type MenuItem, type PageId, type SectionHighlight } from '../../../features/toc'
import { useMenuItems } from '../Context/hooks'
import { useIsLoading, useMenuItems } from '../Context/hooks'
import { Item } from '../Item/Item'

type SectionProps = {
Expand All @@ -12,29 +12,31 @@ type SectionProps = {
type SubMenuProps = {
item: MenuItem
level: number
isLoading: boolean
}

export function Section({ parentId, level, highlight }: SectionProps): JSX.Element {
const isLoading = useIsLoading()
const items = useMenuItems(parentId, level, highlight)

return (
<>
{items.map((item) => {
if (!item.hasChildren) {
return (<Item key={item.id} item={item} />)
return (<Item isLoading={isLoading} key={item.id} item={item} />)
}

return (
<SubMenu key={item.id} item={item} level={level + 1} />
<SubMenu isLoading={isLoading} key={item.id} item={item} level={level + 1} />
)
})}
</>
)
}

function SubMenu({ item, level }: SubMenuProps): JSX.Element {
function SubMenu({ item, level, isLoading }: SubMenuProps): JSX.Element {
const subMenuHighlight = item.highlight === 'active' ? 'child' : item.highlight
const [ isOpen, setOpen ] = useState(item.defaultOpenState)
const [ isOpen, setOpen ] = useState(isLoading ? true : item.defaultOpenState)

const onToggle = () => {
setOpen(value => !value)
Expand All @@ -44,6 +46,7 @@ function SubMenu({ item, level }: SubMenuProps): JSX.Element {
<>
<Item
hasChildren
isLoading={isLoading}
item={item}
open={isOpen}
onToggle={onToggle}
Expand Down
91 changes: 91 additions & 0 deletions src/components/Menu/__snapshots__/Menu.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -325,3 +325,94 @@ exports[`components/Menu > should close a submenu when clicking on a chevron 2`]
</ul>
</nav>
`;

exports[`components/Menu > should render skeletons while TOC is loading 1`] = `
<nav
class="_menu_29d207"
>
<ul
class="_list_29d207"
>
<li
aria-level="1"
class="_item_daf4a6"
>
<span
class="_link_daf4a6"
>
<span
class="_text_daf4a6 _skeleton_daf4a6"
>
<span
class="_skeleton_2cc9eb _loader_daf4a6"
/>
</span>
</span>
</li>
<li
aria-level="2"
class="_item_daf4a6"
>
<span
class="_link_daf4a6"
>
<span
class="_text_daf4a6 _skeleton_daf4a6"
>
<span
class="_skeleton_2cc9eb _loader_daf4a6"
/>
</span>
</span>
</li>
<li
aria-level="2"
class="_item_daf4a6"
>
<span
class="_link_daf4a6"
>
<span
class="_text_daf4a6 _skeleton_daf4a6"
>
<span
class="_skeleton_2cc9eb _loader_daf4a6"
/>
</span>
</span>
</li>
<li
aria-level="1"
class="_item_daf4a6"
>
<span
class="_link_daf4a6"
>
<span
class="_text_daf4a6 _skeleton_daf4a6"
>
<span
class="_skeleton_2cc9eb _loader_daf4a6"
/>
</span>
</span>
</li>
<li
aria-level="1"
class="_item_daf4a6"
>
<span
class="_link_daf4a6"
>
<span
class="_text_daf4a6 _skeleton_daf4a6"
>
<span
class="_skeleton_2cc9eb _loader_daf4a6"
/>
</span>
</span>
</li>
</ul>
</nav>
`;
10 changes: 6 additions & 4 deletions src/components/Root/Root.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import { Layout } from '../Layout'
import { Menu } from '../Menu'

import tocUrl from '/toc.json?url'
import { DocPage } from '../DocPage/DocPage'
import { useGetTocQuery } from '../../features/toc'
import { DocPage } from '../DocPage/DocPage'
import { Error } from '../Error/Error'
import tocUrl from '/toc.json?url'

export function Root() {
const query = useGetTocQuery(tocUrl)
const query = useGetTocQuery('2' + tocUrl)

return (
<Layout>
<Layout.Sidebar>
{query.isSuccess && (<Menu toc={query.data} />)}
{!query.isError && (<Menu toc={query.data} isLoading={query.isLoading} />)}
</Layout.Sidebar>
<Layout.Main>
{query.isSuccess && (<DocPage toc={query.data} />)}
{query.isError && <Error />}
</Layout.Main>
</Layout>
)
Expand Down

0 comments on commit 6f492ea

Please sign in to comment.