@ankhorage/surface 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +7 -0
  3. package/dist/components/navigation/DrawerNavigation.d.ts +4 -0
  4. package/dist/components/navigation/DrawerNavigation.d.ts.map +1 -0
  5. package/dist/components/navigation/DrawerNavigation.js +13 -0
  6. package/dist/components/navigation/DrawerNavigation.js.map +1 -0
  7. package/dist/components/navigation/DrawerNavigationItem.d.ts +4 -0
  8. package/dist/components/navigation/DrawerNavigationItem.d.ts.map +1 -0
  9. package/dist/components/navigation/DrawerNavigationItem.js +9 -0
  10. package/dist/components/navigation/DrawerNavigationItem.js.map +1 -0
  11. package/dist/components/navigation/NavigationItem.d.ts +4 -0
  12. package/dist/components/navigation/NavigationItem.d.ts.map +1 -0
  13. package/dist/components/navigation/NavigationItem.js +41 -0
  14. package/dist/components/navigation/NavigationItem.js.map +1 -0
  15. package/dist/components/navigation/NavigationList.d.ts +4 -0
  16. package/dist/components/navigation/NavigationList.d.ts.map +1 -0
  17. package/dist/components/navigation/NavigationList.js +9 -0
  18. package/dist/components/navigation/NavigationList.js.map +1 -0
  19. package/dist/components/navigation/TabBar.d.ts +4 -0
  20. package/dist/components/navigation/TabBar.d.ts.map +1 -0
  21. package/dist/components/navigation/TabBar.js +15 -0
  22. package/dist/components/navigation/TabBar.js.map +1 -0
  23. package/dist/components/navigation/TabBarItem.d.ts +4 -0
  24. package/dist/components/navigation/TabBarItem.d.ts.map +1 -0
  25. package/dist/components/navigation/TabBarItem.js +44 -0
  26. package/dist/components/navigation/TabBarItem.js.map +1 -0
  27. package/dist/components/navigation/helpers.d.ts +15 -0
  28. package/dist/components/navigation/helpers.d.ts.map +1 -0
  29. package/dist/components/navigation/helpers.js +40 -0
  30. package/dist/components/navigation/helpers.js.map +1 -0
  31. package/dist/components/navigation/index.d.ts +8 -0
  32. package/dist/components/navigation/index.d.ts.map +1 -0
  33. package/dist/components/navigation/index.js +7 -0
  34. package/dist/components/navigation/index.js.map +1 -0
  35. package/dist/components/navigation/types.d.ts +51 -0
  36. package/dist/components/navigation/types.d.ts.map +1 -0
  37. package/dist/components/navigation/types.js +2 -0
  38. package/dist/components/navigation/types.js.map +1 -0
  39. package/dist/index.d.ts +2 -0
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +1 -0
  42. package/dist/index.js.map +1 -1
  43. package/dist/primitives/icon/resolveExpoIconComponent.d.ts.map +1 -1
  44. package/dist/primitives/icon/resolveExpoIconComponent.js +11 -1
  45. package/dist/primitives/icon/resolveExpoIconComponent.js.map +1 -1
  46. package/package.json +1 -1
  47. package/src/components/navigation/DrawerNavigation.tsx +25 -0
  48. package/src/components/navigation/DrawerNavigationItem.tsx +17 -0
  49. package/src/components/navigation/NavigationItem.tsx +77 -0
  50. package/src/components/navigation/NavigationList.tsx +24 -0
  51. package/src/components/navigation/TabBar.tsx +25 -0
  52. package/src/components/navigation/TabBarItem.tsx +82 -0
  53. package/src/components/navigation/helpers.test.ts +74 -0
  54. package/src/components/navigation/helpers.ts +72 -0
  55. package/src/components/navigation/index.ts +16 -0
  56. package/src/components/navigation/policy.test.ts +44 -0
  57. package/src/components/navigation/types.ts +59 -0
  58. package/src/index.test.ts +1 -0
  59. package/src/index.ts +18 -0
  60. package/src/primitives/icon/resolveExpoIconComponent.test.ts +16 -0
  61. package/src/primitives/icon/resolveExpoIconComponent.ts +14 -1
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+
3
+ import { NavigationItem } from './NavigationItem';
4
+ import type { DrawerNavigationItemProps } from './types';
5
+
6
+ export function DrawerNavigationItem({ item, compact = false, testID }: DrawerNavigationItemProps) {
7
+ return (
8
+ <NavigationItem
9
+ compact={compact}
10
+ item={{
11
+ ...item,
12
+ accessibilityRole: item.accessibilityRole ?? 'button',
13
+ }}
14
+ testID={testID}
15
+ />
16
+ );
17
+ }
@@ -0,0 +1,77 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { Box } from '../../layout';
5
+ import { ButtonBase } from '../../primitives/button-base';
6
+ import { Icon } from '../../primitives/icon';
7
+ import { Text } from '../../primitives/text';
8
+ import { useTheme } from '../../theme/ThemeContext';
9
+ import { resolveNavigationAccessibilityState, resolveNavigationItemPresentation } from './helpers';
10
+ import type { NavigationItemProps } from './types';
11
+
12
+ export function NavigationItem({ item, compact = false, testID }: NavigationItemProps) {
13
+ const { theme } = useTheme();
14
+ const active = Boolean(item.active);
15
+ const disabled = Boolean(item.disabled);
16
+ const resolvedTestID = item.testID ?? (testID ? `${testID}-item-${item.id}` : undefined);
17
+ const resolvedAccessibilityState = resolveNavigationAccessibilityState(
18
+ active,
19
+ disabled,
20
+ item.accessibilityState,
21
+ );
22
+
23
+ return (
24
+ <ButtonBase
25
+ accessibilityLabel={item.accessibilityLabel}
26
+ accessibilityRole={item.accessibilityRole ?? 'button'}
27
+ accessibilityState={resolvedAccessibilityState}
28
+ disabled={disabled}
29
+ onPress={item.onPress}
30
+ testID={resolvedTestID}
31
+ >
32
+ {(state) => {
33
+ const presentation = resolveNavigationItemPresentation(theme, {
34
+ active,
35
+ disabled,
36
+ state,
37
+ });
38
+
39
+ return (
40
+ <Box
41
+ px="m"
42
+ py={compact ? 's' : 'm'}
43
+ radius="m"
44
+ style={{
45
+ backgroundColor: presentation.backgroundColor,
46
+ flexDirection: 'row',
47
+ alignItems: 'center',
48
+ opacity: presentation.opacity,
49
+ }}
50
+ >
51
+ {item.icon ? (
52
+ <View style={{ marginRight: theme.spacing.s }}>
53
+ <Icon
54
+ color={presentation.contentColor}
55
+ name={item.icon.name}
56
+ provider={item.icon.provider}
57
+ size={compact ? 's' : 'm'}
58
+ />
59
+ </View>
60
+ ) : null}
61
+ <Box flex={1}>
62
+ <Text
63
+ color={presentation.contentColor}
64
+ numberOfLines={1}
65
+ variant={compact ? 'bodySmall' : 'body'}
66
+ weight="medium"
67
+ >
68
+ {item.label}
69
+ </Text>
70
+ </Box>
71
+ {item.badge ? <View style={{ marginLeft: theme.spacing.s }}>{item.badge}</View> : null}
72
+ </Box>
73
+ );
74
+ }}
75
+ </ButtonBase>
76
+ );
77
+ }
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+
3
+ import { Stack } from '../../layout';
4
+ import { NavigationItem } from './NavigationItem';
5
+ import type { NavigationListProps } from './types';
6
+
7
+ export function NavigationList({
8
+ items,
9
+ orientation = 'vertical',
10
+ compact = false,
11
+ testID,
12
+ }: NavigationListProps) {
13
+ return (
14
+ <Stack
15
+ direction={orientation === 'horizontal' ? 'row' : 'column'}
16
+ gap={compact ? 'xs' : 's'}
17
+ testID={testID}
18
+ >
19
+ {items.map((item) => (
20
+ <NavigationItem compact={compact} item={item} key={item.id} testID={testID} />
21
+ ))}
22
+ </Stack>
23
+ );
24
+ }
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+
3
+ import { Box } from '../../layout';
4
+ import { useTheme } from '../../theme/ThemeContext';
5
+ import { TabBarItem } from './TabBarItem';
6
+ import type { TabBarProps } from './types';
7
+
8
+ export function TabBar({ items, compact = false, testID }: TabBarProps) {
9
+ const { theme } = useTheme();
10
+
11
+ return (
12
+ <Box
13
+ style={{
14
+ flexDirection: 'row',
15
+ borderTopColor: theme.semantics.border.default,
16
+ borderTopWidth: 1,
17
+ }}
18
+ testID={testID}
19
+ >
20
+ {items.map((item) => (
21
+ <TabBarItem compact={compact} item={item} key={item.id} testID={testID} />
22
+ ))}
23
+ </Box>
24
+ );
25
+ }
@@ -0,0 +1,82 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { Box } from '../../layout';
5
+ import { ButtonBase } from '../../primitives/button-base';
6
+ import { Icon } from '../../primitives/icon';
7
+ import { Text } from '../../primitives/text';
8
+ import { useTheme } from '../../theme/ThemeContext';
9
+ import { resolveNavigationAccessibilityState, resolveNavigationItemPresentation } from './helpers';
10
+ import type { TabBarItemProps } from './types';
11
+
12
+ export function TabBarItem({ item, compact = false, testID }: TabBarItemProps) {
13
+ const { theme } = useTheme();
14
+ const active = Boolean(item.active);
15
+ const disabled = Boolean(item.disabled);
16
+ const resolvedTestID = item.testID ?? (testID ? `${testID}-item-${item.id}` : undefined);
17
+ const resolvedAccessibilityState = resolveNavigationAccessibilityState(
18
+ active,
19
+ disabled,
20
+ item.accessibilityState,
21
+ );
22
+
23
+ return (
24
+ <ButtonBase
25
+ accessibilityLabel={item.accessibilityLabel}
26
+ accessibilityRole={item.accessibilityRole ?? 'tab'}
27
+ accessibilityState={resolvedAccessibilityState}
28
+ disabled={disabled}
29
+ onPress={item.onPress}
30
+ style={{ flex: 1 }}
31
+ testID={resolvedTestID}
32
+ >
33
+ {(state) => {
34
+ const presentation = resolveNavigationItemPresentation(theme, {
35
+ active,
36
+ disabled,
37
+ state,
38
+ });
39
+ const contentColor = disabled
40
+ ? theme.semantics.content.muted
41
+ : active
42
+ ? theme.semantics.action.primary.base
43
+ : theme.semantics.content.muted;
44
+
45
+ return (
46
+ <Box
47
+ px="m"
48
+ py={compact ? 's' : 'm'}
49
+ style={{
50
+ flex: 1,
51
+ alignItems: 'center',
52
+ justifyContent: 'center',
53
+ backgroundColor: presentation.backgroundColor,
54
+ opacity: presentation.opacity,
55
+ }}
56
+ >
57
+ {item.icon ? (
58
+ <View style={{ marginBottom: item.label ? theme.spacing.xs : 0 }}>
59
+ <Icon
60
+ color={contentColor}
61
+ name={item.icon.name}
62
+ provider={item.icon.provider}
63
+ size={compact ? 's' : 'm'}
64
+ />
65
+ </View>
66
+ ) : null}
67
+ {item.label ? (
68
+ <Text
69
+ color={contentColor}
70
+ numberOfLines={1}
71
+ variant={compact ? 'bodySmall' : 'label'}
72
+ weight="medium"
73
+ >
74
+ {item.label}
75
+ </Text>
76
+ ) : null}
77
+ </Box>
78
+ );
79
+ }}
80
+ </ButtonBase>
81
+ );
82
+ }
@@ -0,0 +1,74 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { createTheme } from '../../theme/createTheme';
4
+ import { resolveNavigationAccessibilityState, resolveNavigationItemPresentation } from './helpers';
5
+
6
+ describe('navigation helpers', () => {
7
+ it('forces selected and disabled when active/disabled are true', () => {
8
+ expect(
9
+ resolveNavigationAccessibilityState(true, true, {
10
+ selected: false,
11
+ disabled: false,
12
+ }),
13
+ ).toEqual({
14
+ selected: true,
15
+ disabled: true,
16
+ });
17
+ });
18
+
19
+ it('preserves user-provided state when not active/disabled', () => {
20
+ expect(
21
+ resolveNavigationAccessibilityState(false, false, {
22
+ disabled: false,
23
+ selected: false,
24
+ expanded: true,
25
+ }),
26
+ ).toEqual({
27
+ disabled: false,
28
+ selected: false,
29
+ expanded: true,
30
+ });
31
+ });
32
+
33
+ it('resolves active presentation via primary soft semantics', () => {
34
+ const theme = createTheme();
35
+ const state = { pressed: false, hovered: true, focused: false, disabled: false };
36
+ const resolved = resolveNavigationItemPresentation(theme, {
37
+ active: true,
38
+ disabled: false,
39
+ state,
40
+ });
41
+
42
+ expect(resolved.backgroundColor).toBe(theme.semantics.action.primary.softHover);
43
+ expect(resolved.contentColor).toBe(theme.semantics.action.primary.base);
44
+ expect(resolved.opacity).toBe(1);
45
+ });
46
+
47
+ it('resolves inactive presentation via neutral hover/active semantics', () => {
48
+ const theme = createTheme();
49
+ const state = { pressed: true, hovered: true, focused: false, disabled: false };
50
+ const resolved = resolveNavigationItemPresentation(theme, {
51
+ active: false,
52
+ disabled: false,
53
+ state,
54
+ });
55
+
56
+ expect(resolved.backgroundColor).toBe(theme.semantics.neutral.surfaceActive);
57
+ expect(resolved.contentColor).toBe(theme.semantics.content.default);
58
+ expect(resolved.opacity).toBe(1);
59
+ });
60
+
61
+ it('ignores hover/pressed presentation when disabled', () => {
62
+ const theme = createTheme();
63
+ const state = { pressed: true, hovered: true, focused: false, disabled: true };
64
+ const resolved = resolveNavigationItemPresentation(theme, {
65
+ active: true,
66
+ disabled: true,
67
+ state,
68
+ });
69
+
70
+ expect(resolved.backgroundColor).toBe(theme.semantics.action.primary.softBg);
71
+ expect(resolved.contentColor).toBe(theme.semantics.content.muted);
72
+ expect(resolved.opacity).toBe(0.72);
73
+ });
74
+ });
@@ -0,0 +1,72 @@
1
+ import type { AccessibilityState } from 'react-native';
2
+
3
+ import type { InteractionState } from '../../internal/resolvers/resolveInteractiveState';
4
+ import type { SurfaceTheme } from '../../theme/types';
5
+
6
+ export function resolveNavigationAccessibilityState(
7
+ active: boolean,
8
+ disabled: boolean,
9
+ accessibilityState?: AccessibilityState,
10
+ ): AccessibilityState {
11
+ const resolved: AccessibilityState = { ...(accessibilityState ?? {}) };
12
+
13
+ if (active) {
14
+ resolved.selected = true;
15
+ }
16
+
17
+ if (disabled) {
18
+ resolved.disabled = true;
19
+ }
20
+
21
+ return resolved;
22
+ }
23
+
24
+ export interface ResolvedNavigationItemPresentation {
25
+ backgroundColor: string;
26
+ contentColor: string;
27
+ opacity: number;
28
+ }
29
+
30
+ export function resolveNavigationItemPresentation(
31
+ theme: SurfaceTheme,
32
+ {
33
+ active,
34
+ disabled,
35
+ state,
36
+ }: {
37
+ active: boolean;
38
+ disabled: boolean;
39
+ state: InteractionState;
40
+ },
41
+ ): ResolvedNavigationItemPresentation {
42
+ const interactionState = disabled ? { ...state, hovered: false, pressed: false } : state;
43
+
44
+ const opacity = disabled ? 0.72 : 1;
45
+ const contentColor = disabled
46
+ ? theme.semantics.content.muted
47
+ : active
48
+ ? theme.semantics.action.primary.base
49
+ : theme.semantics.content.default;
50
+
51
+ if (active) {
52
+ return {
53
+ backgroundColor: interactionState.pressed
54
+ ? theme.semantics.action.primary.softActive
55
+ : interactionState.hovered
56
+ ? theme.semantics.action.primary.softHover
57
+ : theme.semantics.action.primary.softBg,
58
+ contentColor,
59
+ opacity,
60
+ };
61
+ }
62
+
63
+ return {
64
+ backgroundColor: interactionState.pressed
65
+ ? theme.semantics.neutral.surfaceActive
66
+ : interactionState.hovered
67
+ ? theme.semantics.neutral.surfaceHover
68
+ : 'transparent',
69
+ contentColor,
70
+ opacity,
71
+ };
72
+ }
@@ -0,0 +1,16 @@
1
+ export { DrawerNavigation } from './DrawerNavigation';
2
+ export { DrawerNavigationItem } from './DrawerNavigationItem';
3
+ export { NavigationItem } from './NavigationItem';
4
+ export { NavigationList } from './NavigationList';
5
+ export { TabBar } from './TabBar';
6
+ export { TabBarItem } from './TabBarItem';
7
+ export type {
8
+ DrawerNavigationItemProps,
9
+ DrawerNavigationProps,
10
+ NavigationItemIcon,
11
+ NavigationItemProps,
12
+ NavigationItemSpec,
13
+ NavigationListProps,
14
+ TabBarItemProps,
15
+ TabBarProps,
16
+ } from './types';
@@ -0,0 +1,44 @@
1
+ import { readdirSync, readFileSync } from 'node:fs';
2
+ import { extname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import { describe, expect, it } from 'bun:test';
6
+
7
+ const navigationDir = fileURLToPath(new URL('.', import.meta.url));
8
+
9
+ const forbiddenPatterns = [
10
+ 'expo-router',
11
+ '@react-navigation',
12
+ 'AppManifest',
13
+ 'NavigatorSpec',
14
+ 'generated app',
15
+ 'templates',
16
+ ] as const;
17
+
18
+ function listNavigationSources() {
19
+ return readdirSync(navigationDir)
20
+ .filter((entry) => {
21
+ const extension = extname(entry);
22
+ if (!(extension === '.ts' || extension === '.tsx')) {
23
+ return false;
24
+ }
25
+
26
+ return !entry.includes('.test.');
27
+ })
28
+ .map((entry) => join(navigationDir, entry));
29
+ }
30
+
31
+ describe('navigation module boundaries', () => {
32
+ it('does not import routers, manifests, or generated app code', () => {
33
+ const sources = listNavigationSources().map((path) => ({
34
+ path,
35
+ content: readFileSync(path, 'utf8'),
36
+ }));
37
+
38
+ sources.forEach(({ content, path }) => {
39
+ forbiddenPatterns.forEach((pattern) => {
40
+ expect(content, path).not.toContain(pattern);
41
+ });
42
+ });
43
+ });
44
+ });
@@ -0,0 +1,59 @@
1
+ import type React from 'react';
2
+ import type { AccessibilityRole, AccessibilityState } from 'react-native';
3
+
4
+ import type { IconProps } from '../../primitives/icon';
5
+
6
+ export type NavigationItemIcon = Pick<IconProps, 'name' | 'provider'>;
7
+
8
+ export interface NavigationItemSpec {
9
+ id: string;
10
+ label: React.ReactNode;
11
+ icon?: NavigationItemIcon;
12
+ badge?: React.ReactNode;
13
+ active?: boolean;
14
+ disabled?: boolean;
15
+ onPress?: (() => void) | undefined;
16
+ accessibilityLabel?: string;
17
+ accessibilityRole?: AccessibilityRole;
18
+ accessibilityState?: AccessibilityState;
19
+ testID?: string;
20
+ }
21
+
22
+ export interface NavigationItemProps {
23
+ item: NavigationItemSpec;
24
+ compact?: boolean;
25
+ testID?: string;
26
+ }
27
+
28
+ export interface NavigationListProps {
29
+ items: readonly NavigationItemSpec[];
30
+ orientation?: 'vertical' | 'horizontal';
31
+ compact?: boolean;
32
+ testID?: string;
33
+ }
34
+
35
+ export interface TabBarProps {
36
+ items: readonly NavigationItemSpec[];
37
+ compact?: boolean;
38
+ testID?: string;
39
+ }
40
+
41
+ export interface TabBarItemProps {
42
+ item: NavigationItemSpec;
43
+ compact?: boolean;
44
+ testID?: string;
45
+ }
46
+
47
+ export interface DrawerNavigationProps {
48
+ items: readonly NavigationItemSpec[];
49
+ header?: React.ReactNode;
50
+ footer?: React.ReactNode;
51
+ compact?: boolean;
52
+ testID?: string;
53
+ }
54
+
55
+ export interface DrawerNavigationItemProps {
56
+ item: NavigationItemSpec;
57
+ compact?: boolean;
58
+ testID?: string;
59
+ }
package/src/index.test.ts CHANGED
@@ -21,6 +21,7 @@ const expectedRootExports = [
21
21
  "export { IconButton } from './components/icon-button';",
22
22
  "export { Label } from './components/label';",
23
23
  "export { ListItem } from './components/list-item';",
24
+ "export {\n DrawerNavigation,\n DrawerNavigationItem,\n NavigationItem,\n NavigationList,\n TabBar,\n TabBarItem,\n} from './components/navigation';",
24
25
  "export { Menu } from './components/menu';",
25
26
  "export { Modal } from './components/modal';",
26
27
  "export { Radio } from './components/radio';",
package/src/index.ts CHANGED
@@ -22,6 +22,24 @@ export type { MenuItem, MenuProps } from './components/menu';
22
22
  export { Menu } from './components/menu';
23
23
  export type { ModalProps } from './components/modal';
24
24
  export { Modal } from './components/modal';
25
+ export type {
26
+ DrawerNavigationItemProps,
27
+ DrawerNavigationProps,
28
+ NavigationItemIcon,
29
+ NavigationItemProps,
30
+ NavigationItemSpec,
31
+ NavigationListProps,
32
+ TabBarItemProps,
33
+ TabBarProps,
34
+ } from './components/navigation';
35
+ export {
36
+ DrawerNavigation,
37
+ DrawerNavigationItem,
38
+ NavigationItem,
39
+ NavigationList,
40
+ TabBar,
41
+ TabBarItem,
42
+ } from './components/navigation';
25
43
  export type { RadioProps } from './components/radio';
26
44
  export { Radio } from './components/radio';
27
45
  export type { SwitchProps } from './components/switch';
@@ -2,11 +2,13 @@ import { describe, expect, mock, test } from 'bun:test';
2
2
 
3
3
  const Ionicons = () => null;
4
4
  const MaterialIcons = () => null;
5
+ const MaterialCommunityIcons = () => null;
5
6
 
6
7
  describe('resolveExpoIconComponent', () => {
7
8
  test('returns the requested Expo icon family when it exists', async () => {
8
9
  await mock.module('@expo/vector-icons', () => ({
9
10
  Ionicons,
11
+ MaterialCommunityIcons,
10
12
  MaterialIcons,
11
13
  }));
12
14
 
@@ -14,11 +16,25 @@ describe('resolveExpoIconComponent', () => {
14
16
 
15
17
  expect(resolveExpoIconComponent('Ionicons')).toBe(Ionicons);
16
18
  expect(resolveExpoIconComponent('MaterialIcons')).toBe(MaterialIcons);
19
+ expect(resolveExpoIconComponent('MaterialCommunityIcons')).toBe(MaterialCommunityIcons);
20
+ });
21
+
22
+ test('resolves the canonical material-community provider id', async () => {
23
+ await mock.module('@expo/vector-icons', () => ({
24
+ Ionicons,
25
+ MaterialCommunityIcons,
26
+ MaterialIcons,
27
+ }));
28
+
29
+ const { resolveExpoIconComponent } = await import('./resolveExpoIconComponent');
30
+
31
+ expect(resolveExpoIconComponent('material-community')).toBe(MaterialCommunityIcons);
17
32
  });
18
33
 
19
34
  test('falls back to Ionicons when the provider is unknown', async () => {
20
35
  await mock.module('@expo/vector-icons', () => ({
21
36
  Ionicons,
37
+ MaterialCommunityIcons,
22
38
  MaterialIcons,
23
39
  }));
24
40
 
@@ -10,11 +10,24 @@ export type ExpoIconComponent = React.ElementType<{
10
10
  testID?: string;
11
11
  }>;
12
12
 
13
+ const EXPO_ICON_PROVIDER_ALIASES = {
14
+ 'material-community': 'MaterialCommunityIcons',
15
+ } as const;
16
+
13
17
  export function resolveExpoIconComponent(provider: string): ExpoIconComponent {
14
- const candidate = (ExpoIcons as Record<string, unknown>)[provider];
18
+ const normalizedProvider = resolveExpoIconProviderName(provider);
19
+ const candidate = (ExpoIcons as Record<string, unknown>)[normalizedProvider];
15
20
  if (typeof candidate === 'function') {
16
21
  return candidate as ExpoIconComponent;
17
22
  }
18
23
 
19
24
  return ExpoIcons.Ionicons as ExpoIconComponent;
20
25
  }
26
+
27
+ function resolveExpoIconProviderName(provider: string): string {
28
+ const normalizedProvider = provider.trim().toLowerCase();
29
+
30
+ return normalizedProvider in EXPO_ICON_PROVIDER_ALIASES
31
+ ? EXPO_ICON_PROVIDER_ALIASES[normalizedProvider as keyof typeof EXPO_ICON_PROVIDER_ALIASES]
32
+ : provider;
33
+ }