Skip to content

Commit

Permalink
Add avatar component
Browse files Browse the repository at this point in the history
  • Loading branch information
maltenzo authored and maurobender committed Nov 18, 2022
1 parent e0dec30 commit a89eaa8
Show file tree
Hide file tree
Showing 17 changed files with 342 additions and 16 deletions.
105 changes: 105 additions & 0 deletions docs/docs/components/data display/avatar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { HStack, Avatar } from '@amalgama/embassy-ui'
import CodePreview from '@site/src/components/CodePreview';

# Avatar

This component is used to display a user's profile picture.

To add the `Avatar` component to your project you can import it as follows:

```tsx
import { Avatar } from '@amalgama/embassy-ui';
```

## Example

<CodePreview>
<Avatar
size="md"
source={{uri:"https://images.unsplash.com/photo-1599566150163-29194dcaad36?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80"}}
/>
</CodePreview>

```tsx
<Avatar size="md" source={{uri:"https://images.unsplash.com/photo-1599566150163-29194dcaad36?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80"}} />
```

## Props

### source
The source of the image to display.

| Type | Required |
| ---- | -------- |
| string | Yes |

### size
The size of the avatar.

| Type | Required | Default |
| ---- | -------- | ------- |
| string | No | "md" |

Available sizes:

| Size | Value |
| ---- | ----- |
| "xs" | 40px |
| "sm" | 64px |
| "md" | 96px |
| "lg" | 128px |
| "xl" | 160px |

<CodePreview>
<HStack space="4">
<Avatar size="xs" source={{uri:"https://images.unsplash.com/photo-1599566150163-29194dcaad36?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80"}} />
<Avatar size="sm" source={{uri:"https://images.unsplash.com/photo-1599566150163-29194dcaad36?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80"}} />
<Avatar size="md" source={{uri:"https://images.unsplash.com/photo-1599566150163-29194dcaad36?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80"}} />
<Avatar size="lg" source={{uri:"https://images.unsplash.com/photo-1599566150163-29194dcaad36?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80"}} />
<Avatar size="xl" source={{uri:"https://images.unsplash.com/photo-1599566150163-29194dcaad36?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80"}} />
</HStack>
</CodePreview>

### alt
The alternative text for the image. It will be used for the accessibility label of the image as `{alt}'s avatar`.

| Type | Required |
| ---- | -------- |
| string | No | - |

:::info
If you don't provide an `alt` prop, the accessibility label will be `Avatar`.
:::

## Pseudo Props

### __image

Props to be to the internal `Image` component.

| Type | Required |
| ---------- | -------- |
| `IImageProps`| No |

<CodePreview>
<Avatar
source={{uri:"https://images.unsplash.com/photo-1599566150163-29194dcaad36?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80"}}
size="xl"
__image={{
borderWidth: "xl",
borderColor: "success.500",
}}
/>
</CodePreview>

