diff --git a/apps/src/tests/component-integration-tests/index.tsx b/apps/src/tests/component-integration-tests/index.tsx index 094e59b074..b44336edec 100644 --- a/apps/src/tests/component-integration-tests/index.tsx +++ b/apps/src/tests/component-integration-tests/index.tsx @@ -11,6 +11,7 @@ import OrientationScenarioGroup from './orientation'; import ScrollViewScenarioGroup from './scroll-view'; import FormSheetScenarioGroup from './form-sheet'; import TabsInStackV5ScenarioGroup from './tabs-stack-v5'; +import SvmScenarioGroup from './scroll-view-marker'; import ScenarioSelectionScreen from '@apps/tests/shared/ScenarioScreen'; export const COMPONENT_SCENARIOS = { @@ -18,6 +19,7 @@ export const COMPONENT_SCENARIOS = { ScrollView: ScrollViewScenarioGroup, FormSheet: FormSheetScenarioGroup, TabsInStackV5: TabsInStackV5ScenarioGroup, + ScrollViewMarker: SvmScenarioGroup, } as const; type ParamsList = { [k: keyof typeof COMPONENT_SCENARIOS]: undefined } & { diff --git a/apps/src/tests/component-integration-tests/scroll-view-marker/index.ts b/apps/src/tests/component-integration-tests/scroll-view-marker/index.ts new file mode 100644 index 0000000000..6756f0e391 --- /dev/null +++ b/apps/src/tests/component-integration-tests/scroll-view-marker/index.ts @@ -0,0 +1,16 @@ +import type { ScenarioGroup } from '@apps/tests/shared/helpers'; +import TestSvmTabsScrollEdgeEffects from './test-svm-tabs-scroll-edge-effects'; +import TestStackSvmTabsSpecialEffects from './test-stack-svm-tabs-special-effects'; + +const scenarios = { + TestSvmTabsScrollEdgeEffects, + TestStackSvmTabsSpecialEffects, +}; + +const TestSvmScenarioGroup: ScenarioGroup = { + name: 'ScrollViewMarker Integration Tests', + details: 'Test interaction between ScrollViewMarker and various components', + scenarios, +}; + +export default TestSvmScenarioGroup; diff --git a/apps/src/tests/component-integration-tests/scroll-view-marker/test-stack-svm-tabs-special-effects/index.tsx b/apps/src/tests/component-integration-tests/scroll-view-marker/test-stack-svm-tabs-special-effects/index.tsx new file mode 100644 index 0000000000..ce91ae0fd4 --- /dev/null +++ b/apps/src/tests/component-integration-tests/scroll-view-marker/test-stack-svm-tabs-special-effects/index.tsx @@ -0,0 +1,133 @@ +import { createScenario } from '@apps/tests/shared/helpers'; +import React from 'react'; +import { ScrollView, StyleSheet, View } from 'react-native'; +import { scenarioDescription } from './scenario-description'; +import { + DEFAULT_TAB_ROUTE_OPTIONS, + type TabRouteConfig, + TabsContainer, + useTabsNavigationContext, +} from '@apps/shared/gamma/containers/tabs'; +import { Colors } from '@apps/shared/styling'; +import { Rectangle } from '@apps/shared/Rectangle'; +import { ScrollViewMarker } from 'react-native-screens/experimental'; +import { type ScrollEdgeEffect } from 'react-native-screens'; +import { + StackContainer, + type StackRouteConfig, +} from '@apps/shared/gamma/containers/stack'; + +export function TestStackSvmTabsSpecialEffects() { + return ; +} + +const TABS_ROUTE_CONFIGS: TabRouteConfig[] = [ + { + name: 'Home', + Component: TabContents, + options: { + ...DEFAULT_TAB_ROUTE_OPTIONS, + title: 'Home', + }, + }, + { + name: 'Stack', + Component: StackTabScreen, + options: { + ...DEFAULT_TAB_ROUTE_OPTIONS, + title: 'Stack', + }, + }, +]; + +const STACK_ROUTE_CONFIGS: StackRouteConfig[] = [ + { + name: 'First', + Component: StackContents, + options: {}, + }, + { + name: 'Second', + Component: StackContents, + options: {}, + }, +]; + +function TabContents() { + const edgeEffectStyle: ScrollEdgeEffect = + useTabsNavigationContext().routeKey === 'Home' ? 'hard' : 'soft'; + + return ( + + + + + + + + ); +} + +function StackContents() { + return ( + + + + + + + + + ); +} + +function TabsNavigation() { + return ; +} + +function StackTabScreen() { + return ; +} + +function ScrollViewContents(props: { elementCount?: number }) { + const elementCount = props.elementCount ?? 48; + return ( + <> + {Array.from({ length: elementCount }).map((_, index) => { + return ( + + + + + ); + })} + + ); +} + +function HeuristicBreakingView() { + return ; +} + +export default createScenario(TestStackSvmTabsSpecialEffects, scenarioDescription); + +const styles = StyleSheet.create({ + fillParent: { + flex: 1, + width: '100%', + height: '100%', + }, +}); diff --git a/apps/src/tests/component-integration-tests/scroll-view-marker/test-stack-svm-tabs-special-effects/scenario-description.ts b/apps/src/tests/component-integration-tests/scroll-view-marker/test-stack-svm-tabs-special-effects/scenario-description.ts new file mode 100644 index 0000000000..a3b393666b --- /dev/null +++ b/apps/src/tests/component-integration-tests/scroll-view-marker/test-stack-svm-tabs-special-effects/scenario-description.ts @@ -0,0 +1,12 @@ +import type { ScenarioDescription } from '@apps/tests/shared/helpers'; + +export const scenarioDescription: ScenarioDescription = { + name: 'SVM in Stack & Tabs - tabs special effects', + key: 'test-stack-svm-tabs-special-effects', + details: + 'Test whether special effects (on tab repetition) are performed correctly in nested container scenario ' + + '(stack in tabs)', + platforms: ['ios', 'android'], + e2eCoverage: 'tbd', + smokeTest: false, +}; diff --git a/apps/src/tests/component-integration-tests/scroll-view-marker/test-stack-svm-tabs-special-effects/scenario.md b/apps/src/tests/component-integration-tests/scroll-view-marker/test-stack-svm-tabs-special-effects/scenario.md new file mode 100644 index 0000000000..71a1bd6239 --- /dev/null +++ b/apps/src/tests/component-integration-tests/scroll-view-marker/test-stack-svm-tabs-special-effects/scenario.md @@ -0,0 +1,43 @@ +# Test Scenario: SVM in Stack & Tabs - tabs special effects + +## Details + +**Description:** +This test verifies interaction between `ScrollViewMarker`, `Stack` and `Tabs` components. +The primary goal is to verify that the tabs special effects (scroll-to-top, pop-to-top) do work in +nested container scenario. + +**OS test creation version:** +iOS: 26.5, Android: API Level 36. + +## E2E test + +TBD + +## Prerequisites + +- iOS: simulator with iOS 15+ is enough, +- Android: emulator + +## Note + +Android part of the code is unimplemented yet, therefore it is expected not to work. + +iOS implementation doesn't currently support nested container interaction yet. +This test needs to be updated after such interaction is supported. + +## Steps + +1. Launch the app and navigate to the **SVM in Stack & Tabs - tabs special effects** screen. + +- [ ] There should be two tabs: `Home` and `Stack`. +- [ ] `Home` tab should be selected. + +2. Scrolldown a bit. + + Doesn't really matter how much you scroll - the distance should be "noticeable". + +3. Press `Home` tab item (repeated tab selection) to trigger the special effect. + +- [ ] *scroll-top-top* should be triggered and you should observe the scroll-view scrolling +to it's top. diff --git a/apps/src/tests/component-integration-tests/scroll-view-marker/test-svm-tabs-scroll-edge-effects/index.tsx b/apps/src/tests/component-integration-tests/scroll-view-marker/test-svm-tabs-scroll-edge-effects/index.tsx new file mode 100644 index 0000000000..a58057fcfe --- /dev/null +++ b/apps/src/tests/component-integration-tests/scroll-view-marker/test-svm-tabs-scroll-edge-effects/index.tsx @@ -0,0 +1,96 @@ +import { createScenario } from '@apps/tests/shared/helpers'; +import React from 'react'; +import { ScrollView, StyleSheet, View } from 'react-native'; +import { scenarioDescription } from './scenario-description'; +import { + DEFAULT_TAB_ROUTE_OPTIONS, + type TabRouteConfig, + TabsContainer, + useTabsNavigationContext, +} from '@apps/shared/gamma/containers/tabs'; +import { Colors } from '@apps/shared/styling'; +import { Rectangle } from '@apps/shared/Rectangle'; +import { ScrollViewMarker } from 'react-native-screens/experimental'; +import { type ScrollEdgeEffect } from 'react-native-screens'; + +export function TestSvmTabsScrollEdgeEffects() { + return ; +} + +const TABS_ROUTE_CONFIGS: TabRouteConfig[] = [ + { + name: 'Home', + Component: TabContents, + options: { + ...DEFAULT_TAB_ROUTE_OPTIONS, + title: 'Home', + }, + }, + { + name: 'Second', + Component: TabContents, + options: { + ...DEFAULT_TAB_ROUTE_OPTIONS, + title: 'Second', + }, + }, +]; + +function TabContents() { + const edgeEffectStyle: ScrollEdgeEffect = + useTabsNavigationContext().routeKey === 'Home' ? 'hard' : 'soft'; + + return ( + + + + + + + + + ); +} + +function TabsNavigation() { + return ; +} + +function ScrollViewContents(props: { elementCount?: number }) { + const elementCount = props.elementCount ?? 48; + return ( + <> + {Array.from({ length: elementCount }).map((_, index) => { + return ( + + + + + ); + })} + + ); +} + +function HeuristicBreakingView() { + return ; +} + +export default createScenario(TestSvmTabsScrollEdgeEffects, scenarioDescription); + +const styles = StyleSheet.create({ + fillParent: { + flex: 1, + width: '100%', + height: '100%', + }, +}); diff --git a/apps/src/tests/component-integration-tests/scroll-view-marker/test-svm-tabs-scroll-edge-effects/scenario-description.ts b/apps/src/tests/component-integration-tests/scroll-view-marker/test-svm-tabs-scroll-edge-effects/scenario-description.ts new file mode 100644 index 0000000000..60f7b46782 --- /dev/null +++ b/apps/src/tests/component-integration-tests/scroll-view-marker/test-svm-tabs-scroll-edge-effects/scenario-description.ts @@ -0,0 +1,12 @@ +import type { ScenarioDescription } from '@apps/tests/shared/helpers'; + +export const scenarioDescription: ScenarioDescription = { + name: 'SVM in Tabs - scroll edge effects', + key: 'test-svm-tabs-scroll-edge-effects', + details: + 'Test whether scroll edge effects are applied correctly when ScrollViewMarker ' + + 'is used inside Tabs', + platforms: ['ios'], + e2eCoverage: 'tbd', + smokeTest: false, +}; diff --git a/apps/src/tests/component-integration-tests/scroll-view-marker/test-svm-tabs-scroll-edge-effects/scenario.md b/apps/src/tests/component-integration-tests/scroll-view-marker/test-svm-tabs-scroll-edge-effects/scenario.md new file mode 100644 index 0000000000..2e75b37019 --- /dev/null +++ b/apps/src/tests/component-integration-tests/scroll-view-marker/test-svm-tabs-scroll-edge-effects/scenario.md @@ -0,0 +1,43 @@ +# Test Scenario: Integration: SVM in Tabs - scroll edge effects + +## Details + +**Description:** +This test verifies interaction between `ScrollViewMarker` and `Tabs` in scope of scroll-edge-effects. +It allows to test both whether the scroll-edge-effect is correctly applied AND whether it is correctly +updated between different tabs. + +**OS test creation version:** +iOS 26.5 + +## E2E test + +TBD + +## Prerequisites + +iOS: simulator with iOS 26+ is enough, + +## Note + +Seemingly the edge effect is applied correctly no matter the integration with the scrollview marker. +Likely UITabBarController uses some different logic to UIViewController.contentScrollView to detect scrollview, +because even when I have had returned nil from the method, the edge effect had still been applied. +Nevertheless, I decided to include this test case here, just to make sure it works. +Also this allows us to test that different ScrollViewMarkers from different tab update the edge effect correctly on tab change. + +## Steps + +1. Launch the app and navigate to the **SVM in Tabs - scroll edge effects** screen. + +- [ ] There should be two tabs: `Home` and `Second`. +- [ ] `Home` tab should be selected. +- [ ] `Hard` scroll-edge-effect should be applied (opqaue background of tab bar). + +2. Change tab to `Second`. + +- [ ] The scroll-edge-effect should be now changed to `soft` after/during the transition. + +3. Change tab back to the `Home`. + +- [ ] The scroll-edge-effect should be back at `hard`. diff --git a/ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.mm b/ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.mm index ff1ab01f3f..1258bd047d 100644 --- a/ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.mm +++ b/ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.mm @@ -11,6 +11,7 @@ #import #import +#import namespace react = facebook::react; @@ -78,7 +79,11 @@ - (nullable UIScrollView *)findScrollView if ([superview respondsToSelector:@selector(registerDescendantScrollView:fromMarker:)]) { return static_cast>(superview); } - superview = superview.superview; + if ([superview respondsToSelector:@selector(reactSuperview)]) { + superview = [superview reactSuperview]; + } else { + superview = superview.superview; + } } return nil; } @@ -150,6 +155,14 @@ - (void)willMoveToWindow:(UIWindow *)newWindow [self maybeRegisterWithSeekingAncestor]; } +- (void)didMoveToWindow +{ + [super didMoveToWindow]; + if (self.window == nil) { + _hasAttemptedRegistration = NO; + } +} + #pragma mark - RNSScrollEdgeEffectProviding - (RNSScrollEdgeEffect)leftScrollEdgeEffect diff --git a/ios/tabs/screen/RNSTabsScreenComponentView.mm b/ios/tabs/screen/RNSTabsScreenComponentView.mm index 4c3e65a864..82140fa129 100644 --- a/ios/tabs/screen/RNSTabsScreenComponentView.mm +++ b/ios/tabs/screen/RNSTabsScreenComponentView.mm @@ -16,10 +16,20 @@ #import #import +#if RNS_GAMMA_ENABLED +#import "RNSScrollViewMarkerComponentView.h" +#import "RNSScrollViewSeeking.h" +#endif + namespace react = facebook::react; #pragma mark - View implementation +#if RNS_GAMMA_ENABLED +@interface RNSTabsScreenComponentView () +@end +#endif + @implementation RNSTabsScreenComponentView { RNSTabsScreenViewController *_controller; RNSTabsHostComponentView *__weak _Nullable _reactSuperview; @@ -104,6 +114,15 @@ - (void)invalidateImpl }); } +#pragma mark RNSScrollViewSeeking + +#if RNS_GAMMA_ENABLED +- (void)registerDescendantScrollView:(UIScrollView *)scrollView fromMarker:(RNSScrollViewMarkerComponentView *)marker +{ + [_controller setContentScrollView:scrollView forEdge:NSDirectionalRectEdgeAll]; +} +#endif // RNS_GAMMA_ENABLED + #pragma mark - Events - (nonnull RNSTabsScreenEventEmitter *)reactEventEmitter diff --git a/ios/tabs/screen/RNSTabsScreenViewController.mm b/ios/tabs/screen/RNSTabsScreenViewController.mm index 6bfece3edd..067adf66d5 100644 --- a/ios/tabs/screen/RNSTabsScreenViewController.mm +++ b/ios/tabs/screen/RNSTabsScreenViewController.mm @@ -97,14 +97,23 @@ - (bool)tabScreenSelectedRepeatedly if ([self tabsSpecialEffectsDelegate] != nil) { return [[self tabsSpecialEffectsDelegate] onRepeatedTabSelectionOfTabScreenController:self]; } else if (self.tabScreenComponentView.shouldUseRepeatedTabSelectionScrollToTopSpecialEffect) { - UIScrollView *scrollView = - [RNSScrollViewFinder findScrollViewInFirstDescendantChainFrom:[self tabScreenComponentView]]; - return [scrollView rnscreens_scrollToTop]; + return [[self resolveContentScrollView] rnscreens_scrollToTop]; } return false; } +- (nullable UIScrollView *)resolveContentScrollView +{ + if (auto sv = [self contentScrollViewForEdge:NSDirectionalRectEdgeBottom]; sv != nil) { + return sv; + } + if (auto sv = [self contentScrollViewForEdge:NSDirectionalRectEdgeTop]; sv != nil) { + return sv; + } + return [RNSScrollViewFinder findScrollViewInFirstDescendantChainFrom:[self tabScreenComponentView]]; +} + #if !TARGET_OS_TV - (RNSOrientation)evaluateOrientation