Skip to content

[expo-router][ios] support image source and xcassets in bottom toolbar#43047

Closed
Ubax wants to merge 1 commit into@ubax/02-10-_expo-router_ios_support_xcasset_in_header_itemsfrom
@ubax/02-10-_expo-router_ios_support_image_source_and_xcassets_in_bottom_toolbar
Closed

[expo-router][ios] support image source and xcassets in bottom toolbar#43047
Ubax wants to merge 1 commit into@ubax/02-10-_expo-router_ios_support_xcasset_in_header_itemsfrom
@ubax/02-10-_expo-router_ios_support_image_source_and_xcassets_in_bottom_toolbar

Conversation

@Ubax
Copy link
Copy Markdown
Contributor

@Ubax Ubax commented Feb 10, 2026

Why

How

Test Plan

Checklist

Copy link
Copy Markdown
Contributor Author

Ubax commented Feb 10, 2026

This stack of pull requests is managed by Graphite. Learn more about stacking.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 RouterToolbarItemWithImageSupport and NativeLinkPreviewActionWithImageSupport wrappers that resolve imageSource / xcassetName to an ImageRef via useImage.
  • Add shared extraction helpers (extractXcassetName, extractImageSource) and use them in StackToolbarButton / StackToolbarMenu bottom-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

  • extractXcassetName supports reading props.xcasset, but StackToolbarButtonProps doesn't currently define an xcasset prop (only the StackToolbarIcon child supports it). If passing xcasset directly to Stack.Toolbar.Button is intended, add it to StackToolbarButtonProps and document precedence vs icon/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);
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
const resolvedImage = useImage(imageSource as ImageSource | number);
const normalizedImageSource = Array.isArray(imageSource) ? imageSource[0] : imageSource;
const resolvedImage = useImage(normalizedImageSource as ImageSource | number);

Copilot uses AI. Check for mistakes.
* > **Note**: When used in `placement="bottom"`, only string SFSymbols are supported. Use the `image` prop to provide custom images.
*/
icon?: StackHeaderItemSharedProps['icon'];
/**
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
/**
/**
* 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;
/**

Copilot uses AI. Check for mistakes.
Comment on lines 76 to +80
if (iconComponentProps && 'xcasset' in iconComponentProps) {
return iconComponentProps.xcasset;
}
// Fall back to xcasset prop
return props.xcasset;
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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;

Copilot uses AI. Check for mistakes.
Comment on lines +88 to 104
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;
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +30
function NativeLinkPreviewActionWithResolvedImage(
props: NativeLinkPreviewActionProps & { imageSource: ImageSourcePropType }
) {
const { imageSource, ...rest } = props;
const resolvedImage = useImage(imageSource as ImageSource | number);
return <NativeLinkPreviewAction {...rest} image={rest.image ?? resolvedImage} />;
}
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
@Ubax Ubax force-pushed the @ubax/02-10-_expo-router_ios_support_image_source_and_xcassets_in_bottom_toolbar branch from e04adc5 to d10b6b6 Compare February 11, 2026 09:13
@Ubax Ubax force-pushed the @ubax/02-10-_expo-router_ios_support_xcasset_in_header_items branch from 8a8b560 to 6b517b1 Compare February 11, 2026 09:13
@Ubax Ubax force-pushed the @ubax/02-10-_expo-router_ios_support_image_source_and_xcassets_in_bottom_toolbar branch from d10b6b6 to 082d289 Compare February 11, 2026 09:20
@Ubax Ubax force-pushed the @ubax/02-10-_expo-router_ios_support_xcasset_in_header_items branch from 6b517b1 to f50ae59 Compare February 11, 2026 09:20
@expo-bot
Copy link
Copy Markdown
Collaborator

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) 👇

⚠️ Suggestion: Missing changelog entries


Your changes should be noted in the changelog, e.g.:
- [ios] support image source and xcassets in bottom toolbar ([#43047](https://github.com/expo/expo/pull/43047) by [@Ubax](https://github.com/Ubax))
Read Updating Changelogs guide and consider adding an appropriate entry to the following changelogs:


Generated by ExpoBot 🤖 against 082d289

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants