1/* eslint-env browser */ 2 3import * as React from 'react'; 4import { View } from 'react-native'; 5import { NativeSafeAreaProviderProps } from './SafeArea.types'; 6 7/** 8 * TODO: 9 * Currently insets and frame are based on the window and are not 10 * relative to the provider view. This is inconsistent with iOS and Android. 11 * However in most cases if the provider view covers the screen this is not 12 * an issue. 13 */ 14 15const CSSTransitions: Record<string, string> = { 16 WebkitTransition: 'webkitTransitionEnd', 17 Transition: 'transitionEnd', 18 MozTransition: 'transitionend', 19 MSTransition: 'msTransitionEnd', 20 OTransition: 'oTransitionEnd', 21}; 22 23export default function NativeSafeAreaView({ 24 children, 25 style, 26 onInsetsChange, 27}: NativeSafeAreaProviderProps) { 28 React.useEffect(() => { 29 // Skip for SSR. 30 if (typeof document === 'undefined') { 31 return; 32 } 33 34 const element = createContextElement(); 35 document.body.appendChild(element); 36 const onEnd = () => { 37 const { paddingTop, paddingBottom, paddingLeft, paddingRight } = 38 window.getComputedStyle(element); 39 40 const insets = { 41 top: paddingTop ? parseInt(paddingTop, 10) : 0, 42 bottom: paddingBottom ? parseInt(paddingBottom, 10) : 0, 43 left: paddingLeft ? parseInt(paddingLeft, 10) : 0, 44 right: paddingRight ? parseInt(paddingRight, 10) : 0, 45 }; 46 const frame = { 47 x: 0, 48 y: 0, 49 width: document.documentElement.offsetWidth, 50 height: document.documentElement.offsetHeight, 51 }; 52 // @ts-ignore: missing properties 53 onInsetsChange({ nativeEvent: { insets, frame } }); 54 }; 55 element.addEventListener(getSupportedTransitionEvent(), onEnd); 56 onEnd(); 57 return () => { 58 document.body.removeChild(element); 59 element.removeEventListener(getSupportedTransitionEvent(), onEnd); 60 }; 61 }, [onInsetsChange]); 62 63 return <View style={style}>{children}</View>; 64} 65 66let _supportedTransitionEvent: string | null = null; 67function getSupportedTransitionEvent(): string { 68 if (_supportedTransitionEvent !== null) { 69 return _supportedTransitionEvent; 70 } 71 const element = document.createElement('invalidtype'); 72 73 _supportedTransitionEvent = CSSTransitions.Transition; 74 for (const key in CSSTransitions) { 75 if (element.style[key as keyof CSSStyleDeclaration] !== undefined) { 76 _supportedTransitionEvent = CSSTransitions[key]; 77 break; 78 } 79 } 80 return _supportedTransitionEvent; 81} 82 83type CssEnv = 'constant' | 'env'; 84 85let _supportedEnv: CssEnv | null = null; 86function getSupportedEnv(): CssEnv { 87 if (_supportedEnv !== null) { 88 return _supportedEnv; 89 } 90 const { CSS } = window; 91 if ( 92 CSS && 93 CSS.supports && 94 CSS.supports('top: constant(safe-area-inset-top)') 95 ) { 96 _supportedEnv = 'constant'; 97 } else { 98 _supportedEnv = 'env'; 99 } 100 return _supportedEnv; 101} 102 103function getInset(side: string): string { 104 return `${getSupportedEnv()}(safe-area-inset-${side})`; 105} 106 107function createContextElement(): HTMLElement { 108 const element = document.createElement('div'); 109 const { style } = element; 110 style.position = 'fixed'; 111 style.left = '0'; 112 style.top = '0'; 113 style.width = '0'; 114 style.height = '0'; 115 style.zIndex = '-1'; 116 style.overflow = 'hidden'; 117 style.visibility = 'hidden'; 118 // Bacon: Anything faster than this and the callback will be invoked too early with the wrong insets 119 style.transitionDuration = '0.05s'; 120 style.transitionProperty = 'padding'; 121 style.transitionDelay = '0s'; 122 style.paddingTop = getInset('top'); 123 style.paddingBottom = getInset('bottom'); 124 style.paddingLeft = getInset('left'); 125 style.paddingRight = getInset('right'); 126 return element; 127} 128