Skip to content

Commit

Permalink
Add delete icon to Chip component
Browse files Browse the repository at this point in the history
Show a delete icon in the right side of the Chip component when
the onDeletePress prop is set.
  • Loading branch information
maurobender committed Oct 13, 2022
1 parent 44b78bf commit 697af2a
Show file tree
Hide file tree
Showing 12 changed files with 124 additions and 26 deletions.
2 changes: 1 addition & 1 deletion src/components/main/Button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { HStack } from '../Stack';
import Text from '../Text';
import { useButtonPropsResolver } from './hooks';
import type { IButtonProps } from './types';
import cloneElement from '../../utils/cloneElement';
import { cloneElement } from '../../utils/elements';

const Button = ( {
children,
Expand Down
18 changes: 16 additions & 2 deletions src/components/main/Chip/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import { useMemo } from 'react';
import { useIsPressed, useIsFocused, useIsHovered } from '../../hooks';
import type { IChipProps } from './types';
import { useComponentPropsResolver } from '../../../hooks';
import UIKitIcon from '../../../icons/UIKitIcon';

export const useChipPropsResolver = ( props: Omit<IChipProps, 'label'> ) => {
export const useChipPropsResolver = (
{ onDeletePress, ...props }: Omit<IChipProps, 'label'>
) => {
const { disabled, selected } = props;
const { isPressed, onPressIn, onPressOut } = useIsPressed( props );
const { isHovered, onHoverIn, onHoverOut } = useIsHovered( props );
Expand All @@ -21,6 +24,7 @@ export const useChipPropsResolver = ( props: Omit<IChipProps, 'label'> ) => {
__stack: stackProps,
__label: labelProps,
__icon: iconProps,
__deleteIcon: deleteIconThemeProps,
...containerProps
} = useComponentPropsResolver( 'Chip', props, state ) as IChipProps;

Expand All @@ -31,10 +35,20 @@ export const useChipPropsResolver = ( props: Omit<IChipProps, 'label'> ) => {
containerProps.onFocus = onFocus;
containerProps.onBlur = onBlur;

const showDeleteIcon = !!onDeletePress;
const deleteIconProps = {
...deleteIconThemeProps,
name: 'close-outlined',
as: UIKitIcon,
onPress: onDeletePress
};

return {
containerProps,
stackProps,
labelProps,
iconProps
iconProps,
showDeleteIcon,
deleteIconProps
};
};
21 changes: 19 additions & 2 deletions src/components/main/Chip/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,38 @@ import { HStack } from '../Stack';
import Text from '../Text';
import { useChipPropsResolver } from './hooks';
import type { IChipProps } from './types';
import cloneElement from '../../utils/cloneElement';
import { cloneElement, ComponentType, createComponent } from '../../utils/elements';
import IconButton from '../IconButton';
import type { IIconButtonProps } from '../IconButton/types';

const Chip = ( {
label,
testID,
icon: iconProp,
deleteIcon: deleteIconProp,
...props
}: IChipProps, ref?: React.Ref<View> ) => {
const {
containerProps,
stackProps,
labelProps,
iconProps
iconProps,
showDeleteIcon,
deleteIconProps
} = useChipPropsResolver( props );

const icon = cloneElement( iconProp, iconProps || {} );
const deleteIcon = showDeleteIcon
? createComponent(
IconButton as ComponentType<IIconButtonProps>,
{
from: deleteIconProp,
props: {
testID: testID ? `${testID}-delete-icon` : undefined,
...deleteIconProps
}
}
) : null;

return (
<Pressable
Expand All @@ -36,6 +52,7 @@ const Chip = ( {
>
{label}
</Text>
<>{deleteIcon}</>
</HStack>
</Pressable>
);
Expand Down
9 changes: 7 additions & 2 deletions src/components/main/Chip/types.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import type { ReactElement } from 'react';
import type { ComponentStyledProps } from 'src/core/components/types';
import type { ComponentStyledProps, IHoverableComponent } from '../../../core/components/types';
import type { IIconProps } from '../Icon/types';
import type { IPressableProps } from '../Pressable/types';

export interface IChipProps extends Omit<IPressableProps, 'children' | 'variant' | 'size'>,
ComponentStyledProps<'Chip'>
ComponentStyledProps<'Chip'>,
IHoverableComponent
{
label: string,

selected?: boolean,

icon?: ReactElement<IIconProps>,
deleteIcon?: ReactElement<IIconProps>,
onDeletePress?: IPressableProps[ 'onPress' ]
}
12 changes: 0 additions & 12 deletions src/components/utils/cloneElement.ts

This file was deleted.

31 changes: 31 additions & 0 deletions src/components/utils/elements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React, { ReactElement } from 'react';

export type ComponentPropTypes<PropsType> = Partial<PropsType> & React.Attributes;
export type ComponentType<PropsType extends Partial<PropsType> & React.Attributes> = (
props: PropsType
) => JSX.Element;

export const cloneElement = <PropsType extends ComponentPropTypes<PropsType>>(
element: ReactElement<PropsType> | undefined,
props: PropsType
) => (
element && React.isValidElement( element )
? React.cloneElement( element, { ...props, ...element.props } )
: null
);

export const createComponent = <
PropsType extends ComponentPropTypes<PropsType>,
FromPropsType extends PropsType
>(
componentType: ComponentType<PropsType>,
{ from, props = {} as FromPropsType }: {
from?: ReactElement<FromPropsType>,
props?: Partial<PropsType>
}
) => (
React.createElement(
componentType,
{ ...props, ...( from?.props || {} ) } as PropsType
)
);
3 changes: 2 additions & 1 deletion src/core/components/types/pseudoProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ interface ITextInputPseudoProps {
interface IChipPseudoProps {
__stack: ComponentBaseStyledProps<'Stack'>,
__label: ComponentBaseStyledProps<'Text'>,
__icon: ComponentBaseStyledProps<'Icon'>
__icon: ComponentBaseStyledProps<'Icon'>,
__deleteIcon: ComponentBaseStyledProps<'Icon'>
}

// Pseudoprops config for all components
Expand Down
24 changes: 20 additions & 4 deletions src/core/theme/defaultTheme/components/Chip.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
export default {
defaultProps: {
backgroundColor: 'secondary.300',
backgroundColor: 'neutral.50',
borderRadius: '3xl',
borderWidth: 'xs',
borderColor: 'transparent',
__stack: {
flex: 1,
paddingY: 1.5,
alignItems: 'center',
paddingX: 3
},
__label: {
Expand All @@ -14,7 +16,20 @@ export default {
},
__icon: {
size: 'xs',
marginRight: '2'
marginRight: '2',
color: 'primary.900'
},
__deleteIcon: {
size: 'xs',
marginLeft: '2',
color: 'primary.900',
borderRadius: 'full',
__pressed: {
backgroundColor: 'secondary.300'
},
__hovered: {
backgroundColor: 'secondary.300'
}
},
__disabled: {
backgroundColor: 'primary.50',
Expand All @@ -23,12 +38,13 @@ export default {
}
},
__pressed: {
backgroundColor: 'neutral.50'
backgroundColor: 'secondary.300'
},
__hovered: {
backgroundColor: 'neutral.50'
backgroundColor: 'secondary.300'
},
__selected: {
backgroundColor: 'secondary.300',
borderColor: 'primary.900'
}
}
Expand Down
11 changes: 11 additions & 0 deletions src/icons/UIKitIcon/icons/CloseOutlined.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react';
import Svg, { Path } from 'react-native-svg';
import type { SVGIconProps } from './types';

const CloseOutlined = ( { color, ...props }: SVGIconProps ) => (
<Svg color={color} viewBox="0 0 20 20" fill="none" {...props}>
<Path d="M 11.409 9.998 L 15.705 5.713 C 15.894 5.524 15.998 5.269 15.998 5.003 C 15.998 4.735 15.894 4.482 15.705 4.295 C 15.517 4.105 15.261 4 14.995 4 C 14.73 4 14.474 4.105 14.287 4.295 L 10.001 8.589 L 5.714 4.295 C 5.526 4.105 5.27 4 5.005 4 C 4.739 4 4.483 4.105 4.295 4.295 C 4.106 4.482 4.002 4.735 4.002 5.003 C 4.002 5.269 4.106 5.524 4.295 5.713 L 8.593 9.998 L 4.295 14.286 C 4.201 14.378 4.128 14.49 4.076 14.61 C 4.027 14.732 4 14.863 4 14.995 C 4 15.127 4.027 15.258 4.076 15.379 C 4.128 15.501 4.201 15.612 4.295 15.703 C 4.388 15.798 4.499 15.871 4.621 15.924 C 4.742 15.973 4.873 16 5.005 16 C 5.137 16 5.268 15.973 5.389 15.924 C 5.51 15.871 5.621 15.798 5.714 15.703 L 10.001 11.407 L 14.287 15.703 C 14.379 15.798 14.49 15.871 14.611 15.924 C 14.732 15.973 14.863 16 14.995 16 C 15.127 16 15.258 15.973 15.379 15.924 C 15.501 15.871 15.612 15.798 15.705 15.703 C 15.799 15.612 15.872 15.501 15.924 15.379 C 15.974 15.258 16 15.127 16 14.995 C 16 14.863 15.974 14.732 15.924 14.61 C 15.872 14.49 15.799 14.378 15.705 14.286 L 11.409 9.998 Z" fill={color} />
</Svg>
);

export default CloseOutlined;
1 change: 1 addition & 0 deletions src/icons/UIKitIcon/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export { default as CircleFilled } from './CircleFilled';
export { default as BoxUnchecked } from './BoxUnchecked';
export { default as BoxIndeterminated } from './BoxIndeterminated';
export { default as Eye } from './Eye';
export { default as CloseOutlined } from './CloseOutlined';
3 changes: 2 additions & 1 deletion src/icons/UIKitIcon/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { ElementType } from 'react';
import {
CircleOutilned, CircleFilled, BoxChecked, BoxUnchecked,
BoxIndeterminated, AlertCircle, Eye
BoxIndeterminated, AlertCircle, Eye, CloseOutlined
} from './icons';
import type { AsComponentProps } from '../../components/main/Icon/types';
import IconNotFoundError from './IconNotFoundError';
Expand All @@ -17,6 +17,7 @@ const PACKAGE_ICONS = Object.freeze( {
'box-checked': BoxChecked,
'box-unchecked': BoxUnchecked,
'box-indeterminated': BoxIndeterminated,
'close-outlined': CloseOutlined,
'eye': Eye
} ) as IconsMap;

Expand Down
15 changes: 14 additions & 1 deletion tests/components/main/Chip.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import { fireEvent, render } from '@testing-library/react-native';
import WithThemeProvider from '../../support/withThemeProvider';
import FakeBaseIcon from '../../support/FakeBaseIcon';

Expand Down Expand Up @@ -35,6 +35,19 @@ describe( 'Chip', () => {
expect( queryByTestId( 'test-icon' ) ).not.toBeUndefined();
} );

it( 'shows an delete icon when the onDeletePress prop it\'s set', () => {
const onDeletePress = jest.fn();
const { queryByTestId } = renderChip( { label, onDeletePress } );
expect( queryByTestId( 'test-chip-delete-icon' ) ).not.toBeUndefined();
} );

it( 'calls onDeletePress when the delete icon is pressed', () => {
const onDeletePress = jest.fn();
const { getByTestId } = renderChip( { label, onDeletePress } );
fireEvent.press( getByTestId( 'test-chip-delete-icon' ) );
expect( onDeletePress ).toHaveBeenCalled();
} );

itBehavesLike(
'aStyledSystemComponent',
{
Expand Down

0 comments on commit 697af2a

Please sign in to comment.