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 .changeset/fix-material-community-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@ankhorage/surface': patch
---

Support canonical icon provider aliases for Expo vector icons.

Surface now resolves provider strings such as `material-community` and `material-community-icons` to Expo's `MaterialCommunityIcons` export, so serialized route icons can use stable provider identifiers without falling back to Ionicons.
16 changes: 16 additions & 0 deletions src/primitives/icon/resolveExpoIconComponent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,39 @@ import { describe, expect, mock, test } from 'bun:test';

const Ionicons = () => null;
const MaterialIcons = () => null;
const MaterialCommunityIcons = () => null;

describe('resolveExpoIconComponent', () => {
test('returns the requested Expo icon family when it exists', async () => {
await mock.module('@expo/vector-icons', () => ({
Ionicons,
MaterialCommunityIcons,
MaterialIcons,
}));

const { resolveExpoIconComponent } = await import('./resolveExpoIconComponent');

expect(resolveExpoIconComponent('Ionicons')).toBe(Ionicons);
expect(resolveExpoIconComponent('MaterialIcons')).toBe(MaterialIcons);
expect(resolveExpoIconComponent('MaterialCommunityIcons')).toBe(MaterialCommunityIcons);
});

test('resolves the canonical material-community provider id', async () => {
await mock.module('@expo/vector-icons', () => ({
Ionicons,
MaterialCommunityIcons,
MaterialIcons,
}));

const { resolveExpoIconComponent } = await import('./resolveExpoIconComponent');

expect(resolveExpoIconComponent('material-community')).toBe(MaterialCommunityIcons);
});

test('falls back to Ionicons when the provider is unknown', async () => {
await mock.module('@expo/vector-icons', () => ({
Ionicons,
MaterialCommunityIcons,
MaterialIcons,
}));

Expand Down
15 changes: 14 additions & 1 deletion src/primitives/icon/resolveExpoIconComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,24 @@ export type ExpoIconComponent = React.ElementType<{
testID?: string;
}>;

const EXPO_ICON_PROVIDER_ALIASES = {
'material-community': 'MaterialCommunityIcons',
} as const;

export function resolveExpoIconComponent(provider: string): ExpoIconComponent {
const candidate = (ExpoIcons as Record<string, unknown>)[provider];
const normalizedProvider = resolveExpoIconProviderName(provider);
const candidate = (ExpoIcons as Record<string, unknown>)[normalizedProvider];
if (typeof candidate === 'function') {
return candidate as ExpoIconComponent;
}

return ExpoIcons.Ionicons as ExpoIconComponent;
}

function resolveExpoIconProviderName(provider: string): string {
const normalizedProvider = provider.trim().toLowerCase();

return normalizedProvider in EXPO_ICON_PROVIDER_ALIASES
? EXPO_ICON_PROVIDER_ALIASES[normalizedProvider as keyof typeof EXPO_ICON_PROVIDER_ALIASES]
: provider;
}
Loading