diff --git a/README.md b/README.md index 6407dfd..c04d8bb 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,6 @@ A package that allows you to create React Native StyleSheets with support for Dark/Light/Auto Themes. -- Depends on react-native-appearance to choose the theme based on OS preference(Android 10/iOS 13) - Simple API - Fully typed - Builds on top of StyleSheets and Hooks @@ -19,22 +18,16 @@ A package that allows you to create React Native StyleSheets with support for Da ## Installation -**Using Expo** - -``` -expo install react-native-appearance react-native-themed-stylesheet -``` - **Using Yarn** ``` -yarn add react-native-appearance react-native-themed-stylesheet +yarn add react-native-themed-stylesheet ``` **Using NPM** ``` -npm install --save react-native-appearance react-native-themed-stylesheet +npm install --save react-native-themed-stylesheet ``` ## Usage diff --git a/__mocks__/react-native-appearance.tsx b/__mocks__/react-native-appearance.tsx deleted file mode 100644 index 465ccfd..0000000 --- a/__mocks__/react-native-appearance.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import * as React from 'react' -import { View } from 'react-native' -import { - AppearancePreferences, - ColorSchemeName, - AppearanceListener -} from 'react-native-appearance/src/Appearance.types' - -type EventSubscription = { - remove: () => void -} - -export class Appearance { - private static _preference: ColorSchemeName = 'no-preference' - private static _listeners: AppearanceListener[] = [] - static getColorScheme (): ColorSchemeName { - return this._preference - } - - static get preference (): ColorSchemeName { - return this._preference - } - - static set preference (newPreference: ColorSchemeName) { - this._preference = newPreference - this._listeners.forEach(l => l({ colorScheme: this._preference })) - } - - static set (_preferences: AppearancePreferences): void {} - - static addChangeListener (_listener: AppearanceListener): EventSubscription { - this._listeners.push(_listener) - return { - remove: () => { - this._listeners = this._listeners.filter(l => l !== _listener) - } - } - } - - /** - * Unused: some people might expect to remove the listener like this, but they shouldn't. - */ - static removeChangeListener (_listener: AppearanceListener): void {} -} - -export const AppearanceProvider = (props: any) => ( - -) - -export function useColorScheme (): ColorSchemeName { - return 'no-preference' -} diff --git a/package.json b/package.json index b1cfbe7..5e7934d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-themed-stylesheet", - "version": "0.3.2", + "version": "0.3.3", "description": "React Native StyleSheets with Theming Support", "author": "Andre Pedroza", "license": "MIT", @@ -29,8 +29,7 @@ "peerDependencies": { "@storybook/addons": "*", "react": "*", - "react-native": "*", - "react-native-appearance": "*" + "react-native": "*" }, "devDependencies": { "@babel/core": "^7.12.16", @@ -52,7 +51,6 @@ "jest": "^26.6.3", "react": "16.13.1", "react-native": "0.63.4", - "react-native-appearance": "~0.3.3", "react-test-renderer": "16.13.1", "ts-jest": "^26.5.1", "typescript": "~4.0.0" diff --git a/src/index.ts b/src/index.ts index ed7fc75..958b231 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,8 +7,13 @@ import React, { useEffect, useState } from 'react' -import { ImageStyle, StyleSheet, TextStyle, ViewStyle } from 'react-native' -import { Appearance, AppearanceProvider } from 'react-native-appearance' +import { + ImageStyle, + StyleSheet, + TextStyle, + useColorScheme, + ViewStyle +} from 'react-native' import merge from 'ts-deepmerge' export interface BaseTheme {} @@ -67,7 +72,7 @@ type ThemeProviderProps = { const themeContext = createContext({} as ThemeContext) -const generateTheme = (mode: ThemeMode, themes: Themes) => { +const generateTheme = (mode: Exclude, themes: Themes) => { if ( typeof themes !== 'object' || !themes || @@ -78,42 +83,27 @@ const generateTheme = (mode: ThemeMode, themes: Themes) => { ) { return {} } - const constants = 'constants' in themes ? themes['constants'] : {} - const systemColorScheme = Appearance.getColorScheme() - const currentMode = - mode !== 'auto' - ? mode - : systemColorScheme !== 'no-preference' - ? systemColorScheme - : 'light' - return merge(constants, themes[currentMode]) + const constants = ('constants' in themes ? themes['constants'] : {}) || {} + return merge(constants, themes[mode]) } -const RawThemeProvider: React.FC = ({ +export const ThemeProvider: React.FC = ({ children, mode: initialMode, themes: initialThemes }) => { + const colorScheme = useColorScheme() const [mode, setMode] = useState(initialMode ?? 'auto') const [themes, setThemes] = useState(initialThemes) - const [theme, setTheme] = useState(generateTheme(mode, themes)) + const [theme, setTheme] = useState( + generateTheme(mode !== 'auto' ? mode : colorScheme || 'light', themes) + ) useEffect(() => { - let subscription: any - setTheme(generateTheme(mode, themes)) - if (mode === 'auto') { - subscription = Appearance.addChangeListener(({ colorScheme }) => { - setTheme( - generateTheme( - colorScheme !== 'no-preference' ? colorScheme : 'light', - themes - ) - ) - }) - } - return () => { - subscription && subscription.remove() - } + mode !== 'auto' && setTheme(generateTheme(mode, themes)) }, [mode, themes]) + useEffect(() => { + mode === 'auto' && setTheme(generateTheme(colorScheme || 'light', themes)) + }, [colorScheme, mode, themes]) return createElement( themeContext.Provider, { @@ -129,16 +119,6 @@ const RawThemeProvider: React.FC = ({ ) } -export const ThemeProvider: typeof RawThemeProvider = ({ - children, - ...props -}) => - createElement( - AppearanceProvider, - null, - createElement(RawThemeProvider, props, children) - ) - export const useMode: UseMode = () => { const { mode, setMode } = useContext(themeContext) return [mode, setMode] diff --git a/tests/__mocks__/react-native.ts b/tests/__mocks__/react-native.ts new file mode 100644 index 0000000..c1e4faf --- /dev/null +++ b/tests/__mocks__/react-native.ts @@ -0,0 +1,14 @@ +import { useState } from 'react' +import * as ReactNative from 'react-native' +import { Appearance } from '../fixture' + +// eslint-disable-next-line import/export +export * from 'react-native' + +export const useColorScheme = () => { + const [value, setValue] = useState<'dark' | 'light' | null>(null) + Appearance.listener = setValue + return value +} + +export default Object.setPrototypeOf({ useColorScheme }, ReactNative) diff --git a/tests/fixture.ts b/tests/fixture.ts index 1220003..577b8e5 100644 --- a/tests/fixture.ts +++ b/tests/fixture.ts @@ -1,3 +1,16 @@ +type AppearanceType = { + listener: null | ((v: 'dark' | 'light' | null) => void) + set: (v: 'dark' | 'light' | null) => void +} + +export const Appearance: AppearanceType = { + listener: null, + set (v) { + typeof this.listener === 'function' && this.listener(v) + this.listener = null + } +} + export const themes1 = { light: { colors: { diff --git a/tests/with-constants.ts b/tests/with-constants.ts index cd46018..02dcd0a 100644 --- a/tests/with-constants.ts +++ b/tests/with-constants.ts @@ -1,7 +1,6 @@ import { createElement } from 'react' import { TextStyle } from 'react-native' import { renderHook, act } from '@testing-library/react-hooks/native' -import { Appearance } from '../__mocks__/react-native-appearance' import { ThemeProvider, useCreateStyles, @@ -9,17 +8,12 @@ import { useTheme, useThemes } from '../src/index' -import { createStyles, themes1, themes2 } from './fixture' - +import { Appearance, createStyles, themes1, themes2 } from './fixture' declare module '../src/index' { type Themes1 = typeof themes1 export interface BaseTheme extends Themes1 {} } -beforeEach(() => { - Appearance.preference = 'no-preference' -}) - describe('Auto Mode', () => { const wrapper: typeof ThemeProvider = ({ children }) => createElement(ThemeProvider, { themes: themes1 }, children) @@ -64,14 +58,14 @@ describe('Change System Preference', () => { test('To "no-preference" to Get Light Mode', () => { const { result } = renderHook(() => useTheme(), { wrapper }) act(() => { - Appearance.preference = 'no-preference' + Appearance.set(null) }) expect(result.current.colors.primary).toEqual('#a1a1a1') }) test('To Dark Mode', () => { const { result } = renderHook(() => useTheme(), { wrapper }) act(() => { - Appearance.preference = 'dark' + Appearance.set('dark') }) expect(result.current.colors.primary).toEqual('#a2a2a2') }) @@ -85,7 +79,7 @@ describe('Change System Preference', () => { { wrapper } ) act(() => { - Appearance.preference = 'light' + Appearance.set('light') result.current.setMode('dark') }) expect(result.current.theme.colors.primary).toEqual('#a2a2a2') diff --git a/tests/with-exceptions.ts b/tests/with-exceptions.ts index 42112fe..d10c5ee 100644 --- a/tests/with-exceptions.ts +++ b/tests/with-exceptions.ts @@ -1,12 +1,7 @@ import { createElement } from 'react' import { renderHook } from '@testing-library/react-hooks/native' -import { Appearance } from '../__mocks__/react-native-appearance' import { ThemeProvider, useTheme } from '../src/index' -beforeEach(() => { - Appearance.preference = 'no-preference' -}) - describe('With Exceptions', () => { test('Themes as null', () => { const wrapper: typeof ThemeProvider = ({ children }) => @@ -56,4 +51,20 @@ describe('With Exceptions', () => { const { result } = renderHook(() => useTheme(), { wrapper }) expect(Object.keys(result.current).length).toEqual(0) }) + test('Constants as undefined', () => { + const wrapper: typeof ThemeProvider = ({ children }) => + createElement( + ThemeProvider, + { + themes: { + constants: undefined, + dark: { color: '#a1a1a1' }, + light: { color: '#a2a2a2' } + } + }, + children + ) + const { result } = renderHook(() => useTheme(), { wrapper }) + expect(Object.keys(result.current).length).toEqual(1) + }) }) diff --git a/tests/without-constants.ts b/tests/without-constants.ts index 57e89f6..bdbd4d7 100644 --- a/tests/without-constants.ts +++ b/tests/without-constants.ts @@ -1,6 +1,5 @@ import { createElement } from 'react' import { renderHook } from '@testing-library/react-hooks/native' -import { Appearance } from '../__mocks__/react-native-appearance' import { ThemeProvider, useTheme } from '../src/index' import { themes3 } from './fixture' @@ -9,10 +8,6 @@ declare module '../src/index' { export interface BaseTheme extends Themes3 {} } -beforeEach(() => { - Appearance.preference = 'no-preference' -}) - describe('Without Constants', () => { const wrapper: typeof ThemeProvider = ({ children }) => createElement(ThemeProvider, { themes: themes3 }, children) diff --git a/yarn.lock b/yarn.lock index 6337cad..737b0c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2525,11 +2525,6 @@ copy-descriptor@^0.1.0: resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= -core-js@^1.0.0: - version "1.2.7" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz" - integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY= - core-js@^2.4.1: version "2.6.12" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz" @@ -3340,13 +3335,6 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" -fbemitter@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/fbemitter/-/fbemitter-2.1.1.tgz" - integrity sha1-Uj4U/a9SSIBbsC9i78M75wP1GGU= - dependencies: - fbjs "^0.8.4" - fbjs-css-vars@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz" @@ -3368,19 +3356,6 @@ fbjs-scripts@^1.1.0: semver "^5.1.0" through2 "^2.0.0" -fbjs@^0.8.4: - version "0.8.17" - resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz" - integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90= - dependencies: - core-js "^1.0.0" - isomorphic-fetch "^2.1.1" - loose-envify "^1.0.0" - object-assign "^4.1.0" - promise "^7.1.1" - setimmediate "^1.0.5" - ua-parser-js "^0.7.18" - fbjs@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-1.0.0.tgz" @@ -6115,15 +6090,6 @@ react-lifecycles-compat@^3.0.4: resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== -react-native-appearance@~0.3.3: - version "0.3.4" - resolved "https://registry.yarnpkg.com/react-native-appearance/-/react-native-appearance-0.3.4.tgz" - integrity sha512-Vz3zdJbAEiMDwuw6wH98TT1WVfBvWjvANutYtkIbl16KGRCigtSgt6IIiLsF3/TSS3y3FtHhWDelFeGw/rtuig== - dependencies: - fbemitter "^2.1.1" - invariant "^2.2.4" - use-subscription "^1.0.0" - react-native@0.63.4: version "0.63.4" resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.63.4.tgz"