Skip to content
Merged
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
194 changes: 111 additions & 83 deletions example/src/Examples/CardExample.tsx
Original file line number Diff line number Diff line change
@@ -1,104 +1,126 @@
import * as React from 'react';
import { Alert, ScrollView, StyleSheet } from 'react-native';
import { Alert, ScrollView, StyleSheet, View } from 'react-native';
import {
Avatar,
Paragraph,
Card,
Button,
IconButton,
useTheme,
Text,
Switch,
} from 'react-native-paper';

const CardExample = () => {
const {
colors: { background },
} = useTheme();
const [isOutlined, setIsOutlined] = React.useState(false);
const mode = isOutlined ? 'outlined' : 'elevated';

return (
<ScrollView
style={[styles.container, { backgroundColor: background }]}
contentContainerStyle={styles.content}
>
<Card style={styles.card}>
<Card.Cover source={require('../../assets/images/wrecked-ship.jpg')} />
<Card.Title title="Abandoned Ship" />
<Card.Content>
<Paragraph>
The Abandoned Ship is a wrecked ship located on Route 108 in Hoenn,
originally being a ship named the S.S. Cactus. The second part of
the ship can only be accessed by using Dive and contains the
Scanner.
</Paragraph>
</Card.Content>
</Card>
<Card style={styles.card}>
<Card.Cover source={require('../../assets/images/forest.jpg')} />
<Card.Actions>
<Button onPress={() => {}}>Share</Button>
<Button onPress={() => {}}>Explore</Button>
</Card.Actions>
</Card>
<Card style={styles.card}>
<Card.Title
title="Berries that are trimmed at the end"
subtitle="Omega Ruby"
left={(props: any) => <Avatar.Icon {...props} icon="folder" />}
right={(props: any) => (
<IconButton {...props} icon="dots-vertical" onPress={() => {}} />
)}
<View style={[styles.container, { backgroundColor: background }]}>
<View style={styles.preference}>
<Text>Outlined</Text>
<Switch
value={isOutlined}
onValueChange={() =>
setIsOutlined((prevIsOutlined) => !prevIsOutlined)
}
/>
<Card.Content>
<Paragraph>
Dotted around the Hoenn region, you will find loamy soil, many of
which are housing berries. Once you have picked the berries, then
you have the ability to use that loamy soil to grow your own
berries. These can be any berry and will require attention to get
the best crop.
</Paragraph>
</Card.Content>
</Card>
<Card style={styles.card}>
<Card.Cover source={require('../../assets/images/strawberries.jpg')} />
<Card.Title
title="Just Strawberries"
subtitle="... and only Strawberries"
right={(props: any) => (
<IconButton {...props} icon="chevron-down" onPress={() => {}} />
)}
/>
</Card>
<Card
style={styles.card}
onPress={() => {
Alert.alert('The Chameleon is Pressed');
}}
>
<Card.Cover source={require('../../assets/images/chameleon.jpg')} />
<Card.Title title="Pressable Chameleon" />
<Card.Content>
<Paragraph>
This is a pressable chameleon. If you press me, I will alert.
</Paragraph>
</Card.Content>
</Card>
<Card
style={styles.card}
onLongPress={() => {
Alert.alert('The City is Long Pressed');
}}
</View>
<ScrollView
style={[styles.container, { backgroundColor: background }]}
contentContainerStyle={styles.content}
>
<Card.Cover source={require('../../assets/images/city.jpg')} />
<Card.Title
title="Long Pressable City"
left={(props) => <Avatar.Icon {...props} icon="city" />}
/>
<Card.Content>
<Paragraph>
This is a long press only city. If you long press me, I will alert.
</Paragraph>
</Card.Content>
</Card>
</ScrollView>
<Card style={styles.card} mode={mode}>
<Card.Cover
source={require('../../assets/images/wrecked-ship.jpg')}
/>
<Card.Title title="Abandoned Ship" />
<Card.Content>
<Paragraph>
The Abandoned Ship is a wrecked ship located on Route 108 in
Hoenn, originally being a ship named the S.S. Cactus. The second
part of the ship can only be accessed by using Dive and contains
the Scanner.
</Paragraph>
</Card.Content>
</Card>
<Card style={styles.card} mode={mode}>
<Card.Cover source={require('../../assets/images/forest.jpg')} />
<Card.Actions>
<Button onPress={() => {}}>Share</Button>
<Button onPress={() => {}}>Explore</Button>
</Card.Actions>
</Card>
<Card style={styles.card} mode={mode}>
<Card.Title
title="Berries that are trimmed at the end"
subtitle="Omega Ruby"
left={(props: any) => <Avatar.Icon {...props} icon="folder" />}
right={(props: any) => (
<IconButton {...props} icon="dots-vertical" onPress={() => {}} />
)}
/>
<Card.Content>
<Paragraph>
Dotted around the Hoenn region, you will find loamy soil, many of
which are housing berries. Once you have picked the berries, then
you have the ability to use that loamy soil to grow your own
berries. These can be any berry and will require attention to get
the best crop.
</Paragraph>
</Card.Content>
</Card>
<Card style={styles.card} mode={mode}>
<Card.Cover
source={require('../../assets/images/strawberries.jpg')}
/>
<Card.Title
title="Just Strawberries"
subtitle="... and only Strawberries"
right={(props: any) => (
<IconButton {...props} icon="chevron-down" onPress={() => {}} />
)}
/>
</Card>
<Card
style={styles.card}
onPress={() => {
Alert.alert('The Chameleon is Pressed');
}}
mode={mode}
>
<Card.Cover source={require('../../assets/images/chameleon.jpg')} />
<Card.Title title="Pressable Chameleon" />
<Card.Content>
<Paragraph>
This is a pressable chameleon. If you press me, I will alert.
</Paragraph>
</Card.Content>
</Card>
<Card
style={styles.card}
onLongPress={() => {
Alert.alert('The City is Long Pressed');
}}
mode={mode}
>
<Card.Cover source={require('../../assets/images/city.jpg')} />
<Card.Title
title="Long Pressable City"
left={(props) => <Avatar.Icon {...props} icon="city" />}
/>
<Card.Content>
<Paragraph>
This is a long press only city. If you long press me, I will
alert.
</Paragraph>
</Card.Content>
</Card>
</ScrollView>
</View>
);
};

