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-mutation-observer | |
| 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-mutation-observer')
3 files changed, 78 insertions, 0 deletions
diff --git a/src/utils/hooks/use-mutation-observer/index.ts b/src/utils/hooks/use-mutation-observer/index.ts new file mode 100644 index 0000000..16780fe --- /dev/null +++ b/src/utils/hooks/use-mutation-observer/index.ts @@ -0,0 +1 @@ +export * from './use-mutation-observer'; diff --git a/src/utils/hooks/use-mutation-observer/use-mutation-observer.test.ts b/src/utils/hooks/use-mutation-observer/use-mutation-observer.test.ts new file mode 100644 index 0000000..62ed559 --- /dev/null +++ b/src/utils/hooks/use-mutation-observer/use-mutation-observer.test.ts @@ -0,0 +1,42 @@ +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; +import { renderHook } from '@testing-library/react'; +import { useMutationObserver } from './use-mutation-observer'; + +describe('useMutationObserver', () => { + beforeEach(() => { + jest.restoreAllMocks(); + }); + + it('can create a new observer', () => { + const callback = jest.fn(); + const observerSpy = jest.spyOn(MutationObserver.prototype, 'observe'); + const wrapper = document.createElement('div'); + const options: MutationObserverInit = { childList: true }; + + renderHook(() => + useMutationObserver({ + callback, + options, + ref: { current: wrapper }, + }) + ); + + expect(observerSpy).toHaveBeenCalledTimes(1); + expect(observerSpy).toHaveBeenCalledWith(wrapper, options); + }); + + it('does not create a new observer when ref is null', () => { + const callback = jest.fn(); + const observerSpy = jest.spyOn(MutationObserver.prototype, 'observe'); + + renderHook(() => + useMutationObserver({ + callback, + options: { childList: true }, + ref: { current: null }, + }) + ); + + expect(observerSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/src/utils/hooks/use-mutation-observer/use-mutation-observer.ts b/src/utils/hooks/use-mutation-observer/use-mutation-observer.ts new file mode 100644 index 0000000..6043055 --- /dev/null +++ b/src/utils/hooks/use-mutation-observer/use-mutation-observer.ts @@ -0,0 +1,35 @@ +import { type RefObject, useEffect } from 'react'; +import type { Nullable } from '../../../types'; + +type UseMutationObserverProps<T extends Nullable<HTMLElement>> = { + /** + * A callback to execute when mutations are observed. + */ + callback: MutationCallback; + /** + * The options passed to mutation observer. + */ + options: MutationObserverInit; + /** + * A reference to the DOM node to observe. + */ + ref: RefObject<T>; +}; + +export const useMutationObserver = <T extends Nullable<HTMLElement>>({ + callback, + options, + ref, +}: UseMutationObserverProps<T>) => { + useEffect(() => { + if (!ref.current) return undefined; + + const observer = new MutationObserver(callback); + + observer.observe(ref.current, options); + + return () => { + observer.disconnect(); + }; + }, [callback, options, ref]); +}; |
