Skip to content

Commit 00f116b

Browse files
committed
Allow Icon's as prop to be set through the component's default props
1 parent b92642f commit 00f116b

File tree

8 files changed

+128
-29
lines changed

8 files changed

+128
-29
lines changed

example/src/App.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as React from 'react';
22

33
import { SafeAreaView, ScrollView, StyleSheet } from 'react-native';
44
import { ThemeProvider, VStack, extendThemeConfig } from '@amalgama/react-native-ui-kit';
5+
import FontAwesomeIcon from 'react-native-vector-icons/FontAwesome';
56

67
import TextExamples from './components/TextExamples';
78
import BoxExamples from './components/BoxExamples';
@@ -49,6 +50,11 @@ const customTheme = extendThemeConfig( {
4950
}
5051
}
5152
}
53+
},
54+
Icon: {
55+
defaultProps: {
56+
as: FontAwesomeIcon
57+
}
5258
}
5359
}
5460
} as const );

example/src/components/IconExamples.tsx

+7-7
Original file line numberDiff line numberDiff line change
@@ -164,13 +164,13 @@ const IconExamples = () => (
164164
<Text variant="sh1" color="primary.800">Sizes</Text>
165165
<View style={styles.separator} />
166166
<HStack space={2}>
167-
<Icon name="heart" color="information.100" size="xs" as={FontAwesome} />
168-
<Icon name="heart" color="success.200" size="sm" as={FontAwesome} />
169-
<Icon name="heart" color="success.300" size="md" as={FontAwesome} />
170-
<Icon name="heart" color="accent.400" size="lg" as={FontAwesome} />
171-
<Icon name="heart" color="error.500" size="xl" as={FontAwesome} />
172-
<Icon name="heart" color="success.600" size="2xl" as={FontAwesome} />
173-
<Icon name="heart" color="error.700" size="3xl" as={FontAwesome} />
167+
<Icon name="heart" color="information.100" size="xs" />
168+
<Icon name="heart" color="success.200" size="sm" />
169+
<Icon name="heart" color="success.300" size="md" />
170+
<Icon name="heart" color="accent.400" size="lg" />
171+
<Icon name="heart" color="error.500" size="xl" />
172+
<Icon name="heart" color="success.600" size="2xl" />
173+
<Icon name="heart" color="error.700" size="3xl" />
174174
</HStack>
175175
</VStack>
176176
);

src/components/main/Icon/hooks.ts

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { useMemo } from 'react';
2+
import { useTheme } from '../../../core/theme/hooks';
3+
import useStyleFromPropsResolver from '../../../hooks/useStyleFromPropsResolver';
4+
import type { AsComponent, IIconProps } from './types';
5+
6+
export const useIconPropsResolver = ( props: Omit<IIconProps, 'name'> ) => {
7+
const theme = useTheme();
8+
9+
const {
10+
as: asProp,
11+
size: sizeProp,
12+
color: colorProp,
13+
...resolvedProps
14+
} = useMemo<Omit<IIconProps, 'name'>>( () => (
15+
theme?.resolvePropsFor( 'Icon', props ) || {}
16+
), [ theme, props ] );
17+
18+
const size = sizeProp ? theme?.sizeFor( 'Icon', sizeProp ) : undefined;
19+
const color = colorProp ? theme?.color( colorProp ) : undefined;
20+
21+
const [ style, restProps ] = useStyleFromPropsResolver(
22+
'Icon', resolvedProps
23+
);
24+
25+
const BaseIconComponent = asProp as AsComponent;
26+
27+
if ( !BaseIconComponent ) {
28+
throw new Error(
29+
'The `as` prop is required for the Icon component. You must supply it in the '
30+
+ 'component props or in the Theme config as the component\'s default props.'
31+
);
32+
}
33+
34+
return {
35+
BaseIconComponent, size, color, style, restProps
36+
};
37+
};

src/components/main/Icon/index.tsx

+11-19
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,24 @@
1-
import React, { memo, forwardRef, useMemo } from 'react';
2-
import { useTheme } from '../../../core/theme/hooks';
3-
import useStyleFromPropsResolver from '../../../hooks/useStyleFromPropsResolver';
1+
import React, { memo, forwardRef } from 'react';
2+
import { useIconPropsResolver } from './hooks';
43
import type { IIconProps } from './types';
54

65
const Icon = ( {
7-
name, as: AsComponent, ...props
6+
name, ...props
87
}: IIconProps, ref: any ) => {
9-
const theme = useTheme();
10-
118
const {
12-
size,
9+
BaseIconComponent,
1310
color,
14-
...resolvedProps
15-
} = useMemo<Omit<IIconProps, 'name' | 'as'>>( () => (
16-
theme?.resolvePropsFor( 'Icon', props ) || {}
17-
), [ theme, props ] );
18-
19-
const rawSize = size ? theme?.sizeFor( 'Icon', size ) : undefined;
20-
const rawColor = color ? theme?.color( color ) : undefined;
21-
22-
const [ style, restProps ] = useStyleFromPropsResolver( 'Icon', resolvedProps );
11+
size,
12+
style,
13+
restProps
14+
} = useIconPropsResolver( props );
2315

