Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion FabricExample/__tests__/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
* @format
*/


import React from 'react';
import ReactTestRenderer from 'react-test-renderer';
import App from '../App';
Expand Down
6 changes: 3 additions & 3 deletions FabricExample/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2192,7 +2192,7 @@ EXTERNAL SOURCES:

SPEC CHECKSUMS:
FBLazyVector: 974207232af1c5372a56239516ccc450a6420bfd
hermes-engine: ea345dc52e1a9b901a2b2e66be524a973154ff61
hermes-engine: 83594ce32ee796061eeceb760a80b9dff31adc8f
RCTDeprecation: 8ae59687fd548d481aa5ce8014ac7cd9b47e7316
RCTRequired: d711b3887891fab6a77c6b9cdc49a3d8b171e36a
RCTSwiftUI: 96986e49a4fdc2c2103929dee2641e1b57edf33d
Expand Down Expand Up @@ -2263,10 +2263,10 @@ SPEC CHECKSUMS:
ReactAppDependencyProvider: 625d2f6d9d5ef01acc9dfe2b5385504bbffd2ad0
ReactCodegen: 7cc1904b7db3c7b68dfd4b5424175a013051874f
ReactCommon: ac616b2f169c57d56bd4b1a02f8a2dbb0b395722
ReactNativeDependencies: 8b4dd00142a189920d68b5bb85c46506eff6d39b
ReactNativeDependencies: 97ee09ffb32e629f30549f67a3ebbf71a4dff077
RNGestureHandler: f080747d181c86d346827ba389209e1afb528628
RNReanimated: 72296a949b2e629ed59666d445fa7ac9ab066571
RNScreens: 3a7ff46e00609d263758d646c1e6baad39921a53
RNScreens: 8fc4bf3a466b750a19850eb301d2290733bb703e
RNWorklets: ec060e76f9d7b7786d83a233e310b2ec778fb3eb
Yoga: b8206e6746bd28c028572774cece66d5348240c1

Expand Down
51 changes: 36 additions & 15 deletions FabricExample/metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');

const fs = require('fs');
const path = require('path');
const exclusionList = require('metro-config/private/defaults/exclusionList').default;
const exclusionList =
require('metro-config/private/defaults/exclusionList').default;
const escape = require('escape-string-regexp');

const libPackage = require('../package.json');
Expand All @@ -21,9 +22,11 @@ const appPackage = require('./package.json');
* in `react-navigation` submodule & causes runtime issues.
*/
function reactNavigationOptionalModuleFilter(module) {
return module in appPackage.dependencies === true ||
return (
module in appPackage.dependencies === true ||
module in libPackage.devDependencies === true ||
module in libPackage.dependencies === true;
module in libPackage.dependencies === true
);
}

/**
Expand All @@ -32,8 +35,7 @@ function reactNavigationOptionalModuleFilter(module) {
*/
function blockListProvider(modules, nodeModulesDir) {
return modules.map(
m =>
new RegExp(`^${escape(path.join(nodeModulesDir, m))}\\/.*$`),
m => new RegExp(`^${escape(path.join(nodeModulesDir, m))}\\/.*$`),
);
}

Expand All @@ -55,7 +57,6 @@ const modules = [
...Object.keys(libPackage.peerDependencies),
];


// Currently each `@react-navigation` package has `src/index.tsx`.
const reactNavigationIndexExts = ['tsx', 'ts', 'js', 'jsx'];

Expand All @@ -66,14 +67,18 @@ const reactNavigationDuplicatedModules = [
'react-native',
'react-native-screens',
'react-dom', // TODO: Consider whether this won't conflict, especially that RN 78 uses React 19 & react-navigation still uses React 18.
].concat([
'react-native-safe-area-context',
'react-native-gesture-handler',
].filter(reactNavigationOptionalModuleFilter));
].concat(
['react-native-safe-area-context', 'react-native-gesture-handler'].filter(
reactNavigationOptionalModuleFilter,
),
);

const appNodeModules = path.join(appDir, 'node_modules');
const libNodeModules = path.join(libRootDir, 'node_modules');
const reactNavigationNodeModules = path.join(reactNavigationDir, 'node_modules');
const reactNavigationNodeModules = path.join(
reactNavigationDir,
'node_modules',
);

