@ankhorage/surface 2.2.1 → 3.0.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 (116) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +1 -2
  3. package/dist/components/action-sheet/ActionSheet.d.ts.map +1 -1
  4. package/dist/components/action-sheet/ActionSheet.js +12 -7
  5. package/dist/components/action-sheet/ActionSheet.js.map +1 -1
  6. package/dist/components/button/Button.d.ts.map +1 -1
  7. package/dist/components/button/Button.js +5 -7
  8. package/dist/components/button/Button.js.map +1 -1
  9. package/dist/components/button/types.d.ts +2 -5
  10. package/dist/components/button/types.d.ts.map +1 -1
  11. package/dist/components/button/types.js.map +1 -1
  12. package/dist/components/drawer/Drawer.d.ts.map +1 -1
  13. package/dist/components/drawer/Drawer.js +12 -7
  14. package/dist/components/drawer/Drawer.js.map +1 -1
  15. package/dist/components/icon-button/IconButton.d.ts.map +1 -1
  16. package/dist/components/icon-button/IconButton.js +1 -1
  17. package/dist/components/icon-button/IconButton.js.map +1 -1
  18. package/dist/components/icon-button/types.d.ts +2 -5
  19. package/dist/components/icon-button/types.d.ts.map +1 -1
  20. package/dist/components/icon-button/types.js.map +1 -1
  21. package/dist/components/menu/Menu.d.ts.map +1 -1
  22. package/dist/components/menu/Menu.js +12 -7
  23. package/dist/components/menu/Menu.js.map +1 -1
  24. package/dist/components/modal/Modal.d.ts.map +1 -1
  25. package/dist/components/modal/Modal.js +12 -7
  26. package/dist/components/modal/Modal.js.map +1 -1
  27. package/dist/components/navigation/NavigationItem.d.ts.map +1 -1
  28. package/dist/components/navigation/NavigationItem.js +1 -1
  29. package/dist/components/navigation/NavigationItem.js.map +1 -1
  30. package/dist/components/navigation/TabBarItem.d.ts.map +1 -1
  31. package/dist/components/navigation/TabBarItem.js +1 -1
  32. package/dist/components/navigation/TabBarItem.js.map +1 -1
  33. package/dist/components/navigation/types.d.ts +2 -2
  34. package/dist/components/navigation/types.d.ts.map +1 -1
  35. package/dist/components/navigation/types.js.map +1 -1
  36. package/dist/components/toast/ToastProvider.d.ts.map +1 -1
  37. package/dist/components/toast/ToastProvider.js +3 -2
  38. package/dist/components/toast/ToastProvider.js.map +1 -1
  39. package/dist/components/tooltip/Tooltip.d.ts.map +1 -1
  40. package/dist/components/tooltip/Tooltip.js +10 -5
  41. package/dist/components/tooltip/Tooltip.js.map +1 -1
  42. package/dist/context/TranslationContext.d.ts.map +1 -1
  43. package/dist/context/TranslationContext.js +3 -3
  44. package/dist/context/TranslationContext.js.map +1 -1
  45. package/dist/index.d.ts +2 -2
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +1 -1
  48. package/dist/index.js.map +1 -1
  49. package/dist/internal/overlay/OverlayProvider.d.ts.map +1 -1
  50. package/dist/internal/overlay/OverlayProvider.js +17 -16
  51. package/dist/internal/overlay/OverlayProvider.js.map +1 -1
  52. package/dist/internal/overlay/Portal.d.ts.map +1 -1
  53. package/dist/internal/overlay/Portal.js +2 -1
  54. package/dist/internal/overlay/Portal.js.map +1 -1
  55. package/dist/internal/resolvePointerEvents.d.ts +9 -0
  56. package/dist/internal/resolvePointerEvents.d.ts.map +1 -0
  57. package/dist/internal/resolvePointerEvents.js +17 -0
  58. package/dist/internal/resolvePointerEvents.js.map +1 -0
  59. package/dist/layout/AppBar.js +1 -1
  60. package/dist/layout/AppBar.js.map +1 -1
  61. package/dist/layout/Box.d.ts.map +1 -1
  62. package/dist/layout/Box.js +4 -22
  63. package/dist/layout/Box.js.map +1 -1
  64. package/dist/layout/Grid.js.map +1 -1
  65. package/dist/primitives/icon/Icon.d.ts +5 -12
  66. package/dist/primitives/icon/Icon.d.ts.map +1 -1
  67. package/dist/primitives/icon/Icon.js +5 -11
  68. package/dist/primitives/icon/Icon.js.map +1 -1
  69. package/dist/primitives/icon/PortableIcon.d.ts +51 -0
  70. package/dist/primitives/icon/PortableIcon.d.ts.map +1 -0
  71. package/dist/primitives/icon/PortableIcon.js +64 -0
  72. package/dist/primitives/icon/PortableIcon.js.map +1 -0
  73. package/dist/primitives/icon/index.d.ts +2 -1
  74. package/dist/primitives/icon/index.d.ts.map +1 -1
  75. package/dist/primitives/icon/index.js +1 -0
  76. package/dist/primitives/icon/index.js.map +1 -1
  77. package/dist/primitives/image/Image.d.ts.map +1 -1
  78. package/dist/primitives/image/Image.js +3 -5
  79. package/dist/primitives/image/Image.js.map +1 -1
  80. package/package.json +30 -27
  81. package/src/components/action-sheet/ActionSheet.tsx +14 -8
  82. package/src/components/button/Button.tsx +5 -17
  83. package/src/components/button/types.ts +2 -5
  84. package/src/components/drawer/Drawer.tsx +14 -8
  85. package/src/components/icon-button/IconButton.tsx +1 -6
  86. package/src/components/icon-button/types.ts +2 -5
  87. package/src/components/menu/Menu.tsx +14 -8
  88. package/src/components/modal/Modal.tsx +14 -8
  89. package/src/components/navigation/NavigationItem.tsx +1 -6
  90. package/src/components/navigation/TabBarItem.tsx +1 -6
  91. package/src/components/navigation/types.ts +2 -2
  92. package/src/components/toast/ToastProvider.tsx +3 -2
  93. package/src/components/tooltip/Tooltip.tsx +12 -6
  94. package/src/context/TranslationContext.tsx +12 -15
  95. package/src/index.test.ts +1 -0
  96. package/src/index.ts +2 -2
  97. package/src/internal/overlay/OverlayProvider.tsx +20 -20
  98. package/src/internal/overlay/Portal.tsx +2 -1
  99. package/src/internal/resolvePointerEvents.test.ts +19 -0
  100. package/src/internal/resolvePointerEvents.ts +29 -0
  101. package/src/layout/AppBar.tsx +1 -1
  102. package/src/layout/Box.tsx +5 -36
  103. package/src/layout/Grid.tsx +3 -3
  104. package/src/primitives/icon/Icon.tsx +9 -22
  105. package/src/primitives/icon/PortableIcon.tsx +112 -0
  106. package/src/primitives/icon/PortableIcon.web.test.tsx +119 -0
  107. package/src/primitives/icon/index.ts +2 -1
  108. package/src/primitives/image/Image.tsx +3 -6
  109. package/src/theme/colorEngine.test.ts +2 -2
  110. package/src/webAcceptance.test.tsx +84 -0
  111. package/dist/primitives/icon/resolveExpoIconComponent.d.ts +0 -11
  112. package/dist/primitives/icon/resolveExpoIconComponent.d.ts.map +0 -1
  113. package/dist/primitives/icon/resolveExpoIconComponent.js +0 -20
  114. package/dist/primitives/icon/resolveExpoIconComponent.js.map +0 -1
  115. package/src/primitives/icon/resolveExpoIconComponent.test.ts +0 -45
  116. package/src/primitives/icon/resolveExpoIconComponent.ts +0 -33
