Skip to content

Instantly share code, notes, and snippets.

@vincentriemer
Last active September 9, 2019 16:10
Show Gist options
  • Save vincentriemer/4ed15a8c24866b26c66a56269f478f2e to your computer and use it in GitHub Desktop.
Save vincentriemer/4ed15a8c24866b26c66a56269f478f2e to your computer and use it in GitHub Desktop.
"Safe Area Inset" value tracking (most immediately useful with iPhone X) via React context & hooks.
/**
* @flow
*/
import * as React from "react";
type SafeAreaInsetsContextType = $ReadOnly<{|
top: number,
left: number,
right: number,
bottom: number
|}>;
let defaultInsetValue: SafeAreaInsetsContextType = {
top: 0,
left: 0,
right: 0,
bottom: 0
};
if (process.env.NODE_ENV !== "production") {
// in development, warn if the default inset value is accessed since it means the
// hook was called in a component that wasn't wrapped in the context's provider
const handler = {
get: function() {
console.warn(
"[WARNING]: useSafeAreaInsets called in React tree without being wrapped in a <SafeAreaInsetsProvider />"
);
return Reflect.get(...arguments);
}
};
defaultInsetValue = new Proxy(defaultInsetValue, handler);
}
const SafeAreaInsetsContext: React.Context<SafeAreaInsetsContextType> = React.createContext(
defaultInsetValue
);
function getInsetsFromTestElement(testElement: HTMLElement) {
const style: CSSStyleDeclaration = getComputedStyle(testElement);
const top = style.getPropertyValue("padding-top");
const right = style.getPropertyValue("padding-right");
const left = style.getPropertyValue("padding-left");
const bottom = style.getPropertyValue("padding-bottom");
return {
top: parseFloat(top),
right: parseFloat(right),
left: parseFloat(left),
bottom: parseFloat(bottom)
};
}
// shared measurement element that's lazily initialized in getMeasurementElement
let measurementElement: HTMLElement | null = null;
function getMeasurementElement(): HTMLElement {
if (measurementElement === null) {
measurementElement = document.createElement("div");
Object.assign(measurementElement.style, {
// position hidden & off-screen
position: "absolute",
top: `${-Number.MAX_VALUE}px`,
left: `${-Number.MAX_VALUE}px`,
opacity: "0",
// setup inset properties
paddingTop: "env(safe-area-inset-top)",
paddingRight: "env(safe-area-inset-right)",
paddingLeft: "env(safe-area-inset-left)",
paddingBottom: "env(safe-area-inset-bottom)",
// listen to inset properties through transitionend events
transitionProperty:
"padding-top, padding-right, padding-left, padding-bottom",
transitionDuration: `${Number.MIN_VALUE}s`
});
// mount measurement element to DOM
document.body && document.body.appendChild(measurementElement);
}
return measurementElement;
}
type Props = $ReadOnly<{|
children: React.Node
|}>;
export const SafeAreaInsetsProvider: React.ComponentType<Props> = React.memo(
props => {
const measureElementRef = React.useRef(getMeasurementElement());
const [insets, setInsets] = React.useState(() =>
getInsetsFromTestElement(measureElementRef.current)
);
const handleNewMeasurement = React.useCallback(() => {
const newInsets = getInsetsFromTestElement(measureElementRef.current);
setInsets(() => newInsets);
}, []);
// listen to changes in the safe area insets
React.useEffect(() => {
const measureElement = measureElementRef.current;
measureElement.addEventListener("transitionend", handleNewMeasurement);
return () => {
measureElement.removeEventListener(
"transitionend",
handleNewMeasurement
);
};
}, [handleNewMeasurement]);
return (
<SafeAreaInsetsContext.Provider value={insets}>
{props.children}
</SafeAreaInsetsContext.Provider>
);
}
);
SafeAreaInsetsProvider.displayName = "SafeAreaInsetsProvider";
export const useSafeAreaInsets = (): SafeAreaInsetsContextType => {
return React.useContext(SafeAreaInsetsContext);
};
// App.js ================================================
import * as React from "react";
import { useSafeAreaInsets } from "./SafeAreaInsets";
export default () => {
const minPadding = 20;
const insets = useSafeAreaInsets();
return (
<div
style={{
paddingLeft: Math.max(insets.left, minPadding),
paddingTop: Math.max(insets.top, minPadding),
paddingRight: Math.max(insets.right, minPadding),
paddingBottom: Math.max(insets.bottom, minPadding)
}}
>
{/* ... */}
</div>
);
};
// AppRoot.js ============================================
import * as React from "react";
import App from "./App";
import { SafeAreaInsetsProvider } from "./SafeAreaInsets";
export default () => (
<SafeAreaInsetsProvider>
<App />
</SafeAreaInsetsProvider>
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment