Skip to content

Commit

Permalink
Component/4225 icon button (#4583)
Browse files Browse the repository at this point in the history
* [WIP] Push up icon changes

* Introduce assets

* Create script to generate Icon types based on icons in assets directory

* Change Icon component to use SVGs. Create script to auto generate SVG types. Use an svg transformer to allow for importing SVGs as components

* Update ignores on Icon.assets

* Update generate-assets script to add ignores

* Remove icon test - SVG mock issue. Update Icon docs with color prop

* Fix useStyles for other components

* Update Icon README

* Correct read me

* Reintroduce react-native-svg-asset-plugin

* Provide better typing for svg

* Create AvatarIcon

* Create IconButton

* fix test
  • Loading branch information
Cal-L authored Jun 29, 2022
1 parent 7f32237 commit 20035d1
Show file tree
Hide file tree
Showing 17 changed files with 424 additions and 0 deletions.
21 changes: 21 additions & 0 deletions app/component-library/components/AvatarIcon/AvatarIcon.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import { storiesOf } from '@storybook/react-native';
import { select } from '@storybook/addon-knobs';
import { BaseAvatarSize } from '../BaseAvatar';
import { IconName } from '../Icon';
import AvatarIcon from './AvatarIcon';

storiesOf(' Component Library / AvatarIcon', module)
.addDecorator((getStory) => getStory())
.add('Default', () => {
const groupId = 'Props';
const sizeSelector = select(
'size',
BaseAvatarSize,
BaseAvatarSize.Md,
groupId,
);
const iconSelector = select('name', IconName, IconName.LockFilled, groupId);

return <AvatarIcon size={sizeSelector} icon={iconSelector} />;
});
35 changes: 35 additions & 0 deletions app/component-library/components/AvatarIcon/AvatarIcon.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { StyleSheet, ViewStyle } from 'react-native';
import {
AvatarIconStyleSheet,
AvatarIconStyleSheetVars,
} from './AvatarIcon.types';
import { Theme } from '../../../util/theme/models';

/**
* Style sheet function for AvatarIcon component.
*
* @param params Style sheet params.
* @param params.theme App theme from ThemeContext.
* @param params.vars Inputs that the style sheet depends on.
* @returns StyleSheet object.
*/
const styleSheet = (params: {
theme: Theme;
vars: AvatarIconStyleSheetVars;
}): AvatarIconStyleSheet => {
const { theme, vars } = params;
const { colors } = theme;
const { style } = vars;
return StyleSheet.create({
base: Object.assign(
{
backgroundColor: colors.primary.muted,
alignItems: 'center',
justifyContent: 'center',
} as ViewStyle,
style,
) as ViewStyle,
});
};

export default styleSheet;
14 changes: 14 additions & 0 deletions app/component-library/components/AvatarIcon/AvatarIcon.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';
import { shallow } from 'enzyme';
import AvatarIcon from './AvatarIcon';
import { BaseAvatarSize } from '../BaseAvatar';
import { IconName } from '../Icon';

describe('AvatarIcon', () => {
it('should render correctly', () => {
const wrapper = shallow(
<AvatarIcon size={BaseAvatarSize.Lg} icon={IconName.AddSquareFilled} />,
);
expect(wrapper).toMatchSnapshot();
});
});
28 changes: 28 additions & 0 deletions app/component-library/components/AvatarIcon/AvatarIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* eslint-disable react/prop-types */
import React from 'react';
import { useStyles } from '../../../component-library/hooks';
import BaseAvatar, { BaseAvatarSize } from '../BaseAvatar';
import Icon, { IconSize } from '../Icon';
import stylesheet from './AvatarIcon.styles';
import { AvatarIconProps, IconSizeByAvatarSize } from './AvatarIcon.types';

const iconSizeByAvatarSize: IconSizeByAvatarSize = {
[BaseAvatarSize.Xs]: IconSize.Xs,
[BaseAvatarSize.Sm]: IconSize.Sm,
[BaseAvatarSize.Md]: IconSize.Md,
[BaseAvatarSize.Lg]: IconSize.Lg,
[BaseAvatarSize.Xl]: IconSize.Xl,
};

const AvatarIcon = ({ size, icon, style, ...props }: AvatarIconProps) => {
const { styles, theme } = useStyles(stylesheet, { style });
const iconSize = iconSizeByAvatarSize[size];

return (
<BaseAvatar size={size} style={styles.base} {...props}>
<Icon name={icon} size={iconSize} color={theme.colors.primary.default} />
</BaseAvatar>
);
};

export default AvatarIcon;
35 changes: 35 additions & 0 deletions app/component-library/components/AvatarIcon/AvatarIcon.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ViewStyle } from 'react-native';
import {
BaseAvatarProps,
BaseAvatarSize,
} from '../BaseAvatar/BaseAvatar.types';
import { IconProps, IconSize } from '../Icon/Icon.types';

