diff options
| author | Armand Philippot <git@armandphilippot.com> | 2023-10-28 17:12:58 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2023-11-11 18:15:27 +0100 |
| commit | 60c49f18389ff625177a57277ef8f292a31097bf (patch) | |
| tree | 76b0f1f1792b57659e54d282f93df70088446e3c /src/utils/hooks | |
| parent | 05f1dfc6896d3affa7c494a1b955f230d836a4b7 (diff) | |
refactor(providers,hooks): rewrite PrismThemeProvider & usePrismTheme
* reuse Theme provider logic
* move DOM mutation from provider to hook
* add a script to init theme before page load
Diffstat (limited to 'src/utils/hooks')
| -rw-r--r-- | src/utils/hooks/index.ts | 4 | ||||
| -rw-r--r-- | src/utils/hooks/use-add-classname.tsx | 32 | ||||
| -rw-r--r-- | src/utils/hooks/use-attributes.tsx | 50 | ||||
| -rw-r--r-- | src/utils/hooks/use-prism-theme/index.ts | 1 | ||||
| -rw-r--r-- | src/utils/hooks/use-prism-theme/use-prism-theme.test.tsx | 134 | ||||
| -rw-r--r-- | src/utils/hooks/use-prism-theme/use-prism-theme.ts | 54 | ||||
| -rw-r--r-- | src/utils/hooks/use-query-selector-all.tsx | 22 |
7 files changed, 190 insertions, 107 deletions
diff --git a/src/utils/hooks/index.ts b/src/utils/hooks/index.ts index 4372ca0..f1bb31e 100644 --- a/src/utils/hooks/index.ts +++ b/src/utils/hooks/index.ts @@ -1,7 +1,5 @@ export * from './use-ackee'; -export * from './use-add-classname'; export * from './use-article'; -export * from './use-attributes'; export * from './use-breadcrumb'; export * from './use-comments'; export * from './use-data-from-api'; @@ -15,7 +13,7 @@ export * from './use-mutation-observer'; export * from './use-on-click-outside'; export * from './use-pagination'; export * from './use-prism'; -export * from './use-query-selector-all'; +export * from './use-prism-theme'; export * from './use-reading-time'; export * from './use-redirection'; export * from './use-reduced-motion'; diff --git a/src/utils/hooks/use-add-classname.tsx b/src/utils/hooks/use-add-classname.tsx deleted file mode 100644 index 8b0f6d6..0000000 --- a/src/utils/hooks/use-add-classname.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { useCallback, useEffect } from 'react'; - -export type UseAddClassNameProps = { - className: string; - element?: HTMLElement; - elements?: NodeListOf<HTMLElement> | HTMLElement[]; -}; - -/** - * Add className to the given element(s). - * - * @param {UseAddClassNameProps} props - An object with classnames and one or more elements. - */ -export const useAddClassName = ({ - className, - element, - elements, -}: UseAddClassNameProps) => { - const classNames = className.split(' ').filter((string) => string !== ''); - - const setClassName = useCallback( - (el: HTMLElement) => { - el.classList.add(...classNames); - }, - [classNames] - ); - - useEffect(() => { - if (element) setClassName(element); - if (elements && elements.length > 0) elements.forEach(setClassName); - }, [element, elements, setClassName]); -}; diff --git a/src/utils/hooks/use-attributes.tsx b/src/utils/hooks/use-attributes.tsx deleted file mode 100644 index 20e9947..0000000 --- a/src/utils/hooks/use-attributes.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { useCallback, useEffect } from 'react'; -import { fromKebabCaseToCamelCase } from '../helpers'; - -export type useAttributesProps = { - /** - * An HTML element. - */ - element?: HTMLElement; - /** - * A node list of HTML Element. - */ - elements?: NodeListOf<HTMLElement> | HTMLElement[]; - /** - * The attribute name. - */ - attribute: string; - /** - * The attribute value. - */ - value: string; -}; - -/** - * Set HTML attributes to the given element or to the HTML document. - * - * @param props - An object with element, attribute name and value. - */ -export const useAttributes = ({ - element, - elements, - attribute, - value, -}: useAttributesProps) => { - const setAttribute = useCallback( - (el: HTMLElement) => { - if (attribute.startsWith('data')) { - el.setAttribute(attribute, value); - } else { - const camelCaseAttribute = fromKebabCaseToCamelCase(attribute); - el.dataset[camelCaseAttribute] = value; - } - }, - [attribute, value] - ); - - useEffect(() => { - if (element) setAttribute(element); - if (elements && elements.length > 0) elements.forEach(setAttribute); - }, [element, elements, setAttribute]); -}; diff --git a/src/utils/hooks/use-prism-theme/index.ts b/src/utils/hooks/use-prism-theme/index.ts new file mode 100644 index 0000000..5972519 --- /dev/null +++ b/src/utils/hooks/use-prism-theme/index.ts @@ -0,0 +1 @@ +export * from './use-prism-theme'; diff --git a/src/utils/hooks/use-prism-theme/use-prism-theme.test.tsx b/src/utils/hooks/use-prism-theme/use-prism-theme.test.tsx new file mode 100644 index 0000000..8a178c9 --- /dev/null +++ b/src/utils/hooks/use-prism-theme/use-prism-theme.test.tsx @@ -0,0 +1,134 @@ +import { describe, expect, it } from '@jest/globals'; +import { + act, + render, + renderHook, + screen as rtlScreen, +} from '@testing-library/react'; +import type { FC, ReactNode } from 'react'; +import { + PrismThemeProvider, + type PrismThemeProviderProps, +} from '../../providers/prism-theme-provider'; +import { usePrismTheme } from './use-prism-theme'; + +const codeSample1 = `const foo = 42;`; +const codeSample2 = `const bar = "baz";`; +const codeSample3 = `const baz = () => false;`; + +const ComponentTest: FC = () => { + usePrismTheme(); + + return ( + <div> + <pre className="language-js">{codeSample1}</pre> + <pre className="language-js">{codeSample2}</pre> + <pre>{codeSample3}</pre> + </div> + ); +}; + +const createWrapper = ( + Wrapper: FC<PrismThemeProviderProps>, + config: PrismThemeProviderProps +) => + function CreatedWrapper({ children }: { children: ReactNode }) { + return <Wrapper {...config}>{children}</Wrapper>; + }; + +describe('usePrismTheme', () => { + it('should return the default value without provider and prevent update', () => { + const defaultTheme = 'system'; + const { result } = renderHook(() => usePrismTheme()); + + expect(result.current.theme).toBe(defaultTheme); + + act(() => result.current.setTheme('dark')); + + expect(result.current.theme).toBe(defaultTheme); + }); + + it('should add an attribute on pre elements when matching a Prism block', () => { + const defaultTheme = 'light'; + const attribute = 'data-debitis'; + + render( + <PrismThemeProvider + attribute={attribute} + defaultTheme={defaultTheme} + storageKey="soluta" + > + <ComponentTest /> + </PrismThemeProvider> + ); + + expect(rtlScreen.getByText(codeSample1)).toHaveAttribute( + attribute, + defaultTheme + ); + expect(rtlScreen.getByText(codeSample2)).toHaveAttribute( + attribute, + defaultTheme + ); + expect(rtlScreen.getByText(codeSample3)).not.toHaveAttribute( + attribute, + defaultTheme + ); + }); + + it('can update the theme value using a setter', () => { + const defaultTheme = 'dark'; + + const { result } = renderHook(() => usePrismTheme(), { + wrapper: createWrapper(PrismThemeProvider, { + attribute: 'consequuntur', + defaultTheme, + storageKey: 'deleniti', + }), + }); + + expect(result.current.theme).toBe(defaultTheme); + + const newTheme = 'light'; + + act(() => result.current.setTheme(newTheme)); + + expect(result.current.theme).toBe(newTheme); + }); + + it('can toggle the theme from light to dark', () => { + const defaultTheme = 'light'; + + const { result } = renderHook(() => usePrismTheme(), { + wrapper: createWrapper(PrismThemeProvider, { + attribute: 'et', + defaultTheme, + storageKey: 'velit', + }), + }); + + expect(result.current.theme).toBe(defaultTheme); + + act(() => result.current.toggleTheme()); + + expect(result.current.theme).toBe('dark'); + }); + + it('can toggle the theme from dark to light', () => { + const defaultTheme = 'dark'; + + const { result } = renderHook(() => usePrismTheme(), { + wrapper: createWrapper(PrismThemeProvider, { + attribute: 'et', + defaultTheme, + storageKey: 'velit', + }), + }); + + expect(result.current.theme).toBe(defaultTheme); + + act(() => result.current.toggleTheme()); + + expect(result.current.theme).toBe('light'); + }); +}); diff --git a/src/utils/hooks/use-prism-theme/use-prism-theme.ts b/src/utils/hooks/use-prism-theme/use-prism-theme.ts new file mode 100644 index 0000000..4caec6c --- /dev/null +++ b/src/utils/hooks/use-prism-theme/use-prism-theme.ts @@ -0,0 +1,54 @@ +import { useCallback, useContext, useEffect } from 'react'; +import { themeValidator as isValidTheme } from '../../helpers'; +import { PrismThemeContext } from '../../providers/prism-theme-provider'; + +export const usePrismTheme = () => { + const { attribute, resolvedTheme, setTheme, theme } = + useContext(PrismThemeContext); + const currentTheme = theme === 'system' ? resolvedTheme : theme; + + const handleMutations: MutationCallback = useCallback( + (mutations) => { + for (const mutation of mutations) { + if (mutation.target.nodeName.toLowerCase() !== 'pre') return; + + const newTheme = (mutation.target as HTMLPreElement).getAttribute( + attribute + ); + + if (isValidTheme(newTheme) && newTheme !== theme) setTheme(newTheme); + } + }, + [attribute, setTheme, theme] + ); + + useEffect(() => { + if (typeof window === 'undefined') return undefined; + + const preElements = document.getElementsByTagName('pre'); + const observer = new MutationObserver(handleMutations); + + for (const preEl of Array.from(preElements)) { + if (preEl.className.includes('language-')) { + preEl.setAttribute(attribute, theme); + observer.observe(preEl, { + attributes: true, + attributeFilter: [attribute], + }); + } + } + + return () => { + observer.disconnect(); + }; + }, [attribute, handleMutations, theme]); + + const toggleTheme = useCallback(() => { + setTheme(() => { + if (currentTheme === 'dark') return 'light'; + return 'dark'; + }); + }, [currentTheme, setTheme]); + + return { currentTheme, resolvedTheme, setTheme, theme, toggleTheme }; +}; diff --git a/src/utils/hooks/use-query-selector-all.tsx b/src/utils/hooks/use-query-selector-all.tsx deleted file mode 100644 index a3650ea..0000000 --- a/src/utils/hooks/use-query-selector-all.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; - -/** - * Use `document.querySelectorAll`. - * - * @param {string} query - A query. - * @returns {NodeListOf<HTMLElementTagNameMap[T]|undefined>} - The node list. - */ -export const useQuerySelectorAll = <T extends keyof HTMLElementTagNameMap>( - query: string -): NodeListOf<HTMLElementTagNameMap[T]> | undefined => { - const [elements, setElements] = - useState<NodeListOf<HTMLElementTagNameMap[T]>>(); - const { asPath } = useRouter(); - - useEffect(() => { - setElements(document.querySelectorAll(query)); - }, [asPath, query]); - - return elements; -}; |
