-
-
Notifications
You must be signed in to change notification settings - Fork 643
feat(iOS, FormSheet v5): Add basic setup for standalone FormSheet native component #3947
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
base: main
Are you sure you want to change the base?
Changes from 46 commits
9bcc979
95e8711
6f3db4a
7a8bd70
314ab39
67a9cdd
fb978ca
46ed5d6
84e3268
118f949
f2cfaf9
a93719a
a864044
e980b58
ddc3c4b
a32f25a
b977f45
afcef8c
b70f4df
3f11bbd
10dfdfc
db637a3
0ed38e8
3538ec9
1c1dc96
f76775d
988a970
99702c7
43288c2
ca11eff
668f898
fb805e4
62d1023
239ad71
447d437
c10909b
b988e9f
ecd371a
682998d
7a6caec
7fdd958
e22a373
7f2586d
7a4e047
debc063
a791d94
6d8c63c
99bfdc4
d613f60
c7e29af
d1c50fb
5354e50
3f1ae20
96e8499
a46e7fd
071d11d
367e4a4
8ca5864
b12b1bc
f5217d4
8e649fc
7903fe9
e83be5f
c749d12
e597e45
fb5940d
c9ce145
edd2491
c1a04de
a9355c9
2788160
c29d127
ec52bca
8ef3037
7ea4db6
fe9d576
0912a24
cddfd9e
867c6a4
3569432
43a4f23
3843b49
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import type { ScenarioGroup } from '@apps/tests/shared/helpers'; | ||
| import TestFormSheetWithNestedStackV5 from './test-form-sheet-with-nested-stack-v5'; | ||
|
|
||
| const scenarios = { TestFormSheetWithNestedStackV5 }; | ||
|
|
||
| const FormSheetScenarioGroup: ScenarioGroup<keyof typeof scenarios> = { | ||
| name: 'FormSheet Integration Tests', | ||
| details: 'Test interaction between FormSheet and different components', | ||
| scenarios, | ||
| }; | ||
|
|
||
| export default FormSheetScenarioGroup; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| import React, { useState } from 'react'; | ||
| import { Button, StyleSheet, Text, View } from 'react-native'; | ||
| import { FormSheet } from 'react-native-screens/experimental'; | ||
| import type { ScenarioDescription } from '@apps/tests/shared/helpers'; | ||
| import { createScenario } from '@apps/tests/shared/helpers'; | ||
| import { StackContainer } from '@apps/shared/gamma/containers/stack'; | ||
| import { CenteredLayoutView } from '@apps/shared/CenteredLayoutView'; | ||
| import { Colors } from '@apps/shared/styling'; | ||
| import { StackNavigationButtons } from '@apps/tests/shared/components/stack-v5/StackNavigationButtons'; | ||
|
|
||
| const scenarioDescription: ScenarioDescription = { | ||
| name: 'FormSheet with Nested Stack v5', | ||
| key: 'test-formsheet-nested-stack-v5', | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this is a CIT scenario, the directory name should look slightly different. I would propose: test-form-sheet-in-stack-v5-ios. The platform must be included as this test is iOS-only.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| details: 'Test nesting Stack v5 inside a FormSheet', | ||
| platforms: ['ios'], | ||
| }; | ||
|
|
||
| export function App() { | ||
| const [isOpen, setIsOpen] = useState(false); | ||
|
|
||
| return ( | ||
| <View style={styles.container}> | ||
| <Text style={styles.title}>FormSheet with nested StackV5</Text> | ||
| <Button | ||
| title="Open FormSheet" | ||
| color={Colors.primary} | ||
| onPress={() => setIsOpen(true)} | ||
| /> | ||
| <FormSheet | ||
| isOpen={isOpen} | ||
| onDismiss={() => setIsOpen(false)} | ||
| detents={[0.6, 1.0]}> | ||
| <View style={styles.sheetContent}> | ||
| <StackSetup /> | ||
| </View> | ||
| </FormSheet> | ||
| </View> | ||
| ); | ||
| } | ||
|
|
||
| function StackSetup() { | ||
| return ( | ||
| <StackContainer | ||
| routeConfigs={[ | ||
| { | ||
| name: 'Home', | ||
| Component: HomeScreen, | ||
| options: {}, | ||
| }, | ||
| { | ||
| name: 'A', | ||
| Component: AScreen, | ||
| options: { | ||
| headerConfig: { title: 'A' }, | ||
| }, | ||
| }, | ||
| ]} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| function HomeScreen() { | ||
| return ( | ||
| <CenteredLayoutView style={{ backgroundColor: Colors.BlueLight40 }}> | ||
| <Text style={styles.screenText}>Home Screen</Text> | ||
| <StackNavigationButtons isPopEnabled={false} routeNames={['A']} /> | ||
| </CenteredLayoutView> | ||
| ); | ||
| } | ||
|
|
||
| function AScreen() { | ||
| return ( | ||
| <CenteredLayoutView style={{ backgroundColor: Colors.YellowLight40 }}> | ||
| <Text style={styles.screenText}>Screen A</Text> | ||
| <StackNavigationButtons isPopEnabled={true} routeNames={['A']} /> | ||
| </CenteredLayoutView> | ||
| ); | ||
| } | ||
|
|
||
| const styles = StyleSheet.create({ | ||
| container: { | ||
| flex: 1, | ||
| justifyContent: 'center', | ||
| alignItems: 'center', | ||
| backgroundColor: Colors.offBackground, | ||
| }, | ||
| title: { | ||
| fontSize: 20, | ||
| fontWeight: 'bold', | ||
| marginBottom: 20, | ||
| color: Colors.text, | ||
| }, | ||
| sheetContent: { | ||
| flex: 1, | ||
| backgroundColor: Colors.background, | ||
| }, | ||
| screenText: { | ||
| color: Colors.text, | ||
| fontSize: 20, | ||
| fontWeight: 'bold', | ||
| textAlign: 'center', | ||
| marginBottom: 10, | ||
| }, | ||
| }); | ||
|
|
||
| export default createScenario(App, scenarioDescription); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| # Test Scenario: FormSheet with Nested Stack v5 | ||
|
|
||
| ## Details | ||
|
|
||
| **Description:** Verify the layout and state persistence of a `StackContainer` nested within a `FormSheet`. This test ensures that the Stack layout correctly fills the `FormSheet` container, that content remains properly centered, that the layout smoothly adapts when the FormSheet height changes, and that the Stack's navigation state is preserved when the sheet is dismissed and reopened. | ||
|
|
||
| **OS test creation version:** iOS: 18.6 and 26.4 | ||
|
|
||
| ## E2E test | ||
|
|
||
| Other: Planned, but will be implemented separately. | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - iOS device/simulator | ||
|
|
||
| ## Steps | ||
|
|
||
| ### Baseline | ||
|
|
||
| 1. Launch the app and navigate to the **FormSheet with Nested Stack v5** screen. | ||
|
|
||
| - [ ] Expected: Content with the button "Open FormSheet" is shown | ||
|
|
||
| --- | ||
|
|
||
| ### Initialization & Layout Verification | ||
|
|
||
| 2. Tap the "Open FormSheet" button. | ||
|
|
||
| - [ ] Expected: The FormSheet opens at the initial lower detent (0.6). The "Home Screen" text is visible and centered within the sheet. The light blue background completely covers the FormSheet content area. | ||
|
|
||
|
|
||
| 3. Tap the "Push A" button to push Screen A. | ||
|
|
||
| - [ ] Expected: The stack navigates to "Screen A". The "Screen A" text is centered. The light yellow background completely covers the FormSheet content area. | ||
|
|
||
| --- | ||
|
|
||
| ### Detent Adaptation | ||
|
|
||
| 4. Grab the top edge of the FormSheet and swipe up to expand it to the maximum detent (1.0). | ||
|
|
||
| - [ ] Expected: The FormSheet expands to take up the full screen height. The layout adapts dynamically - the light yellow background stretches to cover the new full height, and the "Screen A" text dynamically re-centers itself within the newly expanded view area. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: I think that we should clarify what we mean by "full screen height" - it's technically not full screen height but full screen height without the top inset. Without video I would think that the first option is expected. Applies to second scenario as well.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| --- | ||
|
|
||
| ### State Persistence | ||
|
|
||
| 5. Swipe down on the FormSheet to dismiss it, then tap the "Open FormSheet" button again. | ||
|
|
||
| - [ ] Expected: The FormSheet re-opens at the initial lower detent (0.6). The stack's navigation state has been kept - the sheet immediately displays "Screen A" (with the yellow background and centered text) rather than resetting back to the Home Screen. | ||
|
LKuchno marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import type { ScenarioGroup } from '@apps/tests/shared/helpers'; | ||
| import TestFormSheetBase from './test-form-sheet-base'; | ||
|
|
||
| const scenarios = { | ||
| TestFormSheetBase, | ||
| }; | ||
|
|
||
| const FormSheetScenarioGroup: ScenarioGroup<keyof typeof scenarios> = { | ||
| name: 'FormSheet', | ||
| details: 'Single feature tests for FormSheets', | ||
| scenarios, | ||
| }; | ||
|
|
||
| export default FormSheetScenarioGroup; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| import React, { useState } from 'react'; | ||
| import { Button, StyleSheet, Text, View } from 'react-native'; | ||
| import { FormSheet } from 'react-native-screens/experimental'; | ||
| import type { ScenarioDescription } from '@apps/tests/shared/helpers'; | ||
| import { createScenario } from '@apps/tests/shared/helpers'; | ||
| import { Colors } from '@apps/shared/styling'; | ||
|
|
||
| const scenarioDescription: ScenarioDescription = { | ||
| name: 'Basic functionality', | ||
| key: 'test-formsheet-base', | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Following naming convention for directories I would set key to 'test-form-sheet-base-ios'
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| details: 'Allows to test the basic functionality of FormSheet component.', | ||
| platforms: ['ios'], | ||
| }; | ||
|
|
||
| export function App() { | ||
| const [isOpen, setIsOpen] = useState(false); | ||
|
|
||
| return ( | ||
| <View style={styles.container}> | ||
| <Text style={styles.title}>FormSheet Test</Text> | ||
| <Button | ||
| title="Open FormSheet" | ||
| color={Colors.primary} | ||
| onPress={() => setIsOpen(true)} | ||
| /> | ||
| <FormSheet | ||
| isOpen={isOpen} | ||
| onDismiss={() => setIsOpen(false)} | ||
| detents={[0.6, 1.0]}> | ||
| <View style={styles.sheetContent}> | ||
| <Text style={styles.sheetTitle}>FormSheet content</Text> | ||
| <View style={styles.spacing} /> | ||
| <Button | ||
| title="Dismiss from JS" | ||
| color={Colors.primary} | ||
| onPress={() => setIsOpen(false)} | ||
| /> | ||
| </View> | ||
| </FormSheet> | ||
| </View> | ||
| ); | ||
| } | ||
|
|
||
| const styles = StyleSheet.create({ | ||
| container: { | ||
| flex: 1, | ||
| justifyContent: 'center', | ||
| alignItems: 'center', | ||
| backgroundColor: Colors.offBackground, | ||
| }, | ||
| title: { | ||
| fontSize: 20, | ||
| fontWeight: 'bold', | ||
| marginBottom: 20, | ||
| color: Colors.text, | ||
| }, | ||
| sheetContent: { | ||
| flex: 1, | ||
| backgroundColor: Colors.background, | ||
| padding: 24, | ||
| justifyContent: 'center', | ||
| alignItems: 'center', | ||
| }, | ||
| sheetTitle: { | ||
| fontSize: 22, | ||
| fontWeight: '600', | ||
| marginBottom: 12, | ||
| color: Colors.text, | ||
| }, | ||
| spacing: { | ||
| height: 32, | ||
| }, | ||
| }); | ||
|
|
||
| export default createScenario(App, scenarioDescription); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| # Test Scenario: Basic functionality | ||
|
|
||
| ## Details | ||
|
|
||
| **Description:** Verify the core functionality and layout stability of the `FormSheet` component. This test ensures that the FormSheet opens correctly, that its internal content is properly centered, and that the content dynamically maintains its centered alignment when the user manually adjusts the sheet's height between different detents. | ||
|
|
||
| **OS test creation version:** iOS: 18.6 and 26.4 | ||
|
|
||
| ## E2E test | ||
|
|
||
| Other: Planned, but will be implemented separately. | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - iOS device/simulator | ||
|
|
||
| ## Steps | ||
|
|
||
| ### Baseline | ||
|
|
||
| 1. Launch the app and navigate to the **Basic functionality** screen. | ||
|
|
||
| - [ ] Expected: Content with the button "Open FormSheet" is shown | ||
|
|
||
| --- | ||
|
|
||
| ### Initialization & Layout Verification | ||
|
|
||
| 2. Tap the "Open FormSheet" button. | ||
|
|
||
| - [ ] Expected: The FormSheet opens at the initial lower detent (0.6). The "FormSheet content" text and the "Dismiss from JS" button are visible and perfectly centered both vertically and horizontally within the sheet. | ||
|
|
||
| --- | ||
|
|
||
| ### Detent Adaptation | ||
|
|
||
| 3. Grab the top edge of the FormSheet and swipe up to expand it to the maximum detent (1.0). | ||
|
|
||
| - [ ] Expected: The FormSheet expands to take up the full screen height. The internal layout adapts dynamically, and the "FormSheet content" text along with the "Dismiss from JS" button remain perfectly centered within the newly expanded view area. | ||
|
|
||
| --- | ||
|
|
||
| ### Dismissal Verification | ||
|
|
||
| 4. Tap the "Dismiss from JS" button (or swipe down completely). | ||
|
|
||
| - [ ] Expected: The FormSheet dismisses smoothly and returns the user to the underlying main screen. Pressables on the main screen are working. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ import TabsScenarioGroup from './tabs'; | |
| import SplitScenarioGroup from './split'; | ||
| import StackV5ScenarioGroup from './stack-v5'; | ||
| import StackV4ScenarioGroup from './stack-v4'; | ||
| import FormSheetScenarioGroup from './form-sheet'; | ||
| import { ScenarioButton } from '@apps/tests/shared/ScenarioButton'; | ||
| import ScenarioSelectionScreen from '@apps/tests/shared/ScenarioScreen'; | ||
|
|
||
|
|
@@ -18,6 +19,7 @@ export const COMPONENT_SCENARIOS = { | |
| Split: SplitScenarioGroup, | ||
| StackV5: StackV5ScenarioGroup, | ||
| StackV4: StackV4ScenarioGroup, | ||
| FormSheet: FormSheetScenarioGroup, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to have a flat hierarchy with different kinds of modals or some Modal scenario group first? Obviously this might be changed later when we add other types of modals.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } as const; | ||
|
|
||
| type ParamsList = { [k: keyof typeof COMPONENT_SCENARIOS]: undefined } & { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| #pragma once | ||
|
|
||
| #ifndef ANDROID | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's okay. I recently started preferring
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| #include <react/debug/react_native_assert.h> | ||
| #include <react/renderer/core/ConcreteComponentDescriptor.h> | ||
| #include "RNSFormSheetShadowNode.h" | ||
|
|
||
| namespace facebook::react { | ||
|
|
||
| class RNSFormSheetComponentDescriptor final | ||
| : public ConcreteComponentDescriptor<RNSFormSheetShadowNode> { | ||
| public: | ||
| using ConcreteComponentDescriptor::ConcreteComponentDescriptor; | ||
|
|
||
| void adopt(ShadowNode &shadowNode) const override { | ||
| react_native_assert(dynamic_cast<RNSFormSheetShadowNode *>(&shadowNode)); | ||
| auto &concreteShadowNode = | ||
| static_cast<RNSFormSheetShadowNode &>(shadowNode); | ||
|
|
||
| react_native_assert( | ||
| dynamic_cast<YogaLayoutableShadowNode *>(&concreteShadowNode)); | ||
| auto &layoutableShadowNode = | ||
| static_cast<YogaLayoutableShadowNode &>(concreteShadowNode); | ||
|
|
||
| auto state = | ||
| std::static_pointer_cast<const RNSFormSheetShadowNode::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}); | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this ever not true? I need to read the code further leaving notes for myself.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Usually what I've done recently is to initialise the frameSize with special empty value and then compare against it here.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| ConcreteComponentDescriptor::adopt(shadowNode); | ||
| } | ||
| }; | ||
|
|
||
| } // namespace facebook::react | ||
|
|
||
| #endif // ANDROID | ||
Uh oh!
There was an error while loading. Please reload this page.