@@ -0,0 +1,119 @@
1
+ import { readFileSync } from 'node:fs';
2
+
3
+ import { describe, expect, mock, test } from 'bun:test';
4
+ import React from 'react';
5
+ import { renderToStaticMarkup } from 'react-dom/server';
6
+ import * as ReactNativeWeb from 'react-native-web';
7
+
8
+ await mock.module('react-native', () => ReactNativeWeb);
9
+
10
+ const { Icon } = await import('./Icon');
11
+ const { PortableIcon, SUPPORTED_ICON_PROVIDERS } = await import('./PortableIcon');
12
+
13
+ describe('portable icon rendering', () => {
14
+ test('renders Ionicons through the public Icon API', () => {
15
+ const markup = renderToStaticMarkup(<Icon color="#123456" name="home-outline" size={18} />);
16
+
17
+ expect(markup).toContain('font-family:Ionicons');
18
+ });
19
+
20
+ test('renders FontAwesome through the public Icon API', () => {
21
+ const markup = renderToStaticMarkup(
22
+ <Icon color="#123456" name="github" provider="FontAwesome" size={18} />,
23
+ );
24
+
25
+ expect(markup).toContain('font-family:FontAwesome');
26
+ });
27
+
28
+ test('renders a FontAwesome5 brand icon through its brand font', () => {
29
+ const markup = renderToStaticMarkup(
30
+ <Icon color="#123456" name="microsoft" provider="FontAwesome5" size={18} variant="brand" />,
31
+ );
32
+
33
+ expect(markup).toContain('font-family:FontAwesome5Brands-Regular');
34
+ });
35
+
36
+ test('renders a FontAwesome5 solid icon through its solid font', () => {
37
+ const markup = renderToStaticMarkup(
38
+ <Icon color="#123456" name="video" provider="FontAwesome5" size={18} variant="solid" />,
39
+ );
40
+
41
+ expect(markup).toContain('font-family:FontAwesome5Free-Solid');
42
+ });
43
+
44
+ test('renders a FontAwesome6 brand icon through its brand font', () => {
45
+ const markup = renderToStaticMarkup(
46
+ <Icon color="#123456" name="x-twitter" provider="FontAwesome6" size={18} variant="brand" />,
47
+ );
48
+
49
+ expect(markup).toContain('font-family:FontAwesome6Brands-Regular');
50
+ });
51
+ });
52
+
53
+ describe('portable icon contract', () => {
54
+ test('publishes the intentional provider inventory', () => {
55
+ expect(SUPPORTED_ICON_PROVIDERS).toEqual([
56
+ 'Ionicons',
57
+ 'FontAwesome',
58
+ 'FontAwesome5',
59
+ 'FontAwesome6',
60
+ ]);
61
+ });
62
+ });
63
+
64
+ describe('portable icon runtime rejection', () => {
65
+ test('throws for an invalid runtime provider instead of falling back to Ionicons', () => {
66
+ const props = {
67
+ color: '#123456',
68
+ name: 'home-outline',
69
+ size: 18,
70
+ } satisfies Parameters<typeof PortableIcon>[0];
71
+ Object.defineProperty(props, 'provider', { enumerable: true, value: 'UnknownIcons' });
72
+
73
+ expect(() => renderToStaticMarkup(<PortableIcon {...props} />)).toThrow(
74
+ 'Unsupported icon provider: UnknownIcons',
75
+ );
76
+ });
77
+
78
+ test('throws for an invalid runtime FontAwesome5 variant', () => {
79
+ const props = {
80
+ color: '#123456',
81
+ name: 'video',
82
+ provider: 'FontAwesome5',
83
+ size: 18,
84
+ variant: 'solid',
85
+ } satisfies Parameters<typeof PortableIcon>[0];
86
+ Object.defineProperty(props, 'variant', { value: 'duotone' });
87
+
88
+ expect(() => renderToStaticMarkup(<PortableIcon {...props} />)).toThrow(
89
+ 'Unsupported icon FontAwesome5 variant: duotone',
90
+ );
91
+ });
92
+
93
+ test('throws for an invalid runtime FontAwesome6 variant', () => {
94
+ const props = {
95
+ color: '#123456',
96
+ name: 'x-twitter',
97
+ provider: 'FontAwesome6',
98
+ size: 18,
99
+ variant: 'brand',
100
+ } satisfies Parameters<typeof PortableIcon>[0];
101
+ Object.defineProperty(props, 'variant', { value: 'duotone' });
102
+
103
+ expect(() => renderToStaticMarkup(<PortableIcon {...props} />)).toThrow(
104
+ 'Unsupported icon FontAwesome6 variant: duotone',
105
+ );
106
+ });
107
+ });
108
+
109
+ describe('portable icon dependency boundary', () => {
110
+ test('has no Expo icon dependency or resolver path', () => {
111
+ const packageSource = readFileSync(new URL('../../../package.json', import.meta.url), 'utf8');
112
+ const iconSource = readFileSync(new URL('./PortableIcon.tsx', import.meta.url), 'utf8');
113
+
114
+ expect(packageSource).not.toContain('@expo/vector-icons');
115
+ expect(packageSource).not.toContain('expo-font');
116
+ expect(iconSource).not.toContain('resolveExpoIconComponent');
117
+ expect(iconSource).not.toContain("from 'expo");
118
+ });
119
+ });
@@ -1,2 +1,3 @@
1
- export type { IconProps, IconProvider } from './Icon';
1
+ export type { IconProps, IconProvider, IconSource, IconVariant } from './Icon';
2
2
  export { Icon } from './Icon';
