diff --git a/src/NativeSafeAreaProvider.web.tsx b/src/NativeSafeAreaProvider.web.tsx index 5bcdf5c..3f9cb2e 100644 --- a/src/NativeSafeAreaProvider.web.tsx +++ b/src/NativeSafeAreaProvider.web.tsx @@ -17,6 +17,7 @@ export function NativeSafeAreaProvider({ style, onInsetsChange, ref, + unstable_disableViewOnWeb, }: NativeSafeAreaProviderProps) { const viewRef = React.useRef(null); @@ -121,6 +122,10 @@ export function NativeSafeAreaProvider({ }; }, [onInsetsChange]); + if (unstable_disableViewOnWeb) { + return <>{children}; + } + return ( {children} diff --git a/src/SafeArea.types.ts b/src/SafeArea.types.ts index 5672e5d..efd4250 100644 --- a/src/SafeArea.types.ts +++ b/src/SafeArea.types.ts @@ -35,6 +35,13 @@ export interface NativeSafeAreaProviderProps extends ViewProps { ref?: React.Ref; children?: React.ReactNode; onInsetsChange: InsetChangeNativeCallback; + /** + * Web only, undocumented and unstable. Render children without the wrapping + * view. Since there is no view to measure, insets and frame fall back to + * window values, and `style` and `ref` have nothing to apply to. Has no + * effect on other platforms. + */ + unstable_disableViewOnWeb?: boolean; } export interface NativeSafeAreaViewProps extends ViewProps { diff --git a/src/SafeAreaContext.tsx b/src/SafeAreaContext.tsx index 72f2c0b..bb03b94 100644 --- a/src/SafeAreaContext.tsx +++ b/src/SafeAreaContext.tsx @@ -35,6 +35,13 @@ export interface SafeAreaProviderProps extends ViewProps { * @deprecated */ initialSafeAreaInsets?: EdgeInsets | null; + /** + * Web only, undocumented and unstable. Render children without the wrapping + * view. Since there is no view to measure, insets and frame fall back to + * window values, and `style` and `ref` have nothing to apply to. Has no + * effect on other platforms. + */ + unstable_disableViewOnWeb?: boolean; } export function SafeAreaProvider({ diff --git a/src/__tests__/NativeSafeAreaProvider.web-test.tsx b/src/__tests__/NativeSafeAreaProvider.web-test.tsx index c34aaf4..498f5db 100644 --- a/src/__tests__/NativeSafeAreaProvider.web-test.tsx +++ b/src/__tests__/NativeSafeAreaProvider.web-test.tsx @@ -13,10 +13,12 @@ import { import * as React from 'react'; import { act } from 'react'; import { createRoot, type Root } from 'react-dom/client'; +import type { View } from 'react-native'; import type { EdgeInsets, InsetChangeNativeCallback, Metrics, + NativeSafeAreaProviderProps, } from '../SafeArea.types'; import { NativeSafeAreaProvider } from '../NativeSafeAreaProvider.web'; @@ -123,12 +125,17 @@ let root: Root | null = null; let host: HTMLElement | null = null; let rectMock: ReturnType; -function mountProvider(onInsetsChange: InsetChangeNativeCallback) { +function mountProvider( + onInsetsChange: InsetChangeNativeCallback, + props?: Partial, +) { const newHost = document.createElement('div'); document.body.appendChild(newHost); const newRoot = createRoot(newHost); act(() => { - newRoot.render(); + newRoot.render( + , + ); }); root = newRoot; host = newHost; @@ -281,4 +288,65 @@ describe('NativeSafeAreaProvider.web', () => { frame: { x: 0, y: 0, width: 800, height: 600 }, }); }); + + describe('unstable_disableViewOnWeb', () => { + it('wraps children in a view by default', () => { + const onInsetsChange = jest.fn(); + mountProvider(onInsetsChange, { children: }); + expect(host?.innerHTML).toBe('
'); + }); + + it('renders children without a wrapping element when enabled', () => { + const onInsetsChange = jest.fn(); + mountProvider(onInsetsChange, { + unstable_disableViewOnWeb: true, + children: , + }); + expect(host?.innerHTML).toBe(''); + }); + + it('attaches ref to the wrapping view by default', () => { + const onInsetsChange = jest.fn(); + const ref = React.createRef(); + mountProvider(onInsetsChange, { ref, children: }); + expect(ref.current).not.toBeNull(); + }); + + it('leaves ref null when enabled, since there is no view to attach', () => { + const onInsetsChange = jest.fn(); + const ref = React.createRef(); + mountProvider(onInsetsChange, { + ref, + unstable_disableViewOnWeb: true, + children: , + }); + expect(ref.current).toBeNull(); + }); + + it('reports window insets and frame when enabled', () => { + // Element rects are still measurable, but there is no provider view to + // measure, so metrics come from the window rather than from this rect. + rectMock.mockReturnValue(makeRect(20, 30, 200, 300)); + const onInsetsChange = jest.fn(); + mountProvider(onInsetsChange, { unstable_disableViewOnWeb: true }); + expect(lastMetrics(onInsetsChange)).toEqual({ + insets: WINDOW_INSETS, + frame: { x: 0, y: 0, width: WINDOW_WIDTH, height: WINDOW_HEIGHT }, + }); + }); + + it('keeps reporting window metrics on resize when enabled', () => { + const onInsetsChange = jest.fn(); + mountProvider(onInsetsChange, { unstable_disableViewOnWeb: true }); + setWindowDimensions(800, 600); + mockWindowInsets({ top: 20, bottom: 10, left: 0, right: 0 }); + act(() => { + window.dispatchEvent(new Event('resize')); + }); + expect(lastMetrics(onInsetsChange)).toEqual({ + insets: { top: 20, bottom: 10, left: 0, right: 0 }, + frame: { x: 0, y: 0, width: 800, height: 600 }, + }); + }); + }); });