```tsx
<Avatar
source={{uri:"https://images.unsplash.com/photo-1599566150163-29194dcaad36?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80"}}
size="xl"
__image={{
borderWidth: "xl",
borderColor: "success.500",
}}
/>


2 changes: 2 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import SwitchExamples from './components/SwitchExamples';
import BannerExamples from './components/BannerExamples';
import DialogExamples from './components/DialogExamples';
import ImageExamples from './components/ImageExamples';
import AvatarExamples from './components/AvatarExamples';

const styles = StyleSheet.create( {
container: {
Expand Down Expand Up @@ -148,6 +149,7 @@ const App = () => (
<BannerExamples />
<DialogExamples />
<ImageExamples />
<AvatarExamples />
</VStack>
</ScrollView>
</SafeAreaView>
Expand Down
43 changes: 43 additions & 0 deletions example/src/components/AvatarExamples.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as React from 'react';

import { StyleSheet, View } from 'react-native';

import {
HStack, Text, VStack, Avatar
} from '@amalgama/embassy-ui';

const styles = StyleSheet.create( {
container: {
marginBottom: 20
},
separator: {
height: 1,
minWidth: '100%',
marginTop: 2,
marginBottom: 6,
backgroundColor: 'black'
},
vspace: {
height: 10,
minWidth: '100%'
}
} );

const SRC = { uri: 'https://images.unsplash.com/photo-1599566150163-29194dcaad36?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80' };

const AvatarExamples = () => (
<VStack style={styles.container} space="2">
<Text variant="headline">Avatar Component</Text>
<Text variant="subtitle">Sizes</Text>
<View style={styles.separator} />
<HStack justifyContent="space-between" flexWrap='wrap'>
<Avatar source={SRC} size="xs" />
<Avatar source={SRC} size="sm" />
<Avatar source={SRC} size="md" />
<Avatar source={SRC} size="lg" />
<Avatar source={SRC} size="xl" />
</HStack>
</VStack>
);

export default AvatarExamples;
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as Avatar } from './main/Avatar';
export { Banner } from './main/Banner';
export { default as Box } from './main/Box';
export { default as Button } from './main/Button';
Expand Down
22 changes: 22 additions & 0 deletions src/components/main/Avatar/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useTheme } from '../../../core/theme/hooks';
import { useComponentPropsResolver } from '../../../hooks';
import type { IAvatarProps } from './types';

type IUseAvatarPropsResolver = Omit<IAvatarProps, 'source'>;
const useAvatarPropsResolver = ( props: IUseAvatarPropsResolver ) => {
const theme = useTheme();
const {
__image: imageProps,
size: sizeProp,
...containerProps
} = useComponentPropsResolver( 'Avatar', props );

const size = sizeProp ? theme?.sizeFor( 'Avatar', sizeProp as string ) : undefined;

containerProps.width = size;
containerProps.height = size;

return { avatarProps: containerProps, imageProps };
};

export default useAvatarPropsResolver;
25 changes: 25 additions & 0 deletions src/components/main/Avatar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import Box from '../Box';
import Image from '../Image';
import useAvatarPropsResolver from './hooks';
import type { IAvatarProps } from './types';

const Avatar = ( {
source, alt, testID, ...props
}: IAvatarProps ) => {
const { avatarProps, imageProps } = useAvatarPropsResolver( props );
const accessibilityLabel = alt ? `${alt}'s avatar` : 'Avatar';
return (
<Box
{...avatarProps}
testID={testID}
accessible
accessibilityRole="image"
accessibilityLabel={accessibilityLabel}
>
<Image source={ source } {...imageProps} />
</Box>
);
};

export default Avatar;
8 changes: 8 additions & 0 deletions src/components/main/Avatar/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { ImageProps } from 'react-native';
import type { IBoxProps } from '../Box/types';

export interface IAvatarProps extends Omit<IBoxProps, 'children'> {
source: ImageProps['source'],
size?: string,
alt?: string,
}
3 changes: 2 additions & 1 deletion src/core/components/types/common.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { StyledProps } from '../../theme/types';

export type ComponentName = 'Text' | 'Box' | 'Stack' | 'Button' | 'Pressable' | 'Icon' | 'Checkbox'
| 'IconButton' | 'Radio' | 'FormControl' | 'TextInput' | 'Chip' | 'Switch' | 'Banner' | 'Dialog' | 'Image';
| 'IconButton' | 'Radio' | 'FormControl' | 'TextInput' | 'Chip' | 'Switch' | 'Banner' | 'Dialog'
| 'Image' | 'Avatar';

export type VariantName = string;

Expand Down
6 changes: 6 additions & 0 deletions src/core/components/types/pseudoProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
import type { ComponentBaseStyledProps } from './styledProps';
import type { ComponentName } from './common';

// Avatar
interface IAvatarPseudoProps {
__image: ComponentBaseStyledProps<'Image'>;
}

