-
Notifications
You must be signed in to change notification settings - Fork 108
feat:migrate to standard navigation shared adapter #581
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
oleksandrzavarzin-callstack
wants to merge
15
commits into
callstack:main
Choose a base branch
from
oleksandrzavarzin-callstack:feat/migrate-to-standard-navigation-shared-adapter
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
15 commits
Select commit
Hold shift + click to select a range
ee9006d
test(react-navigation): cover the current navigator behaviour
oleksandrzavarzin-callstack eff21b1
feat(navigation): add @bottom-tabs/navigation
oleksandrzavarzin-callstack 14e3e17
feat(react-navigation): delegate to @bottom-tabs/navigation
oleksandrzavarzin-callstack 978f887
test(expo-router-test): add an expo-router integration harness
oleksandrzavarzin-callstack 2b2268c
docs: document the standard-navigation entry for Expo Router
oleksandrzavarzin-callstack 56c2c7f
refactor(react-navigation): drop the color dependency
oleksandrzavarzin-callstack 3dcd9e3
fix(react-navigation): keep the full tab bar contract
oleksandrzavarzin-callstack 67f3332
docs: correct the Expo Router warning and list the new package
oleksandrzavarzin-callstack dbc0727
fix(react-navigation): record the peer bump in the lockfile
oleksandrzavarzin-callstack b46c8bf
fix(navigation): let integrators infer the navigator props
oleksandrzavarzin-callstack 33363a5
docs: type the shared navigator recipe
oleksandrzavarzin-callstack ec37666
refactor: rename the shared package to @bottom-tabs/standard-navigation
oleksandrzavarzin-callstack 5d71839
fix: skip packages without a manifest when building the babel aliases
oleksandrzavarzin-callstack 2cc28f1
docs: add quick-start for Expo≤55
oleksandrzavarzin-callstack a65309c
fix(react-navigation): restore the >=7 peer on @react-navigation/native
oleksandrzavarzin-callstack 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
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
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,5 @@ | ||
| --- | ||
| '@bottom-tabs/react-navigation': patch | ||
| --- | ||
|
|
||
| Drop the `color` dependency in favour of React Native's `processColor`. `color` is ESM-only, so consumers had to extend `transformIgnorePatterns` before Jest would run at all |
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,6 @@ | ||
| --- | ||
| '@bottom-tabs/standard-navigation': minor | ||
| '@bottom-tabs/react-navigation': minor | ||
| --- | ||
|
|
||
| Move the native tabs implementation into a new framework-agnostic `@bottom-tabs/standard-navigation` package built on the `standard-navigation` contract, so Expo Router SDK 56+ apps can drive the same navigator through `unstable_createStandardRouterNavigator` without loading a second copy of React Navigation. |
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
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,32 @@ | ||
| const fs = require('node:fs'); | ||
| const path = require('node:path'); | ||
|
|
||
| const PROJECT_COPIES = ['react', 'react-dom']; | ||
|
|
||
| const reactNavigationScope = path.dirname( | ||
| path.dirname(require.resolve('@react-navigation/native/package.json')) | ||
| ); | ||
|
|
||
| const reactNavigationCopies = fs | ||
| .readdirSync(reactNavigationScope) | ||
| .map((name) => [ | ||
| `^@react-navigation/${name}($|/.*)`, | ||
| `${reactNavigationScope}/${name}$1`, | ||
| ]); | ||
|
|
||
| module.exports = { | ||
| preset: 'jest-expo', | ||
|
|
||
| setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'], | ||
|
|
||
| moduleNameMapper: { | ||
| ...Object.fromEntries( | ||
| PROJECT_COPIES.map((name) => [ | ||
| `^${name}($|/.*)`, | ||
| path.dirname(require.resolve(`${name}/package.json`)) + '$1', | ||
| ]) | ||
| ), | ||
| ...Object.fromEntries(reactNavigationCopies), | ||
| }, | ||
|
|
||
| }; |
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,23 @@ | ||
| const consoleMethods = ['warn', 'error'] as const; | ||
|
|
||
| let output: string[] = []; | ||
|
|
||
| beforeEach(() => { | ||
| output = []; | ||
|
|
||
| for (const method of consoleMethods) { | ||
| jest.spyOn(console, method).mockImplementation((...args: unknown[]) => { | ||
| output.push(`console.${method}: ${args.join(' ')}`); | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| const captured = output; | ||
|
|
||
| jest.restoreAllMocks(); | ||
|
|
||
| if (captured.length > 0) { | ||
| throw new Error(`Expected no console output, got:\n${captured.join('\n')}`); | ||
| } | ||
| }); |
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,38 @@ | ||
| { | ||
| "name": "expo-router-test", | ||
| "version": "0.0.1", | ||
| "private": true, | ||
| "scripts": { | ||
| "test": "jest", | ||
| "typecheck": "tsc --noEmit" | ||
| }, | ||
| "dependencies": { | ||
| "@bottom-tabs/react-navigation": "*", | ||
| "@bottom-tabs/standard-navigation": "*", | ||
| "@react-navigation/native": "^7.3.0", | ||
| "expo": "~57.0.21", | ||
| "expo-constants": "~57.0.17", | ||
| "expo-linking": "~57.0.9", | ||
| "expo-router": "~57.0.20", | ||
| "react": "19.2.3", | ||
| "react-dom": "19.2.3", | ||
| "react-native": "0.86.3", | ||
| "react-native-bottom-tabs": "*", | ||
| "react-native-gesture-handler": "~2.32.0", | ||
| "react-native-reanimated": "4.5.1", | ||
| "react-native-safe-area-context": "~5.7.0", | ||
| "react-native-screens": "~4.26.0", | ||
| "react-native-web": "~0.21.0", | ||
| "react-native-worklets": "0.10.1" | ||
| }, | ||
| "devDependencies": { | ||
| "@react-native/jest-preset": "^0.86.3", | ||
| "@testing-library/react-native": "^14.0.1", | ||
| "@types/jest": "^29.5.5", | ||
| "@types/react": "~19.2.2", | ||
| "jest": "^29.7.0", | ||
| "jest-expo": "~57.0.5", | ||
| "test-renderer": "^1.2.0", | ||
| "typescript": "^5.9.2" | ||
| } | ||
| } |
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,116 @@ | ||
| import { NavigationContainer } from '@react-navigation/native'; | ||
| import { act, render } from '@testing-library/react-native'; | ||
| import { renderRouter } from 'expo-router/testing-library'; | ||
| import { Text } from 'react-native'; | ||
|
|
||
| import { ExpoRouterTabs, ReactNavigationTabs } from '../tabs'; | ||
|
|
||
| let tabViewProps: Record<string, any>; | ||
|
|
||
| jest.mock('react-native-bottom-tabs', () => ({ | ||
| __esModule: true, | ||
| default: (props: Record<string, any>) => { | ||
| tabViewProps = props; | ||
| return null; | ||
| }, | ||
| })); | ||
|
|
||
| const OPTIONS = { | ||
| index: { title: 'Home', tabBarBadge: '3' }, | ||
| explore: { title: 'Explore' }, | ||
| }; | ||
|
|
||
| const adapters = [ | ||
| { | ||
| name: 'expo-router', | ||
| render: async () => { | ||
| await renderRouter( | ||
| { | ||
| _layout: () => ( | ||
| <ExpoRouterTabs> | ||
| <ExpoRouterTabs.Screen name="index" options={OPTIONS.index} /> | ||
| <ExpoRouterTabs.Screen name="explore" options={OPTIONS.explore} /> | ||
| </ExpoRouterTabs> | ||
| ), | ||
| index: () => <Text>index scene</Text>, | ||
| explore: () => <Text>explore scene</Text>, | ||
| }, | ||
| { initialUrl: '/' } | ||
| ); | ||
| }, | ||
| }, | ||
| { | ||
| name: 'react-navigation', | ||
| render: async () => { | ||
| await render( | ||
| <NavigationContainer> | ||
| <ReactNavigationTabs.Navigator> | ||
| <ReactNavigationTabs.Screen name="index" options={OPTIONS.index}> | ||
| {() => <Text>index scene</Text>} | ||
| </ReactNavigationTabs.Screen> | ||
| <ReactNavigationTabs.Screen | ||
| name="explore" | ||
| options={OPTIONS.explore} | ||
| > | ||
| {() => <Text>explore scene</Text>} | ||
| </ReactNavigationTabs.Screen> | ||
| </ReactNavigationTabs.Navigator> | ||
| </NavigationContainer> | ||
| ); | ||
| }, | ||
| }, | ||
| ]; | ||
|
|
||
| beforeEach(() => { | ||
| tabViewProps = {}; | ||
| }); | ||
|
|
||
| describe.each(adapters)('$name', ({ render: renderAdapter }) => { | ||
| const routeNamed = (name: string) => | ||
| tabViewProps.navigationState.routes.find( | ||
| (it: { name: string }) => it.name === name | ||
| ); | ||
|
|
||
| it('drives the shared navigator', async () => { | ||
| await renderAdapter(); | ||
|
|
||
| expect( | ||
| tabViewProps.navigationState.routes.map((it: { name: string }) => it.name) | ||
| ).toEqual(['index', 'explore']); | ||
| expect(tabViewProps.navigationState.index).toBe(0); | ||
| }); | ||
|
|
||
| it('maps screen options onto the native view', async () => { | ||
| await renderAdapter(); | ||
|
|
||
| expect(tabViewProps.getLabelText({ route: routeNamed('index') })).toBe( | ||
| 'Home' | ||
| ); | ||
| expect(tabViewProps.getBadge({ route: routeNamed('index') })).toBe('3'); | ||
| expect( | ||
| tabViewProps.getBadge({ route: routeNamed('explore') }) | ||
| ).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('navigates when a tab is pressed', async () => { | ||
| await renderAdapter(); | ||
|
|
||
| await act(async () => { | ||
| tabViewProps.onIndexChange(1); | ||
| }); | ||
|
|
||
| expect(tabViewProps.navigationState.index).toBe(1); | ||
| }); | ||
|
|
||
| it('exposes only the declared screens as tabs', async () => { | ||
| await renderAdapter(); | ||
|
|
||
| expect(tabViewProps.navigationState.routes).toHaveLength(2); | ||
| }); | ||
|
|
||
| it('does not forward router options to the native view', async () => { | ||
| await renderAdapter(); | ||
|
|
||
| expect(tabViewProps).not.toHaveProperty('backBehavior'); | ||
| }); | ||
| }); | ||
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,47 @@ | ||
| import { ExpoRouterTabs } from '../tabs'; | ||
|
|
||
| export const tintColors = () => ( | ||
| <ExpoRouterTabs | ||
| tabBarActiveTintColor="#ff0000" | ||
| tabBarInactiveTintColor="#808080" | ||
| /> | ||
| ); | ||
|
|
||
| export const defaultTintColors = () => ( | ||
| <ExpoRouterTabs defaultTintColors={{ active: '#ff0000' }} /> | ||
| ); | ||
|
|
||
| export const customTabBar = () => ( | ||
| <ExpoRouterTabs | ||
| tabBar={({ state }) => state.routes.map((it) => it.name).join()} | ||
| /> | ||
| ); | ||
|
|
||
| export const nativeProps = () => ( | ||
| <ExpoRouterTabs labeled sidebarAdaptable hapticFeedbackEnabled /> | ||
| ); | ||
|
|
||
| export const screenOptions = () => ( | ||
| <ExpoRouterTabs> | ||
| <ExpoRouterTabs.Screen | ||
| name="index" | ||
| options={{ title: 'Home', tabBarBadge: '3' }} | ||
| /> | ||
| </ExpoRouterTabs> | ||
| ); | ||
|
|
||
| export const rejectsAWrongNavigatorProp = () => ( | ||
| // @ts-expect-error - a number is not a valid tint color | ||
| <ExpoRouterTabs tabBarActiveTintColor={42} /> | ||
| ); | ||
|
|
||
| export const rejectsAnUnknownScreenOption = () => ( | ||
| <ExpoRouterTabs> | ||
| {/* @ts-expect-error - not a screen option this navigator accepts */} | ||
| <ExpoRouterTabs.Screen name="index" options={{ notARealOption: true }} /> | ||
| </ExpoRouterTabs> | ||
| ); | ||
|
|
||
| it('type checks the navigator surface', () => { | ||
| expect(typeof ExpoRouterTabs).toBe('object'); | ||
| }); |
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,10 @@ | ||
| import { NativeBottomTabsContent } from '@bottom-tabs/standard-navigation'; | ||
| import { createNativeBottomTabNavigator } from '@bottom-tabs/react-navigation'; | ||
| import { TabRouter, unstable_createStandardRouterNavigator } from 'expo-router'; | ||
|
|
||
| export const ExpoRouterTabs = unstable_createStandardRouterNavigator( | ||
| NativeBottomTabsContent, | ||
| TabRouter | ||
| ); | ||
|
|
||
| export const ReactNavigationTabs = createNativeBottomTabNavigator(); |
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,8 @@ | ||
| { | ||
| "extends": "expo/tsconfig.base", | ||
| "compilerOptions": { | ||
| "strict": true, | ||
| "types": ["jest"] | ||
| }, | ||
| "include": ["**/*.ts", "**/*.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
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.
Not related to this file specifically, but the example app should be named
example-expo-routerto follow the convention of the other example appsThere 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.
It's a test harness rather than the example Expo router app (for that option, we will have
expo-template, after it's updated). It just checks whether our two adapters, driven by a realExpo Routerand a realReact Navigation, pass the same correct props to the tab view.