Skip to content

Commit

Permalink
Merge pull request #4 from designcise/feat/typescript-support
Browse files Browse the repository at this point in the history
feat: add typescript support
  • Loading branch information
designcise authored Jan 30, 2024
2 parents dcec742 + 079fe85 commit 8431d01
Show file tree
Hide file tree
Showing 37 changed files with 4,361 additions and 7,909 deletions.
22 changes: 0 additions & 22 deletions .babelrc

This file was deleted.

4 changes: 3 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{
"semi": false,
"singleQuote": true,
"printWidth": 100
"bracketSameLine": true,
"printWidth": 100,
"arrowParens": "avoid"
}
79 changes: 26 additions & 53 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ You can then [use different CSS selectors to create styles for dark/light themes
## Features

- Easy implementation with just _two_ lines of code
- TypeScript Support
- Types are automatically loaded, whenever applicable
- No flicker on page load
- Toggle between `light`, `dark` and `auto` modes
- Automatically choose color based on `prefers-color-scheme` when in "`auto`" mode
Expand Down Expand Up @@ -54,16 +56,13 @@ At a bare minimum you need to do the following:
import { ThemeProvider } from '@designcise/next-theme-toggle';
import { themes } from '@designcise/next-theme-toggle/server';

// 1: specify key for storage
const THEME_STORAGE_KEY = 'theme-preference'

export default async function RootLayout() {
// 2: wrap components with `ThemeProvider` to pass theme props down to all components
// 3: pass `storageKey` and (optional) `defaultTheme` to `ThemeProvider`
// 1: wrap components with `ThemeProvider` to pass theme props down to all components
// 2: optionally pass `storageKey` and `defaultTheme` to `ThemeProvider`
return (
<html>
<body>
<ThemeProvider storageKey={THEME_STORAGE_KEY} defaultTheme={themes.dark}>
<ThemeProvider storageKey="user-pref" defaultTheme={themes.dark.type}>
{children}
</ThemeProvider>
</body>
Expand All @@ -74,6 +73,8 @@ export default async function RootLayout() {

With this setup, the `ThemeProvider` component will automatically inject an inline script into DOM that takes care of avoiding flicker on initial page load.

> **NOTE**: If you don't specify a `storageKey` or `defaultTheme` prop on `ThemeProvider`, default value will be used for `storageKey` while absence of `defaultTheme` would mean that the theme is automatically determined based on `prefers-color-scheme`.
2. Create a button to toggle between light and dark theme:

```jsx
Expand All @@ -90,7 +91,7 @@ export default function ToggleThemeButton() {
}
```

You can also do this manually by using `theme`, `themes`, `colors` and `setTheme()`, for example, like so:
You can also do this manually by using `theme`, `themes`, and `setTheme()`, for example, like so:

```jsx
// components/ToggleThemeButton/index.jsx
Expand All @@ -100,10 +101,10 @@ import React, { useContext } from 'react'
import { useTheme } from '@designcise/next-theme-toggle'

export default function ToggleThemeButton() {
const { theme, themes, colors, setTheme } = useTheme()
const { theme, themes, setTheme } = useTheme()

return (
<button onClick={() => setTheme(theme === themes.dark ? colors.light : colors.dark)}>
<button onClick={() => setTheme(theme.type === themes.dark.type ? themes.light : themes.dark)}>
Toggle Theme
</button>
)
Expand Down Expand Up @@ -176,34 +177,32 @@ That's it! You should have light/dark theme toggle in your Next.js application.

You can pass the following props to `ThemeProvider`:

| Prop | Type | Description |
|----------------|:--------------------------------------------:|:--------------------------------------------------------------------------------------:|
| `children` | `React.ReactChild`&vert;`React.ReactChild[]` | Components to which the theme is passed down to via context. |
| `storageKey` | String | Name of the key used for storage. |
| `defaultTheme` | String | Default theme (`'light'`, `'dark'` or `auto`) to use on page load. Defaults to `auto`. |
| Prop | Type | Description |
|----------------|:--------------------------------------------:|:---------------------------------------------------------------------------------------:|
| `children` | `React.ReactChild`&vert;`React.ReactChild[]` | Components to which the theme is passed down to via context. |
| `storageKey` | String | Name of the key used for storage. Defaults to `DEFAULT_STORAGE_KEY` in `env.helper.ts`. |
| `defaultTheme` | String | Default theme (`'light'`, `'dark'` or `auto`) to use on page load. Defaults to `auto`. |

### `useTheme()`

The `useTheme()` hook does not take any params; it returns the following:

| Return Value | Type | Description |
|---------------|:--------:|:--------------------------------------------------------------------------------------------------------------------------------------------------------:|
| `theme` | String | The active theme (`'light'`, `'dark'` or `'auto'`). |
| `themes` | Object | Allowed themes (`{ light: 'light', dark: 'dark', auto: 'auto' }`). |
| `color` | String | The active color (`light` or `dark`). |
| `colors` | Object | Allowed colors (`{ light: 'light', dark: 'dark', auto: 'dark'&vert;'light' }`). `colors.auto` returns `dark` or `light` based on `prefers-color-scheme`. |
| `setTheme` | Function | Setter to set new theme. |
| `toggleTheme` | Function | Toggles the theme between `light` and `dark`. When toggling from `auto`, the opposite color to active color is automatically chosen. |
| Return Value | Type | Description |
|---------------|:--------:|:------------------------------------------------------------------------------------------------------------------------------------:|
| `theme` | Object | The active theme (e.g. `{ type: 'light', color: 'light' }`). |
| `themes` | Object | Allowed themes (e.g. `{ light: { type: 'light', color: 'light' }, dark: ..., auto: ... }`). |
| `setTheme` | Function | Setter to set new theme. |
| `toggleTheme` | Function | Toggles the theme between `light` and `dark`. When toggling from `auto`, the opposite color to active color is automatically chosen. |

### `themes`

An object, with the following properties:

| Property | Type | Value | Description |
|----------|:------:|:---------:|:------------------------------------------------------------:|
| `light` | String | `'light'` | Color value used for "light" theme. |
| `dark` | String | `'dark'`. | Color value used for "dark" theme. |
| `auto` | String | `'auto'`. | Auto-determine color scheme based on `prefers-color-scheme`. |
| Property | Type | Value | Description |
|----------|:------:|:------------------------------------------------:|:-----------------------------------------------------------:|
| `light` | Object | `{ type: 'light', color: 'light' }` | Light theme. |
| `dark` | Object | `{ type: 'dark', color: 'dark' }` | Dark theme. |
| `auto` | Object | `{ type: 'auto', color: 'dark' &vert; 'light' }` | Auto-determine theme color based on `prefers-color-scheme`. |

> **NOTE**: The `themes` object can be used in both, client components and server components.
Expand Down Expand Up @@ -244,32 +243,6 @@ $ yarn test

### Troubleshooting Common Issues

#### Tailwind not updating dark mode styling

This can happen when you have your CSS or SASS file in a sub-folder that is not listed in `content` array in the `tailwind.config.js`:

```js
// ...
content: [
'./pages/**/*.{js,jsx}',
'./components/**/*.{js,jsx}',
'./app/**/*.{js,jsx}',
'./src/**/*.{js,jsx}',
],
// ...
```

To fix this, you can add the folder where your CSS or SASS file is located. For example:

```js
// ...
content: [
// ...
'./src/styles/**/*.css',
],
// ...
```

#### `Warning: Extra attributes from the server: class,style` in Console

You can safely ignore this warning as it _only_ shows on dev build and _not_ in the production build. This happens because the injected inline script adds _additional_ `class` and `style` attributes to the `html` element which _do not_ originally exist on the server-side generated page, leading to a _mismatch_ in the server-side and client-side rendered page.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React from 'react'
// @ts-nocheck
import { render, screen, fireEvent } from '@testing-library/react'
import { ThemeProvider } from '../src/client'
import { mockLocalStorage, mockMatchMedia, mockPreferredColorScheme } from './mocks/device.mock'
import { read, write, clear } from '../src/adapter/storage.adapter'
import ThemeAutoToggle from './assets/ThemeAutoToggle'
import ThemeManualToggle from './assets/ThemeManualToggle'
import ThemeSwitcher from './assets/ThemeSwitcher'
import { DEFAULT_STORAGE_KEY } from '../src/helper/env.helper'

beforeAll(() => {
mockLocalStorage()
Expand All @@ -18,7 +19,7 @@ beforeEach(() => {
document.documentElement.removeAttribute('class')
})

describe('provider', () => {
describe('ThemeProvider', () => {
test('should set storage key according to the specified value', () => {
const storageKey = 'theme-test'
const expectedTheme = 'light'
Expand All @@ -32,9 +33,21 @@ describe('provider', () => {
expect(read(storageKey)).toEqual(expectedTheme)
})

test('should use default storage key when none is specified value', () => {
const expectedThemeType = 'light'

render(
<ThemeProvider defaultTheme={expectedThemeType}>
<ThemeAutoToggle />
</ThemeProvider>,
)

expect(read(DEFAULT_STORAGE_KEY)).toEqual(expectedThemeType)
})

test.each(['light', 'dark'])(
'should use the `defaultTheme` when nothing is stored in `localStorage`',
(theme) => {
theme => {
const storageKey = 'test'

render(
Expand All @@ -51,7 +64,7 @@ describe('provider', () => {

test.each(['light', 'dark'])(
'should auto-determine theme color when nothing is stored in `localStorage` and `defaultTheme` is set to "auto"',
(color) => {
color => {
const storageKey = 'test'
mockPreferredColorScheme(color)

Expand All @@ -69,7 +82,7 @@ describe('provider', () => {

test.each(['light', 'dark'])(
'should set `color-scheme` and `class` to "%s" theme color according to saved theme preference',
(color) => {
color => {
const storageKey = 'test'
write(storageKey, color)

Expand All @@ -86,7 +99,7 @@ describe('provider', () => {

test.each(['light', 'dark', 'auto'])(
'should use system resolved "%s" color and "auto" theme when no `defaultTheme` is provided and nothing is stored in `localStorage`',
(color) => {
color => {
const storageKey = 'sys-resolved-theme'
const prefColor = color === 'auto' ? 'dark' : color

Expand All @@ -106,7 +119,7 @@ describe('provider', () => {

test.each(['light', 'dark'])(
'should set theme color automatically based on user system preference',
(sysPrefColor) => {
sysPrefColor => {
const storageKey = 'sys-resolved-theme'
mockPreferredColorScheme(sysPrefColor)

Expand Down Expand Up @@ -183,7 +196,7 @@ describe('provider', () => {
},
)

test.each(['light', 'dark'])('should switch from "auto" to "%s"', (theme) => {
test.each(['light', 'dark'])('should switch from "auto" to "%s"', theme => {
const storageKey = 'sys-resolved-theme'
const oppositeTheme = theme === 'dark' ? 'light' : 'dark'
mockPreferredColorScheme(oppositeTheme)
Expand All @@ -205,7 +218,7 @@ describe('provider', () => {
expect(document.documentElement.style.colorScheme).toBe(theme)
})

test.each(['light', 'dark'])('should switch from "%s" to "auto"', (theme) => {
test.each(['light', 'dark'])('should switch from "%s" to "auto"', theme => {
const storageKey = 'sys-resolved-theme'
const oppositeTheme = theme === 'dark' ? 'light' : 'dark'
mockPreferredColorScheme(oppositeTheme)
Expand All @@ -231,7 +244,7 @@ describe('provider', () => {
const storageKey = 'sys-resolved-theme'

render(
<ThemeProvider storageKey={storageKey} theme="auto">
<ThemeProvider storageKey={storageKey} defaultTheme="auto">
<ThemeSwitcher />
</ThemeProvider>,
)
Expand Down
13 changes: 0 additions & 13 deletions __tests__/assets/ThemeAutoColor.jsx

This file was deleted.

12 changes: 12 additions & 0 deletions __tests__/assets/ThemeAutoColor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useTheme } from '../../src/client'

export default function ThemeAutoColor() {
const { theme, themes } = useTheme()

return (
<>
<div>Active Theme: {theme.type}</div>
<div>Auto-determined Color: {themes.auto.color}</div>
</>
)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use client'

import React from 'react'
import { useTheme } from '../../src/client'

export default function ToggleThemeButton() {
Expand Down
14 changes: 0 additions & 14 deletions __tests__/assets/ThemeManualToggle.jsx

This file was deleted.

13 changes: 13 additions & 0 deletions __tests__/assets/ThemeManualToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
'use client'

import { useTheme } from '../../src/client'

export default function ToggleThemeButton() {
const { theme, themes, setTheme } = useTheme()

return (
<button onClick={() => setTheme(theme.type === themes.dark.type ? themes.light : themes.dark)}>
Toggle Theme
</button>
)
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import React from 'react'
import { useTheme } from '../../src/client'

export default function ThemeSwitcher() {
const { theme, themes, color, setTheme } = useTheme()
const { theme, themes, setTheme } = useTheme()

return (
<>
<div>Active Theme: {theme}</div>
<div>Active Color: {color}</div>
<div>Active Theme: {theme.type}</div>
<div>Active Color: {theme.color}</div>
<button onClick={() => setTheme(themes.light)}>Light Theme</button>
<button onClick={() => setTheme(themes.dark)}>Dark Theme</button>
<button onClick={() => setTheme(themes.auto)}>Auto Theme</button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// @ts-nocheck
export function mockMatchMedia() {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
Expand All @@ -14,11 +15,11 @@ export function mockMatchMedia() {
})
}

export function mockPreferredColorScheme(theme, options = {}) {
export function mockPreferredColorScheme(color, options = {}) {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: theme === 'dark',
value: jest.fn().mockImplementation(query => ({
matches: color === 'dark',
media: query,
onchange: null,
addEventListener: jest.fn(),
Expand Down
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit 8431d01

Please sign in to comment.