const config = {
projectRoot: appDir,
Expand All @@ -84,7 +89,14 @@ const config = {
resolver: {
resolverMainFields: ['react-native', 'browser', 'main'],

blockList: exclusionList(blockListProvider(modules, libNodeModules).concat(blockListProvider(reactNavigationDuplicatedModules, reactNavigationNodeModules))),
blockList: exclusionList(
blockListProvider(modules, libNodeModules).concat(
blockListProvider(
reactNavigationDuplicatedModules,
reactNavigationNodeModules,
),
),
),

extraNodeModules: modules.reduce((acc, name) => {
acc[name] = path.join(__dirname, 'node_modules', name);
Expand All @@ -108,15 +120,25 @@ const config = {
// Project node modules + directory where `react-native-screens` repo lives in + react navigation node modules.
// These are consulted in order of definition.
// TODO: make it so this does not depend on whether the user renamed the repo or not...
nodeModulesPaths: [appNodeModules, path.join(appDir, '../../'), libNodeModules, reactNavigationNodeModules],
nodeModulesPaths: [
appNodeModules,
path.join(appDir, '../../'),
libNodeModules,
reactNavigationNodeModules,
],

resolveRequest: (context, moduleName, platform) => {
// We want to enforce that in case of react navigation the `src` files
// are transformed & bundled instead of the pretransformed ones in `@react-navigation/xxx/lib` directory.
if (moduleName.startsWith('@react-navigation/')) {
for (const fileExt of reactNavigationIndexExts) {
// App node modules contain symlink to react-navigation submodule.
const moduleEntryPoint = path.join(appNodeModules, moduleName, 'src', `index.${fileExt}`);
const moduleEntryPoint = path.join(
appNodeModules,
moduleName,
'src',
`index.${fileExt}`,
);
if (fs.existsSync(moduleEntryPoint)) {
return {
filePath: moduleEntryPoint,
Expand Down Expand Up @@ -146,4 +168,3 @@ const config = {
};

module.exports = mergeConfig(getDefaultConfig(__dirname), config);

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.facebook.react.module.annotations.ReactModuleList
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider
import com.facebook.react.uimanager.ViewManager
import com.swmansion.rnscreens.gamma.scrollviewmarker.ScrollViewMarkerViewManager
import com.swmansion.rnscreens.gamma.stack.host.StackHostViewManager
import com.swmansion.rnscreens.gamma.stack.screen.StackScreenViewManager
import com.swmansion.rnscreens.gamma.tabs.TabsHostViewManager
Expand Down Expand Up @@ -55,6 +56,7 @@ class RNScreensPackage : BaseReactPackage() {
SafeAreaViewManager(),
StackHostViewManager(),
StackScreenViewManager(),
ScrollViewMarkerViewManager(),
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.swmansion.rnscreens.gamma.scrollviewmarker

import android.annotation.SuppressLint
import android.view.ViewGroup
import android.widget.ScrollView
import androidx.core.view.children
import androidx.core.widget.NestedScrollView
import com.facebook.react.bridge.UIManager
import com.facebook.react.bridge.UIManagerListener
import com.facebook.react.common.annotations.UnstableReactNativeAPI
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.common.UIManagerType
import com.facebook.react.views.view.ReactViewGroup

@OptIn(UnstableReactNativeAPI::class)
@SuppressLint("ViewConstructor") // Should never be inflated / restored
class ScrollViewMarker(
private val reactContext: ThemedReactContext,
) : ReactViewGroup(reactContext),
UIManagerListener {
init {
// We're adding ourselves during a batch, therefore we expect to receive its finalization callbacks
val uiManager =
checkNotNull(UIManagerHelper.getUIManager(reactContext, UIManagerType.FABRIC)) {
"[RNScreens] UIManager must not be null."
}
uiManager.addUIManagerEventListener(this)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see we're adding self as a listener, but we never remove, is this intentional?

}

private var hasAttemptedRegistration: Boolean = false

/**
* Currently we discover only ScrollView or NestedScrollView.
* It'll crash in case scroll view detection fails.
*
* Call it only after the children have been already attached and not yet detached.
*/
private fun findScrollView(): ViewGroup {
val childScrollView =
checkNotNull(children.find { childView -> childView is ScrollView || childView is NestedScrollView }) {
"[RNScreens] Failed to find supported type of ScrollView in children of ScrollViewMarker"
}

return childScrollView as ViewGroup
}

private fun findSeekingParent(): ScrollViewSeeking? {
var currentView = parent

while (currentView != null) {
if (currentView is ScrollViewSeeking) {
return currentView
}
currentView = currentView.parent
}

return null
}

private fun registerWithSeekingAncestor() {
val scrollView = findScrollView()
findSeekingParent()?.registerScrollView(this, scrollView)
}

private fun maybeRegisterWithSeekingAncestor() {
if (hasAttemptedRegistration) {
return
}

registerWithSeekingAncestor()
hasAttemptedRegistration = true
}

// UIManagerListener

override fun didMountItems(uiManager: UIManager) {
maybeRegisterWithSeekingAncestor()
}

override fun willDispatchViewUpdates(uiManager: UIManager) = Unit

override fun willMountItems(uiManager: UIManager) = Unit

override fun didDispatchMountItems(uiManager: UIManager) = Unit

override fun didScheduleMountItems(uiManager: UIManager) = Unit
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.swmansion.rnscreens.gamma.scrollviewmarker

import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewGroupManager
import com.facebook.react.uimanager.ViewManagerDelegate
import com.facebook.react.viewmanagers.RNSScrollViewMarkerManagerDelegate
import com.facebook.react.viewmanagers.RNSScrollViewMarkerManagerInterface

@ReactModule(name = ScrollViewMarkerViewManager.REACT_CLASS)
class ScrollViewMarkerViewManager :
ViewGroupManager<ScrollViewMarker>(),
RNSScrollViewMarkerManagerInterface<ScrollViewMarker> {
private val delegate: ViewManagerDelegate<ScrollViewMarker> =
RNSScrollViewMarkerManagerDelegate<ScrollViewMarker, ScrollViewMarkerViewManager>(this)

override fun getName() = REACT_CLASS

override fun getDelegate() = delegate

override fun createViewInstance(reactContext: ThemedReactContext): ScrollViewMarker = ScrollViewMarker(reactContext)

// iOS only
override fun setLeftScrollEdgeEffect(
view: ScrollViewMarker?,
value: String?,
) = Unit

// iOS only
override fun setTopScrollEdgeEffect(
view: ScrollViewMarker?,
value: String?,
) = Unit

// iOS only
override fun setRightScrollEdgeEffect(
view: ScrollViewMarker?,
value: String?,
) = Unit

// iOS only
override fun setBottomScrollEdgeEffect(
view: ScrollViewMarker?,
value: String?,
) = Unit

companion object {
const val REACT_CLASS = "RNSScrollViewMarker"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.swmansion.rnscreens.gamma.scrollviewmarker

import android.view.ViewGroup

interface ScrollViewSeeking {
// scrollView is a ViewGroup, because there is no universal ScrollView component on Android.
// It might a be ScrollView, NestedScrollView (different inheritance hierarchies) or any other
// ViewGroup that implements appropriate scrolling interfaces.
fun registerScrollView(
maker: ScrollViewMarker,
scrollView: ViewGroup,
)
}
21 changes: 21 additions & 0 deletions apps/src/shared/utils/color-generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Colors from '../styling/Colors';

let GLOBAL_NEXT_COLOR_ID = 0;

export function generateNextColor() {
const colors = [
Colors.BlueDark100,
Colors.GreenDark100,
Colors.RedDark100,
Colors.YellowDark100,
Colors.PurpleDark100,
Colors.BlueLight100,
Colors.GreenLight100,
Colors.RedLight100,
Colors.YellowLight100,
Colors.PurpleLight100,
];
const index = GLOBAL_NEXT_COLOR_ID;
GLOBAL_NEXT_COLOR_ID += 1;
return colors[index % colors.length];
}
10 changes: 10 additions & 0 deletions apps/src/tests/single-feature-tests/scroll-view-marker/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ScenarioGroup } from '../../shared/helpers';
import TestSvmDetectsScrollView from './test-svm-configures-scroll-view';

const ScrollViewMarkerScenarioGroup: ScenarioGroup = {
name: 'ScrollViewMarker scenarios',
details: 'Scenarios related to ScrollViewMarker component',
scenarios: [TestSvmDetectsScrollView],
Comment on lines +2 to +7
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The imported constant name TestSvmDetectsScrollView does not match the actual scenario's name 'Basic functionality' or key 'test-svm-configures-scroll-view'. The import name suggests it's about detecting the scroll view, while the actual scenario key indicates it configures the scroll view. Consider renaming the import to match the scenario, such as TestSvmConfiguresScrollView.

Suggested change
import TestSvmDetectsScrollView from './test-svm-configures-scroll-view';
const ScrollViewMarkerScenarioGroup: ScenarioGroup = {
name: 'ScrollViewMarker scenarios',
details: 'Scenarios related to ScrollViewMarker component',
scenarios: [TestSvmDetectsScrollView],
import TestSvmConfiguresScrollView from './test-svm-configures-scroll-view';
const ScrollViewMarkerScenarioGroup: ScenarioGroup = {
name: 'ScrollViewMarker scenarios',
details: 'Scenarios related to ScrollViewMarker component',
scenarios: [TestSvmConfiguresScrollView],

Copilot uses AI. Check for mistakes.
};

export default ScrollViewMarkerScenarioGroup;
Loading
Loading