diff options
| author | Armand Philippot <git@armandphilippot.com> | 2023-12-07 18:48:53 +0100 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2023-12-08 19:13:47 +0100 |
| commit | d375e5c9f162cbd84a6e6462977db56519d09f75 (patch) | |
| tree | aed9bc81c426e3e9fb60292cb244613cb8083dea /src/utils/hooks/use-headings-tree/use-headings-tree.ts | |
| parent | b8eb008dd5927fb736e56699637f5f8549965eae (diff) | |
refactor(pages): refine Project pages
* refactor ProjectOverview component to let consumers handle the value
* extract project overview depending on Github to avoid fetching
Github API if the project is not on Github
* wrap dynamic import in a useMemo hook to avoid infinite rerender
* fix table of contents by adding a useMutationObserver hook to refresh
headings tree (without it useHeadingsTree is not retriggered once the
dynamic import is done)
* add Cypress tests
Diffstat (limited to 'src/utils/hooks/use-headings-tree/use-headings-tree.ts')
| -rw-r--r-- | src/utils/hooks/use-headings-tree/use-headings-tree.ts | 50 |
1 files changed, 39 insertions, 11 deletions
diff --git a/src/utils/hooks/use-headings-tree/use-headings-tree.ts b/src/utils/hooks/use-headings-tree/use-headings-tree.ts index 68bdde8..802d843 100644 --- a/src/utils/hooks/use-headings-tree/use-headings-tree.ts +++ b/src/utils/hooks/use-headings-tree/use-headings-tree.ts @@ -1,5 +1,7 @@ -import { useState, useCallback, type RefCallback } from 'react'; +import { useState, useCallback, type RefCallback, useEffect } from 'react'; import type { HeadingLevel } from '../../../components'; +import type { Nullable } from '../../../types'; +import { useMutationObserver } from '../use-mutation-observer'; export type HeadingsTreeNode = { /** @@ -140,17 +142,43 @@ export const useHeadingsTree = <T extends HTMLElement = HTMLElement>( 'Invalid options: `fromLevel` must be lower or equal to `toLevel`.' ); - const [tree, setTree] = useState<HeadingsTreeNode[]>([]); + const [headings, setHeadings] = useState<NodeListOf<HTMLHeadingElement>>(); const requestedHeadingTags = getHeadingTagsList(options); const query = requestedHeadingTags.join(', '); - const ref: RefCallback<T> = useCallback( - (el) => { - const headingNodes = el?.querySelectorAll<HTMLHeadingElement>(query); - if (headingNodes) setTree(buildHeadingsTreeFrom(headingNodes)); - }, - [query] - ); - - return { ref, tree }; + /* + * With a mutable ref, the headings are not always updated because of loading + * states. So we need to use a RefCallback to detect when the component is + * effectively rendered. However, to be able to compare the mutation records, + * we need to keep track of the current ref so we also need to use useState... + */ + const [wrapper, setWrapper] = useState<Nullable<T>>(); + const ref: RefCallback<T> = useCallback((el) => { + setWrapper(el); + }, []); + + const updateHeadings = useCallback(() => { + const headingNodes = wrapper?.querySelectorAll<HTMLHeadingElement>(query); + + if (headingNodes) setHeadings(headingNodes); + }, [query, wrapper]); + + useEffect(() => { + if (wrapper) updateHeadings(); + }, [updateHeadings, wrapper]); + + useMutationObserver({ + callback: useCallback( + (records) => { + for (const record of records) { + if (record.target === wrapper) updateHeadings(); + } + }, + [updateHeadings, wrapper] + ), + options: { childList: true, subtree: true }, + ref: { current: typeof window === 'undefined' ? null : document.body }, + }); + + return { ref, tree: headings ? buildHeadingsTreeFrom(headings) : [] }; }; |
