Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(hooks): use-theme hook #3169

Merged
merged 27 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0348b44
feat(docs): update dark mode content
wingkwong Jun 3, 2024
2095ebd
feat(hooks): @nextui-org/use-theme
wingkwong Jun 3, 2024
bf3cf14
chore(docs): revise ThemeSwitcher code
wingkwong Jun 3, 2024
81ca59c
refactor(hooks): simplify useTheme and support custom theme names
wingkwong Jun 3, 2024
fb3203a
feat(hooks): add use-theme test cases
wingkwong Jun 4, 2024
aab2644
feat(changeset): add changeset
wingkwong Jun 4, 2024
f91afbf
refactor(hooks): make localStorageMock globally and clear before each…
wingkwong Jun 4, 2024
d2cfb1a
fix(docs): typo
wingkwong Jun 4, 2024
2d1c358
fix(hooks): coderabbitai comments
wingkwong Jun 4, 2024
dfb8700
chore(hooks): remove unnecessary +
wingkwong Jun 14, 2024
4bdca1a
Merge branch 'canary' into feat/eng-855
wingkwong Sep 18, 2024
a0e81ea
chore(changeset): change to minor
wingkwong Sep 18, 2024
da61263
feat(hooks): handle system theme
wingkwong Sep 18, 2024
c2df1c9
Merge branch 'canary' into feat/eng-855
wingkwong Sep 28, 2024
615bd87
Merge branch 'canary' into feat/eng-855
wingkwong Oct 3, 2024
b84afaa
chore(hooks): add EOL
wingkwong Oct 3, 2024
ea2e293
refactor(hooks): add default theme
wingkwong Oct 3, 2024
6c5573e
refactor(hooks): revise useTheme
wingkwong Oct 3, 2024
1723387
refactor(hooks): resolve pr comments
wingkwong Oct 3, 2024
fdc1f92
refactor(hooks): resolve pr comments
wingkwong Oct 3, 2024
a87ad5f
refactor(hooks): resolve pr comments
wingkwong Oct 3, 2024
b40cbf8
refactor(hooks): remove unused theme in dependency array
wingkwong Oct 3, 2024
08b5044
Merge branch 'canary' into feat/eng-855
wingkwong Oct 6, 2024
721f279
chore(docs): typos
wingkwong Oct 6, 2024
b7db3d3
refactor(hooks): mark system as key for system theme
wingkwong Oct 9, 2024
4a96555
Merge branch 'canary' of github.com:nextui-org/nextui into feat/eng-855
jrgarciadev Nov 4, 2024
dcb81a4
chore: merged with canary
jrgarciadev Nov 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/light-needles-behave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nextui-org/use-theme": minor
---

