Last active
September 9, 2019 16:10
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @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); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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