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