diff options
Diffstat (limited to 'src/utils/providers')
| -rw-r--r-- | src/utils/providers/index.ts | 2 | ||||
| -rw-r--r-- | src/utils/providers/prism-theme-provider/index.ts | 1 | ||||
| -rw-r--r-- | src/utils/providers/prism-theme-provider/prism-theme-provider.test.tsx | 50 | ||||
| -rw-r--r-- | src/utils/providers/prism-theme-provider/prism-theme-provider.tsx | 86 | ||||
| -rw-r--r-- | src/utils/providers/prism-theme.tsx | 158 | ||||
| -rw-r--r-- | src/utils/providers/theme-provider/theme-provider.tsx | 17 |
6 files changed, 145 insertions, 169 deletions
diff --git a/src/utils/providers/index.ts b/src/utils/providers/index.ts index 90dca15..bc521b3 100644 --- a/src/utils/providers/index.ts +++ b/src/utils/providers/index.ts @@ -1,4 +1,4 @@ export * from './ackee-provider'; export * from './motion-provider'; -export * from './prism-theme'; +export * from './prism-theme-provider'; export * from './theme-provider'; diff --git a/src/utils/providers/prism-theme-provider/index.ts b/src/utils/providers/prism-theme-provider/index.ts new file mode 100644 index 0000000..fec2444 --- /dev/null +++ b/src/utils/providers/prism-theme-provider/index.ts @@ -0,0 +1 @@ +export * from './prism-theme-provider'; diff --git a/src/utils/providers/prism-theme-provider/prism-theme-provider.test.tsx b/src/utils/providers/prism-theme-provider/prism-theme-provider.test.tsx new file mode 100644 index 0000000..d0c2630 --- /dev/null +++ b/src/utils/providers/prism-theme-provider/prism-theme-provider.test.tsx @@ -0,0 +1,50 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import { type FC, useContext } from 'react'; +import { PrismThemeContext, PrismThemeProvider } from './prism-theme-provider'; + +const bodyPrefix = 'Current Prism theme is:'; + +const ComponentTest: FC = () => { + const { theme } = useContext(PrismThemeContext); + + return ( + <div> + {bodyPrefix} {theme} + </div> + ); +}; + +describe('PrismThemeProvider', () => { + it('uses the default value when the provider is not used', () => { + const defaultValue = 'system'; + + render(<ComponentTest />); + + expect(rtlScreen.getByText(new RegExp(bodyPrefix))).toHaveTextContent( + `${bodyPrefix} ${defaultValue}` + ); + }); + + it('provides the given value to its children and set a matching attribute', () => { + const attribute = 'ratione'; + const theme = 'dark'; + + const { baseElement } = render( + <PrismThemeProvider + attribute={attribute} + defaultTheme={theme} + storageKey="qui" + > + <ComponentTest /> + </PrismThemeProvider> + ); + + expect(rtlScreen.getByText(new RegExp(bodyPrefix))).toHaveTextContent( + `${bodyPrefix} ${theme}` + ); + expect(baseElement.parentElement?.getAttribute(`data-${attribute}`)).toBe( + theme + ); + }); +}); diff --git a/src/utils/providers/prism-theme-provider/prism-theme-provider.tsx b/src/utils/providers/prism-theme-provider/prism-theme-provider.tsx new file mode 100644 index 0000000..0f0a4a1 --- /dev/null +++ b/src/utils/providers/prism-theme-provider/prism-theme-provider.tsx @@ -0,0 +1,86 @@ +import { + type Dispatch, + type FC, + type ReactNode, + type SetStateAction, + createContext, + useEffect, + useMemo, +} from 'react'; +import type { Theme } from '../../../types'; +import { + getDataAttributeFrom, + getThemeFromSystem, + themeValidator, +} from '../../helpers'; +import { useLocalStorage, useSystemColorScheme } from '../../hooks'; + +export type PrismThemeContextProps = { + attribute: string; + resolvedTheme: Exclude<Theme, 'system'>; + setTheme: Dispatch<SetStateAction<Theme>>; + theme: Theme; +}; + +export const PrismThemeContext = createContext<PrismThemeContextProps>({ + attribute: 'data-prism-theme', + resolvedTheme: getThemeFromSystem(), + setTheme: (value) => value, + theme: 'system', +}); + +export type PrismThemeProviderProps = { + /** + * The attribute name to append to document root. + */ + attribute: string; + /** + * The provider children. + */ + children?: ReactNode; + /** + * The default theme. + * + * @default 'system' + */ + defaultTheme?: Theme; + /** + * The key to use in local storage. + */ + storageKey: string; +}; + +export const PrismThemeProvider: FC<PrismThemeProviderProps> = ({ + attribute, + children, + defaultTheme = 'system', + storageKey, +}) => { + const [theme, setTheme] = useLocalStorage( + storageKey, + defaultTheme, + themeValidator + ); + const resolvedTheme = useSystemColorScheme(); + const dataAttribute = getDataAttributeFrom(attribute); + + useEffect(() => { + if (typeof window !== 'undefined') { + document.documentElement.setAttribute(dataAttribute, `${theme}`); + } + + return () => { + document.documentElement.removeAttribute(dataAttribute); + }; + }, [dataAttribute, theme]); + + const value = useMemo(() => { + return { attribute: dataAttribute, resolvedTheme, setTheme, theme }; + }, [dataAttribute, resolvedTheme, setTheme, theme]); + + return ( + <PrismThemeContext.Provider value={value}> + {children} + </PrismThemeContext.Provider> + ); +}; diff --git a/src/utils/providers/prism-theme.tsx b/src/utils/providers/prism-theme.tsx deleted file mode 100644 index 603e8a7..0000000 --- a/src/utils/providers/prism-theme.tsx +++ /dev/null @@ -1,158 +0,0 @@ -/* eslint-disable max-statements */ -import { - createContext, - type FC, - type ReactNode, - useCallback, - useContext, - useEffect, - useState, - useMemo, -} from 'react'; -import { useAttributes, useLocalStorage, useQuerySelectorAll } from '../hooks'; - -export type PrismTheme = 'dark' | 'light' | 'system'; -export type ResolvedPrismTheme = Exclude<PrismTheme, 'system'>; - -export type UsePrismThemeProps = { - themes: PrismTheme[]; - theme?: PrismTheme; - setTheme: (theme: PrismTheme) => void; - resolvedTheme?: ResolvedPrismTheme; - codeBlocks?: NodeListOf<HTMLPreElement>; -}; - -export type PrismThemeProviderProps = { - attribute?: string; - children: ReactNode; - storageKey?: string; - themes?: PrismTheme[]; -}; - -export const PrismThemeContext = createContext<UsePrismThemeProps>({ - themes: ['dark', 'light', 'system'], - setTheme: (_) => { - // This is intentional. - }, -}); - -export const usePrismTheme = () => useContext(PrismThemeContext); - -/** - * Check if user prefers dark color scheme. - * - * @returns {boolean|undefined} True if `prefers-color-scheme` is set to `dark`. - */ -const prefersDarkScheme = (): boolean | undefined => { - if (typeof window === 'undefined') return undefined; - - return window.matchMedia('(prefers-color-scheme: dark)').matches; -}; - -/** - * Check if a given string is a Prism theme name. - * - * @param {string} theme - A string. - * @returns {boolean} True if the given string match a Prism theme name. - */ -const isValidTheme = (theme: string): boolean => - theme === 'dark' || theme === 'light' || theme === 'system'; - -const defaultThemes = ['dark', 'light', 'system'] satisfies PrismTheme[]; - -const validator = (value: unknown): value is PrismTheme => - typeof value === 'string' && (defaultThemes as string[]).includes(value); - -export const PrismThemeProvider: FC<PrismThemeProviderProps> = ({ - attribute = 'data-prismjs-color-scheme-current', - storageKey = 'prismjs-color-scheme', - themes = defaultThemes, - children, -}) => { - /** - * Retrieve the theme to use depending on `prefers-color-scheme`. - */ - const getThemeFromSystem = useCallback(() => { - if (prefersDarkScheme()) return 'dark'; - return 'light'; - }, []); - - const [prismTheme, setPrismTheme] = useLocalStorage<PrismTheme>( - storageKey, - 'system', - validator - ); - - useEffect(() => { - if (!isValidTheme(prismTheme)) setPrismTheme('system'); - }, [prismTheme, setPrismTheme]); - - const [resolvedTheme, setResolvedTheme] = useState<ResolvedPrismTheme>(); - - useEffect(() => { - if (prismTheme === 'dark' || prismTheme === 'light') { - setResolvedTheme(prismTheme); - } else { - setResolvedTheme(getThemeFromSystem()); - } - }, [prismTheme, getThemeFromSystem]); - - const updateResolvedTheme = useCallback(() => { - setResolvedTheme(getThemeFromSystem()); - }, [getThemeFromSystem]); - - useEffect(() => { - window - .matchMedia('(prefers-color-scheme: dark)') - .addEventListener('change', updateResolvedTheme); - - return () => - window - .matchMedia('(prefers-color-scheme: dark)') - .removeEventListener('change', updateResolvedTheme); - }, [updateResolvedTheme]); - - const preTags = useQuerySelectorAll<'pre'>('pre'); - useAttributes({ elements: preTags, attribute, value: prismTheme }); - - /** - * Listen for changes on pre attributes and update theme. - */ - const listenAttributeChange = useCallback( - (pre: HTMLPreElement) => { - const observer = new MutationObserver((mutations) => { - mutations.forEach((record) => { - const mutatedPre = record.target as HTMLPreElement; - const newTheme = mutatedPre.getAttribute(attribute) as PrismTheme; - setPrismTheme(newTheme); - }); - }); - observer.observe(pre, { - attributes: true, - attributeFilter: [attribute], - }); - }, - [attribute, setPrismTheme] - ); - - useEffect(() => { - if (!preTags) return; - preTags.forEach(listenAttributeChange); - }, [preTags, listenAttributeChange]); - - const value = useMemo(() => { - return { - themes, - theme: prismTheme, - setTheme: setPrismTheme, - codeBlocks: preTags, - resolvedTheme, - }; - }, [preTags, prismTheme, resolvedTheme, setPrismTheme, themes]); - - return ( - <PrismThemeContext.Provider value={value}> - {children} - </PrismThemeContext.Provider> - ); -}; diff --git a/src/utils/providers/theme-provider/theme-provider.tsx b/src/utils/providers/theme-provider/theme-provider.tsx index b743a7f..f09f74d 100644 --- a/src/utils/providers/theme-provider/theme-provider.tsx +++ b/src/utils/providers/theme-provider/theme-provider.tsx @@ -7,13 +7,14 @@ import { useMemo, useEffect, } from 'react'; -import { getDataAttributeFrom, getThemeFromSystem } from '../../helpers'; +import type { Theme } from '../../../types'; +import { + getDataAttributeFrom, + getThemeFromSystem, + themeValidator, +} from '../../helpers'; import { useLocalStorage, useSystemColorScheme } from '../../hooks'; -const validThemes = ['dark', 'light', 'system'] as const; - -type Theme = (typeof validThemes)[number]; - type ThemeContextProps = { resolvedTheme: Exclude<Theme, 'system'>; setTheme: Dispatch<SetStateAction<Theme>>; @@ -26,10 +27,6 @@ export const ThemeContext = createContext<ThemeContextProps>({ theme: 'system', }); -const validator = (value: unknown): value is Theme => - typeof value === 'string' && - (validThemes as readonly string[]).includes(value); - export type ThemeProviderProps = { /** * The attribute name to append to document root. @@ -60,7 +57,7 @@ export const ThemeProvider: FC<ThemeProviderProps> = ({ const [theme, setTheme] = useLocalStorage( storageKey, defaultTheme, - validator + themeValidator ); const userColorScheme = useSystemColorScheme(); const resolvedTheme: Exclude<Theme, 'system'> = |
