diff --git a/native-views/react-native-native-overlay/docs/SPEC.md b/native-views/react-native-native-overlay/docs/SPEC.md index 0443d7421..4759d1880 100644 --- a/native-views/react-native-native-overlay/docs/SPEC.md +++ b/native-views/react-native-native-overlay/docs/SPEC.md @@ -156,6 +156,7 @@ visible)`, `removePage(ownerKey)`. dismissOnBackPress dismissOnPanDown // sheet and toast only anchor? // anchored only: a trigger ref or a window rect + contentSize? // native non-sheet only: content frame instead of the window size onPresented onDismissRequest onDismissed(reason) /> ``` @@ -432,6 +433,27 @@ the correction: - Rects already in screen space (for example from native lists) need no correction. +### Content size for windows about to rotate + +Implemented; Android runtime-verified with OneKey's native chart fullscreen on +an API 36 emulator. iOS takes the same JS layout path but was not +runtime-verified. Non-sheet content is laid out by JS at the window size it +last saw (`useWindowDimensions`), and the Android content host clips the +content to that frame. JS hears about a rotation only after the window has +rotated, so until then the content keeps the previous orientation's frame and +is clipped to it. + +- `contentSize?: { width, height }` (non-sheet presentations) replaces the + window size for the content frame. A caller that prepares content for the + window it is about to rotate into passes the rotated size, so the first + rotated frame shows all of it. Omitted, the content follows the window size + as before. +- The content stays anchored at the host origin. While the window has not + rotated yet, content larger than the window draws past its edges and takes + no touches there; the caller keeps it hidden until the window matches. +- Sheets ignore it: they size their content from the sheet geometry. Web + ignores it: the DOM overlay sizes itself with CSS. + ### Web keyboard focus containment The topmost active, unsuspended blocking Web entry MUST contain sequential @@ -580,6 +602,9 @@ Automated (P0): - page suspend, restore and removal; - back resolution (global first, `block`, `pass`, focused host) and the blocking top. +- `src/__tests__/contentSize.test.ts` covers the non-sheet content frame: + the window size by default, and a caller size before and after the window + size reaches JS. - `src/__tests__/animation.test.ts` covers: - the spring response in all damping regimes, overshoot clamping and settle time; diff --git a/native-views/react-native-native-overlay/src/OverlayView.tsx b/native-views/react-native-native-overlay/src/OverlayView.tsx index 3f0f1465f..e3a34fd28 100644 --- a/native-views/react-native-native-overlay/src/OverlayView.tsx +++ b/native-views/react-native-native-overlay/src/OverlayView.tsx @@ -13,6 +13,7 @@ import { import NativeOverlay from './NativeOverlayNativeComponent'; import { OverlayLevelProvider } from './OverlayLevelContext'; import { isBlockingLevel } from './OverlayLevels'; +import { resolveContentSize } from './contentSize'; import { resolveSheetWidth } from './sheetWidth'; import { useOverlayController } from './useOverlayController'; @@ -72,6 +73,7 @@ export function OverlayView(props: IOverlayViewProps) { testID, windowOffsetRequest, onWindowOffset, + contentSize, } = props; // A plain sibling at the host's spot: its JS-measured origin shares the // modal-relative space of the caller's measurements, while native reports @@ -284,7 +286,14 @@ export function OverlayView(props: IOverlayViewProps) { {children} diff --git a/native-views/react-native-native-overlay/src/OverlayViewTypes.ts b/native-views/react-native-native-overlay/src/OverlayViewTypes.ts index c3c6ffba7..8efa4c2b9 100644 --- a/native-views/react-native-native-overlay/src/OverlayViewTypes.ts +++ b/native-views/react-native-native-overlay/src/OverlayViewTypes.ts @@ -88,6 +88,14 @@ export interface IOverlayViewProps { */ windowOffsetRequest?: number; onWindowOffset?: (offset: { x: number; y: number }) => void; + /** + * Native only, non-sheet presentations. Lays the content out at this size + * instead of the window size JS last saw. The content host clips the + * content to that frame, and JS hears about a rotation only after the + * window has rotated: content prepared for the window it is about to rotate + * into passes the rotated size, so the first rotated frame shows all of it. + */ + contentSize?: { width: number; height: number }; /** The exit animation finished and the content unmounted. */ onClose?: (reason: IOverlayDismissReason) => void; children?: ReactNode; diff --git a/native-views/react-native-native-overlay/src/__tests__/contentSize.test.ts b/native-views/react-native-native-overlay/src/__tests__/contentSize.test.ts new file mode 100644 index 000000000..cd8144e4a --- /dev/null +++ b/native-views/react-native-native-overlay/src/__tests__/contentSize.test.ts @@ -0,0 +1,29 @@ +import { resolveContentSize } from '../contentSize'; + +it('follows the window size by default', () => { + expect(resolveContentSize({ windowWidth: 360, windowHeight: 640 })).toEqual({ + width: 360, + height: 640, + }); +}); + +it('keeps a caller size while the window has not rotated yet', () => { + // Prepared for the landscape window the portrait one is rotating into. + expect( + resolveContentSize({ + windowWidth: 360, + windowHeight: 640, + contentSize: { width: 640, height: 360 }, + }) + ).toEqual({ width: 640, height: 360 }); +}); + +it('keeps a caller size after the window size reaches JS', () => { + expect( + resolveContentSize({ + windowWidth: 640, + windowHeight: 360, + contentSize: { width: 640, height: 360 }, + }) + ).toEqual({ width: 640, height: 360 }); +}); diff --git a/native-views/react-native-native-overlay/src/contentSize.ts b/native-views/react-native-native-overlay/src/contentSize.ts new file mode 100644 index 000000000..78e41b9cf --- /dev/null +++ b/native-views/react-native-native-overlay/src/contentSize.ts @@ -0,0 +1,21 @@ +/** + * Non-sheet content geometry. JS lays the content out at the window size it + * last saw, and the native content host clips the content to that frame. Right + * after the window rotates, JS still holds the previous size, so a caller that + * prepared its content for the window it is about to rotate into passes that + * size instead. + */ +export function resolveContentSize({ + windowWidth, + windowHeight, + contentSize, +}: { + windowWidth: number; + windowHeight: number; + contentSize?: { width: number; height: number }; +}): { width: number; height: number } { + return { + width: contentSize?.width ?? windowWidth, + height: contentSize?.height ?? windowHeight, + }; +}