-
-
Notifications
You must be signed in to change notification settings - Fork 661
feat(iOS, ContainedModal): Add basic setup for ContainedModal native component #4190
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
sgaczol
wants to merge
25
commits into
main
Choose a base branch
from
@sgaczol/contained-modal
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
eaa00c7
ContainedModal and ContainedModalProvider codegen setup
sgaczol 07914eb
remove interfaceOnly: true from ContainedModal codegen file
sgaczol c10bebb
update codegen files
sgaczol 2b83e6e
added files on js side:
sgaczol f4469ac
import fix
sgaczol e54aab0
naming convention
sgaczol 12a3a27
add ContainedModal to experimental
sgaczol 5beb11e
import type
sgaczol 77afa9a
add interfaceOnly: true
sgaczol fc9e7e8
rename codegen files
sgaczol 138f7da
codegen name fix
sgaczol 6b70b12
package.json fix
sgaczol 9475b3d
update
sgaczol 93288c6
provider name change
sgaczol 4c17253
stubs added
sgaczol 3628223
providerKey renamed into containerId for the provider and targetConta…
sgaczol 3421d9c
comment update
sgaczol 1742512
add RNSContainedModal on native side
sgaczol cab800c
added a single feature test
sgaczol 2d121b6
scenario.md update
sgaczol 7b54b14
scenario.md update
sgaczol 5537bd6
update RNSGammaStubs
sgaczol 094f60d
typo
sgaczol 6274851
typo in todo
sgaczol d617a7f
scenario description update
sgaczol File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
14 changes: 14 additions & 0 deletions
14
apps/src/tests/single-feature-tests/contained-modal/index.ts
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import type { ScenarioGroup } from '@apps/tests/shared/helpers'; | ||
| import TestContainedModalBase from './test-contained-modal-base-ios'; | ||
|
|
||
| const scenarios = { | ||
| TestContainedModalBase, | ||
| }; | ||
|
|
||
| const ContainedModalScenarioGroup: ScenarioGroup<keyof typeof scenarios> = { | ||
| name: 'ContainedModal', | ||
| details: 'Single feature tests for ContainedModals', | ||
| scenarios, | ||
| }; | ||
|
|
||
| export default ContainedModalScenarioGroup; | ||
141 changes: 141 additions & 0 deletions
141
apps/src/tests/single-feature-tests/contained-modal/test-contained-modal-base-ios/index.tsx
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| import React, { useState } from 'react'; | ||
| import { Button, Pressable, StyleSheet, Text, View } from 'react-native'; | ||
| import { useSafeAreaInsets } from 'react-native-safe-area-context'; | ||
| import { | ||
| ContainedModal, | ||
| ContainedModalProvider, | ||
| } from 'react-native-screens/experimental'; | ||
| import { scenarioDescription } from './scenario-description'; | ||
| import { createScenario } from '@apps/tests/shared/helpers'; | ||
| import { Colors } from '@apps/shared/styling'; | ||
|
|
||
| const CONTAINER_ID = 'contained-modal-base'; | ||
|
|
||
| export function App() { | ||
| const insets = useSafeAreaInsets(); | ||
| const [isOpen, setIsOpen] = useState(false); | ||
| const [partialProvider, setPartialProvider] = useState(false); | ||
| const [insideCount, setInsideCount] = useState(0); | ||
| const [outsideCount, setOutsideCount] = useState(0); | ||
|
|
||
| return ( | ||
| <View style={[styles.screen, { paddingTop: insets.top }]}> | ||
| <View style={styles.content}> | ||
| <Text style={styles.title}>ContainedModal Test</Text> | ||
| <Button | ||
| title={ | ||
| partialProvider | ||
| ? 'Provider: partial (tap for full screen)' | ||
| : 'Provider: full screen (tap for partial)' | ||
| } | ||
| color={Colors.primary} | ||
| onPress={() => setPartialProvider(value => !value)} | ||
| /> | ||
| <View style={styles.spacing} /> | ||
| <Button | ||
| title="Open contained modal" | ||
| color={Colors.primary} | ||
| onPress={() => setIsOpen(true)} | ||
| /> | ||
| <View style={styles.spacing} /> | ||
| <Pressable | ||
| style={styles.counter} | ||
| onPress={() => setOutsideCount(value => value + 1)}> | ||
| <Text style={styles.counterText}>Outside count: {outsideCount}</Text> | ||
| </Pressable> | ||
| </View> | ||
|
|
||
| <ContainedModalProvider | ||
| containerId={CONTAINER_ID} | ||
| style={partialProvider ? styles.partialProvider : styles.fullProvider}> | ||
| <View style={styles.providerContent}> | ||
| <Pressable | ||
| style={styles.counter} | ||
| onPress={() => setInsideCount(value => value + 1)}> | ||
| <Text style={styles.counterText}>Inside count: {insideCount}</Text> | ||
| </Pressable> | ||
| </View> | ||
| <ContainedModal targetContainerId={CONTAINER_ID} isOpen={isOpen}> | ||
| <View style={styles.modal}> | ||
| <Text style={styles.sheetTitle}>🎉 This is a contained modal</Text> | ||
| <View style={styles.spacing} /> | ||
| <Button | ||
| title="Close" | ||
| color={Colors.primary} | ||
| onPress={() => setIsOpen(false)} | ||
| /> | ||
| </View> | ||
| </ContainedModal> | ||
| </ContainedModalProvider> | ||
| </View> | ||
| ); | ||
| } | ||
|
|
||
| const styles = StyleSheet.create({ | ||
| screen: { | ||
| flex: 1, | ||
| backgroundColor: Colors.offBackground, | ||
| }, | ||
| content: { | ||
| paddingTop: 16, | ||
| alignItems: 'center', | ||
| }, | ||
| title: { | ||
| fontSize: 20, | ||
| fontWeight: 'bold', | ||
| marginBottom: 20, | ||
| color: Colors.text, | ||
| }, | ||
| fullProvider: { | ||
| flex: 1, | ||
| marginTop: 16, | ||
| borderWidth: 2, | ||
| borderColor: 'transparent', | ||
| }, | ||
| partialProvider: { | ||
| height: 320, | ||
| marginTop: 16, | ||
| marginHorizontal: 24, | ||
| marginBottom: 24, | ||
| borderWidth: 2, | ||
| borderColor: Colors.primary, | ||
| borderRadius: 12, | ||
| overflow: 'hidden', | ||
| }, | ||
| providerContent: { | ||
| padding: 16, | ||
| alignItems: 'center', | ||
| }, | ||
| counter: { | ||
| paddingVertical: 12, | ||
| paddingHorizontal: 20, | ||
| borderRadius: 8, | ||
| borderWidth: 2, | ||
| borderColor: Colors.primary, | ||
| backgroundColor: Colors.background, | ||
| alignItems: 'center', | ||
| }, | ||
| counterText: { | ||
| fontSize: 16, | ||
| fontWeight: '600', | ||
| color: Colors.primary, | ||
| }, | ||
| modal: { | ||
| flex: 1, | ||
| justifyContent: 'center', | ||
| alignItems: 'center', | ||
| padding: 24, | ||
| backgroundColor: 'transparent', | ||
| }, | ||
| sheetTitle: { | ||
| fontSize: 22, | ||
| fontWeight: '600', | ||
| marginBottom: 12, | ||
| color: Colors.text, | ||
| }, | ||
| spacing: { | ||
| height: 32, | ||
| }, | ||
| }); | ||
|
|
||
| export default createScenario(App, scenarioDescription); |
11 changes: 11 additions & 0 deletions
11
...ingle-feature-tests/contained-modal/test-contained-modal-base-ios/scenario-description.ts
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import type { ScenarioDescription } from '@apps/tests/shared/helpers'; | ||
|
|
||
| export const scenarioDescription: ScenarioDescription = { | ||
| name: 'Basic functionality', | ||
| key: 'test-contained-modal-base-ios', | ||
| details: | ||
| 'Allows testing the basic functionality of the ContainedModal component.', | ||
| platforms: ['ios'], | ||
| e2eCoverage: 'tbd', | ||
| smokeTest: false, | ||
| }; |
119 changes: 119 additions & 0 deletions
119
.../single-feature-tests/contained-modal/test-contained-modal-base-ios/scenario.md
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| # Test Scenario: Basic functionality | ||
|
|
||
| ## Details | ||
|
|
||
| **Description:** Verify the core functionality of the `ContainedModal` component together with `ContainedModalProvider`. This test ensures that the modal is presented within the bounds of the provider it targets (matched by `containerId` / `targetContainerId`), that it works both with a full-screen and a partial-size provider, that its transparent background lets the provider's content show through (over-current-context presentation), and that presenting/dismissing repeatedly keeps working. | ||
|
|
||
| **OS test creation version:** iOS: 18.6 and 26.4, iPadOS 26.4 | ||
|
|
||
| ## E2E test | ||
|
|
||
| TBD. | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - iOS device or simulator: iPhone and iPad | ||
| - `ContainedModal` is currently an iOS-only component. | ||
|
|
||
| ## Note | ||
|
|
||
| - The modal is presented from the provider whose `containerId` equals the modal's `targetContainerId`, so it is contained within that provider's bounds, not presented over the whole window. | ||
| - The modal's content has a transparent background, so the provider's content (the "Inside count" button) stays visible behind it. | ||
| - Two counters exercise touch routing: | ||
| - **"Outside count"** lives outside the provider (in the top section) and is never covered by the modal. | ||
| - **"Inside count"** lives inside the provider, directly underneath where the modal is presented. | ||
|
|
||
| ## Touch-routing invariant (applies in every step below) | ||
|
|
||
| Regardless of whether the provider is full-screen or partial: | ||
|
|
||
| - **"Outside count" must always be tappable** - before, while, and after a modal is presented. It sits outside the provider, so the modal must never block it. | ||
| - **"Inside count" must be blocked _only while the modal is open_.** While the modal is presented it sits underneath the (transparent) modal content and must not receive taps. As soon as the modal is dismissed it must be tappable again. | ||
|
|
||
| ## Steps | ||
|
|
||
| ### Baseline | ||
|
|
||
| 1. Launch the app and navigate to the **Basic functionality** screen. | ||
|
|
||
| - [ ] Expected: Content is shown with the "Provider: full screen (tap for partial)" button, an "Open contained modal" button, an "Outside count: 0" button, and (inside the provider region) an "Inside count: 0" button. | ||
|
|
||
| --- | ||
|
|
||
| ### Background pressables work (no modal presented) | ||
|
|
||
| 2. Tap "Outside count" and "Inside count" a few times each. | ||
|
|
||
| - [ ] Expected: Both counters increment on every tap (all of the provider's content and the surrounding content are interactive while no modal is presented). | ||
|
|
||
| --- | ||
|
|
||
| ### Present within a full-screen provider | ||
|
|
||
| 3. Tap "Open contained modal". | ||
|
|
||
| - [ ] Expected: The modal appears with a cross-dissolve (fade-in) animation. The "🎉 This is a contained modal" text and the "Close" button are centered. Because the modal background is transparent, the underlying "Inside count" button is still visible behind it. | ||
|
|
||
| --- | ||
|
|
||
| ### Touch routing while the modal is open (full-screen provider) | ||
|
|
||
| 4. With the modal still open, tap where the "Inside count" button is (underneath the modal), then tap "Outside count". | ||
|
|
||
| - [ ] Expected: "Inside count" does **not** increment (it is covered by the open modal). "Outside count" **does** increment (it is outside the provider and stays interactive). See the touch-routing invariant above. | ||
|
|
||
| --- | ||
|
|
||
| ### Dismiss | ||
|
|
||
| 5. Tap "Close". | ||
|
|
||
| - [ ] Expected: The modal dismisses smoothly. | ||
|
|
||
| --- | ||
|
|
||
| ### Background pressable works again after dismiss (touch-handler cleanup) | ||
|
|
||
| > Regression check: after dismissal, the modal's touch handler must be detached. | ||
| > If it is not, it keeps consuming touches in the area the modal used to occupy and | ||
| > silently swallows taps meant for the "Inside count" button behind it, even though | ||
| > the modal is no longer visible. | ||
|
|
||
| 6. Note the current "Inside count" value, then tap the "Inside count" button (the one | ||
| that sits in the region the modal was just covering) several times. Tap "Outside | ||
| count" too. | ||
|
|
||
| - [ ] Expected: Both counters increment on every tap. Taps on "Inside count" are NOT | ||
| swallowed - there is no dead/unresponsive zone left behind where the dismissed | ||
| modal used to be. | ||
|
|
||
| 7. Re-open the modal ("Open contained modal"), tap "Close" again, and repeat the taps | ||
| from step 6. | ||
|
|
||
| - [ ] Expected: Both counters still increment on every tap after this second | ||
| open/close cycle (the touch handler is correctly re-attached on re-present and | ||
| detached again on dismiss). | ||
|
|
||
| --- | ||
|
|
||
| ### Present within a partial provider | ||
|
|
||
| 8. Tap "Provider: full screen (tap for partial)" to switch to the partial provider, then tap "Open contained modal". | ||
|
|
||
| - [ ] Expected: A bordered region (the provider bounds) is visible. The modal is presented **within that bordered region only**, not over the whole screen. The modal content is sized and positioned within the provider's bounds. | ||
|
|
||
| --- | ||
|
|
||
| ### Touch routing while the modal is open (partial provider) | ||
|
|
||
| 9. With the modal still open, tap where the "Inside count" button is (underneath the modal, inside the bordered region), then tap "Outside count". | ||
|
|
||
| - [ ] Expected: Same invariant as the full-screen case - "Inside count" does **not** increment while the modal is open, "Outside count" **does**. The smaller provider size must not change this behavior. | ||
|
|
||
| --- | ||
|
|
||
| ### Dismiss from partial provider | ||
|
|
||
| 10. Tap "Close". | ||
|
|
||
| - [ ] Expected: The modal dismisses smoothly and the screen returns to the partial-provider layout. "Inside count" is tappable again and "Outside count" keeps working. |
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
44 changes: 44 additions & 0 deletions
44
common/cpp/react/renderer/components/rnscreens/RNSContainedModalHostComponentDescriptor.h
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| #pragma once | ||
|
|
||
| #if !defined(ANDROID) | ||
|
|
||
| #include <react/debug/react_native_assert.h> | ||
| #include <react/renderer/core/ConcreteComponentDescriptor.h> | ||
| #include "RNSContainedModalHostShadowNode.h" | ||
|
|
||
| namespace facebook::react { | ||
|
|
||
| class RNSContainedModalHostComponentDescriptor final | ||
| : public ConcreteComponentDescriptor<RNSContainedModalHostShadowNode> { | ||
| public: | ||
| using ConcreteComponentDescriptor::ConcreteComponentDescriptor; | ||
|
|
||
| void adopt(ShadowNode &shadowNode) const override { | ||
| react_native_assert( | ||
| dynamic_cast<RNSContainedModalHostShadowNode *>(&shadowNode)); | ||
| auto &concreteShadowNode = | ||
| static_cast<RNSContainedModalHostShadowNode &>(shadowNode); | ||
|
|
||
| react_native_assert( | ||
| dynamic_cast<YogaLayoutableShadowNode *>(&concreteShadowNode)); | ||
| auto &layoutableShadowNode = | ||
| static_cast<YogaLayoutableShadowNode &>(concreteShadowNode); | ||
|
|
||
| auto state = std::static_pointer_cast< | ||
| const RNSContainedModalHostShadowNode::ConcreteState>( | ||
| shadowNode.getState()); | ||
|
|
||
| auto stateData = state->getData(); | ||
|
|
||
| if (stateData.frameSize.width > 0 && stateData.frameSize.height > 0) { | ||
| layoutableShadowNode.setSize( | ||
| Size{stateData.frameSize.width, stateData.frameSize.height}); | ||
| } | ||
|
|
||
| ConcreteComponentDescriptor::adopt(shadowNode); | ||
| } | ||
| }; | ||
|
|
||
| } // namespace facebook::react | ||
|
|
||
| #endif // !defined(ANDROID) |
17 changes: 17 additions & 0 deletions
17
common/cpp/react/renderer/components/rnscreens/RNSContainedModalHostShadowNode.cpp
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| #include "RNSContainedModalHostShadowNode.h" | ||
|
|
||
| #if !defined(ANDROID) | ||
|
|
||
| namespace facebook::react { | ||
|
|
||
| extern const char RNSContainedModalHostComponentName[] = | ||
| "RNSContainedModalHost"; | ||
|
|
||
| Point RNSContainedModalHostShadowNode::getContentOriginOffset( | ||
| bool /*includeTransform*/) const { | ||
| return getStateData().contentOffset; | ||
| } | ||
|
|
||
| } // namespace facebook::react | ||
|
|
||
| #endif // !defined(ANDROID) |
29 changes: 29 additions & 0 deletions
29
common/cpp/react/renderer/components/rnscreens/RNSContainedModalHostShadowNode.h
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| #pragma once | ||
|
|
||
| #if !defined(ANDROID) | ||
|
|
||
| #include <jsi/jsi.h> | ||
| #include <react/renderer/components/rnscreens/EventEmitters.h> | ||
| #include <react/renderer/components/rnscreens/Props.h> | ||
| #include <react/renderer/components/view/ConcreteViewShadowNode.h> | ||
| #include "RNSContainedModalHostState.h" | ||
|
|
||
| namespace facebook::react { | ||
|
|
||
| JSI_EXPORT extern const char RNSContainedModalHostComponentName[]; | ||
|
|
||
| class JSI_EXPORT RNSContainedModalHostShadowNode final | ||
| : public ConcreteViewShadowNode< | ||
| RNSContainedModalHostComponentName, | ||
| RNSContainedModalHostProps, | ||
| RNSContainedModalHostEventEmitter, | ||
| RNSContainedModalHostState> { | ||
| public: | ||
| using ConcreteViewShadowNode::ConcreteViewShadowNode; | ||
|
|
||
| Point getContentOriginOffset(bool includeTransform) const override; | ||
| }; | ||
|
|
||
| } // namespace facebook::react | ||
|
|
||
| #endif // !defined(ANDROID) |
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'ContainedModals' makes more sense, especially when we add a different style (CurrentContext) in the future