From 6f492ea3f4d305aa2755af7bcd7c54e21cf03ede Mon Sep 17 00:00:00 2001 From: isqua Date: Tue, 24 Oct 2023 00:24:25 +0300 Subject: [PATCH] feat: show a skeleton while menu is loading --- src/components/Error/Error.tsx | 5 + src/components/Error/index.ts | 1 + src/components/Menu/Context/MenuProvider.tsx | 6 +- src/components/Menu/Context/contexts.ts | 11 ++- src/components/Menu/Context/hooks.ts | 8 +- src/components/Menu/Item/Item.module.css | 6 ++ src/components/Menu/Item/Item.tsx | 26 ++++-- src/components/Menu/Menu.test.tsx | 11 +++ src/components/Menu/Menu.tsx | 5 +- src/components/Menu/Section/Section.tsx | 13 ++- .../Menu/__snapshots__/Menu.test.tsx.snap | 91 +++++++++++++++++++ src/components/Root/Root.tsx | 10 +- 12 files changed, 171 insertions(+), 22 deletions(-) create mode 100644 src/components/Error/Error.tsx create mode 100644 src/components/Error/index.ts diff --git a/src/components/Error/Error.tsx b/src/components/Error/Error.tsx new file mode 100644 index 0000000..da6d7de --- /dev/null +++ b/src/components/Error/Error.tsx @@ -0,0 +1,5 @@ +export function Error() { + return ( +

Something went wrong, refresh the page

+ ) +} diff --git a/src/components/Error/index.ts b/src/components/Error/index.ts new file mode 100644 index 0000000..93c9972 --- /dev/null +++ b/src/components/Error/index.ts @@ -0,0 +1 @@ +export { Error } from './Error' diff --git a/src/components/Menu/Context/MenuProvider.tsx b/src/components/Menu/Context/MenuProvider.tsx index e80bbea..cdf445c 100644 --- a/src/components/Menu/Context/MenuProvider.tsx +++ b/src/components/Menu/Context/MenuProvider.tsx @@ -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 ( - + {children} diff --git a/src/components/Menu/Context/contexts.ts b/src/components/Menu/Context/contexts.ts index c0469d1..4956f4a 100644 --- a/src/components/Menu/Context/contexts.ts +++ b/src/components/Menu/Context/contexts.ts @@ -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[] @@ -14,7 +19,11 @@ const defaultToc: TableOfContent = { const defaultUrl = '/' const defaultBreadCrumbs: PageDescriptor[] = [] -export const TocContext = createContext(defaultToc) +export const TocContext = createContext({ + toc: defaultToc, + isLoading: true, +}) + export const LocationContext = createContext({ url: defaultUrl, breadcrumbs: defaultBreadCrumbs, diff --git a/src/components/Menu/Context/hooks.ts b/src/components/Menu/Context/hooks.ts index a340df4..a400b98 100644 --- a/src/components/Menu/Context/hooks.ts +++ b/src/components/Menu/Context/hooks.ts @@ -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, { @@ -16,3 +16,9 @@ export const useMenuItems = (parentId: PageId = '', level: number = 0, highlight return items } + +export const useIsLoading = () => { + const { isLoading } = useContext(TocContext) + + return isLoading +} diff --git a/src/components/Menu/Item/Item.module.css b/src/components/Menu/Item/Item.module.css index 8ad9afd..8ef59f8 100644 --- a/src/components/Menu/Item/Item.module.css +++ b/src/components/Menu/Item/Item.module.css @@ -17,6 +17,12 @@ .text { position: relative; + flex-grow: 1; +} + +.loader { + /* Make right side of the skeletons column more dynamic */ + width: 80%; } .toggle { diff --git a/src/components/Menu/Item/Item.tsx b/src/components/Menu/Item/Item.tsx index 146dbf7..f2a67c6 100644 --- a/src/components/Menu/Item/Item.tsx +++ b/src/components/Menu/Item/Item.tsx @@ -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 @@ -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 (
  • - - - {isSubMenuItemProps(props) && ( + + + {isSubMenuItemProps(props) && !isLoading && ( )} - {item.title} + {isLoading ? + : + item.title + }
  • diff --git a/src/components/Menu/Menu.test.tsx b/src/components/Menu/Menu.test.tsx index 50f9ade..934f9fd 100644 --- a/src/components/Menu/Menu.test.tsx +++ b/src/components/Menu/Menu.test.tsx @@ -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(, { 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' diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx index e0d7e63..5ceb322 100644 --- a/src/components/Menu/Menu.tsx +++ b/src/components/Menu/Menu.tsx @@ -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 ( `; + +exports[`components/Menu > should render skeletons while TOC is loading 1`] = ` + +`; diff --git a/src/components/Root/Root.tsx b/src/components/Root/Root.tsx index b52a5fc..2142798 100644 --- a/src/components/Root/Root.tsx +++ b/src/components/Root/Root.tsx @@ -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 ( - {query.isSuccess && ()} + {!query.isError && ()} {query.isSuccess && ()} + {query.isError && } )