-
Notifications
You must be signed in to change notification settings - Fork 50
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(theme): preparing for multi-theme + sub vars
- Loading branch information
1 parent
c025e4e
commit e57f9ee
Showing
35 changed files
with
1,048 additions
and
1,374 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
module.exports = { | ||
presets: ["@babel/preset-env", "module:metro-react-native-babel-preset"], | ||
presets: ["module:metro-react-native-babel-preset"], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
jest.mock('@react-native-async-storage/async-storage', () => | ||
require('@react-native-async-storage/async-storage/jest/async-storage-mock'), | ||
) | ||
|
||
jest.setTimeout(30000) |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
# Yoroi Theme | ||
|
||
## Usage | ||
|
||
It uses "t-shirt" sizes e.g `sm`, `md`, `lg`, etc. | ||
|
||
Naming conventions follow Tailwind, delimited by `_` instead of `-` to | ||
enable object access. | ||
|
||
### Atoms | ||
|
||
The style definitions that "match" Tailwind CSS selectors. | ||
|
||
```tsx | ||
import {atoms as a} from '@yoroi/theme' | ||
|
||
<View style={[a.flex_1]} /> | ||
``` | ||
|
||
### Theme | ||
|
||
The palette definition, prefer to use `useThemeColor` most of the time. | ||
The `useTheme` was designed to manage the theme and not to consume it. | ||
Sub vars should be consumed from the theme always, everything else should be consumed from atoms directly. | ||
|
||
```tsx | ||
import {atoms as a, useThemeColor} from '@yoroi/theme' | ||
|
||
const {gray_500} = useThemeColor() | ||
|
||
<View style={[a.flex_1, {backgroundColor: gray_500}]} /> | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import React from 'react' | ||
import {render, fireEvent} from '@testing-library/react-native' | ||
import {Button, Text} from 'react-native' | ||
|
||
import {ThemeProvider, useTheme, useThemeColor} from './ThemeProvider' | ||
import {ThemeStorage} from './types' | ||
import {ErrorBoundary} from '@yoroi/common' | ||
|
||
describe('ThemeProvider', () => { | ||
const mockStorage: ThemeStorage = { | ||
key: 'theme-name', | ||
save: jest.fn(), | ||
read: jest.fn(), | ||
} | ||
|
||
it('should render children', () => { | ||
const {getByText} = render( | ||
<ThemeProvider storage={mockStorage}> | ||
<Text>Test</Text> | ||
</ThemeProvider>, | ||
) | ||
|
||
expect(getByText('Test')).toBeTruthy() | ||
}) | ||
|
||
it('should provide the theme context', () => { | ||
const TestComponent = () => { | ||
const theme = useTheme() | ||
return <Text>{theme.name}</Text> | ||
} | ||
|
||
const {getByText} = render( | ||
<ThemeProvider storage={mockStorage}> | ||
<TestComponent /> | ||
</ThemeProvider>, | ||
) | ||
|
||
expect(getByText('default-light')).toBeTruthy() | ||
}) | ||
|
||
it('should update the theme when selectThemeName is called', () => { | ||
const TestComponent = () => { | ||
const theme = useTheme() | ||
const color = useThemeColor() | ||
return ( | ||
<> | ||
<Text>{theme.name}</Text> | ||
<Button | ||
onPress={() => theme.selectThemeName('default-dark')} | ||
title="Change Theme" | ||
/> | ||
<Text>{color.black_static}</Text> | ||
</> | ||
) | ||
} | ||
|
||
const {getByText} = render( | ||
<ThemeProvider storage={mockStorage}> | ||
<TestComponent /> | ||
</ThemeProvider>, | ||
) | ||
|
||
expect(getByText('default-light')).toBeTruthy() | ||
|
||
expect(getByText('#000000')).toBeTruthy() | ||
|
||
fireEvent.press(getByText('Change Theme')) | ||
|
||
expect(getByText('default-dark')).toBeTruthy() | ||
}) | ||
|
||
it('should throw an error when useTheme is called without a provider', () => { | ||
const TestComponent = () => { | ||
useTheme() | ||
return null | ||
} | ||
|
||
const {getByTestId} = render( | ||
<ErrorBoundary> | ||
<TestComponent /> | ||
</ErrorBoundary>, | ||
) | ||
|
||
expect(getByTestId('hasError')).toBeTruthy() | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,53 +1,62 @@ | ||
import React from 'react' | ||
import {useColorScheme} from 'react-native' | ||
|
||
import {darkTheme} from './themes/darkTheme' | ||
import {lightTheme} from './themes/lightTheme' | ||
import {Theme} from './types' | ||
import {Palette, SupportedThemes, Theme, ThemeStorage} from './types' | ||
import {defaultLightTheme} from './themes/default-light' | ||
import {defaultDarkTheme} from './themes/default-dark' | ||
import {detectTheme} from './helpers/detect-theme' | ||
|
||
const ThemeContext = React.createContext<undefined | ThemeContext>(undefined) | ||
export const ThemeProvider = ({children}: {children: React.ReactNode}) => { | ||
const [colorScheme, setColorScheme] = React.useState<'light' | 'dark'>( | ||
'light', | ||
) | ||
|
||
const selectColorScheme = React.useCallback( | ||
(themeColor: 'light' | 'dark') => { | ||
setColorScheme(themeColor) | ||
}, | ||
[], | ||
) | ||
export const ThemeProvider = ({ | ||
children, | ||
storage, | ||
}: { | ||
children: React.ReactNode | ||
storage: ThemeStorage | ||
}) => { | ||
const colorScheme = useColorScheme() | ||
const [themeName, setThemeName] = React.useState< | ||
Exclude<SupportedThemes, 'system'> | ||
>(detectTheme(colorScheme, storage.read() ?? 'system')) | ||
|
||
const value = React.useMemo( | ||
() => ({ | ||
theme: themes[colorScheme], | ||
colorScheme, | ||
selectColorScheme, | ||
isLight: colorScheme === 'light', | ||
isDark: colorScheme === 'dark', | ||
name: themes[themeName].name, | ||
color: themes[themeName].color, | ||
|
||
selectThemeName: (newTheme: SupportedThemes) => { | ||
const detectedTheme = detectTheme(colorScheme, newTheme) | ||
setThemeName(detectedTheme) | ||
storage.save(newTheme) | ||
}, | ||
|
||
isLight: themes[themeName].base === 'light', | ||
isDark: themes[themeName].base === 'dark', | ||
}), | ||
[colorScheme, selectColorScheme], | ||
[colorScheme, storage, themeName], | ||
) | ||
|
||
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider> | ||
} | ||
|
||
export const useTheme = () => | ||
React.useContext(ThemeContext) || missingProvider() | ||
React.useContext(ThemeContext) ?? missingProvider() | ||
|
||
const missingProvider = () => { | ||
throw new Error('ThemeProvider is missing') | ||
} | ||
export const useThemeColor = () => useTheme().color | ||
|
||
type ColorSchemeOption = 'light' | 'dark' | ||
type ThemeContext = { | ||
theme: Theme | ||
colorScheme: ColorSchemeOption | ||
selectColorScheme: (colorTheme: ColorSchemeOption) => void | ||
name: SupportedThemes | ||
color: Palette | ||
selectThemeName: (name: SupportedThemes) => void | ||
isLight: boolean | ||
isDark: boolean | ||
} | ||
|
||
const themes: Record<'light' | 'dark', Theme> = { | ||
light: lightTheme, | ||
dark: darkTheme, | ||
const themes: Record<Exclude<SupportedThemes, 'system'>, Theme> = { | ||
['default-light']: defaultLightTheme, | ||
['default-dark']: defaultDarkTheme, | ||
} | ||
|
||
const missingProvider = () => { | ||
throw new Error('ThemeProvider is missing') | ||
} |
41 changes: 41 additions & 0 deletions
41
packages/theme/src/adapters/mmkv-storage/theme-storage-maker.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import {themeStorageMaker} from './theme-storage-maker' | ||
import {SupportedThemes} from '../../types' | ||
|
||
describe('themeStorageMaker', () => { | ||
const mockStorage = { | ||
setItem: jest.fn(), | ||
getItem: jest.fn(), | ||
} as any | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks() | ||
}) | ||
|
||
it('should save the theme name to storage', () => { | ||
const themeStorage = themeStorageMaker({storage: mockStorage}) | ||
const themeName: SupportedThemes = 'default-dark' | ||
|
||
themeStorage.save(themeName) | ||
|
||
expect(mockStorage.setItem).toHaveBeenCalledWith('theme-name', themeName) | ||
}) | ||
|
||
it('should read the theme name from storage', () => { | ||
const themeStorage = themeStorageMaker({storage: mockStorage}) | ||
const themeName: SupportedThemes = 'default-light' | ||
mockStorage.getItem.mockReturnValue(themeName) | ||
|
||
const result = themeStorage.read() | ||
|
||
expect(mockStorage.getItem).toHaveBeenCalledWith('theme-name') | ||
expect(result).toBe(themeName) | ||
}) | ||
|
||
it('should return the correct key', () => { | ||
const themeStorage = themeStorageMaker({storage: mockStorage}) | ||
|
||
const result = themeStorage.key | ||
|
||
expect(result).toBe('theme-name') | ||
}) | ||
}) |
23 changes: 23 additions & 0 deletions
23
packages/theme/src/adapters/mmkv-storage/theme-storage-maker.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import {App} from '@yoroi/types' | ||
import {freeze} from 'immer' | ||
|
||
import {SupportedThemes, ThemeStorage} from '../../types' | ||
|
||
const themeNameKey = 'theme-name' | ||
|
||
export const themeStorageMaker = ({ | ||
storage, | ||
}: { | ||
storage: App.ObservableStorage<false> | ||
}): ThemeStorage => { | ||
const save = (name: SupportedThemes) => | ||
storage.setItem<SupportedThemes>(themeNameKey, name) | ||
|
||
const read = () => storage.getItem<SupportedThemes>(themeNameKey) | ||
|
||
return freeze({ | ||
save, | ||
read, | ||
key: themeNameKey, | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import {atoms} from './atoms' | ||
|
||
describe('atoms', () => { | ||
it('should be defined', () => { | ||
expect(atoms).toBeDefined() | ||
}) | ||
}) |
Oops, something went wrong.