├── .gitattributes ├── .eslintrc.js ├── tsconfig.json ├── package.json ├── README.md ├── LICENSE ├── .gitignore └── src └── index.tsx /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | module.exports = require('expo-module-scripts/eslintrc.base.js'); 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | { 3 | "extends": "expo-module-scripts/tsconfig.base", 4 | "compilerOptions": { 5 | "outDir": "./build" 6 | }, 7 | "include": ["./src"], 8 | "exclude": ["**/__mocks__/*", "**/__tests__/*"] 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bacons/expo-background-color", 3 | "version": "1.0.1", 4 | "description": "Stack based React component for updating the native background color", 5 | "main": "build/index.js", 6 | "scripts": { 7 | "build": "expo-module build", 8 | "clean": "expo-module clean", 9 | "lint": "expo-module lint", 10 | "test": "expo-module test", 11 | "prepare": "expo-module prepare", 12 | "prepublishOnly": "expo-module prepublishOnly", 13 | "expo-module": "expo-module" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/evanbacon/expo-background-color.git" 18 | }, 19 | "peerDependencies": { 20 | "expo-system-ui": "*", 21 | "react-native": "*" 22 | }, 23 | "keywords": [ 24 | "expo", 25 | "expo-system-ui", 26 | "react-native" 27 | ], 28 | "author": "Evan Bacon", 29 | "license": "MIT", 30 | "devDependencies": { 31 | "expo-module-scripts": "^2.0.0", 32 | "expo-system-ui": "^1.2.0", 33 | "react-native": "0.68.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @bacons/expo-background-color 2 | 3 | A stack based Expo component for setting the background color of the root view. Useful for changing the background color on certain screens or inside of native modals. Updates based on `Appearance` and `AppState` native modules. 4 | 5 | This is a published version of my [original gist](https://gist.github.com/EvanBacon/d148b2425c5a0bd11b6cecb5f4b72bb8). 6 | 7 | ## Add the package to your npm dependencies 8 | 9 | > Runs in any React Native project. Supports iOS, Android, web. 10 | 11 | ``` 12 | expo add expo-system-ui @bacons/expo-background-color 13 | ``` 14 | 15 | ## Usage 16 | 17 | Drop the `BackgroundColor` component anywhere, background color respect the component instance at the highest level (i.e. `StatusBar` module in `react-native`). 18 | 19 | ```tsx 20 | import { BackgroundColor } from "@bacons/expo-background-color"; 21 | 22 | function App() { 23 | return ( 24 | <> 25 | 26 | 27 | 28 | ); 29 | } 30 | ``` 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 evanbacon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Serverless directories 108 | .serverless/ 109 | 110 | # FuseBox cache 111 | .fusebox/ 112 | 113 | # DynamoDB Local files 114 | .dynamodb/ 115 | 116 | # TernJS port file 117 | .tern-port 118 | 119 | # Stores VSCode versions used for testing VSCode extensions 120 | .vscode-test 121 | 122 | # yarn v2 123 | .yarn/cache 124 | .yarn/unplugged 125 | .yarn/build-state.yml 126 | .yarn/install-state.gz 127 | .pnp.* 128 | 129 | /build -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as SystemUI from 'expo-system-ui'; 2 | import * as React from 'react'; 3 | import { Appearance, AppState, AppStateStatus, ColorSchemeName, ColorValue } from 'react-native'; 4 | 5 | export type ThemedColorValue = { light: ColorValue, dark: ColorValue }; 6 | 7 | export type Props = { color: ColorValue | ThemedColorValue } 8 | 9 | const propsStack: Props[] = []; 10 | 11 | const defaultProps = createStackEntry({ 12 | color: '#fff', 13 | }); 14 | 15 | // Timer for updating the native module values at the end of the frame. 16 | let updateImmediate: any | null = null; 17 | 18 | let appearanceListener: Appearance.AppearanceListener | null = null; 19 | let appStateListener: ((state: AppStateStatus) => void) | null = null; 20 | 21 | /** 22 | * A stack based component for setting the background color of the root view. 23 | * Useful for changing the background color on certain screens or inside of native modals. 24 | * Updates based on Appearance and AppState. 25 | * 26 | * @example 27 | * ```tsx 28 | * function App() { 29 | * return ( 30 | * <> 31 | * 32 | * 33 | * 34 | * ) 35 | * } 36 | * ``` 37 | */ 38 | export function BackgroundColor(props: Props) { 39 | let stack = React.useRef(null); 40 | 41 | React.useEffect(() => { 42 | // Create a stack entry on component mount 43 | stack.current = BackgroundColor.pushStackEntry(props) 44 | return () => { 45 | if (stack.current) { 46 | // Update on component unmount 47 | BackgroundColor.popStackEntry(stack.current); 48 | } 49 | } 50 | }, []) 51 | 52 | React.useEffect(() => { 53 | if (stack.current) { 54 | // Update the current stack entry 55 | stack.current = BackgroundColor.replaceStackEntry( 56 | stack.current, 57 | props, 58 | ); 59 | } 60 | }, [props.color]); 61 | 62 | return null; 63 | } 64 | 65 | function isThemedColor(color?: Props['color']): color is ThemedColorValue { 66 | return !!color && typeof color !== 'string' && ('light' in color) && ('dark' in color); 67 | } 68 | 69 | /** 70 | * Merges the prop stack with the default values. 71 | */ 72 | function mergePropsStack( 73 | propsStack: Array, 74 | defaultValues: Partial, 75 | ): Partial { 76 | return propsStack.reduce((prev, cur) => { 77 | for (const prop in cur) { 78 | // @ts-ignore 79 | if (cur[prop] != null) { 80 | // @ts-ignore 81 | prev[prop] = cur[prop]; 82 | } 83 | } 84 | return prev; 85 | }, Object.assign({}, defaultValues)); 86 | } 87 | 88 | function setColorAsync(scheme: ColorSchemeName, color: Props['color']) { 89 | if (isThemedColor(color)) { 90 | return SystemUI.setBackgroundColorAsync(scheme === 'dark' ? color.dark ?? '#000' : color.light ?? '#fff'); 91 | } 92 | return SystemUI.setBackgroundColorAsync(color ?? '#fff'); 93 | } 94 | 95 | 96 | /** 97 | * Returns an object to insert in the props stack from the props 98 | * and the transition/animation info. 99 | */ 100 | function createStackEntry(props: Props): Props { 101 | return { 102 | color: props.color 103 | }; 104 | } 105 | 106 | /** 107 | * Set the background color for the app 108 | * @param color Background color. 109 | * @param animated Animate the style change. 110 | */ 111 | BackgroundColor.setColor = (color: ThemedColorValue) => { 112 | defaultProps.color = color; 113 | setColorAsync(Appearance.getColorScheme(), color); 114 | } 115 | 116 | /** 117 | * Push a BackgroundColor entry onto the stack. 118 | * The return value should be passed to `popStackEntry` when complete. 119 | * 120 | * @param props Object containing the BackgroundColor props to use in the stack entry. 121 | */ 122 | BackgroundColor.pushStackEntry = (props: Props): any => { 123 | const entry = createStackEntry(props); 124 | propsStack.push(entry); 125 | 126 | // Ensure we only have one appearance change listener. 127 | if (!appearanceListener) { 128 | appearanceListener = ({ colorScheme }) => { 129 | setColorAsync(colorScheme, propsStack[propsStack.length - 1].color); 130 | } 131 | Appearance.addChangeListener(appearanceListener); 132 | } 133 | 134 | if (!appStateListener) { 135 | appStateListener = () => { 136 | setColorAsync(Appearance.getColorScheme(), propsStack[propsStack.length - 1].color); 137 | } 138 | AppState.addEventListener('change', appStateListener); 139 | } 140 | 141 | BackgroundColor._updatePropsStack(); 142 | return entry; 143 | } 144 | 145 | /** 146 | * Pop a BackgroundColor entry from the stack. 147 | * 148 | * @param entry Entry returned from `pushStackEntry`. 149 | */ 150 | BackgroundColor.popStackEntry = (entry: Props) => { 151 | const index = propsStack.indexOf(entry); 152 | if (index !== -1) { 153 | propsStack.splice(index, 1); 154 | } 155 | if (propsStack.length === 0) { 156 | if (appearanceListener) { 157 | Appearance.removeChangeListener(appearanceListener); 158 | appearanceListener = null; 159 | } 160 | if (appStateListener) { 161 | AppState.removeEventListener('change', appStateListener); 162 | appStateListener = null; 163 | } 164 | } 165 | BackgroundColor._updatePropsStack(); 166 | } 167 | 168 | /** 169 | * Replace an existing BackgroundColor stack entry with new props. 170 | * 171 | * @param entry Entry returned from `pushStackEntry` to replace. 172 | * @param props Object containing the BackgroundColor props to use in the replacement stack entry. 173 | */ 174 | BackgroundColor.replaceStackEntry = (entry: Props, props: Props): any => { 175 | const newEntry = createStackEntry(props); 176 | const index = propsStack.indexOf(entry); 177 | if (index !== -1) { 178 | propsStack[index] = newEntry; 179 | } 180 | BackgroundColor._updatePropsStack(); 181 | return newEntry; 182 | } 183 | 184 | /** 185 | * Updates the native background color with the props from the stack. 186 | */ 187 | BackgroundColor._updatePropsStack = () => { 188 | // Send the update to the native module only once at the end of the frame. 189 | clearImmediate(updateImmediate); 190 | updateImmediate = setImmediate(() => { 191 | const { color } = mergePropsStack( 192 | propsStack, 193 | defaultProps, 194 | ); 195 | 196 | if (color) { 197 | setColorAsync(Appearance.getColorScheme(), color); 198 | } 199 | }); 200 | }; --------------------------------------------------------------------------------