Skip to content

BREAKING: Chips redesign #369

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 42 additions & 11 deletions example/src/ChipExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,58 @@ class ChipExample extends React.Component<Props> {
return (
<View style={[styles.container, { backgroundColor: colors.background }]}>
<View style={styles.row}>
<Chip onPress={() => {}} style={styles.chip}>
Simple Chip
</Chip>
<Chip onDelete={() => {}} style={styles.chip}>
<Chip onPress={() => {}} onDelete={() => {}} style={styles.chip}>
Chip with delete button
</Chip>
<Chip icon="info" style={styles.chip}>
<Chip icon="favorite" style={styles.chip}>
Chip with icon
</Chip>
<Chip
icon={({ size }) => (
<Image
source={require('../assets/avatar.jpg')}
style={{ height: size, width: size, borderRadius: size / 2 }}
/>
)}
avatar={<Image source={require('../assets/avatar.jpg')} />}
onDelete={() => {}}
style={styles.chip}
>
Chip with image
</Chip>
<Chip
avatar={<Image source={require('../assets/avatar.jpg')} />}
onDelete={() => {}}
style={styles.chip}
selected
>
Selected with image
</Chip>
</View>
<View style={styles.row}>
<Chip style={styles.chip}>Simple Chip</Chip>
<Chip selected style={styles.chip}>
Selected chip
</Chip>
<Chip
icon="favorite"
onDelete={() => {}}
disabled
style={styles.chip}
>
Disabled chip
</Chip>
</View>
<View style={styles.row}>
<Chip mode="outlined" style={styles.chip}>
Chip with outline
</Chip>
<Chip mode="outlined" selected style={styles.chip}>
Selected chip
</Chip>
<Chip
icon="favorite"
onDelete={() => {}}
mode="outlined"
disabled
style={styles.chip}
>
Disabled chip
</Chip>
</View>
</View>
);
Expand Down
212 changes: 178 additions & 34 deletions src/components/Chip.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,50 @@
/* @flow */

import * as React from 'react';
import { View, StyleSheet, TouchableWithoutFeedback } from 'react-native';
import {
View,
StyleSheet,
Animated,
TouchableWithoutFeedback,
} from 'react-native';
import color from 'color';
import Icon from './Icon';
import Surface from './Surface';
import Text from './Typography/Text';
import { black, white } from '../styles/colors';
import TouchableRipple from './TouchableRipple';
import withTheme from '../core/withTheme';
import type { Theme } from '../types';
import type { IconSource } from './Icon';

const AnimatedSurface = Animated.createAnimatedComponent(Surface);

type Props = {
/**
* Mode of the chip.
* - `flat` - flat chip without outline
* - `outlined` - chip with an outline
*/
mode?: 'flat' | 'outlined',
/**
* Text content of the `Chip`.
*/
children: React.Node,
/**
* Icon to display for the `Chip`.
* Icon to display for the `Chip`. Both icon and avatar cannot be specified.
*/
icon?: IconSource,
/**
* Avatar to display for the `Chip`. Both icon and avatar cannot be specified.
*/
avatar?: React.Node,
/**
* Display the chip as selected.
*/
selected?: boolean,
/**
* Disables the chip. `onPress` function won't execute.
*/
disabled?: boolean,
/**
* Function to execute on press.
*/
Expand All @@ -34,6 +60,10 @@ type Props = {
theme: Theme,
};

type State = {
elevation: Animated.Value,
};

/**
* Chips can be used to display entities in small blocks.
*
Expand All @@ -52,69 +82,183 @@ type Props = {
* );
* ```
*/
class Chip extends React.Component<Props> {
class Chip extends React.Component<Props, State> {
static defaultProps = {
mode: 'flat',
disabled: false,
selected: false,
style: {},
};

state = {
elevation: new Animated.Value(0),
};

_handlePressIn = () => {
Animated.timing(this.state.elevation, {
toValue: 4,
duration: 200,
}).start();
};

_handlePressOut = () => {
Animated.timing(this.state.elevation, {
toValue: 0,
duration: 150,
}).start();
};

render() {
const { children, icon, onPress, onDelete, style, theme } = this.props;
const {
mode,
children,
icon,
avatar,
selected,
disabled,
onPress,
onDelete,
style,
theme,
} = this.props;
const { dark, colors } = theme;

const backgroundColor = color(dark ? white : black)
.alpha(0.12)
.rgb()
.string();
const textColor = dark
? colors.text
const backgroundColor =
mode === 'outlined'
? colors.background
: color(colors.text)
.alpha(disabled ? 0.05 : 0.12)
.rgb()
.string();
const textColor = disabled
? colors.disabled
: color(colors.text)
.alpha(dark ? 0.7 : 0.87)
.rgb()
.string();
const iconColor = disabled
? colors.disabled
: color(colors.text)
.alpha(0.87)
.alpha(dark ? 0.7 : 0.54)
.rgb()
.string();
const iconColor = color(colors.text)
.alpha(dark ? 0.7 : 0.54)
const selectedColor = color(colors.text)
.alpha(mode === 'outlined' ? 0.1 : 0.3)
.rgb()
.string();

return (
<TouchableWithoutFeedback onPress={onPress}>
<View style={[styles.content, { backgroundColor }, style]}>
{icon ? <Icon source={icon} color={iconColor} size={32} /> : null}
<Text
numberOfLines={1}
<AnimatedSurface
style={[
styles.touchable,
{
elevation: this.state.elevation,
},
style,
]}
>
<TouchableRipple
style={styles.touchable}
onPress={onPress}
onPressIn={this._handlePressIn}
onPressOut={this._handlePressOut}
underlayColor={selectedColor}
disabled={disabled}
>
<View
style={[
styles.text,
styles.content,
{
color: textColor,
marginLeft: icon ? 8 : 12,
marginRight: onDelete ? 0 : 12,
backgroundColor,
borderColor: mode === 'outlined' ? colors.text : 'transparent',
},
selected
? {
backgroundColor: selectedColor,
}
: null,
]}
>
{children}
</Text>
{onDelete ? (
<TouchableWithoutFeedback onPress={onDelete}>
<View style={styles.delete}>
<Icon source="cancel" size={20} color={iconColor} />
{avatar && !icon
? /* $FlowFixMe */
React.cloneElement(avatar, {
/* $FlowFixMe */
style: [styles.avatar, avatar.props.style],
})
: null}
{icon || selected ? (
<View
style={[styles.icon, avatar ? styles.avatarSelected : null]}
>
{/* $FlowFixMe */}
<Icon
source={selected ? 'done' : icon}
color={avatar ? colors.background : iconColor}
size={18}
/>
</View>
</TouchableWithoutFeedback>
) : null}
</View>
</TouchableWithoutFeedback>
) : null}
<Text
numberOfLines={1}
style={[
styles.text,
{
color: textColor,
marginRight: onDelete ? 4 : 8,
marginLeft: avatar || icon || selected ? 4 : 8,
},
]}
>
{children}
</Text>
{onDelete ? (
<TouchableWithoutFeedback onPress={onDelete}>
<View style={styles.delete}>
<Icon source="cancel" size={16} color={iconColor} />
</View>
</TouchableWithoutFeedback>
) : null}
</View>
</TouchableRipple>
</AnimatedSurface>
);
}
}

const styles = StyleSheet.create({
touchable: {
borderRadius: 16,
},
content: {
borderRadius: 16,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-around',
paddingHorizontal: 4,
borderWidth: StyleSheet.hairlineWidth,
},
icon: {
paddingHorizontal: 4,
paddingVertical: 7,
},
delete: {
padding: 6,
paddingHorizontal: 4,
paddingVertical: 8,
},
text: {
marginVertical: 8,
},
avatar: {
width: 24,
height: 24,
borderRadius: 12,
marginRight: 4,
},
avatarSelected: {
position: 'relative',
left: -30,
marginRight: -26,
},
});

export default withTheme(Chip);
18 changes: 18 additions & 0 deletions src/components/__tests__/Chip.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,21 @@ it('renders deletable chip', () => {

expect(tree).toMatchSnapshot();
});

it('renders outlined disabled chip', () => {
const tree = renderer
.create(
<Chip mode="outlined" disabled>
Example Chip
</Chip>
)
.toJSON();

expect(tree).toMatchSnapshot();
});

it('renders selected chip', () => {
const tree = renderer.create(<Chip selected>Example Chip</Chip>).toJSON();

expect(tree).toMatchSnapshot();
});
Loading