UNPKG

react-native-navigation-mode

Version:

Detect Android navigation mode (3-button, 2-button, or gesture)

158 lines (144 loc) 5.69 kB
"use strict"; import { Platform, Dimensions, AppState } from 'react-native'; import { useEffect, useRef, useState } from 'react'; import NavigationModeModule from "./NativeNavigationMode.js"; // Delay before the post-rotation settle re-fetch (see useNavigationMode). const INSETS_SETTLE_DELAY_MS = 250; /** * Get detailed navigation mode information * Returns navigation type, interaction mode, and device info */ export function getNavigationMode() { // null check is redundant as it's always null for iOS but it's there to satisfy TypeScript if (Platform.OS === 'ios' || NavigationModeModule === null) { // iOS always uses gesture navigation (no 3-button navigation exists) return Promise.resolve({ type: 'gesture', isGestureNavigation: true, navigationBarHeight: 0 // iOS doesn't have a navigation bar like Android }); } // Only call native module on Android return NavigationModeModule.getNavigationMode(); } /** * Quick check if the device is using gesture-based navigation * @returns Promise<boolean> - true if gesture navigation is active */ export function isGestureNavigation() { // null check is redundant as it's always null for iOS but it's there to satisfy TypeScript if (Platform.OS === 'ios' || NavigationModeModule === null) { // iOS always uses gesture navigation return Promise.resolve(true); } // Only call native module on Android return NavigationModeModule.isGestureNavigation(); } /** * Get the navigation bar height in dp * @returns Promise<number> - navigation bar height in dp */ export function getNavigationBarHeight() { // null check is redundant as it's always null for iOS but it's there to satisfy TypeScript if (Platform.OS === 'ios' || NavigationModeModule === null) { // iOS doesn't have a navigation bar like Android return Promise.resolve(0); } // Only call native module on Android return NavigationModeModule.getNavigationBarHeight(); } /** * Hook for React components to get navigation mode. * * Re-fetches automatically when: * - the device rotates / the window is resized (the navigation bar height * differs between portrait and landscape, and on phones the 3-/2-button bar * even moves to a different edge), and * - the app returns to the foreground (the user may switch the system * navigation mode in Settings while the app is backgrounded). */ export function useNavigationMode() { const [navigationMode, setNavigationMode] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // Tracks whether the hook is still mounted so async resolves that land after // unmount (or after a superseding re-fetch) don't call setState. const mountedRef = useRef(true); // Latest-wins guard: rotation can fire several events in quick succession, so // ignore the result of any fetch that has been superseded by a newer one. const requestIdRef = useRef(0); useEffect(() => { mountedRef.current = true; // Note: we intentionally do NOT set `loading` back to true on re-fetch. // `loading` reflects the initial load only; flipping it on every rotation // would flash consumer UIs (spinners) on each orientation change. async function fetchNavigationMode() { const requestId = ++requestIdRef.current; try { const mode = await getNavigationMode(); if (mountedRef.current && requestId === requestIdRef.current) { setNavigationMode(mode); setError(null); } } catch (err) { if (mountedRef.current && requestId === requestIdRef.current) { setError(err instanceof Error ? err : new Error('Unknown error')); } } finally { // Same latest-wins guard as above: a superseded fetch must not end the // loading state, or consumers would briefly see loading=false with // navigationMode still null. if (mountedRef.current && requestId === requestIdRef.current) { setLoading(false); } } } // Initial fetch. fetchNavigationMode(); // Android recalculates and dispatches WindowInsets asynchronously after a // configuration change, so a fetch fired straight from the Dimensions // 'change' event can still read the pre-rotation insets. A single delayed // re-fetch corrects that; the latest-wins guard keeps ordering safe. let settleTimer = null; // Re-fetch on orientation / window-size change (rotation). const dimensionsSub = Dimensions.addEventListener('change', () => { fetchNavigationMode(); if (settleTimer !== null) { clearTimeout(settleTimer); } settleTimer = setTimeout(() => { settleTimer = null; fetchNavigationMode(); }, INSETS_SETTLE_DELAY_MS); }); // Re-fetch when the app returns to the foreground. const appStateSub = AppState.addEventListener('change', nextState => { if (nextState === 'active') { fetchNavigationMode(); } }); return () => { mountedRef.current = false; if (settleTimer !== null) { clearTimeout(settleTimer); } // RN 0.79+: addEventListener returns an EventSubscription with .remove(). // Always wrap in an arrow (do not return the bare `.remove` reference) to // avoid the lost-`this` crash in facebook/react-native#34508. dimensionsSub.remove(); appStateSub.remove(); }; }, []); return { navigationMode, loading, error }; } export default { getNavigationMode, isGestureNavigation, getNavigationBarHeight, useNavigationMode }; //# sourceMappingURL=index.js.map