diff --git a/helpers/device-selector.js b/helpers/device-selector.js index fd21dcc1..ba3a15a5 100644 --- a/helpers/device-selector.js +++ b/helpers/device-selector.js @@ -4,11 +4,22 @@ import { initialWindowMetrics } from 'react-native-safe-area-context'; export const NAVIGATION_HEADER_HEIGHT = 64; +// Fallback inset values used when initialWindowMetrics is null (race condition +// in large apps where JS evaluates before the safe-area native module inits). +// Bottom: 34pt — consistent across all notched iPhones since iPhone X (2017). +// Top: 59pt — matches Dynamic Island devices (iPhone 14+). +// Slightly too large for older notched devices (44–48pt), but using the +// largest one prevents content from being hidden under the notch/Dynamic Island. +const DEFAULT_IOS_TOP_INSET = 59; +const DEFAULT_IOS_BOTTOM_INSET = 34; + export const isNotchedAndroid = Platform.OS === 'android' && DeviceInfo.hasNotch(); export const NAVIGATION_BAR_HEIGHT = Platform.select({ - ios: NAVIGATION_HEADER_HEIGHT + initialWindowMetrics?.insets?.top, + ios: + NAVIGATION_HEADER_HEIGHT + + (initialWindowMetrics?.insets?.top ?? DEFAULT_IOS_TOP_INSET), android: isNotchedAndroid ? NAVIGATION_HEADER_HEIGHT + StatusBar.currentHeight : NAVIGATION_HEADER_HEIGHT, @@ -16,10 +27,12 @@ export const NAVIGATION_BAR_HEIGHT = Platform.select({ }); export const HOME_INDICATOR_PADDING = - Platform.OS === 'ios' ? initialWindowMetrics?.insets?.bottom : 0; + Platform.OS === 'ios' + ? initialWindowMetrics?.insets?.bottom ?? DEFAULT_IOS_BOTTOM_INSET + : 0; export const NOTCH_AREA_HEIGHT = Platform.select({ - ios: initialWindowMetrics?.insets?.top, + ios: initialWindowMetrics?.insets?.top ?? DEFAULT_IOS_TOP_INSET, android: isNotchedAndroid ? StatusBar.currentHeight : 0, default: 0, }); @@ -30,3 +43,49 @@ export const Device = { NOTCH_AREA_HEIGHT, NAVIGATION_HEADER_HEIGHT, }; + +// Lazy getters — resolve initialWindowMetrics on first render, +// when the native module is guaranteed to be ready. +let _topInset; +let _bottomInset; + +const getTopInset = () => { + if (_topInset === undefined) { + // eslint-disable-next-line global-require + const metrics = require('react-native-safe-area-context') + .initialWindowMetrics; + _topInset = metrics?.insets?.top ?? DEFAULT_IOS_TOP_INSET; + } + + return _topInset; +}; + +const getBottomInset = () => { + if (_bottomInset === undefined) { + // eslint-disable-next-line global-require + const metrics = require('react-native-safe-area-context') + .initialWindowMetrics; + _bottomInset = metrics?.insets?.bottom ?? DEFAULT_IOS_BOTTOM_INSET; + } + + return _bottomInset; +}; + +export const getNavigationBarHeight = () => + Platform.select({ + ios: NAVIGATION_HEADER_HEIGHT + getTopInset(), + android: isNotchedAndroid + ? NAVIGATION_HEADER_HEIGHT + StatusBar.currentHeight + : NAVIGATION_HEADER_HEIGHT, + default: NAVIGATION_HEADER_HEIGHT, + }); + +export const getHomeIndicatorPadding = () => + Platform.OS === 'ios' ? getBottomInset() : 0; + +export const getNotchAreaHeight = () => + Platform.select({ + ios: getTopInset(), + android: isNotchedAndroid ? StatusBar.currentHeight : 0, + default: 0, + }); diff --git a/helpers/index.js b/helpers/index.js index d032a487..034bd7d7 100644 --- a/helpers/index.js +++ b/helpers/index.js @@ -1,5 +1,8 @@ export { Device, + getHomeIndicatorPadding, + getNavigationBarHeight, + getNotchAreaHeight, HOME_INDICATOR_PADDING, isNotchedAndroid, NAVIGATION_BAR_HEIGHT, diff --git a/index.js b/index.js index 2341a826..0d98f4ad 100644 --- a/index.js +++ b/index.js @@ -78,7 +78,14 @@ export { YearRangePicker } from './components/YearRangePicker'; export * from './hooks'; // Helpers -export { calculateKeyboardOffset, Device, Keyboard } from './helpers'; +export { + calculateKeyboardOffset, + Device, + getHomeIndicatorPadding, + getNavigationBarHeight, + getNotchAreaHeight, + Keyboard, +} from './helpers'; // HTML export { Html } from './html'; diff --git a/package.json b/package.json index 34ea05fa..bb7cf0d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@shoutem/ui", - "version": "9.0.4", + "version": "9.0.5", "description": "Styleable set of components for React Native applications", "scripts": { "lint": "eslint .",