Skip to content

Commit

Permalink
Merge branch 'main' into example/multi-theme
Browse files Browse the repository at this point in the history
  • Loading branch information
trm217 committed May 6, 2024
2 parents 3743c88 + 4452e3d commit ff72479
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 85 deletions.
36 changes: 36 additions & 0 deletions .github/ISSUE_TEMPLATE/1-bug-report.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Bug Report
description: File a bug report.
title: "[Bug]: "
labels: ["bug", "triage"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
placeholder: Tell us what you see!
value: "A bug happened!"
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: What version of next-themes are you using?
validations:
required: true
- type: dropdown
id: browsers
attributes:
label: What browsers are you seeing the problem on?
multiple: true
options:
- Firefox
- Chrome
- Safari
- Microsoft Edge
- other (please specify in the description)
17 changes: 17 additions & 0 deletions .github/ISSUE_TEMPLATE/2-feature-request.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Feature request
description: Feature request for next-themes.
title: "[Feature request]: "
labels: ["request"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this feature request!
- type: textarea
id: functionality
attributes:
label: What feature would you like to see?
description: Specify the feature you would like to see and perhaps what problem it would solve for you.
placeholder: "I would like to be able to..."
validations:
required: true
2 changes: 1 addition & 1 deletion next-themes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ All your theme configuration is passed to ThemeProvider.
useTheme takes no parameters, but returns:

- `theme`: Active theme name
- `setTheme(name)`: Function to update the theme
- `setTheme(name)`: Function to update the theme. The API is identical to the [set function](https://react.dev/reference/react/useState#setstate) returned by `useState`-hook. Pass the new theme value or use a callback to set the new theme based on the current theme.
- `forcedTheme`: Forced page theme or falsy. If `forcedTheme` is set, you should disable any theme switching UI
- `resolvedTheme`: If `enableSystem` is true and the active theme is "system", this returns whether the system preference resolved to "dark" or "light". Otherwise, identical to `theme`
- `systemTheme`: If `enableSystem` is true, represents the System theme preference ("dark" or "light"), regardless what the active theme is
Expand Down
186 changes: 105 additions & 81 deletions next-themes/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
// @vitest-environment jsdom

import * as React from 'react'
import { act, render, screen } from '@testing-library/react'
import { act, render, renderHook, screen } from '@testing-library/react'
import { vi, beforeAll, beforeEach, afterEach, afterAll, describe, test, it, expect } from 'vitest'
import { cleanup } from '@testing-library/react'

import { ThemeProvider, useTheme } from '../src/index'
import { ThemeProviderProps } from '../src/types'

let originalLocalStorage: Storage
const localStorageMock: Storage = (() => {
Expand Down Expand Up @@ -90,86 +91,81 @@ afterAll(() => {
window.localStorage = originalLocalStorage
})

function makeWrapper(props: ThemeProviderProps) {
return ({ children }: { children: React.ReactNode }) => (
<ThemeProvider {...props}>{children}</ThemeProvider>
)
}

describe('defaultTheme', () => {
test('should return system when no default-theme is set', () => {
render(
<ThemeProvider>
<HelperComponent />
</ThemeProvider>
)

expect(screen.getByTestId('theme').textContent).toBe('system')
test('should return system-theme when no default-theme is set', () => {
setDeviceTheme('light')

const { result } = renderHook(() => useTheme(), {
wrapper: makeWrapper({})
})
expect(result.current.theme).toBe('system')
expect(result.current.systemTheme).toBe('light')
expect(result.current.resolvedTheme).toBe('light')
})

test('should return light when no default-theme is set and enableSystem=false', () => {
render(
<ThemeProvider enableSystem={false}>
<HelperComponent />
</ThemeProvider>
)
const { result } = renderHook(() => useTheme(), {
wrapper: makeWrapper({ enableSystem: false })
})

expect(screen.getByTestId('theme').textContent).toBe('light')
expect(result.current.theme).toBe('light')
expect(result.current.resolvedTheme).toBe('light')
})

test('should return light when light is set as default-theme', () => {
render(
<ThemeProvider defaultTheme="light">
<HelperComponent />
</ThemeProvider>
)
const { result } = renderHook(() => useTheme(), {
wrapper: makeWrapper({ defaultTheme: 'light' })
})

expect(screen.getByTestId('theme').textContent).toBe('light')
expect(result.current.theme).toBe('light')
expect(result.current.resolvedTheme).toBe('light')
})

test('should return dark when dark is set as default-theme', () => {
render(
<ThemeProvider defaultTheme="dark">
<HelperComponent />
</ThemeProvider>
)

expect(screen.getByTestId('theme').textContent).toBe('dark')
const { result } = renderHook(() => useTheme(), {
wrapper: makeWrapper({ defaultTheme: 'dark' })
})
expect(result.current.theme).toBe('dark')
expect(result.current.resolvedTheme).toBe('dark')
})
})

describe('provider', () => {
it('ignores nested ThemeProviders', () => {
act(() => {
render(
const { result } = renderHook(() => useTheme(), {
wrapper: ({ children }) => (
<ThemeProvider defaultTheme="dark">
<ThemeProvider defaultTheme="light">
<HelperComponent />
</ThemeProvider>
<ThemeProvider defaultTheme="light">{children}</ThemeProvider>
</ThemeProvider>
)
})

expect(screen.getByTestId('theme').textContent).toBe('dark')
expect(result.current.theme).toBe('dark')
expect(result.current.resolvedTheme).toBe('dark')
})
})

describe('storage', () => {
test('should not set localStorage with default value', () => {
act(() => {
render(
<ThemeProvider defaultTheme="dark">
<HelperComponent />
</ThemeProvider>
)
renderHook(() => useTheme(), {
wrapper: makeWrapper({ defaultTheme: 'dark' })
})

expect(window.localStorage.setItem).toBeCalledTimes(0)
expect(window.localStorage.getItem('theme')).toBeNull()
})

test('should set localStorage when switching themes', () => {
act(() => {
render(
<ThemeProvider>
<HelperComponent forceSetTheme="dark" />
</ThemeProvider>
)
const { result } = renderHook(() => useTheme(), {
wrapper: makeWrapper({})
})
result.current.setTheme('dark')

expect(window.localStorage.setItem).toBeCalledTimes(1)
expect(window.localStorage.getItem('theme')).toBe('dark')
Expand Down Expand Up @@ -290,47 +286,40 @@ describe('forcedTheme', () => {
test('should render saved theme when no forcedTheme is set', () => {
localStorageMock.setItem('theme', 'dark')

render(
<ThemeProvider>
<HelperComponent />
</ThemeProvider>
)
const { result } = renderHook(() => useTheme(), {
wrapper: makeWrapper({})
})

expect(screen.getByTestId('theme').textContent).toBe('dark')
expect(screen.getByTestId('forcedTheme').textContent).toBe('')
expect(result.current.theme).toBe('dark')
expect(result.current.forcedTheme).toBeUndefined()
})

test('should render light theme when forcedTheme is set to light', () => {
localStorageMock.setItem('theme', 'dark')

act(() => {
render(
<ThemeProvider forcedTheme="light">
<HelperComponent />
</ThemeProvider>
)
const { result } = renderHook(() => useTheme(), {
wrapper: makeWrapper({
forcedTheme: 'light'
})
})

expect(screen.getByTestId('theme').textContent).toBe('dark')
expect(screen.getByTestId('forcedTheme').textContent).toBe('light')
expect(result.current.theme).toBe('dark')
expect(result.current.forcedTheme).toBe('light')
})
})

describe('system', () => {
describe('system theme', () => {
test('resolved theme should be set', () => {
setDeviceTheme('dark')

act(() => {
render(
<ThemeProvider>
<HelperComponent />
</ThemeProvider>
)
const { result } = renderHook(() => useTheme(), {
wrapper: makeWrapper({})
})

expect(screen.getByTestId('theme').textContent).toBe('system')
expect(screen.getByTestId('forcedTheme').textContent).toBe('')
expect(screen.getByTestId('resolvedTheme').textContent).toBe('dark')
expect(result.current.theme).toBe('system')
expect(result.current.systemTheme).toBe('dark')
expect(result.current.resolvedTheme).toBe('dark')
expect(result.current.forcedTheme).toBeUndefined()
})

test('system theme should be set, even if theme is not system', () => {
Expand All @@ -353,18 +342,14 @@ describe('system', () => {
test('system theme should not be set if enableSystem is false', () => {
setDeviceTheme('dark')

act(() => {
render(
<ThemeProvider defaultTheme="light" enableSystem={false}>
<HelperComponent />
</ThemeProvider>
)
const { result } = renderHook(() => useTheme(), {
wrapper: makeWrapper({ enableSystem: false, defaultTheme: 'light' })
})

expect(screen.getByTestId('theme').textContent).toBe('light')
expect(screen.getByTestId('forcedTheme').textContent).toBe('')
expect(screen.getByTestId('resolvedTheme').textContent).toBe('light')
expect(screen.getByTestId('systemTheme').textContent).toBe('')
expect(result.current.theme).toBe('light')
expect(result.current.systemTheme).toBeUndefined()
expect(result.current.resolvedTheme).toBe('light')
expect(result.current.forcedTheme).toBeUndefined()
})
})

Expand Down Expand Up @@ -407,3 +392,42 @@ describe('color-scheme', () => {
expect(document.documentElement.style.colorScheme).toBe('dark')
})
})

describe('setTheme', () => {
test('setTheme(<literal>)', () => {
const { result, rerender } = renderHook(() => useTheme(), {
wrapper: ({ children }) => <ThemeProvider defaultTheme="light">{children}</ThemeProvider>
})
expect(result.current?.setTheme).toBeDefined()
expect(result.current.resolvedTheme).toBe('light')
result.current.setTheme('dark')
rerender()
expect(result.current.resolvedTheme).toBe('dark')
})

test('setTheme(<function>)', () => {
const { result, rerender } = renderHook(() => useTheme(), {
wrapper: ({ children }) => <ThemeProvider defaultTheme="light">{children}</ThemeProvider>
})
expect(result.current?.setTheme).toBeDefined()
expect(result.current.theme).toBe('light')
expect(result.current.resolvedTheme).toBe('light')

const toggleTheme = vi.fn((theme: string) => (theme === 'light' ? 'dark' : 'light'))

result.current.setTheme(toggleTheme)
expect(toggleTheme).toBeCalledTimes(1)
rerender()

expect(result.current.theme).toBe('dark')
expect(result.current.resolvedTheme).toBe('dark')

result.current.setTheme(toggleTheme)
expect(toggleTheme).toBeCalledTimes(2)
rerender()

expect(result.current.theme).toBe('light')
expect(result.current.resolvedTheme).toBe('light')
})

})
6 changes: 3 additions & 3 deletions next-themes/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ const Theme = ({
}, [])

const setTheme = React.useCallback(
theme => {
const newTheme = typeof theme === 'function' ? theme(theme) : theme
value => {
const newTheme = typeof value === 'function' ? value(theme) : value
setThemeState(newTheme)

// Save to storage
Expand All @@ -90,7 +90,7 @@ const Theme = ({
// Unsupported
}
},
[forcedTheme]
[theme]
)

const handleMediaQuery = React.useCallback(
Expand Down

0 comments on commit ff72479

Please sign in to comment.