react-native-edge-to-edge
Version:
Effortlessly enable edge-to-edge display in React Native
255 lines (223 loc) • 7.93 kB
text/typescript
import { useEffect, useMemo, useRef } from "react";
import { Appearance, Platform, StatusBar, useColorScheme } from "react-native";
import NativeModule from "./specs/NativeEdgeToEdgeModule";
import { SystemBarsEntry, SystemBarsProps, SystemBarStyle } from "./types";
type MergeableBarStyle = "light" | "dark" | undefined;
function getColorScheme(): "light" | "dark" {
return Appearance?.getColorScheme() ?? "light";
}
function toNativeBarStyle(
style: MergeableBarStyle,
): "default" | "light-content" | "dark-content" {
return style === "light" || style === "dark" ? `${style}-content` : "default";
}
/**
* Merges the entries stack.
*/
function mergeEntriesStack(entriesStack: SystemBarsEntry[]) {
return entriesStack.reduce<{
statusBarStyle: SystemBarStyle | undefined;
navigationBarStyle: SystemBarStyle | undefined;
statusBarHidden: boolean;
navigationBarHidden: boolean;
}>(
(prev, cur) => {
for (const prop in cur) {
if (cur[prop as keyof SystemBarsEntry] != null) {
// @ts-expect-error
prev[prop] = cur[prop];
}
}
return prev;
},
{
statusBarStyle: undefined,
navigationBarStyle: undefined,
statusBarHidden: false,
navigationBarHidden: false,
},
);
}
/**
* Returns an object to insert in the props stack from the props.
*/
function createStackEntry(props: SystemBarsProps): SystemBarsEntry {
return {
statusBarStyle:
typeof props.style === "string" ? props.style : props.style?.statusBar,
navigationBarStyle:
typeof props.style === "string"
? props.style
: props.style?.navigationBar,
statusBarHidden:
typeof props.hidden === "boolean"
? props.hidden
: props.hidden?.statusBar,
navigationBarHidden:
typeof props.hidden === "boolean"
? props.hidden
: props.hidden?.navigationBar,
};
}
const entriesStack: SystemBarsEntry[] = [];
// Timer for updating the native module values at the end of the frame.
let updateImmediate: NodeJS.Immediate | null = null;
// The current merged values from the entries stack.
let currentMergedEntries: {
statusBarStyle: MergeableBarStyle;
navigationBarStyle: MergeableBarStyle;
statusBarHidden: boolean;
navigationBarHidden: boolean;
} = {
statusBarStyle: undefined,
navigationBarStyle: undefined,
statusBarHidden: false,
navigationBarHidden: false,
};
/**
* Updates the native system bars with the entries from the stack.
*/
function updateEntriesStack() {
if (Platform.OS === "android" || Platform.OS === "ios") {
if (updateImmediate != null) {
clearImmediate(updateImmediate);
}
updateImmediate = setImmediate(() => {
const isLightColorScheme = getColorScheme() === "light";
const autoBarStyle = isLightColorScheme ? "dark" : "light";
const invertedBarStyle = isLightColorScheme ? "light" : "dark";
const mergedEntries = mergeEntriesStack(entriesStack);
const statusBarStyle: MergeableBarStyle =
mergedEntries.statusBarStyle === "auto"
? autoBarStyle
: mergedEntries.statusBarStyle === "inverted"
? invertedBarStyle
: mergedEntries.statusBarStyle;
const navigationBarStyle: MergeableBarStyle =
mergedEntries.navigationBarStyle === "auto"
? autoBarStyle
: mergedEntries.navigationBarStyle === "inverted"
? invertedBarStyle
: mergedEntries.navigationBarStyle;
const { statusBarHidden, navigationBarHidden } = mergedEntries;
if (statusBarStyle !== currentMergedEntries.statusBarStyle) {
const style = toNativeBarStyle(statusBarStyle);
Platform.OS === "android"
? NativeModule?.setStatusBarStyle(style)
: StatusBar.setBarStyle(style, true);
}
if (statusBarHidden !== currentMergedEntries.statusBarHidden) {
Platform.OS === "android"
? NativeModule?.setStatusBarHidden(statusBarHidden)
: StatusBar.setHidden(statusBarHidden, "fade"); // 'slide' doesn't work in this context
}
if (Platform.OS === "android") {
if (navigationBarStyle !== currentMergedEntries.navigationBarStyle) {
const style = toNativeBarStyle(navigationBarStyle);
NativeModule?.setNavigationBarStyle(style);
}
if (navigationBarHidden !== currentMergedEntries.navigationBarHidden) {
NativeModule?.setNavigationBarHidden(navigationBarHidden);
}
}
currentMergedEntries = {
statusBarStyle,
navigationBarStyle,
statusBarHidden,
navigationBarHidden,
};
});
}
}
/**
* Push a SystemBars entry onto the stack.
* The return value should be passed to `popStackEntry` when complete.
*
* @param props Object containing the SystemBars props to use in the stack entry.
*/
function pushStackEntry(props: SystemBarsProps): SystemBarsEntry {
const entry = createStackEntry(props);
entriesStack.push(entry);
updateEntriesStack();
return entry;
}
/**
* Pop a SystemBars entry from the stack.
*
* @param entry Entry returned from `pushStackEntry`.
*/
function popStackEntry(entry: SystemBarsEntry): void {
const index = entriesStack.indexOf(entry);
if (index !== -1) {
entriesStack.splice(index, 1);
}
updateEntriesStack();
}
/**
* Replace an existing SystemBars stack entry with new props.
*
* @param entry Entry returned from `pushStackEntry` to replace.
* @param props Object containing the SystemBars props to use in the replacement stack entry.
*/
function replaceStackEntry(
entry: SystemBarsEntry,
props: SystemBarsProps,
): SystemBarsEntry {
const newEntry = createStackEntry(props);
const index = entriesStack.indexOf(entry);
if (index !== -1) {
entriesStack[index] = newEntry;
}
updateEntriesStack();
return newEntry;
}
export function SystemBars({ hidden, style }: SystemBarsProps) {
const statusBarStyle = typeof style === "string" ? style : style?.statusBar;
const navigationBarStyle =
typeof style === "string" ? style : style?.navigationBar;
const statusBarHidden =
typeof hidden === "boolean" ? hidden : hidden?.statusBar;
const navigationBarHidden =
typeof hidden === "boolean" ? hidden : hidden?.navigationBar;
const stableProps = useMemo<SystemBarsProps>(
() => ({
style:
statusBarStyle === navigationBarStyle
? statusBarStyle
: { statusBar: statusBarStyle, navigationBar: navigationBarStyle },
hidden:
statusBarHidden === navigationBarHidden
? statusBarHidden
: { statusBar: statusBarHidden, navigationBar: navigationBarHidden },
}),
[statusBarStyle, navigationBarStyle, statusBarHidden, navigationBarHidden],
);
const colorScheme = useColorScheme();
const stackEntryRef = useRef<SystemBarsEntry | null>(null);
useEffect(() => {
// Every time a SystemBars component is mounted, we push it's prop to a stack
// and always update the native system bars with the props from the top of then
// stack. This allows having multiple SystemBars components and the one that is
// added last or is deeper in the view hierarchy will have priority.
stackEntryRef.current = pushStackEntry(stableProps);
return () => {
// When a SystemBars is unmounted, remove itself from the stack and update
// the native bars with the next props.
if (stackEntryRef.current) {
popStackEntry(stackEntryRef.current);
}
};
}, []);
useEffect(() => {
if (stackEntryRef.current) {
stackEntryRef.current = replaceStackEntry(
stackEntryRef.current,
stableProps,
);
}
}, [colorScheme, stableProps]);
return null;
}
SystemBars.pushStackEntry = pushStackEntry;
SystemBars.popStackEntry = popStackEntry;
SystemBars.replaceStackEntry = replaceStackEntry;