/**
* AvatarIcon component props.
*/
export interface AvatarIconProps extends BaseAvatarProps {
/**
* Icon to use.
*/
icon: IconProps['name'];
}

/**
* AvatarIcon component style sheet.
*/
export interface AvatarIconStyleSheet {
base: ViewStyle;
}

/**
* Style sheet input parameters.
*/
export type AvatarIconStyleSheetVars = Pick<AvatarIconProps, 'style'>;

/**
* Mapping of IconSize by BaseAvatarSize.
*/
export type IconSizeByAvatarSize = {
[key in BaseAvatarSize]: IconSize;
};
23 changes: 23 additions & 0 deletions app/component-library/components/AvatarIcon/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# AvatarIcon

AvatarIcon is a component that renders an icon contained within an avatar. This component is based on the [BaseAvatar](../BaseAvatar/BaseAvatar.tsx) component.

## Props

This component extends [BaseAvatarProps](../BaseAvatar/BaseAvatar.types.ts#L17) from `BaseAvatar` component.

### `size`

Enum to select between size variants.

| <span style="color:gray;font-size:14px">TYPE</span> | <span style="color:gray;font-size:14px">REQUIRED</span> |
| :----------------------------------------------------- | :------------------------------------------------------ |
| [BaseAvatarSize](../BaseAvatar/BaseAvatar.types.ts#L6) | Yes |

### `icon`

Icon to use.

| <span style="color:gray;font-size:14px">TYPE</span> | <span style="color:gray;font-size:14px">REQUIRED</span> |
| :-------------------------------------------------- | :------------------------------------------------------ |
| [IconName](../Icon/Icon.types.ts#L53) | Yes |
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`AvatarIcon should render correctly 1`] = `
<BaseAvatar
size="40"
style={
Object {
"alignItems": "center",
"backgroundColor": "#037DD619",
"justifyContent": "center",
}
}
>
<Icon
color="#037DD6"
name="AddSquareFilled"
size="24"
/>
</BaseAvatar>
`;
1 change: 1 addition & 0 deletions app/component-library/components/AvatarIcon/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './AvatarIcon';
30 changes: 30 additions & 0 deletions app/component-library/components/IconButton/IconButton.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/* eslint-disable no-console */
import React from 'react';
import { storiesOf } from '@storybook/react-native';
import { select, boolean } from '@storybook/addon-knobs';
import { IconName } from '../Icon';
import IconButton from './IconButton';
import { IconButtonVariant } from './IconButton.types';

storiesOf(' Component Library / IconButton', module)
.addDecorator((getStory) => getStory())
.add('Default', () => {
const groupId = 'Props';
const iconSelector = select('icon', IconName, IconName.LockFilled, groupId);
const variantSelector = select(
'variant',
IconButtonVariant,
IconButtonVariant.Primary,
groupId,
);
const disabledSelector = boolean('disabled', false, groupId);

return (
<IconButton
variant={variantSelector}
icon={iconSelector}
disabled={disabledSelector}
onPress={() => console.log("I'm clicked!")}
/>
);
});
32 changes: 32 additions & 0 deletions app/component-library/components/IconButton/IconButton.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { StyleSheet, ViewStyle } from 'react-native';
import {
IconButtonStyleSheet,
IconButtonStyleSheetVars,
} from './IconButton.types';

/**
* Style sheet function for IconButton component.
*
* @param params Style sheet params.
* @param params.vars Inputs that the style sheet depends on.
* @returns StyleSheet object.
*/
const styleSheet = (params: {
vars: IconButtonStyleSheetVars;
}): IconButtonStyleSheet => {
const { vars } = params;
const { style } = vars;
return StyleSheet.create({
base: Object.assign(
{
alignItems: 'center',
justifyContent: 'center',
height: 32,
width: 32,
} as ViewStyle,
style,
) as ViewStyle,
});
};

export default styleSheet;
18 changes: 18 additions & 0 deletions app/component-library/components/IconButton/IconButton.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import { shallow } from 'enzyme';
import { IconName } from '../Icon';
import IconButton from './IconButton';
import { IconButtonVariant } from './IconButton.types';

describe('IconButton', () => {
it('should render correctly', () => {
const wrapper = shallow(
<IconButton
variant={IconButtonVariant.Primary}
icon={IconName.AddSquareFilled}
onPress={jest.fn}
/>,
);
expect(wrapper).toMatchSnapshot();
});
});
69 changes: 69 additions & 0 deletions app/component-library/components/IconButton/IconButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/* eslint-disable react/prop-types */
import React, { useCallback, useMemo, useState } from 'react';
import { GestureResponderEvent, TouchableOpacity } from 'react-native';
import { useStyles } from '../../../component-library/hooks';
import Icon, { IconSize } from '../Icon';
import stylesheet from './IconButton.styles';
import { IconButtonProps, IconButtonVariant } from './IconButton.types';

const IconButton = ({
icon,
variant,
disabled,
onPressIn,
onPressOut,
style,
...props
}: IconButtonProps) => {
const {
styles,
theme: { colors },
} = useStyles(stylesheet, { style });
const [pressed, setPressed] = useState(false);
const iconColor = useMemo(() => {
let color: string;
if (disabled) {
color = colors.icon.muted;
} else {
switch (variant) {
case IconButtonVariant.Primary:
color = pressed ? colors.primary.alternative : colors.primary.default;
break;
case IconButtonVariant.Secondary:
color = pressed ? colors.icon.alternative : colors.icon.default;
break;
}
}
return color;
}, [colors, variant, disabled, pressed]);

const triggerOnPressedIn = useCallback(
(e: GestureResponderEvent) => {
setPressed(true);
onPressIn?.(e);
},
[setPressed, onPressIn],
);

const triggerOnPressedOut = useCallback(
(e: GestureResponderEvent) => {
setPressed(false);
onPressOut?.(e);
},
[setPressed, onPressOut],
);

return (
<TouchableOpacity
style={styles.base}
onPressIn={triggerOnPressedIn}
onPressOut={triggerOnPressedOut}
activeOpacity={1}
{...props}
>
<Icon name={icon} size={IconSize.Lg} color={iconColor} />
</TouchableOpacity>
);
};

export default IconButton;
37 changes: 37 additions & 0 deletions app/component-library/components/IconButton/IconButton.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { TouchableOpacityProps, ViewStyle } from 'react-native';
import { IconProps } from '../Icon/Icon.types';

export enum IconButtonVariant {
Primary = 'Primary',
Secondary = 'Secondary',
}

/**
* IconButton component props.
*/
export interface IconButtonProps extends TouchableOpacityProps {
/**
* Icon to use.
*/
icon: IconProps['name'];
/**
* Function to trigger when pressed.
*/
onPress: () => void;
/**
* Enum to select between variants.
*/
variant: IconButtonVariant;
}

/**
* IconButton component style sheet.
*/
export interface IconButtonStyleSheet {
base: ViewStyle;
}

/**
* Style sheet input parameters.
*/
export type IconButtonStyleSheetVars = Pick<IconButtonProps, 'style'>;
Loading

0 comments on commit 20035d1

Please sign in to comment.