diff options
Diffstat (limited to 'src/utils/hooks')
| -rw-r--r-- | src/utils/hooks/index.ts | 3 | ||||
| -rw-r--r-- | src/utils/hooks/use-match-media/index.ts | 1 | ||||
| -rw-r--r-- | src/utils/hooks/use-match-media/use-match-media.ts | 17 | ||||
| -rw-r--r-- | src/utils/hooks/use-system-color-scheme/index.ts | 1 | ||||
| -rw-r--r-- | src/utils/hooks/use-system-color-scheme/use-system-color-scheme.ts | 24 | ||||
| -rw-r--r-- | src/utils/hooks/use-theme/index.ts | 1 | ||||
| -rw-r--r-- | src/utils/hooks/use-theme/use-theme.test.tsx | 82 | ||||
| -rw-r--r-- | src/utils/hooks/use-theme/use-theme.ts | 15 |
8 files changed, 144 insertions, 0 deletions
diff --git a/src/utils/hooks/index.ts b/src/utils/hooks/index.ts index 606c259..4372ca0 100644 --- a/src/utils/hooks/index.ts +++ b/src/utils/hooks/index.ts @@ -10,6 +10,7 @@ export * from './use-headings-tree'; export * from './use-input-autofocus'; export * from './use-is-mounted'; export * from './use-local-storage'; +export * from './use-match-media'; export * from './use-mutation-observer'; export * from './use-on-click-outside'; export * from './use-pagination'; @@ -22,3 +23,5 @@ export * from './use-route-change'; export * from './use-scroll-position'; export * from './use-settings'; export * from './use-state-change'; +export * from './use-system-color-scheme'; +export * from './use-theme'; diff --git a/src/utils/hooks/use-match-media/index.ts b/src/utils/hooks/use-match-media/index.ts new file mode 100644 index 0000000..7d79e64 --- /dev/null +++ b/src/utils/hooks/use-match-media/index.ts @@ -0,0 +1 @@ +export * from './use-match-media'; diff --git a/src/utils/hooks/use-match-media/use-match-media.ts b/src/utils/hooks/use-match-media/use-match-media.ts new file mode 100644 index 0000000..50ac038 --- /dev/null +++ b/src/utils/hooks/use-match-media/use-match-media.ts @@ -0,0 +1,17 @@ +import { useEffect } from 'react'; + +export type UseMatchMediaCallback = (ev: MediaQueryListEvent) => void; + +/** + * React hook to watch for media changes based on the given query. + * + * @param {string} query - A media query. + * @param {UseMatchMediaCallback} cb - A callback function to execute on change. + */ +export const useMatchMedia = (query: string, cb: UseMatchMediaCallback) => { + useEffect(() => { + window.matchMedia(query).addEventListener('change', cb); + + return () => window.matchMedia(query).removeEventListener('change', cb); + }, [cb, query]); +}; diff --git a/src/utils/hooks/use-system-color-scheme/index.ts b/src/utils/hooks/use-system-color-scheme/index.ts new file mode 100644 index 0000000..78d1665 --- /dev/null +++ b/src/utils/hooks/use-system-color-scheme/index.ts @@ -0,0 +1 @@ +export * from './use-system-color-scheme'; diff --git a/src/utils/hooks/use-system-color-scheme/use-system-color-scheme.ts b/src/utils/hooks/use-system-color-scheme/use-system-color-scheme.ts new file mode 100644 index 0000000..1956f32 --- /dev/null +++ b/src/utils/hooks/use-system-color-scheme/use-system-color-scheme.ts @@ -0,0 +1,24 @@ +import { useCallback, useState } from 'react'; +import { getThemeFromSystem } from '../../helpers'; +import { useMatchMedia } from '../use-match-media'; + +export type SystemColorScheme = 'dark' | 'light'; + +/** + * React hook to retrieve the system color scheme based on user preferences, + * and to watch for changes. + * + * @returns {SystemColorScheme} The system color scheme + */ +export const useSystemColorScheme = () => { + const [colorScheme, setColorScheme] = + useState<SystemColorScheme>(getThemeFromSystem); + + const updateColorScheme = useCallback(() => { + setColorScheme(getThemeFromSystem); + }, []); + + useMatchMedia('(prefers-color-scheme: dark)', updateColorScheme); + + return colorScheme; +}; diff --git a/src/utils/hooks/use-theme/index.ts b/src/utils/hooks/use-theme/index.ts new file mode 100644 index 0000000..4e8fc4a --- /dev/null +++ b/src/utils/hooks/use-theme/index.ts @@ -0,0 +1 @@ +export * from './use-theme'; diff --git a/src/utils/hooks/use-theme/use-theme.test.tsx b/src/utils/hooks/use-theme/use-theme.test.tsx new file mode 100644 index 0000000..feaabfa --- /dev/null +++ b/src/utils/hooks/use-theme/use-theme.test.tsx @@ -0,0 +1,82 @@ +import { describe, expect, it } from '@jest/globals'; +import { act, renderHook } from '@testing-library/react'; +import type { FC, ReactNode } from 'react'; +import { ThemeProvider, type ThemeProviderProps } from '../../providers'; +import { useTheme } from './use-theme'; + +const createWrapper = ( + Wrapper: FC<ThemeProviderProps>, + config: ThemeProviderProps +) => + function CreatedWrapper({ children }: { children: ReactNode }) { + return <Wrapper {...config}>{children}</Wrapper>; + }; + +describe('useTheme', () => { + it('should return the default value without provider and prevent update', () => { + const defaultTheme = 'system'; + const { result } = renderHook(() => useTheme()); + + expect(result.current.theme).toBe(defaultTheme); + + act(() => result.current.setTheme('dark')); + + expect(result.current.theme).toBe(defaultTheme); + }); + + it('can update the value', () => { + const defaultTheme = 'dark'; + + const { result } = renderHook(() => useTheme(), { + wrapper: createWrapper(ThemeProvider, { + attribute: 'magnam', + defaultTheme, + storageKey: 'repellat', + }), + }); + + 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 dark to light', () => { + const defaultTheme = 'dark'; + + const { result } = renderHook(() => useTheme(), { + wrapper: createWrapper(ThemeProvider, { + attribute: 'voluptatibus', + defaultTheme, + storageKey: 'qui', + }), + }); + + expect(result.current.theme).toBe(defaultTheme); + + act(() => result.current.toggleTheme()); + + expect(result.current.theme).toBe('light'); + }); + + it('can toggle the theme from light to dark', () => { + const defaultTheme = 'light'; + + const { result } = renderHook(() => useTheme(), { + wrapper: createWrapper(ThemeProvider, { + attribute: 'sed', + defaultTheme, + storageKey: 'ut', + }), + }); + + expect(result.current.theme).toBe(defaultTheme); + + act(() => result.current.toggleTheme()); + + expect(result.current.theme).toBe('dark'); + }); +}); diff --git a/src/utils/hooks/use-theme/use-theme.ts b/src/utils/hooks/use-theme/use-theme.ts new file mode 100644 index 0000000..0605d8b --- /dev/null +++ b/src/utils/hooks/use-theme/use-theme.ts @@ -0,0 +1,15 @@ +import { useCallback, useContext } from 'react'; +import { ThemeContext } from '../../providers'; + +export const useTheme = () => { + const { resolvedTheme, theme, setTheme } = useContext(ThemeContext); + + const toggleTheme = useCallback(() => { + setTheme(() => { + if (resolvedTheme === 'dark') return 'light'; + return 'dark'; + }); + }, [resolvedTheme, setTheme]); + + return { resolvedTheme, setTheme, theme, toggleTheme }; +}; |
