[expo-router][ios] support image source and xcassets in bottom toolbar#43047
Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. |
There was a problem hiding this comment.
Pull request overview
Adds iOS bottom-toolbar support for passing toolbar/menu icons as either ImageSourcePropType (resolved via expo-image’s useImage) or Xcode asset catalog names (xcassets), and wires this through the Stack toolbar components with accompanying iOS tests.
Changes:
- Introduce
RouterToolbarItemWithImageSupportandNativeLinkPreviewActionWithImageSupportwrappers that resolveimageSource/xcassetNameto anImageRefviauseImage. - Add shared extraction helpers (
extractXcassetName,extractImageSource) and use them inStackToolbarButton/StackToolbarMenubottom-placement rendering. - Add/extend iOS tests covering
imageSource+ xcasset behavior and update generated build outputs.
Reviewed changes
Copilot reviewed 16 out of 34 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/expo-router/src/toolbar/tests/RouterToolbarItemWithImageSupport.test.ios.tsx | Adds iOS unit tests for toolbar item wrapper image resolution + precedence. |
| packages/expo-router/src/toolbar/RouterToolbarItemWithImageSupport.tsx | New wrapper component that resolves imageSource/xcassetName using useImage. |
| packages/expo-router/src/link/preview/tests/NativeLinkPreviewActionWithImageSupport.test.ios.tsx | Adds iOS unit tests for link preview action wrapper image resolution + precedence. |
| packages/expo-router/src/link/preview/NativeLinkPreviewActionWithImageSupport.tsx | New wrapper component that resolves imageSource/xcassetName for native link preview actions. |
| packages/expo-router/src/layouts/stack-utils/toolbar/toolbar-primitives.tsx | Minor doc tweak for xcasset icon docs. |
| packages/expo-router/src/layouts/stack-utils/toolbar/shared.ts | Adds xcasset to shared props and introduces image/xcasset extraction helpers used by bottom toolbar. |
| packages/expo-router/src/layouts/stack-utils/toolbar/StackToolbarMenu.tsx | Wires imageSource/xcassetName into bottom menu rendering; adds useImage resolution for menu actions. |
| packages/expo-router/src/layouts/stack-utils/toolbar/StackToolbarButton.tsx | Wires imageSource/xcassetName into bottom button rendering via new toolbar item wrapper. |
| packages/expo-router/src/layouts/stack-utils/tests/shared.test.ios.tsx | Adds tests for xcasset extraction and extractImageSource helper. |
| packages/expo-router/src/layouts/stack-utils/tests/StackToolbarMenu.test.ios.tsx | Updates mocks and adds tests for passing xcassetName/imageSource to bottom menu native component wrapper. |
| packages/expo-router/src/layouts/stack-utils/tests/StackToolbarButton.test.ios.tsx | Updates mocks and adds tests for passing xcassetName/imageSource to bottom toolbar item wrapper. |
| packages/expo-router/build/toolbar/RouterToolbarItemWithImageSupport.js.map | Build output for new toolbar wrapper (sourcemap). |
| packages/expo-router/build/toolbar/RouterToolbarItemWithImageSupport.js | Build output for new toolbar wrapper (JS). |
| packages/expo-router/build/toolbar/RouterToolbarItemWithImageSupport.d.ts.map | Build output for new toolbar wrapper (types map). |
| packages/expo-router/build/toolbar/RouterToolbarItemWithImageSupport.d.ts | Build output for new toolbar wrapper (types). |
| packages/expo-router/build/link/preview/NativeLinkPreviewActionWithImageSupport.js.map | Build output for new link preview wrapper (sourcemap). |
| packages/expo-router/build/link/preview/NativeLinkPreviewActionWithImageSupport.js | Build output for new link preview wrapper (JS). |
| packages/expo-router/build/link/preview/NativeLinkPreviewActionWithImageSupport.d.ts.map | Build output for new link preview wrapper (types map). |
| packages/expo-router/build/link/preview/NativeLinkPreviewActionWithImageSupport.d.ts | Build output for new link preview wrapper (types). |
| packages/expo-router/build/layouts/stack-utils/toolbar/toolbar-primitives.js.map | Build output updated for primitives doc change (sourcemap). |
| packages/expo-router/build/layouts/stack-utils/toolbar/toolbar-primitives.d.ts.map | Build output updated for primitives doc change (types map). |
| packages/expo-router/build/layouts/stack-utils/toolbar/toolbar-primitives.d.ts | Build output updated for primitives doc change (types). |
| packages/expo-router/build/layouts/stack-utils/toolbar/shared.js.map | Build output updated for shared helpers/xcasset support (sourcemap). |
| packages/expo-router/build/layouts/stack-utils/toolbar/shared.js | Build output updated for shared helpers/xcasset support (JS). |
| packages/expo-router/build/layouts/stack-utils/toolbar/shared.d.ts.map | Build output updated for shared helpers/xcasset support (types map). |
| packages/expo-router/build/layouts/stack-utils/toolbar/shared.d.ts | Build output updated for shared helpers/xcasset support (types). |
| packages/expo-router/build/layouts/stack-utils/toolbar/StackToolbarMenu.js.map | Build output updated for bottom menu image support (sourcemap). |
| packages/expo-router/build/layouts/stack-utils/toolbar/StackToolbarMenu.js | Build output updated for bottom menu image support (JS). |
| packages/expo-router/build/layouts/stack-utils/toolbar/StackToolbarMenu.d.ts.map | Build output updated for bottom menu image support (types map). |
| packages/expo-router/build/layouts/stack-utils/toolbar/StackToolbarMenu.d.ts | Build output updated for bottom menu image support (types). |
| packages/expo-router/build/layouts/stack-utils/toolbar/StackToolbarButton.js.map | Build output updated for bottom button image support (sourcemap). |
| packages/expo-router/build/layouts/stack-utils/toolbar/StackToolbarButton.js | Build output updated for bottom button image support (JS). |
| packages/expo-router/build/layouts/stack-utils/toolbar/StackToolbarButton.d.ts.map | Build output updated for bottom button image support (types map). |
| packages/expo-router/build/layouts/stack-utils/toolbar/StackToolbarButton.d.ts | Build output updated for bottom button image support (types). |
Comments suppressed due to low confidence (1)
packages/expo-router/src/layouts/stack-utils/toolbar/StackToolbarButton.tsx:95
extractXcassetNamesupports readingprops.xcasset, butStackToolbarButtonPropsdoesn't currently define anxcassetprop (only theStackToolbarIconchild supports it). If passingxcassetdirectly toStack.Toolbar.Buttonis intended, add it toStackToolbarButtonPropsand document precedence vsicon/Icon child; otherwise consider removing the fallback to avoid an unreachable path for TypeScript users.
/**
* Icon to display in the button.
*
* Can be a string representing an SFSymbol or an image source.
*/
icon?: StackHeaderItemSharedProps['icon'];
/**
* Image to display in the button.
*
* > **Note**: This prop is only supported in toolbar with `placement="bottom"`.
*/
image?: ImageRef;
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
| ) { | ||
| const { imageSource, ...rest } = props; | ||
| const resolvedImage = useImage(imageSource as ImageSource | number); |
There was a problem hiding this comment.
NativeToolbarMenuActionWithImage casts imageSource and calls useImage, but ImageSourcePropType can be an array. Without normalizing/guarding, arrays can reach useImage (which expects ImageSource | string | number) and fail at runtime. Consider aligning behavior with RouterToolbarItemWithResolvedImage by either rejecting arrays with a clear error or normalizing to a single source.
| const resolvedImage = useImage(imageSource as ImageSource | number); | |
| const normalizedImageSource = Array.isArray(imageSource) ? imageSource[0] : imageSource; | |
| const resolvedImage = useImage(normalizedImageSource as ImageSource | number); |
| * > **Note**: When used in `placement="bottom"`, only string SFSymbols are supported. Use the `image` prop to provide custom images. | ||
| */ | ||
| icon?: StackHeaderItemSharedProps['icon']; | ||
| /** |
There was a problem hiding this comment.
The new extractXcassetName fallback uses props.xcasset, but StackToolbarMenuProps doesn't currently expose an xcasset prop (only the StackToolbarIcon child supports it). If xcasset is intended to be part of the public API for Stack.Toolbar.Menu, consider adding it to StackToolbarMenuProps (and documenting precedence vs icon/Icon child). Otherwise, removing the fallback would avoid an effectively-unreachable code path for typed users.
| /** | |
| /** | |
| * Name of an image asset from your iOS asset catalog to use as the menu icon. | |
| * | |
| * This is used as a fallback/source for `extractXcassetName`. If both an `icon` | |
| * prop or `Stack.Toolbar.Icon` child and `xcasset` are provided, the explicit | |
| * `icon`/Icon configuration takes precedence and `xcasset` is used only as a | |
| * fallback image source. | |
| */ | |
| xcasset?: string; | |
| /** |
| if (iconComponentProps && 'xcasset' in iconComponentProps) { | ||
| return iconComponentProps.xcasset; | ||
| } | ||
| // Fall back to xcasset prop | ||
| return props.xcasset; |
There was a problem hiding this comment.
extractXcassetName falls back to props.xcasset even when a StackToolbarIcon child is present but is not an xcasset icon (e.g. sf or src). This can cause bottom-toolbar components to pass an xcassetName that contradicts the icon actually chosen by convertStackHeaderSharedPropsToRNSharedHeaderItem, and may override the intended SF Symbol/icon child. Consider only falling back to props.xcasset when there is no StackToolbarIcon child at all; if an icon child exists and is not xcasset, return undefined.
| if (iconComponentProps && 'xcasset' in iconComponentProps) { | |
| return iconComponentProps.xcasset; | |
| } | |
| // Fall back to xcasset prop | |
| return props.xcasset; | |
| if (!iconComponentProps) { | |
| // No icon child: fall back to the xcasset prop on the header item | |
| return props.xcasset; | |
| } | |
| if ('xcasset' in iconComponentProps && iconComponentProps.xcasset != null) { | |
| // Icon child explicitly uses an xcasset | |
| return iconComponentProps.xcasset; | |
| } | |
| // An icon child exists but is not using an xcasset (e.g. `sf` or `src`); | |
| // avoid returning a conflicting xcasset name. | |
| return undefined; |
| export function extractImageSource( | ||
| props: StackHeaderItemSharedProps | ||
| ): { source: ImageSourcePropType; renderingMode?: 'template' | 'original' } | undefined { | ||
| // Icon child takes precedence | ||
| const iconComponentProps = getFirstChildOfType(props.children, StackToolbarIcon)?.props; | ||
| if (iconComponentProps && 'src' in iconComponentProps) { | ||
| return { | ||
| source: iconComponentProps.src, | ||
| renderingMode: | ||
| 'renderingMode' in iconComponentProps ? iconComponentProps.renderingMode : undefined, | ||
| }; | ||
| } | ||
| // Fall back to icon prop when non-string | ||
| if (props.icon && typeof props.icon !== 'string') { | ||
| return { source: props.icon }; | ||
| } | ||
| return undefined; |
There was a problem hiding this comment.
extractImageSource claims the Icon child takes precedence, but it still falls back to props.icon when an Icon child exists and is not src (e.g. sf or xcasset). This can lead to passing an imageSource that overrides the intended Icon child (since RouterToolbarItemWithImageSupport prioritizes imageSource). Consider returning undefined whenever an Icon child exists but is not src, and only falling back to props.icon when there is no Icon child.
| function NativeLinkPreviewActionWithResolvedImage( | ||
| props: NativeLinkPreviewActionProps & { imageSource: ImageSourcePropType } | ||
| ) { | ||
| const { imageSource, ...rest } = props; | ||
| const resolvedImage = useImage(imageSource as ImageSource | number); | ||
| return <NativeLinkPreviewAction {...rest} image={rest.image ?? resolvedImage} />; | ||
| } |
There was a problem hiding this comment.
NativeLinkPreviewActionWithResolvedImage passes imageSource directly to useImage via a cast, but ImageSourcePropType can be an array; unlike RouterToolbarItemWithResolvedImage, there is no runtime guard for arrays here. This can result in an invalid useImage call at runtime. Consider either rejecting arrays consistently (throwing a clear error) or normalizing them to a single source before calling useImage.
e04adc5 to
d10b6b6
Compare
8a8b560 to
6b517b1
Compare
d10b6b6 to
082d289
Compare
6b517b1 to
f50ae59
Compare
|
Hi there! 👋 I'm a bot whose goal is to ensure your contributions meet our guidelines. I've found some issues in your pull request that should be addressed (click on them for more details) 👇
|

Why
How
Test Plan
Checklist
changelog.mdentry and rebuilt the package sources according to this short guidenpx expo prebuild& EAS Build (eg: updated a module plugin).