Expand All @@ -114,6 +136,12 @@ const styles = StyleSheet.create({
card: {
margin: 4,
},
preference: {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'flex-end',
paddingVertical: 12,
},
});

export default CardExample;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"registry": "https://registry.npmjs.org/"
},
"dependencies": {
"@callstack/react-theme-provider": "^3.0.5",
"@callstack/react-theme-provider": "^3.0.6",
"color": "^3.1.2",
"react-native-iphone-x-helper": "^1.3.1"
},
Expand Down
50 changes: 40 additions & 10 deletions src/components/Card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
View,
ViewStyle,
} from 'react-native';
import color from 'color';
import { white, black } from '../../styles/colors';
import CardContent from './CardContent';
import CardActions from './CardActions';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand All @@ -16,11 +18,21 @@ import CardTitle, { CardTitle as _CardTitle } from './CardTitle';
import Surface from '../Surface';
import { withTheme } from '../../core/theming';

type OutlinedCardProps = {
mode: 'outlined';
elevation?: never;
};

type ElevatedCardProps = {
mode?: 'elevated';
elevation?: number;
};

type Props = React.ComponentProps<typeof Surface> & {
/**
* Resting elevation of the card which controls the drop shadow.
*/
elevation?: number;
elevation?: never | number;
/**
* Function to execute on long press.
*/
Expand All @@ -29,6 +41,12 @@ type Props = React.ComponentProps<typeof Surface> & {
* Function to execute on press.
*/
onPress?: () => void;
/**
* Mode of the Card.
* - `elevated` - Card with elevation.
* - `outlined` - Card with an outline.
*/
mode?: 'elevated' | 'outlined';
Copy link
Contributor

@Trancever Trancever Jan 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the way it's typed currently. The outlined card shouldn't accept elevation prop. Could you try something like that:

type OutlinedCardProps = {
  mode: 'outlined';
  elevation: never;
};

type ElevatedCardProps = {
  mode?: 'elevated';
  elevation?: number;
};

type Props = (OutlinedCardProps | ElevatedCardProps) & { Common Props };

I haven't tried that, so it might not work, but I would like to explore this way of handling conditional types.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Trancever I tried your approach and it wasn't working at first. So after some time debugging, I found a problem in the HOC withTheme. I removed it and it worked as follow:

Raises error
<Card mode="outlined" elevation={3} />

Works
<Card mode="elevated" elevation={3} />

After looking up the issue in withTheme I found a problem in its type declaration here
$Without it's not working as expected because Pick doesn't distribute over discriminated union types and you can read more about here

Should we fix the type declaration first?
type $Without<T, K extends keyof any> = T extends any ? Pick<T, Exclude<keyof T, K>> : never;
works for me

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should first fix the type of the withTheme HOC. Could you submit a PR? 🙏

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/**
* Content of the `Card`.
*/
Expand Down Expand Up @@ -85,16 +103,18 @@ const Card = ({
elevation: cardElevation = 1,
onLongPress,
onPress,
mode: cardMode = 'elevated',
children,
style,
theme,
testID,
accessible,
...rest
}: Props) => {
}: (OutlinedCardProps | ElevatedCardProps) & Props) => {
const { current: elevation } = React.useRef<Animated.Value>(
new Animated.Value(cardElevation)
);
const { animation, dark, mode, roundness } = theme;

const handlePressIn = () => {
const {
Expand All @@ -110,27 +130,33 @@ const Card = ({
};

const handlePressOut = () => {
const {
dark,
mode,
animation: { scale },
} = theme;
Animated.timing(elevation, {
toValue: cardElevation,
duration: 150 * scale,
duration: 150 * animation.scale,
useNativeDriver: !dark || mode === 'exact',
}).start();
};

const { roundness } = theme;
const total = React.Children.count(children);
const siblings = React.Children.map(children, (child) =>
React.isValidElement(child) && child.type
? (child.type as any).displayName
: null
);
const borderColor = color(theme.dark ? white : black)
.alpha(0.12)
.rgb()
.string();

return (
<Surface style={[{ borderRadius: roundness, elevation }, style]} {...rest}>
<Surface
style={[
{ borderRadius: roundness, elevation, borderColor },
cardMode === 'outlined' ? styles.outlined : {},
style,
]}
{...rest}
>
<TouchableWithoutFeedback
delayPressIn={0}
disabled={!(onPress || onLongPress)}
Expand Down Expand Up @@ -171,6 +197,10 @@ const styles = StyleSheet.create({
flexGrow: 1,
flexShrink: 1,
},
outlined: {
elevation: 0,
borderWidth: 1,
},
});

export default withTheme(Card);
11 changes: 11 additions & 0 deletions src/components/__tests__/Card/Card.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react';
import renderer from 'react-test-renderer';
import Card from '../../Card/Card';

describe('Card', () => {
it('renders an outlined card', () => {
const tree = renderer.create(<Card mode="outlined" />).toJSON();

expect(tree).toMatchSnapshot();
});
});
33 changes: 33 additions & 0 deletions src/components/__tests__/Card/__snapshots__/Card.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Card renders an outlined card 1`] = `
<View
style={
Object {
"backgroundColor": "#ffffff",
"borderColor": "rgba(0, 0, 0, 0.12)",
"borderRadius": 4,
"borderWidth": 1,
"elevation": 0,
}
}
>
<View
accessible={true}
focusable={false}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"flexGrow": 1,
"flexShrink": 1,
}
}
/>
</View>
`;
Loading