// Button pseudoprops
interface IButtonPseudoProps {
__label: ComponentBaseStyledProps<'Text'>,
Expand Down Expand Up @@ -92,6 +97,7 @@ interface IDialogPseudoProps {

// Pseudoprops config for all components
interface ComponentsPseudoPropsConfig {
Avatar: IAvatarPseudoProps,
Button: IButtonPseudoProps,
Checkbox: ICheckboxPseudoProps,
IconButton: IIconButtonPseudoProps,
Expand Down
20 changes: 20 additions & 0 deletions src/core/theme/defaultTheme/components/Avatar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export default {
defaultProps: {
rounded: 'full',
justifyContent: 'center',
alignItems: 'center',
size: 'xs',
__image: {
width: '100%',
height: '100%',
rounded: 'full'
}
},
sizes: {
'xs': 40,
'sm': 64,
'md': 96,
'lg': 128,
'xl': 160
}
};
2 changes: 2 additions & 0 deletions src/core/theme/defaultTheme/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Avatar from './Avatar';
import Banner from './Banner';
import Button from './Button';
import Chip from './Chip';
Expand All @@ -13,6 +14,7 @@ import Text from './Text';
import TextInput from './TextInput';

export default {
Avatar,
Banner,
Button,
Checkbox,
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export { default as extendThemeConfig } from './core/theme/extendThemeConfig';
export { ThemeProvider, ThemeConsumer } from './core/theme/context';
export { useTheme, useThemeColorModeSwtich } from './core/theme/hooks';
export {
Avatar,
Banner,
Box,
Button,
Expand Down
51 changes: 51 additions & 0 deletions tests/components/main/Avatar.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import WithThemeProvider from '../../support/withThemeProvider';
import Avatar from '../../../src/components/main/Avatar';

const { itBehavesLike } = require( '../../support/sharedExamples' );

const TEST_SRC = 'https://avatars.githubusercontent.com/u/1174405?v=4';
describe( 'Avatar', () => {
const renderAvatar = ( { src, ...props } = {} ) => render(
<Avatar src={src} testID="test-avatar" {...props} />,
{ wrapper: WithThemeProvider }
);

it( 'renders the provided source image', () => {
const { getByTestId } = renderAvatar( { src: TEST_SRC } );
expect( getByTestId( 'test-avatar' ) ).not.toBeNull();
} );

it( 'renders the provided size', () => {
const { getByTestId } = renderAvatar( { size: 'xl' } );
expect( getByTestId( 'test-avatar' ) ).toHaveStyle( { width: 160, height: 160 } );
} );

describe( 'when an alternative text is provided', () => {
it( 'uses it for the accessibilityLabel', () => {
const { getByTestId } = renderAvatar( { alt: 'user' } );
expect( getByTestId( 'test-avatar' ) ).toHaveProp( 'accessibilityLabel', "user's avatar" );
} );
} );

itBehavesLike(
'aStyledSystemComponent',
{
renderComponent: props => renderAvatar( props ),
testId: 'test-avatar',
omitProps: [ 'borderRadius' ]
}
);

itBehavesLike(
'anAccessibleComponent',
{
renderComponent: props => renderAvatar( props ),
testID: 'test-avatar',
accessible: true,
accessibilityRole: 'image',
accessibilityLabel: 'Avatar',
omitProps: [ 'accessibilityState' ]
} );
} );
3 changes: 2 additions & 1 deletion tests/components/main/Switch.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ const accessibilityTest = ( {
renderComponent,
testID: 'test-switch',
accessibilityRole: 'switch',
accessibilityState: { checked, disabled }
accessibilityState: { checked, disabled },
omitProps: [ 'accessibilityLabel' ]
} );
};

Expand Down
21 changes: 7 additions & 14 deletions tests/support/sharedExamples/anAccessibleComponent.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
const testingProps = [ 'accessible', 'accessibilityRole', 'accessibilityState', 'accessibilityLabel' ];

module.exports = ( {
renderComponent, testID, accessibilityRole, accessibilityState
renderComponent, testID, omitProps = [], ...accessibilityProps
} ) => {
describe( 'is an accessible component', () => {
it( 'has accessible prop', () => {
const { getByTestId } = renderComponent();
expect( getByTestId( testID ) ).toHaveProp( 'accessible', true );
} );
it( 'has accessibilityRole prop', () => {
const { getByTestId } = renderComponent();
expect( getByTestId( testID ) ).toHaveProp( 'accessibilityRole', accessibilityRole );
} );
it( 'has accessibilityState prop', () => {
const { getByTestId } = renderComponent();
expect( getByTestId( testID ) ).toHaveProp( 'accessibilityState', accessibilityState );
} );
const props = testingProps.filter( prop => !omitProps.includes( prop ) );
it.each( props )( 'has %s prop', ( prop ) => {
const { getByTestId } = renderComponent();
expect( getByTestId( testID ) ).toHaveProp( prop, accessibilityProps[ prop ] );
} );
};
Loading

0 comments on commit a89eaa8

Please sign in to comment.