Skip to content
Merged
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
7 changes: 7 additions & 0 deletions apps/router-e2e/__e2e__/native-tabs/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ export default function Layout() {
<NativeTabs.Trigger.Badge>9</NativeTabs.Trigger.Badge>
<NativeTabs.Trigger.Label>Dynamic</NativeTabs.Trigger.Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="icon-test">
<NativeTabs.Trigger.Icon
src={require('../../../assets/explore_orange.png')}
renderingMode="original"
/>
<NativeTabs.Trigger.Label>Original Icon</NativeTabs.Trigger.Label>
</NativeTabs.Trigger>
<NativeTabs.BottomAccessory>
<MiniPlayer isPlaying={isPlaying} setIsPlaying={setIsPlaying} />
</NativeTabs.BottomAccessory>
Expand Down
56 changes: 56 additions & 0 deletions apps/router-e2e/__e2e__/native-tabs/app/icon-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { View, Text, StyleSheet } from 'react-native';

export default function IconTestTab() {
return (
<View style={styles.container}>
<Text style={styles.title}>Icon Rendering Mode Test</Text>
<Text style={styles.subtitle}>
This tab uses an image icon with 'original' renderingMode.
</Text>
<Text style={styles.description}>
The icon should display its original colors (the Expo orange icon) without being tinted by
the system tab bar color.
</Text>
<Text style={styles.note}>
Compare with other tabs that use SF Symbols or the default 'template' mode, which will be
tinted with the system color.
</Text>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 24,
backgroundColor: '#f5f5f5',
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 16,
textAlign: 'center',
},
subtitle: {
fontSize: 18,
color: '#333',
marginBottom: 12,
textAlign: 'center',
},
description: {
fontSize: 14,
color: '#666',
textAlign: 'center',
marginBottom: 12,
paddingHorizontal: 20,
},
note: {
fontSize: 12,
color: '#888',
textAlign: 'center',
fontStyle: 'italic',
paddingHorizontal: 20,
},
});
28 changes: 28 additions & 0 deletions docs/pages/router/advanced/native-tabs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,34 @@ export default function TabLayout() {
}
```

#### Icon rendering mode

When using the `src` prop for custom images on iOS, you can control how the icon is rendered with the `renderingMode` prop:

- **`template` (default)**: The icon is rendered as a template image, allowing iOS to apply the tint color. This is ideal for single-color icons that should match your app's color scheme.
- **`original`**: The icon is rendered with its original colors preserved. This is useful for icons with gradients or multiple colors.

```tsx app/_layout.tsx
import { NativeTabs, Icon } from 'expo-router/unstable-native-tabs';