2416
return (
25-
<AsComponent
17+
<BaseIconComponent
2618
name={name}
2719
ref={ref}
28-
color={rawColor}
29-
size={rawSize}
20+
color={color}
21+
size={size}
3022
style={style}
3123
{...restProps}
3224
/>

src/components/main/Icon/types.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,18 @@ interface AsComponentProps {
88
color?: ColorValue | number | undefined
99
}
1010

11-
type AsComponent = React.ComponentClass<AsComponentProps>
11+
export type AsComponent = React.ComponentClass<AsComponentProps>
1212
| React.FunctionComponent<AsComponentProps>;
1313

1414
export interface IIconProps extends ComponentStyledProps<'Icon'> {
15+
// The name of the icon. It should be a valid name for
16+
// the base icon component set as the `as` prop.
1517
name: string,
16-
as: AsComponent
18+
19+
// The base icon to be used to render the final icon. It should
20+
// support the following props:
21+
// - name (string): The name of the icon to render.
22+
// - size (number | undefined): The size of the icon.
23+
// - color (ColorValue | undefined): The color of the icon.
24+
as?: AsComponent
1725
}

src/core/components/types.ts

+7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable @typescript-eslint/ban-types */
12
import type { ComponentStyledProps, StyledProps } from '../theme/types';
23
import type { ElementType, ValueOf } from '../types';
34

@@ -34,3 +35,9 @@ export const COMPONENT_STATE_PROPS_MAP = {
3435
export type ComponentStateKey = keyof typeof COMPONENT_STATE_PROPS_MAP;
3536
export type ComponentStateProp = ValueOf<typeof COMPONENT_STATE_PROPS_MAP>;
3637
export type ComponentState = Partial<Record<ComponentStateKey, boolean>>;
38+
39+
export interface IconCustomProps { as?: any }
40+
41+
export type ComponentCustomProps<C extends ComponentName> = C extends 'Icon'
42+
? IconCustomProps
43+
: {};

src/core/theme/types.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import type { Properties as CSSProperties } from 'csstype';
55
import type { ImageStyle, TextStyle, ViewStyle } from 'react-native';
66
import type {
7+
ComponentCustomProps,
78
ComponentName, ComponentPseudoProps, ComponentStateProp
89
} from '../components/types';
910
import type { StlyePropsMapping } from '../styles/propsMapping';
@@ -94,12 +95,17 @@ export type ComponentStateProps<C extends ComponentName> =
9495
Partial<Record<ComponentStateProp, StyledPropsWithPseudoProps<C>>>;
9596

9697
export type ComponentStyledProps<C extends ComponentName> = {
97-
[K in ( keyof StyledPropsWithPseudoProps<C> | keyof ComponentStateProps<C>
98+
[K in (
99+
keyof StyledPropsWithPseudoProps<C>
100+
| keyof ComponentStateProps<C>
101+
| keyof ComponentCustomProps<C>
98102
| 'size' )]?:
99103
K extends keyof StyledPropsWithPseudoProps<C>
100104
? StyledPropsWithPseudoProps<C>[K]
101105
: K extends ComponentStateProp
102106
? ComponentStateProps<C>[K]
107+
: K extends keyof ComponentCustomProps<C>
108+
? ComponentCustomProps<C>[K]
103109
: K extends 'size'
104110
? C extends keyof IThemeConfig['components']
105111
? ComponentSizeType<C>

tests/components/main/Icon.test.js

+43
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import React from 'react';
22
import { render } from '@testing-library/react-native';
33
import { View } from 'react-native';
4+
import { ThemeProvider } from '../../../src/core/theme/context';
45
import WithThemeProvider from '../../support/withThemeProvider';
6+
import extendThemeConfig from '../../../src/core/theme/extendThemeConfig';
57

68
import Icon from '../../../src/components/main/Icon';
79

@@ -11,6 +13,17 @@ const FakeBaseIcon = props => (
1113
<View {...props} />
1214
);
1315

16+
const hideConsoleErrors = ( callback ) => {
17+
/* eslint-disable no-console */
18+
const originalConsoleError = console.error;
19+
console.error = jest.fn();
20+
21+
callback();
22+
23+
console.error = originalConsoleError;
24+
/* eslint-enable no-console */
25+
};
26+
1427
describe( 'Icon', () => {
1528
const renderComponent = props => render(
1629
<Icon testID="test-icon" name="test" as={FakeBaseIcon} {...props} />,
@@ -35,6 +48,36 @@ describe( 'Icon', () => {
3548
expect( getByTestId( 'test-icon' ) ).toHaveProp( 'color', '#999AB8' );
3649
} );
3750

51+
it( 'renders normally when the `as` prop is not provided but there is a default prop for it', () => {
52+
const theme = extendThemeConfig( {
53+
components: {
54+
Icon: {
55+
defaultProps: {
56+
as: FakeBaseIcon
57+
}
58+
}
59+
}
60+
} );
61+
62+
const { getByTestId } = render(
63+
<ThemeProvider theme={theme}>
64+
<Icon name="test" testID="test-icon" />
65+
</ThemeProvider>
66+
);
67+
68+
expect( getByTestId( 'test-icon' ) ).toHaveProp( 'name', 'test' );
69+
} );
70+
71+
it( 'throws an error when the `as` prop is not provided and there is not default prop for it', () => hideConsoleErrors(
72+
() => {
73+
expect( () => render( <Icon name="test" /> ) )
74+
.toThrow(
75+
'The `as` prop is required for the Icon component. You must supply it in the component '
76+
+ 'props or in the Theme config as the component\'s default props.'
77+
);
78+
} )
79+
);
80+
3881
itBehavesLike(
3982
'aStyledSystemComponent',
4083
{

0 commit comments

Comments
 (0)