3
+ export { SUPPORTED_ICON_PROVIDERS } from './PortableIcon';
@@ -29,12 +29,9 @@ export function Image({
29
29
  onError,
30
30
  }: ImageProps) {
31
31
  const { theme } = useTheme();
32
- const [showFallback, setShowFallback] = React.useState(false);
33
32
  const primaryKey = React.useMemo(() => getSourceKey(source), [source]);
34
-
35
- React.useEffect(() => {
36
- setShowFallback(false);
37
- }, [primaryKey]);
33
+ const [failedPrimaryKey, setFailedPrimaryKey] = React.useState<string | null>(null);
34
+ const showFallback = primaryKey !== null && failedPrimaryKey === primaryKey;
38
35
 
39
36
  const normalizedPrimary = normalizeSource(source);
40
37
  const normalizedFallback = normalizeSource(fallbackSource);
@@ -56,7 +53,7 @@ export function Image({
56
53
  onError?.(event);
57
54
 
58
55
  if (!showFallback && normalizedFallback) {
59
- setShowFallback(true);
56
+ setFailedPrimaryKey(primaryKey);
60
57
  }
61
58
  }}
62
59
  resizeMode={resolvedResizeMode}
@@ -105,12 +105,12 @@ describe('colorEngine', () => {
105
105
 
106
106
  it('preserves the primary color at swatch step 500 in light mode', () => {
107
107
  const { swatches } = generatePalette(mockConfig, 'light');
108
- expect(swatches.primary[500]).toBe(mockConfig.light.primaryColor);
108
+ expect(String(swatches.primary[500])).toBe(mockConfig.light.primaryColor);
109
109
  });
110
110
 
111
111
  it('preserves the primary color at swatch step 500 in dark mode', () => {
112
112
  const { swatches } = generatePalette(mockConfig, 'dark');
113
- expect(swatches.primary[500]).toBe(mockConfig.dark.primaryColor);
113
+ expect(String(swatches.primary[500])).toBe(mockConfig.dark.primaryColor);
114
114
  });
115
115
 
116
116
  it('provides a neutral swatch with neutralKeyColor at step 500', () => {
@@ -0,0 +1,84 @@
1
+ import { expect, mock, test } from 'bun:test';
2
+ import { Window } from 'happy-dom';
3
+ import React, { act } from 'react';
4
+ import { renderToString } from 'react-dom/server';
5
+ import * as ReactNativeWeb from 'react-native-web';
6
+
7
+ await mock.module('react-native', () => ReactNativeWeb);
8
+
9
+ const scrollContentStyle = { padding: 4 };
10
+
11
+ const { Container } = await import('./layout/Container');
12
+ const { Grid } = await import('./layout/Grid');
13
+ const { ScrollArea } = await import('./layout/ScrollArea');
14
+ const { Show } = await import('./layout/Show');
15
+ const { ThemeProvider } = await import('./theme/ThemeContext');
16
+
17
+ function ResponsiveAcceptanceTree() {
18
+ return (
19
+ <ThemeProvider>
20
+ <Container maxWidth={{ base: 640, md: 960 }} px={{ base: 12, md: 24 }} testID="container">
21
+ <ScrollArea contentContainerStyle={scrollContentStyle} testID="scroll-area">
22
+ <Grid cols={{ base: 1, md: 2 }} gap={{ base: 8, md: 16 }} testID="grid">
23
+ <ReactNativeWeb.View testID="grid-first" />
24
+ <ReactNativeWeb.View testID="grid-second" />
25
+ </Grid>
26
+ <Show
27
+ fallback={<ReactNativeWeb.Text>static-fallback</ReactNativeWeb.Text>}
28
+ when={{ base: false, md: true }}
29
+ >
30
+ <ReactNativeWeb.Text>wide-content</ReactNativeWeb.Text>
31
+ </Show>
32
+ </ScrollArea>
33
+ </Container>
34
+ </ThemeProvider>
35
+ );
36
+ }
37
+
38
+ test('RN Web 0.21 statically renders and hydrates representative responsive primitives', async () => {
39
+ const markup = renderToString(<ResponsiveAcceptanceTree />);
40
+
41
+ expect(markup).toContain('data-testid="container"');
42
+ expect(markup).toContain('data-testid="scroll-area"');
43
+ expect(markup).toContain('data-testid="grid"');
44
+ expect(markup).toContain('static-fallback');
45
+ expect(markup).not.toContain('wide-content');
46
+
47
+ const browserWindow = new Window({ url: 'https://surface.test/' });
48
+ Object.assign(globalThis, {
49
+ IS_REACT_ACT_ENVIRONMENT: true,
50
+ Node: browserWindow.Node,
51
+ document: browserWindow.document,
52
+ navigator: browserWindow.navigator,
53
+ window: browserWindow,
54
+ });
55
+ const container = browserWindow.document.createElement('div');
56
+ container.innerHTML = markup;
57
+ browserWindow.document.body.append(container);
58
+ const hydrationErrors: string[] = [];
59
+ const originalError = console.error;
60
+ console.error = (...values: unknown[]) => {
61
+ hydrationErrors.push(values.map(String).join(' '));
62
+ };
63
+
64
+ try {
65
+ const { hydrateRoot } = await import('react-dom/client');
66
+ const root = hydrateRoot(container as unknown as Element, <ResponsiveAcceptanceTree />);
67
+ await act(async () => Promise.resolve());
68
+
69
+ expect(container.querySelector('[data-testid="container"]')).not.toBeNull();
70
+ expect(container.querySelector('[data-testid="scroll-area"]')).not.toBeNull();
71
+ expect(container.querySelectorAll('[data-testid^="grid-"]')).toHaveLength(2);
72
+ expect(hydrationErrors).toEqual([]);
73
+
74
+ act(() => root.unmount());
75
+ } finally {
76
+ console.error = originalError;
77
+ browserWindow.close();
78
+ Reflect.deleteProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT');
79
+ Reflect.deleteProperty(globalThis, 'Node');
80
+ Reflect.deleteProperty(globalThis, 'document');
81
+ Reflect.deleteProperty(globalThis, 'navigator');
82
+ Reflect.deleteProperty(globalThis, 'window');
83
+ }
84
+ });
@@ -1,11 +0,0 @@
1
- import type React from 'react';
2
- import { type StyleProp, type TextStyle } from 'react-native';
3
- export type ExpoIconComponent = React.ElementType<{
4
- color?: string;
5
- name?: string;
6
- size?: number;
7
- style?: StyleProp<TextStyle>;
8
- testID?: string;
9
- }>;
10
- export declare function resolveExpoIconComponent(provider: string): ExpoIconComponent;
11
- //# sourceMappingURL=resolveExpoIconComponent.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"resolveExpoIconComponent.d.ts","sourceRoot":"","sources":["../../../src/primitives/icon/resolveExpoIconComponent.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9D,MAAM,MAAM,iBAAiB,GAAG,KAAK,CAAC,WAAW,CAAC;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC,CAAC;AAMH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,iBAAiB,CAQ5E"}
@@ -1,20 +0,0 @@
1
- import * as ExpoIcons from '@expo/vector-icons';
2
- import {} from 'react-native';
3
- const EXPO_ICON_PROVIDER_ALIASES = {
4
- 'material-community': 'MaterialCommunityIcons',
5
- };
6
- export function resolveExpoIconComponent(provider) {
7
- const normalizedProvider = resolveExpoIconProviderName(provider);
8
- const candidate = ExpoIcons[normalizedProvider];
9
- if (typeof candidate === 'function') {
10
- return candidate;
11
- }
12
- return ExpoIcons.Ionicons;
13
- }
14
- function resolveExpoIconProviderName(provider) {
15
- const normalizedProvider = provider.trim().toLowerCase();
16
- return normalizedProvider in EXPO_ICON_PROVIDER_ALIASES
17
- ? EXPO_ICON_PROVIDER_ALIASES[normalizedProvider]
18
- : provider;
19
- }
20
- //# sourceMappingURL=resolveExpoIconComponent.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"resolveExpoIconComponent.js","sourceRoot":"","sources":["../../../src/primitives/icon/resolveExpoIconComponent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,SAAS,MAAM,oBAAoB,CAAC;AAEhD,OAAO,EAAkC,MAAM,cAAc,CAAC;AAU9D,MAAM,0BAA0B,GAAG;IACjC,oBAAoB,EAAE,wBAAwB;CACtC,CAAC;AAEX,MAAM,UAAU,wBAAwB,CAAC,QAAgB;IACvD,MAAM,kBAAkB,GAAG,2BAA2B,CAAC,QAAQ,CAAC,CAAC;IACjE,MAAM,SAAS,GAAI,SAAqC,CAAC,kBAAkB,CAAC,CAAC;IAC7E,IAAI,OAAO,SAAS,KAAK,UAAU,EAAE,CAAC;QACpC,OAAO,SAA8B,CAAC;IACxC,CAAC;IAED,OAAO,SAAS,CAAC,QAA6B,CAAC;AACjD,CAAC;AAED,SAAS,2BAA2B,CAAC,QAAgB;IACnD,MAAM,kBAAkB,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAEzD,OAAO,kBAAkB,IAAI,0BAA0B;QACrD,CAAC,CAAC,0BAA0B,CAAC,kBAA6D,CAAC;QAC3F,CAAC,CAAC,QAAQ,CAAC;AACf,CAAC","sourcesContent":["import * as ExpoIcons from '@expo/vector-icons';\nimport type React from 'react';\nimport { type StyleProp, type TextStyle } from 'react-native';\n\nexport type ExpoIconComponent = React.ElementType<{\n color?: string;\n name?: string;\n size?: number;\n style?: StyleProp<TextStyle>;\n testID?: string;\n}>;\n\nconst EXPO_ICON_PROVIDER_ALIASES = {\n 'material-community': 'MaterialCommunityIcons',\n} as const;\n\nexport function resolveExpoIconComponent(provider: string): ExpoIconComponent {\n const normalizedProvider = resolveExpoIconProviderName(provider);\n const candidate = (ExpoIcons as Record<string, unknown>)[normalizedProvider];\n if (typeof candidate === 'function') {\n return candidate as ExpoIconComponent;\n }\n\n return ExpoIcons.Ionicons as ExpoIconComponent;\n}\n\nfunction resolveExpoIconProviderName(provider: string): string {\n const normalizedProvider = provider.trim().toLowerCase();\n\n return normalizedProvider in EXPO_ICON_PROVIDER_ALIASES\n ? EXPO_ICON_PROVIDER_ALIASES[normalizedProvider as keyof typeof EXPO_ICON_PROVIDER_ALIASES]\n : provider;\n}\n"]}
@@ -1,45 +0,0 @@
1
- import { describe, expect, mock, test } from 'bun:test';
2
-
3
- const Ionicons = () => null;
4
- const MaterialIcons = () => null;
5
- const MaterialCommunityIcons = () => null;
6
-
7
- describe('resolveExpoIconComponent', () => {
8
- test('returns the requested Expo icon family when it exists', async () => {
9
- await mock.module('@expo/vector-icons', () => ({
10
- Ionicons,
11
- MaterialCommunityIcons,
12
- MaterialIcons,
13
- }));
14
-
15
- const { resolveExpoIconComponent } = await import('./resolveExpoIconComponent');
16
-
17
- expect(resolveExpoIconComponent('Ionicons')).toBe(Ionicons);
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);
32
- });
33
-
34
- test('falls back to Ionicons when the provider is unknown', async () => {
35
- await mock.module('@expo/vector-icons', () => ({
36
- Ionicons,
37
- MaterialCommunityIcons,
38
- MaterialIcons,
39
- }));
40
-
41
- const { resolveExpoIconComponent } = await import('./resolveExpoIconComponent');
42
-
43
- expect(resolveExpoIconComponent('MissingIconFamily')).toBe(Ionicons);
44
- });
45
- });
@@ -1,33 +0,0 @@
1
- import * as ExpoIcons from '@expo/vector-icons';
2
- import type React from 'react';
3
- import { type StyleProp, type TextStyle } from 'react-native';
4
-
5
- export type ExpoIconComponent = React.ElementType<{
6
- color?: string;
7
- name?: string;
8
- size?: number;
9
- style?: StyleProp<TextStyle>;
10
- testID?: string;
11
- }>;
12
-
13
- const EXPO_ICON_PROVIDER_ALIASES = {
14
- 'material-community': 'MaterialCommunityIcons',
15
- } as const;
16
-
17
- export function resolveExpoIconComponent(provider: string): ExpoIconComponent {
18
- const normalizedProvider = resolveExpoIconProviderName(provider);
19
- const candidate = (ExpoIcons as Record<string, unknown>)[normalizedProvider];
20
- if (typeof candidate === 'function') {
21
- return candidate as ExpoIconComponent;
22
- }
23
-
24
- return ExpoIcons.Ionicons as ExpoIconComponent;
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
- }