export default function TabLayout() {
return (
<NativeTabs>
{/* Icon with original colors preserved (e.g., for gradient or multi-color icons) */}
<NativeTabs.Trigger name="colorful">
<Icon src={require('../../../assets/colorful_icon.png')} renderingMode="original" />
</NativeTabs.Trigger>
{/* Icon rendered as a template (default behavior) */}
<NativeTabs.Trigger name="simple">
<Icon src={require('../../../assets/simple_icon.png')} renderingMode="template" />
</NativeTabs.Trigger>
</NativeTabs>
);
}
```

> **info** The `renderingMode` prop only affects iOS. On Android, all image icons are rendered with their original colors.

Liquid glass on iOS automatically changes colors based on if the background color is light or dark. There is no callback for this, so you need to use a `PlatformColor` to set the color of the icon.

```tsx app/_layout.tsx
Expand Down

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions packages/@expo/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
- Restore `resetCache`, `maxWorkers`, and `port` override args when instantiating Metro ([#41854](https://github.com/expo/expo/pull/41854) by [@shottah](https://github.com/shottah))
- Fix response streaming from dev server (remove compression) ([#41955](https://github.com/expo/expo/pull/41955) by [@krystofwoldrich](https://github.com/krystofwoldrich))
- Restrict debugger (`/inspector/network`) and devtools (`/expo-dev-plugins/broadcast`) sockets to local connections ([#42156](https://github.com/expo/expo/pull/42156) by [@kitten](https://github.com/kitten))
- Use `ImmutableRequest` for loader functions ([#42149](https://github.com/expo/expo/pull/42149) by [@hassankhan](https://github.com/hassankhan))
- Avoid module ID collision between loader and render bundles ([#42245](https://github.com/expo/expo/pull/42245) by [@hassankhan](https://github.com/hassankhan))

### 💡 Others

Expand Down
24 changes: 20 additions & 4 deletions packages/@expo/cli/e2e/__tests__/export/server-loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
RUNTIME_WORKERD,
setupServer,
} from '../../utils/runtime';
import { findProjectFiles } from '../utils';
import { findProjectFiles, getHtml } from '../utils';

runExportSideEffects();

Expand Down Expand Up @@ -113,10 +113,26 @@ describe.each(
expect(data).toBeNull();
});

it('receives `Request` object', async () => {
const response = await server.fetchAsync('/_expo/loaders/request');
it.each([
{
name: 'loader endpoint',
url: '/_expo/loaders/request',
getData: (response: Response) => {
return response.json();
},
},
{
name: 'page',
url: '/request',
getData: async (response: Response) => {
const html = getHtml(await response.text());
return JSON.parse(html.querySelector('[data-testid="loader-result"]')!.textContent);
},
},
])('$name $url does not receive `Request` object', async ({ getData, url }) => {
const response = await server.fetchAsync(url);
expect(response.status).toBe(200);
const data = await response.json();
const data = await getData(response);

expect(new URL(data.url).pathname).toBe('/request');
expect(data.method).toBe('GET');
Expand Down
33 changes: 15 additions & 18 deletions packages/@expo/cli/e2e/__tests__/export/static-loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ describe.each(
});

it.each([
{
name: 'loader endpoint',
url: '/_expo/loaders/request',
getData: (response: Response) => {
return response.json();
},
},
{
name: 'page',
url: '/request',
Expand All @@ -93,23 +100,13 @@ describe.each(
return JSON.parse(html.querySelector('[data-testid="loader-result"]')!.textContent);
},
},
{
name: 'loader endpoint',
url: '/_expo/loaders/request',
getData: (response: Response) => {
return response.json();
},
},
])(
'$name $url does not receive `Request` object',
async ({ getData, url }) => {
const response = await server.fetchAsync(url);
expect(response.status).toBe(200);
const data = await getData(response);
])('$name $url does not receive `Request` object', async ({ getData, url }) => {
const response = await server.fetchAsync(url);
expect(response.status).toBe(200);
const data = await getData(response);

expect(data.url).toBeNull();
expect(data.method).toBeNull();
expect(data.headers).toBeNull();
}
);
expect(data.url).toBeNull();
expect(data.method).toBeNull();
expect(data.headers).toBeNull();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import type { GetStaticContentOptions } from '@expo/router-server/build/static/r
import assert from 'assert';
import chalk from 'chalk';
import type { RouteNode } from 'expo-router/build/Route';
import { type RouteInfo, type RoutesManifest } from 'expo-server/private';
import { type RouteInfo, type RoutesManifest, type ImmutableRequest } from 'expo-server/private';
import path from 'path';

import {
Expand Down Expand Up @@ -573,7 +573,11 @@ export class MetroBundlerDevServer extends BundlerDevServer {
/**
* This function is invoked when running in development via `expo start`
*/
private async getStaticPageAsync(pathname: string, route: RouteInfo<RegExp>, request?: Request) {
private async getStaticPageAsync(
pathname: string,
route: RouteInfo<RegExp>,
request?: ImmutableRequest
) {
const { exp } = getConfig(this.projectRoot);
const { mode, isExporting, clientBoundaries, baseUrl, reactCompiler, routerRoot, asyncRoutes } =
this.instanceMetroOptions;
Expand Down Expand Up @@ -1701,7 +1705,7 @@ export class MetroBundlerDevServer extends BundlerDevServer {
location: URL,
route: ResolvedLoaderRoute,
// The `request` object is only available when using SSR
request?: Request
request?: ImmutableRequest
): Promise<{ data: unknown } | undefined> {
const { exp } = getConfig(this.projectRoot);
const { unstable_useServerDataLoaders } = exp.extra?.router;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import type { ProjectConfig } from '@expo/config';
import type { MiddlewareSettings } from 'expo-server';
import { createRequestHandler } from 'expo-server/adapter/http';
import { type RouteInfo } from 'expo-server/private';
import { ImmutableRequest, type RouteInfo } from 'expo-server/private';
import path from 'path';
import resolveFrom from 'resolve-from';

Expand All @@ -31,14 +31,14 @@ export function createRouteHandlerMiddleware(
getStaticPageAsync: (
pathname: string,
route: RouteInfo<RegExp>,
request?: Request
request?: ImmutableRequest
) => Promise<{ content: string }>;
bundleApiRoute: (
functionFilePath: string
) => Promise<null | Record<string, Function> | Response>;
executeLoaderAsync: (
route: RouteInfo<RegExp>,
request: Request
request: ImmutableRequest
) => Promise<{ data: unknown } | undefined>;
config: ProjectConfig;
headers: Record<string, string | string[]>;
Expand Down Expand Up @@ -98,7 +98,7 @@ export function createRouteHandlerMiddleware(
const { content } = await options.getStaticPageAsync(
request.url,
route,
isSSREnabled ? request : undefined
isSSREnabled ? new ImmutableRequest(request) : undefined
);
return content;
} catch (error: any) {
Expand Down Expand Up @@ -233,7 +233,7 @@ export function createRouteHandlerMiddleware(
}
},
async getLoaderData(request, route) {
return options.executeLoaderAsync(route, request);
return options.executeLoaderAsync(route, new ImmutableRequest(request));
},
}
);
Expand Down
1 change: 1 addition & 0 deletions packages/@expo/metro-config/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

- Fix output typings to remove unreachable transitive dependencies from them ([#41676](https://github.com/expo/expo/pull/41676) by [@kitten](https://github.com/kitten))
- Fix `minifierPath` resolution for cache key generation for strict isolated installations ([#41686](https://github.com/expo/expo/pull/41686) by [@kitten](https://github.com/kitten))
- Avoid module ID collision between loader and render bundles ([#42245](https://github.com/expo/expo/pull/42245) by [@hassankhan](https://github.com/hassankhan))

### 💡 Others

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading