diff --git a/.changeset/fix-material-community-provider.md b/.changeset/fix-material-community-provider.md new file mode 100644 index 0000000..d68dbdb --- /dev/null +++ b/.changeset/fix-material-community-provider.md @@ -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. diff --git a/src/primitives/icon/resolveExpoIconComponent.test.ts b/src/primitives/icon/resolveExpoIconComponent.test.ts index 56649f9..e5e9410 100644 --- a/src/primitives/icon/resolveExpoIconComponent.test.ts +++ b/src/primitives/icon/resolveExpoIconComponent.test.ts @@ -2,11 +2,13 @@ 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, })); @@ -14,11 +16,25 @@ describe('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, })); diff --git a/src/primitives/icon/resolveExpoIconComponent.ts b/src/primitives/icon/resolveExpoIconComponent.ts index b10a19e..217308f 100644 --- a/src/primitives/icon/resolveExpoIconComponent.ts +++ b/src/primitives/icon/resolveExpoIconComponent.ts @@ -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)[provider]; + const normalizedProvider = resolveExpoIconProviderName(provider); + const candidate = (ExpoIcons as Record)[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; +}