introduce `use-theme` hook
3 changes: 2 additions & 1 deletion apps/docs/config/routes.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@
{
"key": "dark-mode",
"title": "Dark mode",
"path": "/docs/customization/dark-mode.mdx"
"path": "/docs/customization/dark-mode.mdx",
"updated": true
},
{
"key": "override-styles",
Expand Down
37 changes: 17 additions & 20 deletions apps/docs/content/docs/customization/dark-mode.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -191,24 +191,22 @@ export const ThemeSwitcher = () => {
</Steps>


## Using use-dark-mode hook
## Using use-theme hook

In case you're using plain React with [Vite](/docs/frameworks/vite) or [Create React App](https://create-react-app.dev/)
you can use the [use-dark-mode](https://github.com/donavon/use-dark-mode) hook to switch between themes.
you can use the [@nextui-org/use-theme](https://github.com/nextui-org/nextui/tree/canary/packages/hooks/use-theme) hook to switch between themes.

> See the [use-dark-mode](https://github.com/donavon/use-dark-mode) documentation for more details.
<Steps>

<Steps>

### Install use-dark-mode
### Install @nextui-org/use-theme

Install `use-dark-mode` in your project.
Install `@nextui-org/use-theme` in your project.

<PackageManagers
commands={{
npm: 'npm install use-dark-mode',
yarn: 'yarn add use-dark-mode',
pnpm: 'pnpm add use-dark-mode',
npm: 'npm install @nextui-org/use-theme',
yarn: 'yarn add @nextui-org/use-theme',
pnpm: 'pnpm add @nextui-org/use-theme',
}}
/>

Expand All @@ -217,13 +215,13 @@ Install `use-dark-mode` in your project.
```jsx
// App.tsx or App.jsx
import React from "react";
import useDarkMode from "use-dark-mode";
import {useTheme} from "@nextui-org/use-theme";

export default function App() {
const darkMode = useDarkMode(false);
const {theme} = useTheme();

return (
<main className={`${darkMode.value ? 'dark' : ''} text-foreground bg-background`}>
<main className={`${theme} text-foreground bg-background`}>
<App />
</main>
)
Expand All @@ -238,23 +236,22 @@ Add the theme switcher to your app.
// 'use client'; // uncomment this line if you're using Next.js App Directory Setup

// components/ThemeSwitcher.tsx
import useDarkMode from "use-dark-mode";
import {useTheme} from "@nextui-org/use-theme";

export const ThemeSwitcher = () => {
const darkMode = useDarkMode(false);
const { theme, setTheme } = useTheme()

return (
<div>
<button onClick={darkMode.disable}>Light Mode</button>
<button onClick={darkMode.enable}>Dark Mode</button>
The current theme is: {theme}
<button onClick={() => setTheme('light')}>Light Mode</button>
<button onClick={() => setTheme('dark')}>Dark Mode</button>
</div>
)
};
```



> **Note**: You can use any theme name you want, but make sure it exits in your
> **Note**: You can use any theme name you want, but make sure it exists in your
`tailwind.config.js` file. See [Create Theme](/docs/customization/create-theme) for more details.


Expand Down
55 changes: 55 additions & 0 deletions packages/hooks/use-theme/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# @nextui-org/use-theme

React hook to switch between light and dark themes

## Installation

```sh
yarn add @nextui-org/use-theme
# or
npm i @nextui-org/use-theme
```

## Usage

Import `useTheme`

```tsx
import {useTheme} from "@nextui-org/use-theme";
```

### theme

```tsx
// `theme` is the active theme name
// by default, it will use the one in localStorage.
// if it is no such value in localStorage, `light` theme will be used
const {theme} = useTheme();
```

### setTheme

You can use any theme name you want, but make sure it exists in your
`tailwind.config.js` file. See [Create Theme](https://nextui.org/docs/customization/create-theme) for more details.

```tsx
// set `theme` by using `setTheme`
const {setTheme} = useTheme();
// setting to light theme
setTheme('light')
// setting to dark theme
setTheme('dark')
// setting to purple-dark theme
setTheme('purple-dark')
```

## Contribution

Yes please! See the
[contributing guidelines](https://github.com/nextui-org/nextui/blob/master/CONTRIBUTING.md)
for details.

## License

This project is licensed under the terms of the
[MIT license](https://github.com/nextui-org/nextui/blob/master/LICENSE).
147 changes: 147 additions & 0 deletions packages/hooks/use-theme/__tests__/use-theme.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import * as React from "react";
import {render, act} from "@testing-library/react";

import {useTheme, ThemeProps, Theme} from "../src";

const TestComponent = ({defaultTheme}: {defaultTheme?: Theme}) => {
const {theme, setTheme} = useTheme(defaultTheme);

return (
<div>
<span data-testid="theme-display">{theme}</span>
<button type="button" onClick={() => setTheme(ThemeProps.DARK)}>
Set Dark
</button>
<button type="button" onClick={() => setTheme(ThemeProps.LIGHT)}>
Set Light
</button>
<button type="button" onClick={() => setTheme(ThemeProps.SYSTEM)}>
Set System
</button>
</div>
);
};

TestComponent.displayName = "TestComponent";

const localStorageMock = (() => {
let store: {[key: string]: string} = {};

return {
getItem: (key: string) => store[key] || null,
setItem: (key: string, value: string) => {
store[key] = value;
},
clear: () => {
store = {};
},
};
})();

Object.defineProperty(window, "localStorage", {
value: localStorageMock,
});

describe("useTheme hook", () => {
beforeEach(() => {
jest.clearAllMocks();

localStorage.clear();

document.documentElement.className = "";
});

it("should initialize with default theme if no theme is stored in localStorage", () => {
const {getByTestId} = render(<TestComponent />);

expect(getByTestId("theme-display").textContent).toBe(ThemeProps.LIGHT);
expect(document.documentElement.classList.contains(ThemeProps.LIGHT)).toBe(true);
});

it("should initialize with the given theme if no theme is stored in localStorage", () => {
const customTheme = "purple-dark";
const {getByTestId} = render(<TestComponent defaultTheme={customTheme} />);

expect(getByTestId("theme-display").textContent).toBe(customTheme);
expect(document.documentElement.classList.contains(customTheme)).toBe(true);
});

it("should initialize with stored theme from localStorage", () => {
localStorage.setItem(ThemeProps.KEY, ThemeProps.DARK);

const {getByTestId} = render(<TestComponent />);

expect(localStorage.getItem(ThemeProps.KEY)).toBe(ThemeProps.DARK);

expect(getByTestId("theme-display").textContent).toBe(ThemeProps.DARK);
expect(document.documentElement.classList.contains(ThemeProps.DARK)).toBe(true);
});

it("should set new theme correctly and update localStorage and DOM (dark)", () => {
const {getByText, getByTestId} = render(<TestComponent />);

act(() => {
getByText("Set Dark").click();
});
expect(getByTestId("theme-display").textContent).toBe(ThemeProps.DARK);
expect(localStorage.getItem(ThemeProps.KEY)).toBe(ThemeProps.DARK);
expect(document.documentElement.classList.contains(ThemeProps.DARK)).toBe(true);
});

it("should set new theme correctly and update localStorage and DOM (light)", () => {
const {getByText, getByTestId} = render(<TestComponent />);

act(() => {
getByText("Set Light").click();
});
expect(getByTestId("theme-display").textContent).toBe(ThemeProps.LIGHT);
expect(localStorage.getItem(ThemeProps.KEY)).toBe(ThemeProps.LIGHT);
expect(document.documentElement.classList.contains(ThemeProps.LIGHT)).toBe(true);
});

it("should set new theme correctly and update localStorage and DOM (system - prefers-color-scheme: light)", () => {
const {getByText, getByTestId} = render(<TestComponent />);

Object.defineProperty(window, "matchMedia", {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});

act(() => {
getByText("Set System").click();
});
expect(getByTestId("theme-display").textContent).toBe(ThemeProps.SYSTEM);
expect(localStorage.getItem(ThemeProps.KEY)).toBe(ThemeProps.SYSTEM);
expect(document.documentElement.classList.contains(ThemeProps.LIGHT)).toBe(true);
});

it("should set new theme correctly and update localStorage and DOM (system - prefers-color-scheme: dark)", () => {
const {getByText, getByTestId} = render(<TestComponent />);

Object.defineProperty(window, "matchMedia", {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: true,
media: query,
onchange: null,
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});

act(() => {
getByText("Set System").click();
});
expect(getByTestId("theme-display").textContent).toBe(ThemeProps.SYSTEM);
expect(localStorage.getItem(ThemeProps.KEY)).toBe(ThemeProps.SYSTEM);
expect(document.documentElement.classList.contains(ThemeProps.DARK)).toBe(true);
});
});
52 changes: 52 additions & 0 deletions packages/hooks/use-theme/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"name": "@nextui-org/use-theme",
"version": "2.0.0",
"description": "React hook to switch between light and dark themes",
"keywords": [
"use-theme"
],
"author": "WK Wong <wingkwong.code@gmail.com>",
"homepage": "https://nextui.org",
"license": "MIT",
"main": "src/index.ts",
"sideEffects": false,
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/nextui-org/nextui.git",
"directory": "packages/hooks/use-theme"
},
"bugs": {
"url": "https://github.com/nextui-org/nextui/issues"
},
"scripts": {
"build": "tsup src --dts",
"build:fast": "tsup src",
"dev": "pnpm build:fast --watch",
"clean": "rimraf dist .turbo",
"typecheck": "tsc --noEmit",
"prepack": "clean-package",
"postpack": "clean-package restore"
},
"peerDependencies": {
"react": ">=18"
},
"devDependencies": {
"clean-package": "2.2.0",
"react": "^18.0.0"
},
"clean-package": "../../../clean-package.config.json",
"tsup": {
"clean": true,
"target": "es2019",
"format": [
"cjs",
"esm"
]
}
}
Loading