@dryanovski/react-native-components 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/module/providers/README.md +22 -0
- package/lib/module/providers/ThemeProvider.js +59 -4
- package/lib/module/providers/ThemeProvider.js.map +1 -1
- package/lib/module/providers/ThemeProvider.test.js +384 -0
- package/lib/module/providers/ThemeProvider.test.js.map +1 -0
- package/lib/typescript/src/providers/ThemeProvider.d.ts +23 -1
- package/lib/typescript/src/providers/ThemeProvider.d.ts.map +1 -1
- package/lib/typescript/src/providers/ThemeProvider.test.d.ts +2 -0
- package/lib/typescript/src/providers/ThemeProvider.test.d.ts.map +1 -0
- package/lib/typescript/src/providers/types.d.ts +17 -1
- package/lib/typescript/src/providers/types.d.ts.map +1 -1
- package/package.json +19 -16
- package/src/providers/README.md +22 -0
- package/src/providers/ThemeProvider.test.tsx +340 -0
- package/src/providers/ThemeProvider.tsx +73 -4
- package/src/providers/types.ts +23 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# ThemeProvider
|
|
2
|
+
|
|
3
|
+
Design to manage and provide theming capabilities to your application.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { ThemeProvider } from '@dryanovski/react-native-components';
|
|
9
|
+
|
|
10
|
+
const App = () => {
|
|
11
|
+
return (
|
|
12
|
+
<ThemeProvider theme="dark">
|
|
13
|
+
<YourAppComponents />
|
|
14
|
+
</ThemeProvider>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Props
|
|
20
|
+
|
|
21
|
+
- `theme`: A string representing the theme to be applied (e.g., "light", "dark").
|
|
22
|
+
- `children`: The child components that will have access to the theme context.
|
|
@@ -1,11 +1,66 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import React from 'react';
|
|
4
|
-
import {
|
|
3
|
+
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
|
4
|
+
import { useColorScheme as useDeviceColorScheme } from 'react-native';
|
|
5
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
6
|
+
/**
|
|
7
|
+
* Create a ThemeContext with default value null
|
|
8
|
+
* @type {React.Context<ThemeContextType | null>}
|
|
9
|
+
*/
|
|
10
|
+
const ThemeContext = /*#__PURE__*/React.createContext(null);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Give access to theme context
|
|
14
|
+
* @returns {ThemeContextType} The current theme context
|
|
15
|
+
* @throws Will throw an error if used outside of ThemeProvider
|
|
16
|
+
* @example
|
|
17
|
+
* const { theme, changeTheme } = useTheme();
|
|
18
|
+
* console.log(theme); // 'light' | 'dark' | 'system'
|
|
19
|
+
*/
|
|
20
|
+
export const useTheme = () => {
|
|
21
|
+
const context = useContext(ThemeContext);
|
|
22
|
+
if (!context) {
|
|
23
|
+
throw new Error('useTheme must be used within a ThemeProvider');
|
|
24
|
+
}
|
|
25
|
+
return context;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* ThemeProvider component to manage and provide theme context
|
|
30
|
+
* @param {React.ReactNode} children - The child components
|
|
31
|
+
* @param {ThemeVariants} theme - The current theme ('light', 'dark', 'system')
|
|
32
|
+
* @param {ThemeVariants} defaultTheme - The default theme if none is provided
|
|
33
|
+
* @param {(theme: ThemeVariants) => void} onThemeChange - Callback when theme changes
|
|
34
|
+
* @returns {JSX.Element} The ThemeProvider component
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* <ThemeProvider theme="dark" onThemeChange={(theme) => console.log(theme)}>
|
|
38
|
+
* <App />
|
|
39
|
+
* </ThemeProvider>
|
|
40
|
+
*/
|
|
5
41
|
export const ThemeProvider = ({
|
|
6
|
-
children
|
|
42
|
+
children,
|
|
43
|
+
theme,
|
|
44
|
+
defaultTheme = 'light',
|
|
45
|
+
onThemeChange = () => undefined
|
|
7
46
|
}) => {
|
|
8
|
-
|
|
47
|
+
const deviceColorscheme = useDeviceColorScheme();
|
|
48
|
+
const [currentTheme, setTheme] = useState(theme || defaultTheme);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
onThemeChange && theme && onThemeChange(theme);
|
|
51
|
+
}, [theme, onThemeChange]);
|
|
52
|
+
const changeTheme = useCallback(newTheme => {
|
|
53
|
+
if (newTheme === 'system') {
|
|
54
|
+
setTheme(deviceColorscheme === 'dark' ? 'dark' : 'light');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
setTheme(newTheme);
|
|
58
|
+
}, [deviceColorscheme]);
|
|
59
|
+
return /*#__PURE__*/_jsx(ThemeContext.Provider, {
|
|
60
|
+
value: {
|
|
61
|
+
theme: currentTheme,
|
|
62
|
+
changeTheme
|
|
63
|
+
},
|
|
9
64
|
children: children
|
|
10
65
|
});
|
|
11
66
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["React","
|
|
1
|
+
{"version":3,"names":["React","useCallback","useContext","useEffect","useState","useColorScheme","useDeviceColorScheme","jsx","_jsx","ThemeContext","createContext","useTheme","context","Error","ThemeProvider","children","theme","defaultTheme","onThemeChange","undefined","deviceColorscheme","currentTheme","setTheme","changeTheme","newTheme","Provider","value"],"sourceRoot":"../../../src","sources":["providers/ThemeProvider.tsx"],"mappings":";;AAAA,OAAOA,KAAK,IAAIC,WAAW,EAAEC,UAAU,EAAEC,SAAS,EAAEC,QAAQ,QAAQ,OAAO;AAC3E,SAASC,cAAc,IAAIC,oBAAoB,QAAQ,cAAc;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAOtE;AACA;AACA;AACA;AACA,MAAMC,YAAY,gBAAGT,KAAK,CAACU,aAAa,CAA0B,IAAI,CAAC;;AAEvE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMC,QAAQ,GAAGA,CAAA,KAAM;EAC5B,MAAMC,OAAO,GAAGV,UAAU,CAACO,YAAY,CAAC;EACxC,IAAI,CAACG,OAAO,EAAE;IACZ,MAAM,IAAIC,KAAK,CAAC,8CAA8C,CAAC;EACjE;EACA,OAAOD,OAAO;AAChB,CAAC;;AAED;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAME,aAA2C,GAAGA,CAAC;EAC1DC,QAAQ;EACRC,KAAK;EACLC,YAAY,GAAG,OAAO;EACtBC,aAAa,GAAGA,CAAA,KAAMC;AACxB,CAAC,KAAK;EACJ,MAAMC,iBAAiB,GAAGd,oBAAoB,CAAC,CAAC;EAChD,MAAM,CAACe,YAAY,EAAEC,QAAQ,CAAC,GAAGlB,QAAQ,CACvCY,KAAK,IAAIC,YACX,CAAC;EAEDd,SAAS,CAAC,MAAM;IACde,aAAa,IAAIF,KAAK,IAAIE,aAAa,CAACF,KAAK,CAAC;EAChD,CAAC,EAAE,CAACA,KAAK,EAAEE,aAAa,CAAC,CAAC;EAE1B,MAAMK,WAAW,GAAGtB,WAAW,CAC5BuB,QAAuB,IAAK;IAC3B,IAAIA,QAAQ,KAAK,QAAQ,EAAE;MACzBF,QAAQ,CAACF,iBAAiB,KAAK,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;MACzD;IACF;IACAE,QAAQ,CAACE,QAAQ,CAAC;EACpB,CAAC,EACD,CAACJ,iBAAiB,CACpB,CAAC;EAED,oBACEZ,IAAA,CAACC,YAAY,CAACgB,QAAQ;IAACC,KAAK,EAAE;MAAEV,KAAK,EAAEK,YAAY;MAAEE;IAAY,CAAE;IAAAR,QAAA,EAChEA;EAAQ,CACY,CAAC;AAE5B,CAAC;AAED,eAAeD,aAAa","ignoreList":[]}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { act, renderHook, waitFor } from '@testing-library/react-native';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { useColorScheme as useDeviceColorScheme } from 'react-native';
|
|
6
|
+
import { ThemeProvider, useTheme } from "./ThemeProvider.js";
|
|
7
|
+
|
|
8
|
+
// Mock react-native's useColorScheme hook
|
|
9
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
10
|
+
jest.mock('react-native', () => ({
|
|
11
|
+
useColorScheme: jest.fn()
|
|
12
|
+
}));
|
|
13
|
+
const mockedUseDeviceColorScheme = useDeviceColorScheme;
|
|
14
|
+
describe('ThemeProvider', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
jest.clearAllMocks();
|
|
17
|
+
mockedUseDeviceColorScheme.mockReturnValue('light');
|
|
18
|
+
});
|
|
19
|
+
describe('useTheme hook', () => {
|
|
20
|
+
it('should throw error when used outside ThemeProvider', () => {
|
|
21
|
+
// Suppress console.error for this test
|
|
22
|
+
const originalError = console.error;
|
|
23
|
+
console.error = jest.fn();
|
|
24
|
+
expect(() => {
|
|
25
|
+
renderHook(() => useTheme());
|
|
26
|
+
}).toThrow('useTheme must be used within a ThemeProvider');
|
|
27
|
+
console.error = originalError;
|
|
28
|
+
});
|
|
29
|
+
it('should return theme context when used inside ThemeProvider', () => {
|
|
30
|
+
const wrapper = ({
|
|
31
|
+
children
|
|
32
|
+
}) => /*#__PURE__*/_jsx(ThemeProvider, {
|
|
33
|
+
children: children
|
|
34
|
+
});
|
|
35
|
+
const {
|
|
36
|
+
result
|
|
37
|
+
} = renderHook(() => useTheme(), {
|
|
38
|
+
wrapper
|
|
39
|
+
});
|
|
40
|
+
expect(result.current).toHaveProperty('theme');
|
|
41
|
+
expect(result.current).toHaveProperty('changeTheme');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
describe('ThemeProvider component', () => {
|
|
45
|
+
it('should use defaultTheme when no theme prop is provided and device is in light mode', () => {
|
|
46
|
+
mockedUseDeviceColorScheme.mockReturnValue('light');
|
|
47
|
+
const wrapper = ({
|
|
48
|
+
children
|
|
49
|
+
}) => /*#__PURE__*/_jsx(ThemeProvider, {
|
|
50
|
+
defaultTheme: "light",
|
|
51
|
+
children: children
|
|
52
|
+
});
|
|
53
|
+
const {
|
|
54
|
+
result
|
|
55
|
+
} = renderHook(() => useTheme(), {
|
|
56
|
+
wrapper
|
|
57
|
+
});
|
|
58
|
+
expect(result.current.theme).toBe('light');
|
|
59
|
+
});
|
|
60
|
+
it('should use defaultTheme even when device is in dark mode', () => {
|
|
61
|
+
mockedUseDeviceColorScheme.mockReturnValue('dark');
|
|
62
|
+
const wrapper = ({
|
|
63
|
+
children
|
|
64
|
+
}) => /*#__PURE__*/_jsx(ThemeProvider, {
|
|
65
|
+
children: children
|
|
66
|
+
});
|
|
67
|
+
const {
|
|
68
|
+
result
|
|
69
|
+
} = renderHook(() => useTheme(), {
|
|
70
|
+
wrapper
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// defaultTheme is 'light' by default, so it should use 'light' even though device is 'dark'
|
|
74
|
+
expect(result.current.theme).toBe('light');
|
|
75
|
+
});
|
|
76
|
+
it('should use provided theme prop over device color scheme', () => {
|
|
77
|
+
mockedUseDeviceColorScheme.mockReturnValue('dark');
|
|
78
|
+
const wrapper = ({
|
|
79
|
+
children
|
|
80
|
+
}) => /*#__PURE__*/_jsx(ThemeProvider, {
|
|
81
|
+
theme: "light",
|
|
82
|
+
children: children
|
|
83
|
+
});
|
|
84
|
+
const {
|
|
85
|
+
result
|
|
86
|
+
} = renderHook(() => useTheme(), {
|
|
87
|
+
wrapper
|
|
88
|
+
});
|
|
89
|
+
expect(result.current.theme).toBe('light');
|
|
90
|
+
});
|
|
91
|
+
it('should use custom defaultTheme when provided', () => {
|
|
92
|
+
mockedUseDeviceColorScheme.mockReturnValue('light');
|
|
93
|
+
const wrapper = ({
|
|
94
|
+
children
|
|
95
|
+
}) => /*#__PURE__*/_jsx(ThemeProvider, {
|
|
96
|
+
defaultTheme: "dark",
|
|
97
|
+
children: children
|
|
98
|
+
});
|
|
99
|
+
const {
|
|
100
|
+
result
|
|
101
|
+
} = renderHook(() => useTheme(), {
|
|
102
|
+
wrapper
|
|
103
|
+
});
|
|
104
|
+
expect(result.current.theme).toBe('dark');
|
|
105
|
+
});
|
|
106
|
+
it('should call onThemeChange when theme prop is provided', async () => {
|
|
107
|
+
const onThemeChange = jest.fn();
|
|
108
|
+
const wrapper = ({
|
|
109
|
+
children
|
|
110
|
+
}) => /*#__PURE__*/_jsx(ThemeProvider, {
|
|
111
|
+
theme: "light",
|
|
112
|
+
onThemeChange: onThemeChange,
|
|
113
|
+
children: children
|
|
114
|
+
});
|
|
115
|
+
renderHook(() => useTheme(), {
|
|
116
|
+
wrapper
|
|
117
|
+
});
|
|
118
|
+
await waitFor(() => {
|
|
119
|
+
expect(onThemeChange).toHaveBeenCalledWith('light');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
it('should not call onThemeChange when no theme prop is provided', () => {
|
|
123
|
+
const onThemeChange = jest.fn();
|
|
124
|
+
const wrapper = ({
|
|
125
|
+
children
|
|
126
|
+
}) => /*#__PURE__*/_jsx(ThemeProvider, {
|
|
127
|
+
onThemeChange: onThemeChange,
|
|
128
|
+
children: children
|
|
129
|
+
});
|
|
130
|
+
renderHook(() => useTheme(), {
|
|
131
|
+
wrapper
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// onThemeChange should not be called when theme prop is not provided
|
|
135
|
+
expect(onThemeChange).not.toHaveBeenCalled();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe('changeTheme function', () => {
|
|
139
|
+
it('should change theme to dark', () => {
|
|
140
|
+
mockedUseDeviceColorScheme.mockReturnValue('light');
|
|
141
|
+
const wrapper = ({
|
|
142
|
+
children
|
|
143
|
+
}) => /*#__PURE__*/_jsx(ThemeProvider, {
|
|
144
|
+
defaultTheme: "light",
|
|
145
|
+
children: children
|
|
146
|
+
});
|
|
147
|
+
const {
|
|
148
|
+
result
|
|
149
|
+
} = renderHook(() => useTheme(), {
|
|
150
|
+
wrapper
|
|
151
|
+
});
|
|
152
|
+
expect(result.current.theme).toBe('light');
|
|
153
|
+
act(() => {
|
|
154
|
+
result.current.changeTheme('dark');
|
|
155
|
+
});
|
|
156
|
+
expect(result.current.theme).toBe('dark');
|
|
157
|
+
});
|
|
158
|
+
it('should change theme to light', () => {
|
|
159
|
+
mockedUseDeviceColorScheme.mockReturnValue('dark');
|
|
160
|
+
const wrapper = ({
|
|
161
|
+
children
|
|
162
|
+
}) => /*#__PURE__*/_jsx(ThemeProvider, {
|
|
163
|
+
defaultTheme: "dark",
|
|
164
|
+
children: children
|
|
165
|
+
});
|
|
166
|
+
const {
|
|
167
|
+
result
|
|
168
|
+
} = renderHook(() => useTheme(), {
|
|
169
|
+
wrapper
|
|
170
|
+
});
|
|
171
|
+
expect(result.current.theme).toBe('dark');
|
|
172
|
+
act(() => {
|
|
173
|
+
result.current.changeTheme('light');
|
|
174
|
+
});
|
|
175
|
+
expect(result.current.theme).toBe('light');
|
|
176
|
+
});
|
|
177
|
+
it('should use device color scheme when theme is set to "system"', () => {
|
|
178
|
+
mockedUseDeviceColorScheme.mockReturnValue('dark');
|
|
179
|
+
const wrapper = ({
|
|
180
|
+
children
|
|
181
|
+
}) => /*#__PURE__*/_jsx(ThemeProvider, {
|
|
182
|
+
defaultTheme: "light",
|
|
183
|
+
children: children
|
|
184
|
+
});
|
|
185
|
+
const {
|
|
186
|
+
result
|
|
187
|
+
} = renderHook(() => useTheme(), {
|
|
188
|
+
wrapper
|
|
189
|
+
});
|
|
190
|
+
expect(result.current.theme).toBe('light');
|
|
191
|
+
act(() => {
|
|
192
|
+
result.current.changeTheme('system');
|
|
193
|
+
});
|
|
194
|
+
expect(result.current.theme).toBe('dark');
|
|
195
|
+
});
|
|
196
|
+
it('should switch to light theme when system theme is set and device is in light mode', () => {
|
|
197
|
+
mockedUseDeviceColorScheme.mockReturnValue('light');
|
|
198
|
+
const wrapper = ({
|
|
199
|
+
children
|
|
200
|
+
}) => /*#__PURE__*/_jsx(ThemeProvider, {
|
|
201
|
+
defaultTheme: "dark",
|
|
202
|
+
children: children
|
|
203
|
+
});
|
|
204
|
+
const {
|
|
205
|
+
result
|
|
206
|
+
} = renderHook(() => useTheme(), {
|
|
207
|
+
wrapper
|
|
208
|
+
});
|
|
209
|
+
expect(result.current.theme).toBe('dark');
|
|
210
|
+
act(() => {
|
|
211
|
+
result.current.changeTheme('system');
|
|
212
|
+
});
|
|
213
|
+
expect(result.current.theme).toBe('light');
|
|
214
|
+
});
|
|
215
|
+
it('should handle multiple theme changes', () => {
|
|
216
|
+
mockedUseDeviceColorScheme.mockReturnValue('light');
|
|
217
|
+
const wrapper = ({
|
|
218
|
+
children
|
|
219
|
+
}) => /*#__PURE__*/_jsx(ThemeProvider, {
|
|
220
|
+
defaultTheme: "light",
|
|
221
|
+
children: children
|
|
222
|
+
});
|
|
223
|
+
const {
|
|
224
|
+
result
|
|
225
|
+
} = renderHook(() => useTheme(), {
|
|
226
|
+
wrapper
|
|
227
|
+
});
|
|
228
|
+
expect(result.current.theme).toBe('light');
|
|
229
|
+
act(() => {
|
|
230
|
+
result.current.changeTheme('dark');
|
|
231
|
+
});
|
|
232
|
+
expect(result.current.theme).toBe('dark');
|
|
233
|
+
act(() => {
|
|
234
|
+
result.current.changeTheme('light');
|
|
235
|
+
});
|
|
236
|
+
expect(result.current.theme).toBe('light');
|
|
237
|
+
act(() => {
|
|
238
|
+
result.current.changeTheme('system');
|
|
239
|
+
});
|
|
240
|
+
expect(result.current.theme).toBe('light');
|
|
241
|
+
});
|
|
242
|
+
it('should respect device color scheme when changing to system theme', () => {
|
|
243
|
+
// Mock device as dark initially
|
|
244
|
+
mockedUseDeviceColorScheme.mockReturnValue('dark');
|
|
245
|
+
const wrapper = ({
|
|
246
|
+
children
|
|
247
|
+
}) => /*#__PURE__*/_jsx(ThemeProvider, {
|
|
248
|
+
defaultTheme: "light",
|
|
249
|
+
children: children
|
|
250
|
+
});
|
|
251
|
+
const {
|
|
252
|
+
result
|
|
253
|
+
} = renderHook(() => useTheme(), {
|
|
254
|
+
wrapper
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Initially should be light (from defaultTheme)
|
|
258
|
+
expect(result.current.theme).toBe('light');
|
|
259
|
+
|
|
260
|
+
// Change to system should use device color scheme (dark)
|
|
261
|
+
act(() => {
|
|
262
|
+
result.current.changeTheme('system');
|
|
263
|
+
});
|
|
264
|
+
expect(result.current.theme).toBe('dark');
|
|
265
|
+
});
|
|
266
|
+
it('should update system theme when device color scheme is light', () => {
|
|
267
|
+
// Mock device as light
|
|
268
|
+
mockedUseDeviceColorScheme.mockReturnValue('light');
|
|
269
|
+
const wrapper = ({
|
|
270
|
+
children
|
|
271
|
+
}) => /*#__PURE__*/_jsx(ThemeProvider, {
|
|
272
|
+
defaultTheme: "dark",
|
|
273
|
+
children: children
|
|
274
|
+
});
|
|
275
|
+
const {
|
|
276
|
+
result
|
|
277
|
+
} = renderHook(() => useTheme(), {
|
|
278
|
+
wrapper
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Initially should be dark (from defaultTheme)
|
|
282
|
+
expect(result.current.theme).toBe('dark');
|
|
283
|
+
|
|
284
|
+
// Change to system should use device color scheme (light)
|
|
285
|
+
act(() => {
|
|
286
|
+
result.current.changeTheme('system');
|
|
287
|
+
});
|
|
288
|
+
expect(result.current.theme).toBe('light');
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
describe('edge cases', () => {
|
|
292
|
+
it('should handle undefined device color scheme', () => {
|
|
293
|
+
mockedUseDeviceColorScheme.mockReturnValue(undefined);
|
|
294
|
+
const wrapper = ({
|
|
295
|
+
children
|
|
296
|
+
}) => /*#__PURE__*/_jsx(ThemeProvider, {
|
|
297
|
+
defaultTheme: "light",
|
|
298
|
+
children: children
|
|
299
|
+
});
|
|
300
|
+
const {
|
|
301
|
+
result
|
|
302
|
+
} = renderHook(() => useTheme(), {
|
|
303
|
+
wrapper
|
|
304
|
+
});
|
|
305
|
+
expect(result.current.theme).toBe('light');
|
|
306
|
+
});
|
|
307
|
+
it('should handle null device color scheme', () => {
|
|
308
|
+
mockedUseDeviceColorScheme.mockReturnValue(null);
|
|
309
|
+
const wrapper = ({
|
|
310
|
+
children
|
|
311
|
+
}) => /*#__PURE__*/_jsx(ThemeProvider, {
|
|
312
|
+
defaultTheme: "dark",
|
|
313
|
+
children: children
|
|
314
|
+
});
|
|
315
|
+
const {
|
|
316
|
+
result
|
|
317
|
+
} = renderHook(() => useTheme(), {
|
|
318
|
+
wrapper
|
|
319
|
+
});
|
|
320
|
+
expect(result.current.theme).toBe('dark');
|
|
321
|
+
});
|
|
322
|
+
it('should maintain theme stability with same changeTheme callback reference', () => {
|
|
323
|
+
const wrapper = ({
|
|
324
|
+
children
|
|
325
|
+
}) => /*#__PURE__*/_jsx(ThemeProvider, {
|
|
326
|
+
defaultTheme: "light",
|
|
327
|
+
children: children
|
|
328
|
+
});
|
|
329
|
+
const {
|
|
330
|
+
result,
|
|
331
|
+
rerender
|
|
332
|
+
} = renderHook(() => useTheme(), {
|
|
333
|
+
wrapper
|
|
334
|
+
});
|
|
335
|
+
const initialChangeTheme = result.current.changeTheme;
|
|
336
|
+
rerender({});
|
|
337
|
+
|
|
338
|
+
// changeTheme should maintain reference stability due to useCallback
|
|
339
|
+
expect(result.current.changeTheme).toBe(initialChangeTheme);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
describe('integration tests', () => {
|
|
343
|
+
it('should work with nested providers', () => {
|
|
344
|
+
const wrapper = ({
|
|
345
|
+
children
|
|
346
|
+
}) => /*#__PURE__*/_jsx(ThemeProvider, {
|
|
347
|
+
defaultTheme: "light",
|
|
348
|
+
children: /*#__PURE__*/_jsx(ThemeProvider, {
|
|
349
|
+
defaultTheme: "dark",
|
|
350
|
+
children: children
|
|
351
|
+
})
|
|
352
|
+
});
|
|
353
|
+
const {
|
|
354
|
+
result
|
|
355
|
+
} = renderHook(() => useTheme(), {
|
|
356
|
+
wrapper
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Should use the innermost provider
|
|
360
|
+
expect(result.current.theme).toBe('dark');
|
|
361
|
+
});
|
|
362
|
+
it('should maintain theme state across re-renders', () => {
|
|
363
|
+
const wrapper = ({
|
|
364
|
+
children
|
|
365
|
+
}) => /*#__PURE__*/_jsx(ThemeProvider, {
|
|
366
|
+
defaultTheme: "light",
|
|
367
|
+
children: children
|
|
368
|
+
});
|
|
369
|
+
const {
|
|
370
|
+
result,
|
|
371
|
+
rerender
|
|
372
|
+
} = renderHook(() => useTheme(), {
|
|
373
|
+
wrapper
|
|
374
|
+
});
|
|
375
|
+
act(() => {
|
|
376
|
+
result.current.changeTheme('dark');
|
|
377
|
+
});
|
|
378
|
+
expect(result.current.theme).toBe('dark');
|
|
379
|
+
rerender({});
|
|
380
|
+
expect(result.current.theme).toBe('dark');
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
//# sourceMappingURL=ThemeProvider.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["act","renderHook","waitFor","React","useColorScheme","useDeviceColorScheme","ThemeProvider","useTheme","jsx","_jsx","jest","mock","fn","mockedUseDeviceColorScheme","describe","beforeEach","clearAllMocks","mockReturnValue","it","originalError","console","error","expect","toThrow","wrapper","children","result","current","toHaveProperty","defaultTheme","theme","toBe","onThemeChange","toHaveBeenCalledWith","not","toHaveBeenCalled","changeTheme","undefined","rerender","initialChangeTheme"],"sourceRoot":"../../../src","sources":["providers/ThemeProvider.test.tsx"],"mappings":"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","ignoreList":[]}
|
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import type { ThemeProviderProps } from './types';
|
|
2
|
+
import type { ThemeContextType, ThemeProviderProps } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Give access to theme context
|
|
5
|
+
* @returns {ThemeContextType} The current theme context
|
|
6
|
+
* @throws Will throw an error if used outside of ThemeProvider
|
|
7
|
+
* @example
|
|
8
|
+
* const { theme, changeTheme } = useTheme();
|
|
9
|
+
* console.log(theme); // 'light' | 'dark' | 'system'
|
|
10
|
+
*/
|
|
11
|
+
export declare const useTheme: () => ThemeContextType;
|
|
12
|
+
/**
|
|
13
|
+
* ThemeProvider component to manage and provide theme context
|
|
14
|
+
* @param {React.ReactNode} children - The child components
|
|
15
|
+
* @param {ThemeVariants} theme - The current theme ('light', 'dark', 'system')
|
|
16
|
+
* @param {ThemeVariants} defaultTheme - The default theme if none is provided
|
|
17
|
+
* @param {(theme: ThemeVariants) => void} onThemeChange - Callback when theme changes
|
|
18
|
+
* @returns {JSX.Element} The ThemeProvider component
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* <ThemeProvider theme="dark" onThemeChange={(theme) => console.log(theme)}>
|
|
22
|
+
* <App />
|
|
23
|
+
* </ThemeProvider>
|
|
24
|
+
*/
|
|
3
25
|
export declare const ThemeProvider: React.FC<ThemeProviderProps>;
|
|
4
26
|
export default ThemeProvider;
|
|
5
27
|
//# sourceMappingURL=ThemeProvider.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThemeProvider.d.ts","sourceRoot":"","sources":["../../../../src/providers/ThemeProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"ThemeProvider.d.ts","sourceRoot":"","sources":["../../../../src/providers/ThemeProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAuD,MAAM,OAAO,CAAC;AAE5E,OAAO,KAAK,EACV,gBAAgB,EAChB,kBAAkB,EAEnB,MAAM,SAAS,CAAC;AAQjB;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ,wBAMpB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CA+BtD,CAAC;AAEF,eAAe,aAAa,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ThemeProvider.test.d.ts","sourceRoot":"","sources":["../../../../src/providers/ThemeProvider.test.tsx"],"names":[],"mappings":""}
|
|
@@ -1,5 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme variants type definition
|
|
3
|
+
*/
|
|
4
|
+
export type ThemeVariants = 'light' | 'dark' | 'system';
|
|
5
|
+
/**
|
|
6
|
+
* ThemeProvider component props definition
|
|
7
|
+
*/
|
|
1
8
|
export type ThemeProviderProps = {
|
|
2
9
|
children: React.ReactNode;
|
|
3
|
-
|
|
10
|
+
theme?: ThemeVariants;
|
|
11
|
+
defaultTheme?: ThemeVariants;
|
|
12
|
+
onThemeChange?: (theme: ThemeVariants) => void;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Theme context type definition
|
|
16
|
+
*/
|
|
17
|
+
export type ThemeContextType = {
|
|
18
|
+
theme: ThemeVariants;
|
|
19
|
+
changeTheme: (theme: ThemeVariants) => void;
|
|
4
20
|
};
|
|
5
21
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/providers/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,aAAa,CAAC,EAAE,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/providers/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;AAExD;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,YAAY,CAAC,EAAE,aAAa,CAAC;IAG7B,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;CAChD,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,aAAa,CAAC;IAGrB,WAAW,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;CAC7C,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dryanovski/react-native-components",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Collection of reusable React Native components for building mobile applications.",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|
|
@@ -17,12 +17,6 @@
|
|
|
17
17
|
"src",
|
|
18
18
|
"lib"
|
|
19
19
|
],
|
|
20
|
-
"scripts": {
|
|
21
|
-
"test": "jest",
|
|
22
|
-
"lint": "eslint \"**/*.{js,ts,tsx}\"",
|
|
23
|
-
"clean": "del-cli lib",
|
|
24
|
-
"prepare": "bob build"
|
|
25
|
-
},
|
|
26
20
|
"keywords": [
|
|
27
21
|
"react-native",
|
|
28
22
|
"ios",
|
|
@@ -36,6 +30,7 @@
|
|
|
36
30
|
"@react-native-community/cli": "latest",
|
|
37
31
|
"@react-native/babel-preset": "0.78.2",
|
|
38
32
|
"@react-native/eslint-config": "^0.78.0",
|
|
33
|
+
"@testing-library/react-native": "^13.3.3",
|
|
39
34
|
"@types/jest": "^29.5.5",
|
|
40
35
|
"@types/react": "^19.0.12",
|
|
41
36
|
"babel-plugin-module-resolver": "^5.0.2",
|
|
@@ -56,6 +51,7 @@
|
|
|
56
51
|
"react-native-screens": "~4.16.0",
|
|
57
52
|
"react-native-svg": "15.12.1",
|
|
58
53
|
"react-native-worklets": "0.5.1",
|
|
54
|
+
"react-test-renderer": "19.1.0",
|
|
59
55
|
"typescript": "^5.8.3"
|
|
60
56
|
},
|
|
61
57
|
"peerDependencies": {
|
|
@@ -67,14 +63,6 @@
|
|
|
67
63
|
"react-native-svg": "^15.12.1",
|
|
68
64
|
"react-native-worklets": "0.5.1"
|
|
69
65
|
},
|
|
70
|
-
"jest": {
|
|
71
|
-
"preset": "react-native",
|
|
72
|
-
"passWithNoTests": true,
|
|
73
|
-
"modulePathIgnorePatterns": [
|
|
74
|
-
"<rootDir>/example/node_modules",
|
|
75
|
-
"<rootDir>/lib/"
|
|
76
|
-
]
|
|
77
|
-
},
|
|
78
66
|
"prettier": {
|
|
79
67
|
"quoteProps": "consistent",
|
|
80
68
|
"singleQuote": true,
|
|
@@ -108,5 +96,20 @@
|
|
|
108
96
|
"languages": "js",
|
|
109
97
|
"type": "library",
|
|
110
98
|
"version": "0.51.1"
|
|
99
|
+
},
|
|
100
|
+
"dependencies": {
|
|
101
|
+
"@react-native/eslint-plugin": "^0.82.1",
|
|
102
|
+
"@typescript-eslint/eslint-plugin": "^8.48.1",
|
|
103
|
+
"eslint-plugin-eslint-comments": "^3.2.0",
|
|
104
|
+
"eslint-plugin-ft-flow": "^3.0.11",
|
|
105
|
+
"eslint-plugin-jest": "^29.2.1",
|
|
106
|
+
"eslint-plugin-react": "^7.37.5",
|
|
107
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
108
|
+
"eslint-plugin-react-native": "^5.0.0"
|
|
109
|
+
},
|
|
110
|
+
"scripts": {
|
|
111
|
+
"test": "jest",
|
|
112
|
+
"lint": "eslint \"**/*.{js,ts,tsx}\"",
|
|
113
|
+
"clean": "del-cli lib"
|
|
111
114
|
}
|
|
112
|
-
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# ThemeProvider
|
|
2
|
+
|
|
3
|
+
Design to manage and provide theming capabilities to your application.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { ThemeProvider } from '@dryanovski/react-native-components';
|
|
9
|
+
|
|
10
|
+
const App = () => {
|
|
11
|
+
return (
|
|
12
|
+
<ThemeProvider theme="dark">
|
|
13
|
+
<YourAppComponents />
|
|
14
|
+
</ThemeProvider>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Props
|
|
20
|
+
|
|
21
|
+
- `theme`: A string representing the theme to be applied (e.g., "light", "dark").
|
|
22
|
+
- `children`: The child components that will have access to the theme context.
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { act, renderHook, waitFor } from '@testing-library/react-native';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { useColorScheme as useDeviceColorScheme } from 'react-native';
|
|
4
|
+
import { ThemeProvider, useTheme } from './ThemeProvider';
|
|
5
|
+
|
|
6
|
+
// Mock react-native's useColorScheme hook
|
|
7
|
+
jest.mock('react-native', () => ({
|
|
8
|
+
useColorScheme: jest.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
const mockedUseDeviceColorScheme = useDeviceColorScheme as jest.MockedFunction<
|
|
12
|
+
typeof useDeviceColorScheme
|
|
13
|
+
>;
|
|
14
|
+
|
|
15
|
+
describe('ThemeProvider', () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
jest.clearAllMocks();
|
|
18
|
+
mockedUseDeviceColorScheme.mockReturnValue('light');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('useTheme hook', () => {
|
|
22
|
+
it('should throw error when used outside ThemeProvider', () => {
|
|
23
|
+
// Suppress console.error for this test
|
|
24
|
+
const originalError = console.error;
|
|
25
|
+
console.error = jest.fn();
|
|
26
|
+
|
|
27
|
+
expect(() => {
|
|
28
|
+
renderHook(() => useTheme());
|
|
29
|
+
}).toThrow('useTheme must be used within a ThemeProvider');
|
|
30
|
+
|
|
31
|
+
console.error = originalError;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should return theme context when used inside ThemeProvider', () => {
|
|
35
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
36
|
+
<ThemeProvider>{children}</ThemeProvider>
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const { result } = renderHook(() => useTheme(), { wrapper });
|
|
40
|
+
|
|
41
|
+
expect(result.current).toHaveProperty('theme');
|
|
42
|
+
expect(result.current).toHaveProperty('changeTheme');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('ThemeProvider component', () => {
|
|
47
|
+
it('should use defaultTheme when no theme prop is provided and device is in light mode', () => {
|
|
48
|
+
mockedUseDeviceColorScheme.mockReturnValue('light');
|
|
49
|
+
|
|
50
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
51
|
+
<ThemeProvider defaultTheme="light">{children}</ThemeProvider>
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const { result } = renderHook(() => useTheme(), { wrapper });
|
|
55
|
+
|
|
56
|
+
expect(result.current.theme).toBe('light');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should use defaultTheme even when device is in dark mode', () => {
|
|
60
|
+
mockedUseDeviceColorScheme.mockReturnValue('dark');
|
|
61
|
+
|
|
62
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
63
|
+
<ThemeProvider>{children}</ThemeProvider>
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const { result } = renderHook(() => useTheme(), { wrapper });
|
|
67
|
+
|
|
68
|
+
// defaultTheme is 'light' by default, so it should use 'light' even though device is 'dark'
|
|
69
|
+
expect(result.current.theme).toBe('light');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should use provided theme prop over device color scheme', () => {
|
|
73
|
+
mockedUseDeviceColorScheme.mockReturnValue('dark');
|
|
74
|
+
|
|
75
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
76
|
+
<ThemeProvider theme="light">{children}</ThemeProvider>
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const { result } = renderHook(() => useTheme(), { wrapper });
|
|
80
|
+
|
|
81
|
+
expect(result.current.theme).toBe('light');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should use custom defaultTheme when provided', () => {
|
|
85
|
+
mockedUseDeviceColorScheme.mockReturnValue('light');
|
|
86
|
+
|
|
87
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
88
|
+
<ThemeProvider defaultTheme="dark">{children}</ThemeProvider>
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const { result } = renderHook(() => useTheme(), { wrapper });
|
|
92
|
+
|
|
93
|
+
expect(result.current.theme).toBe('dark');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should call onThemeChange when theme prop is provided', async () => {
|
|
97
|
+
const onThemeChange = jest.fn();
|
|
98
|
+
|
|
99
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
100
|
+
<ThemeProvider theme="light" onThemeChange={onThemeChange}>
|
|
101
|
+
{children}
|
|
102
|
+
</ThemeProvider>
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
renderHook(() => useTheme(), { wrapper });
|
|
106
|
+
|
|
107
|
+
await waitFor(() => {
|
|
108
|
+
expect(onThemeChange).toHaveBeenCalledWith('light');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should not call onThemeChange when no theme prop is provided', () => {
|
|
113
|
+
const onThemeChange = jest.fn();
|
|
114
|
+
|
|
115
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
116
|
+
<ThemeProvider onThemeChange={onThemeChange}>{children}</ThemeProvider>
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
renderHook(() => useTheme(), { wrapper });
|
|
120
|
+
|
|
121
|
+
// onThemeChange should not be called when theme prop is not provided
|
|
122
|
+
expect(onThemeChange).not.toHaveBeenCalled();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('changeTheme function', () => {
|
|
127
|
+
it('should change theme to dark', () => {
|
|
128
|
+
mockedUseDeviceColorScheme.mockReturnValue('light');
|
|
129
|
+
|
|
130
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
131
|
+
<ThemeProvider defaultTheme="light">{children}</ThemeProvider>
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const { result } = renderHook(() => useTheme(), { wrapper });
|
|
135
|
+
|
|
136
|
+
expect(result.current.theme).toBe('light');
|
|
137
|
+
|
|
138
|
+
act(() => {
|
|
139
|
+
result.current.changeTheme('dark');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(result.current.theme).toBe('dark');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should change theme to light', () => {
|
|
146
|
+
mockedUseDeviceColorScheme.mockReturnValue('dark');
|
|
147
|
+
|
|
148
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
149
|
+
<ThemeProvider defaultTheme="dark">{children}</ThemeProvider>
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const { result } = renderHook(() => useTheme(), { wrapper });
|
|
153
|
+
|
|
154
|
+
expect(result.current.theme).toBe('dark');
|
|
155
|
+
|
|
156
|
+
act(() => {
|
|
157
|
+
result.current.changeTheme('light');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
expect(result.current.theme).toBe('light');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should use device color scheme when theme is set to "system"', () => {
|
|
164
|
+
mockedUseDeviceColorScheme.mockReturnValue('dark');
|
|
165
|
+
|
|
166
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
167
|
+
<ThemeProvider defaultTheme="light">{children}</ThemeProvider>
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const { result } = renderHook(() => useTheme(), { wrapper });
|
|
171
|
+
|
|
172
|
+
expect(result.current.theme).toBe('light');
|
|
173
|
+
|
|
174
|
+
act(() => {
|
|
175
|
+
result.current.changeTheme('system');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(result.current.theme).toBe('dark');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should switch to light theme when system theme is set and device is in light mode', () => {
|
|
182
|
+
mockedUseDeviceColorScheme.mockReturnValue('light');
|
|
183
|
+
|
|
184
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
185
|
+
<ThemeProvider defaultTheme="dark">{children}</ThemeProvider>
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const { result } = renderHook(() => useTheme(), { wrapper });
|
|
189
|
+
|
|
190
|
+
expect(result.current.theme).toBe('dark');
|
|
191
|
+
|
|
192
|
+
act(() => {
|
|
193
|
+
result.current.changeTheme('system');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(result.current.theme).toBe('light');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should handle multiple theme changes', () => {
|
|
200
|
+
mockedUseDeviceColorScheme.mockReturnValue('light');
|
|
201
|
+
|
|
202
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
203
|
+
<ThemeProvider defaultTheme="light">{children}</ThemeProvider>
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const { result } = renderHook(() => useTheme(), { wrapper });
|
|
207
|
+
|
|
208
|
+
expect(result.current.theme).toBe('light');
|
|
209
|
+
|
|
210
|
+
act(() => {
|
|
211
|
+
result.current.changeTheme('dark');
|
|
212
|
+
});
|
|
213
|
+
expect(result.current.theme).toBe('dark');
|
|
214
|
+
|
|
215
|
+
act(() => {
|
|
216
|
+
result.current.changeTheme('light');
|
|
217
|
+
});
|
|
218
|
+
expect(result.current.theme).toBe('light');
|
|
219
|
+
|
|
220
|
+
act(() => {
|
|
221
|
+
result.current.changeTheme('system');
|
|
222
|
+
});
|
|
223
|
+
expect(result.current.theme).toBe('light');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should respect device color scheme when changing to system theme', () => {
|
|
227
|
+
// Mock device as dark initially
|
|
228
|
+
mockedUseDeviceColorScheme.mockReturnValue('dark');
|
|
229
|
+
|
|
230
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
231
|
+
<ThemeProvider defaultTheme="light">{children}</ThemeProvider>
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const { result } = renderHook(() => useTheme(), { wrapper });
|
|
235
|
+
|
|
236
|
+
// Initially should be light (from defaultTheme)
|
|
237
|
+
expect(result.current.theme).toBe('light');
|
|
238
|
+
|
|
239
|
+
// Change to system should use device color scheme (dark)
|
|
240
|
+
act(() => {
|
|
241
|
+
result.current.changeTheme('system');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
expect(result.current.theme).toBe('dark');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should update system theme when device color scheme is light', () => {
|
|
248
|
+
// Mock device as light
|
|
249
|
+
mockedUseDeviceColorScheme.mockReturnValue('light');
|
|
250
|
+
|
|
251
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
252
|
+
<ThemeProvider defaultTheme="dark">{children}</ThemeProvider>
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const { result } = renderHook(() => useTheme(), { wrapper });
|
|
256
|
+
|
|
257
|
+
// Initially should be dark (from defaultTheme)
|
|
258
|
+
expect(result.current.theme).toBe('dark');
|
|
259
|
+
|
|
260
|
+
// Change to system should use device color scheme (light)
|
|
261
|
+
act(() => {
|
|
262
|
+
result.current.changeTheme('system');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
expect(result.current.theme).toBe('light');
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe('edge cases', () => {
|
|
270
|
+
it('should handle undefined device color scheme', () => {
|
|
271
|
+
mockedUseDeviceColorScheme.mockReturnValue(undefined);
|
|
272
|
+
|
|
273
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
274
|
+
<ThemeProvider defaultTheme="light">{children}</ThemeProvider>
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
const { result } = renderHook(() => useTheme(), { wrapper });
|
|
278
|
+
|
|
279
|
+
expect(result.current.theme).toBe('light');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('should handle null device color scheme', () => {
|
|
283
|
+
mockedUseDeviceColorScheme.mockReturnValue(null);
|
|
284
|
+
|
|
285
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
286
|
+
<ThemeProvider defaultTheme="dark">{children}</ThemeProvider>
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
const { result } = renderHook(() => useTheme(), { wrapper });
|
|
290
|
+
|
|
291
|
+
expect(result.current.theme).toBe('dark');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('should maintain theme stability with same changeTheme callback reference', () => {
|
|
295
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
296
|
+
<ThemeProvider defaultTheme="light">{children}</ThemeProvider>
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
const { result, rerender } = renderHook(() => useTheme(), { wrapper });
|
|
300
|
+
|
|
301
|
+
const initialChangeTheme = result.current.changeTheme;
|
|
302
|
+
|
|
303
|
+
rerender({});
|
|
304
|
+
|
|
305
|
+
// changeTheme should maintain reference stability due to useCallback
|
|
306
|
+
expect(result.current.changeTheme).toBe(initialChangeTheme);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
describe('integration tests', () => {
|
|
311
|
+
it('should work with nested providers', () => {
|
|
312
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
313
|
+
<ThemeProvider defaultTheme="light">
|
|
314
|
+
<ThemeProvider defaultTheme="dark">{children}</ThemeProvider>
|
|
315
|
+
</ThemeProvider>
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
const { result } = renderHook(() => useTheme(), { wrapper });
|
|
319
|
+
|
|
320
|
+
// Should use the innermost provider
|
|
321
|
+
expect(result.current.theme).toBe('dark');
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('should maintain theme state across re-renders', () => {
|
|
325
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
326
|
+
<ThemeProvider defaultTheme="light">{children}</ThemeProvider>
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const { result, rerender } = renderHook(() => useTheme(), { wrapper });
|
|
330
|
+
|
|
331
|
+
act(() => {
|
|
332
|
+
result.current.changeTheme('dark');
|
|
333
|
+
});
|
|
334
|
+
expect(result.current.theme).toBe('dark');
|
|
335
|
+
|
|
336
|
+
rerender({});
|
|
337
|
+
expect(result.current.theme).toBe('dark');
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
});
|
|
@@ -1,8 +1,77 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import
|
|
1
|
+
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
|
2
|
+
import { useColorScheme as useDeviceColorScheme } from 'react-native';
|
|
3
|
+
import type {
|
|
4
|
+
ThemeContextType,
|
|
5
|
+
ThemeProviderProps,
|
|
6
|
+
ThemeVariants,
|
|
7
|
+
} from './types';
|
|
3
8
|
|
|
4
|
-
|
|
5
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Create a ThemeContext with default value null
|
|
11
|
+
* @type {React.Context<ThemeContextType | null>}
|
|
12
|
+
*/
|
|
13
|
+
const ThemeContext = React.createContext<ThemeContextType | null>(null);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Give access to theme context
|
|
17
|
+
* @returns {ThemeContextType} The current theme context
|
|
18
|
+
* @throws Will throw an error if used outside of ThemeProvider
|
|
19
|
+
* @example
|
|
20
|
+
* const { theme, changeTheme } = useTheme();
|
|
21
|
+
* console.log(theme); // 'light' | 'dark' | 'system'
|
|
22
|
+
*/
|
|
23
|
+
export const useTheme = () => {
|
|
24
|
+
const context = useContext(ThemeContext);
|
|
25
|
+
if (!context) {
|
|
26
|
+
throw new Error('useTheme must be used within a ThemeProvider');
|
|
27
|
+
}
|
|
28
|
+
return context;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* ThemeProvider component to manage and provide theme context
|
|
33
|
+
* @param {React.ReactNode} children - The child components
|
|
34
|
+
* @param {ThemeVariants} theme - The current theme ('light', 'dark', 'system')
|
|
35
|
+
* @param {ThemeVariants} defaultTheme - The default theme if none is provided
|
|
36
|
+
* @param {(theme: ThemeVariants) => void} onThemeChange - Callback when theme changes
|
|
37
|
+
* @returns {JSX.Element} The ThemeProvider component
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* <ThemeProvider theme="dark" onThemeChange={(theme) => console.log(theme)}>
|
|
41
|
+
* <App />
|
|
42
|
+
* </ThemeProvider>
|
|
43
|
+
*/
|
|
44
|
+
export const ThemeProvider: React.FC<ThemeProviderProps> = ({
|
|
45
|
+
children,
|
|
46
|
+
theme,
|
|
47
|
+
defaultTheme = 'light',
|
|
48
|
+
onThemeChange = () => undefined,
|
|
49
|
+
}) => {
|
|
50
|
+
const deviceColorscheme = useDeviceColorScheme();
|
|
51
|
+
const [currentTheme, setTheme] = useState<ThemeVariants>(
|
|
52
|
+
theme || defaultTheme
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
onThemeChange && theme && onThemeChange(theme);
|
|
57
|
+
}, [theme, onThemeChange]);
|
|
58
|
+
|
|
59
|
+
const changeTheme = useCallback(
|
|
60
|
+
(newTheme: ThemeVariants) => {
|
|
61
|
+
if (newTheme === 'system') {
|
|
62
|
+
setTheme(deviceColorscheme === 'dark' ? 'dark' : 'light');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
setTheme(newTheme);
|
|
66
|
+
},
|
|
67
|
+
[deviceColorscheme]
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<ThemeContext.Provider value={{ theme: currentTheme, changeTheme }}>
|
|
72
|
+
{children}
|
|
73
|
+
</ThemeContext.Provider>
|
|
74
|
+
);
|
|
6
75
|
};
|
|
7
76
|
|
|
8
77
|
export default ThemeProvider;
|
package/src/providers/types.ts
CHANGED
|
@@ -1,4 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme variants type definition
|
|
3
|
+
*/
|
|
4
|
+
export type ThemeVariants = 'light' | 'dark' | 'system';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ThemeProvider component props definition
|
|
8
|
+
*/
|
|
1
9
|
export type ThemeProviderProps = {
|
|
2
10
|
children: React.ReactNode;
|
|
3
|
-
|
|
11
|
+
theme?: ThemeVariants;
|
|
12
|
+
defaultTheme?: ThemeVariants;
|
|
13
|
+
|
|
14
|
+
//Callback when theme changes
|
|
15
|
+
onThemeChange?: (theme: ThemeVariants) => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Theme context type definition
|
|
20
|
+
*/
|
|
21
|
+
export type ThemeContextType = {
|
|
22
|
+
theme: ThemeVariants;
|
|
23
|
+
|
|
24
|
+
// Actions
|
|
25
|
+
changeTheme: (theme: ThemeVariants) => void;
|
|
4
26
|
};
|