feat(iOS, ContainedModal): Add basic setup for ContainedModal native component#4190
Open
sgaczol wants to merge 25 commits into
Open
feat(iOS, ContainedModal): Add basic setup for ContainedModal native component#4190sgaczol wants to merge 25 commits into
sgaczol wants to merge 25 commits into
Conversation
such approach is temporary - implementing a custom shadow node will be handled in a separate pr
- ContainedModal.tsx - ContainedModal.types.ts - ContainedModalProvider.tsx - ContainedModalProvider.types.ts - index.tsx
…inerId for the modal
f74bf6a to
2d121b6
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a new experimental iOS-contained modal primitive (ContainedModal) and its bounding container (ContainedModalProvider) to react-native-screens, including the Fabric/codegen surface, native iOS implementation (host anchor + teleported content + presentation manager), and a single-feature test scenario in the example app.
Changes:
- Added JS components/types (with Android/Web no-op shims) and exported them via
react-native-screens/experimental. - Added Fabric codegen specs + component registry wiring for the new host/provider native components.
- Implemented the iOS native contained-modal stack (provider controller, host/proxy/state syncing, presentation manager/update coordination) and added a single-feature test scenario.
Reviewed changes
Copilot reviewed 45 out of 45 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/fabric/gamma/modals/contained-modal/ContainedModalProviderNativeComponent.ts | Codegen spec for the provider native component (Fabric). |
| src/fabric/gamma/modals/contained-modal/ContainedModalHostNativeComponent.ts | Codegen spec for the host native component (Fabric, interfaceOnly). |
| src/experimental/index.ts | Exports ContainedModal/Provider from the experimental entry point. |
| src/components/gamma/modals/contained-modal/index.ts | Barrel exports for the contained-modal components/types. |
| src/components/gamma/modals/contained-modal/ContainedModalProvider.web.tsx | Web no-op stub with warning. |
| src/components/gamma/modals/contained-modal/ContainedModalProvider.types.ts | Public JS types for ContainedModalProvider. |
| src/components/gamma/modals/contained-modal/ContainedModalProvider.tsx | iOS/native implementation wrapper using the provider native component. |
| src/components/gamma/modals/contained-modal/ContainedModalProvider.android.tsx | Android no-op stub with warning. |
| src/components/gamma/modals/contained-modal/ContainedModal.web.tsx | Web no-op stub with warning. |
| src/components/gamma/modals/contained-modal/ContainedModal.types.ts | Public JS types for ContainedModal. |
| src/components/gamma/modals/contained-modal/ContainedModal.tsx | iOS/native implementation wrapper using a zero-sized host anchor. |
| src/components/gamma/modals/contained-modal/ContainedModal.android.tsx | Android no-op stub with warning. |
| package.json | Registers the new Fabric component classes in the codegen component map. |
| ios/stubs/RNSGammaStubs.mm | Adds Gamma stubs for the new component views. |
| ios/stubs/RNSGammaStubs.h | Adds Gamma stub declarations for the new component views. |
| ios/gamma/modals/contained-modal/RNSContainedModalUpdateFlags.h | Defines update flags for presentation/appearance/behavior. |
| ios/gamma/modals/contained-modal/RNSContainedModalUpdateCoordinator.mm | Implements update-flag coordination helpers. |
| ios/gamma/modals/contained-modal/RNSContainedModalUpdateCoordinator.h | Declares update-flag coordination helpers. |
| ios/gamma/modals/contained-modal/RNSContainedModalProviders.h | Declares provider protocol used to drive presentation. |
| ios/gamma/modals/contained-modal/RNSContainedModalProviderController.mm | Defines a provider VC that sets definesPresentationContext. |
| ios/gamma/modals/contained-modal/RNSContainedModalProviderController.h | Header for provider VC. |
| ios/gamma/modals/contained-modal/RNSContainedModalProviderComponentView.mm | Provider Fabric component view embedding the provider VC and reparenting children. |
| ios/gamma/modals/contained-modal/RNSContainedModalProviderComponentView.h | Header for provider component view + containerId API. |
| ios/gamma/modals/contained-modal/RNSContainedModalPresentationState.h | Presentation state enum for manager state machine. |
| ios/gamma/modals/contained-modal/RNSContainedModalPresentationManager.mm | Present/dismiss state machine + provider resolution/caching. |
| ios/gamma/modals/contained-modal/RNSContainedModalPresentationManager.h | Header for presentation manager. |
| ios/gamma/modals/contained-modal/RNSContainedModalHostShadowStateProxy.mm | Updates Fabric shadow state with teleported content bounds/origin offset. |
| ios/gamma/modals/contained-modal/RNSContainedModalHostShadowStateProxy.h | Header for host shadow-state proxy. |
| ios/gamma/modals/contained-modal/RNSContainedModalHostComponentView.mm | Host anchor Fabric component view: mounts children into content VC, manages touch handler + state sync. |
| ios/gamma/modals/contained-modal/RNSContainedModalHostComponentView.h | Header for host component view. |
| ios/gamma/modals/contained-modal/RNSContainedModalContentView.mm | Clear background content view used by the presented controller. |
| ios/gamma/modals/contained-modal/RNSContainedModalContentView.h | Header for content view reparenting helpers. |
| ios/gamma/modals/contained-modal/RNSContainedModalContentController.mm | Presented controller: update flushing + presentation manager invocation + delegate callbacks. |
| ios/gamma/modals/contained-modal/RNSContainedModalContentController.h | Header for content controller and its signals/delegate. |
| ios/gamma/modals/contained-modal/RNSContainedModalConfigurationApplicator.mm | Placeholder configuration applicator (scaffolding). |
| ios/gamma/modals/contained-modal/RNSContainedModalConfigurationApplicator.h | Header for configuration applicator (scaffolding). |
| common/cpp/react/renderer/components/rnscreens/RNSContainedModalHostState.h | C++ state definition for host (frame size + content offset). |
| common/cpp/react/renderer/components/rnscreens/RNSContainedModalHostShadowNode.h | Shadow node definition with overridden content-origin offset. |
| common/cpp/react/renderer/components/rnscreens/RNSContainedModalHostShadowNode.cpp | Shadow node implementation. |
| common/cpp/react/renderer/components/rnscreens/RNSContainedModalHostComponentDescriptor.h | Component descriptor adopting size from state (layout integration). |
| apps/src/tests/single-feature-tests/index.tsx | Adds ContainedModal scenario group to example app scenario registry. |
| apps/src/tests/single-feature-tests/contained-modal/test-contained-modal-base-ios/scenario.md | Manual test plan/scenario for contained modal behavior. |
| apps/src/tests/single-feature-tests/contained-modal/test-contained-modal-base-ios/scenario-description.ts | Scenario metadata shown in the test app. |
| apps/src/tests/single-feature-tests/contained-modal/test-contained-modal-base-ios/index.tsx | Single-feature test implementation showcasing provider-bounded presentation + touch routing. |
| apps/src/tests/single-feature-tests/contained-modal/index.ts | Scenario-group entry point for contained-modal tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+33
to
+34
| @interface RNSContainedModalProviderHostComponentView : NSObject | ||
| @end |
|
|
||
| const ContainedModalScenarioGroup: ScenarioGroup<keyof typeof scenarios> = { | ||
| name: 'ContainedModal', | ||
| details: 'Single feature tests for ContainedModals', |
Collaborator
Author
There was a problem hiding this comment.
'ContainedModals' makes more sense, especially when we add a different style (CurrentContext) in the future
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Introducing the iOS implementation of the standalone
ContainedModalcomponent, together with itsContainedModalProvider.A
ContainedModalis presented within the bounds of a provider instead of over the whole window. The two are matched by id: the modal'stargetContainerIdis compared against a provider'scontainerId, and the modal is presented inside the matching provider's frame. This makes it possible to scope a modal to an arbitrary region of the screen (e.g. one column of a split layout) rather than the full screen.Key architectural decisions:
ContainedModalhost is a zero-sized, absolutely-positioned logical anchor — it never participates in layout of its siblings. The actual React children are "teleported" and mounted inside a separateUIViewControllerhierarchy (RNSContainedModalContentController/RNSContainedModalContentView), so the host'shitTest:always returnsniland never intercepts touches meant for the content behind it.ContainedModalProvideris, by contrast, a regular space-filling container. Its bounds define the presentation area, and it hosts theUIViewControllerthe modal is presented from (resolved once, lazily, by walking up to the provider whosecontainerIdmatches).isOpenfromfalse → truepresents the modal, andtrue → falsedismisses it. The present/dismiss lifecycle and its state machine live inRNSContainedModalPresentationManager, kept separate from the controller and the component view.UIModalPresentationOverCurrentContextwith aCrossDissolvetransition and a transparent content background, so the provider's content stays visible behind the presented modal.This mirrors the architecture established for the standalone
FormSheetcomponent (#3947), reusing the same host-anchor + teleported-content + presentation-manager split.Note
ContainedModalis currently an iOS-only component. The Android and Web entry points are present but no-ops for now. It is exported fromreact-native-screens/experimental.Closes https://github.com/software-mansion/react-native-screens-labs/issues/1359
Closes https://github.com/software-mansion/react-native-screens-labs/issues/1361
Closes https://github.com/software-mansion/react-native-screens-labs/issues/1364
Closes https://github.com/software-mansion/react-native-screens-labs/issues/1367
Closes https://github.com/software-mansion/react-native-screens-labs/issues/1356
Changes
ContainedModal(isOpen,targetContainerId) andContainedModalProvider(containerId,style) components, types, and platform splits (.tsx,.android.tsx,.web.tsx).src/experimental.ContainedModalHostNativeComponentandContainedModalProviderNativeComponent.RNSContainedModalHostShadowNode,RNSContainedModalHostState,RNSContainedModalHostComponentDescriptor).ios/gamma/modals/contained-modal/)RNSGammaStubs).RCTSurfaceTouchHandleris attached lazily on present/layout and detached onviewDidDisappear:(via the controller→host delegate), in addition toinvalidate. This prevents a dismissed modal from leaving behind a dead touch zone that silently swallows taps on the content behind it, and it re-attaches correctly on re-present.package.jsonwiring for the new sources.Caution
Dynamic content-origin updates aren't supported in the context of synchronization with the ShadowTree state. If an ancestor's offset changes, the frame of our host view might not be updated at all — from the host's perspective the frame doesn't change (
syncShadowNodeStateonly schedules a new state update when the computed origin or content bounds actually change), so no content re-layout is triggered.Note
The JavaScript files unrelated to the single-feature test (a separate batch of
.js/.tschanges) are tracked in their own PR — see #4171.Scaffolding for future work
To keep this PR focused on the core present/dismiss flow, several pieces are deliberately included as scaffolding for upcoming appearance/behavior props (mirroring the
FormSheetstructure) but are not wired up yet:RNSContainedModalConfigurationApplicator— currently an empty class (no methods). It is instantiated in the controller but never invoked. It will host the appearance/behavior configuration logic, analogous toRNSFormSheetConfigurationApplicator.RNSContainedModalUpdateFlags—RNSContainedModalUpdateFlagsAppearanceandRNSContainedModalUpdateFlagsBehaviorare defined but never set on a live update path. Only…Presentationis actually driven today (fromdidMoveToWindowand theisOpenprop change).setNeedsAppearanceUpdate/setNeedsBehaviorUpdate(controller) — implemented and set their respective flags, but nothing calls them yet. See the// TODO: determine if setNeedsApperance/BehaviorUpdate is necessary if _isOpennote in the host view.RNSContainedModalUpdateCoordinator'sneedsAny:/updateIfAnyNeeded:— provided for completeness alongsideneedsAll:/updateIfNeeded:, but only the…All/updateIfNeeded:variants are used so far.RNSContainedModalPresentationManager'shandleNativeDismiss— defined to reset state on a UIKit-initiated dismissal, but not yet hooked to a presentation-controller delegate (contained modals are currently only dismissed programmatically viaisOpen).FormSheetalready wires the equivalent throughpresentationControllerDidDismiss:.After - visual documentation
ContainedModalDemo.mov
Test plan
Run
test-contained-modal-base-iosand verify if the modal's behavior matches the expected one inscenario.md.Checklist