├── .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 | };
--------------------------------------------------------------------------------