NativeSafeAreaView.web.tsx 3.3KB

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