diff --git a/example/src/Examples/CardExample.tsx b/example/src/Examples/CardExample.tsx index 1e38c1e23f..d7c9ba9657 100644 --- a/example/src/Examples/CardExample.tsx +++ b/example/src/Examples/CardExample.tsx @@ -7,56 +7,83 @@ import { Button, IconButton, useTheme, + Chip, Text, - Switch, } from 'react-native-paper'; import { PreferencesContext } from '..'; import ScreenWrapper from '../ScreenWrapper'; -const CardExample = () => { - const { colors } = useTheme(); - const [isOutlined, setIsOutlined] = React.useState(false); - const mode = isOutlined ? 'outlined' : 'elevated'; +type Mode = 'elevated' | 'outlined' | 'filled'; +const CardExample = () => { + const { colors, isV3 } = useTheme(); + const [selectedMode, setSelectedMode] = React.useState('elevated' as Mode); + const [isSelected, setIsSelected] = React.useState(false); const preferences = React.useContext(PreferencesContext); + const modes = isV3 + ? ['elevated', 'outlined', 'filled'] + : ['elevated', 'outlined']; + + const TextComponent = isV3 ? Text : Paragraph; + return ( - Outlined - - setIsOutlined((prevIsOutlined) => !prevIsOutlined) - } - /> + {(modes as Mode[]).map((mode) => ( + setSelectedMode(mode)} + style={styles.chip} + > + {mode} + + ))} - + - + 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. - + - + {isV3 && ( + + + + + + This is a card using title and subtitle with specified variants. + + + + )} + - + { )} /> - + 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. - + - + @@ -83,7 +110,11 @@ const CardExample = () => { title="Just Strawberries" subtitle="... and only Strawberries" right={(props: any) => ( - {}} /> + setIsSelected(!isSelected)} + /> )} /> @@ -92,14 +123,14 @@ const CardExample = () => { onPress={() => { Alert.alert('The Chameleon is Pressed'); }} - mode={mode} + mode={selectedMode} > - + This is a pressable chameleon. If you press me, I will alert. - + { onLongPress={() => { Alert.alert('The City is Long Pressed'); }} - mode={mode} + mode={selectedMode} > { left={(props) => } /> - + This is a long press only city. If you long press me, I will alert. - + { onPress={() => { preferences.toggleTheme(); }} - mode={mode} + mode={selectedMode} > } /> - + This is pressable card. If you press me, I will switch the theme. - + @@ -155,10 +186,12 @@ const styles = StyleSheet.create({ card: { margin: 4, }, + chip: { + margin: 4, + }, preference: { alignItems: 'center', flexDirection: 'row', - justifyContent: 'space-between', paddingVertical: 12, paddingHorizontal: 8, }, diff --git a/src/components/Card/Card.tsx b/src/components/Card/Card.tsx index bc0921b8d8..4a19200d35 100644 --- a/src/components/Card/Card.tsx +++ b/src/components/Card/Card.tsx @@ -7,8 +7,6 @@ import { View, ViewStyle, } from 'react-native'; -import color from 'color'; -import { white, black } from '../../styles/themes/v2/colors'; import CardContent from './CardContent'; import CardActions from './CardActions'; // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -18,6 +16,7 @@ import CardTitle, { CardTitle as _CardTitle } from './CardTitle'; import Surface from '../Surface'; import { withTheme } from '../../core/theming'; import type { Theme } from '../../types'; +import { getCardColors } from './helpers'; type OutlinedCardProps = { mode: 'outlined'; @@ -29,8 +28,15 @@ type ElevatedCardProps = { elevation?: number; }; +type FilledCardProps = { + mode?: 'filled'; + elevation?: never; +}; + type HandlePressType = 'in' | 'out'; +type Mode = 'elevated' | 'outlined' | 'filled'; + type Props = React.ComponentProps & { /** * Changes Card shadow and background on iOS and Android. @@ -47,9 +53,10 @@ type Props = React.ComponentProps & { /** * Mode of the Card. * - `elevated` - Card with elevation. + * - `filled` - Card with without outline and elevation @supported Available in v3.x with theme version 3 * - `outlined` - Card with an outline. */ - mode?: 'elevated' | 'outlined'; + mode?: Mode; /** * Content of the `Card`. */ @@ -113,7 +120,14 @@ const Card = ({ testID, accessible, ...rest -}: (OutlinedCardProps | ElevatedCardProps) & Props) => { +}: (OutlinedCardProps | ElevatedCardProps | FilledCardProps) & Props) => { + const isMode = React.useCallback( + (modeToCompare: Mode) => { + return cardMode === modeToCompare; + }, + [cardMode] + ); + // Default animated value const { current: elevation } = React.useRef( new Animated.Value(cardElevation) @@ -185,30 +199,48 @@ const Card = ({ ? (child.type as any).displayName : null ); - const borderColor = color(dark ? white : black) - .alpha(0.12) - .rgb() - .string(); const computedElevation = dark && isAdaptiveMode ? elevationDarkAdaptive : elevation; + const { backgroundColor, borderColor } = getCardColors({ + theme, + mode: cardMode, + isAdaptiveMode, + elevation, + }); + return ( + {isMode('outlined') && ( + + )} & { /** @@ -35,17 +36,27 @@ type Props = React.ComponentPropsWithRef & { * export default MyComponent; * ``` */ -const CardActions = (props: Props) => ( - - {React.Children.map(props.children, (child) => - React.isValidElement(child) - ? React.cloneElement(child, { - compact: child.props.compact !== false, - }) - : child - )} - -); +const CardActions = (props: Props) => { + const { isV3 } = useTheme(); + const justifyContent = isV3 ? 'flex-end' : 'flex-start'; + + return ( + + {React.Children.map(props.children, (child, i) => { + return React.isValidElement(child) + ? React.cloneElement(child, { + compact: !isV3 && child.props.compact !== false, + mode: isV3 && (i === 0 ? 'outlined' : 'contained'), + style: isV3 && styles.button, + }) + : child; + })} + + ); +}; CardActions.displayName = 'Card.Actions'; @@ -53,9 +64,11 @@ const styles = StyleSheet.create({ container: { flexDirection: 'row', alignItems: 'center', - justifyContent: 'flex-start', padding: 8, }, + button: { + marginLeft: 8, + }, }); export default CardActions; diff --git a/src/components/Card/CardCover.tsx b/src/components/Card/CardCover.tsx index 51ed117a8c..d53e22440b 100644 --- a/src/components/Card/CardCover.tsx +++ b/src/components/Card/CardCover.tsx @@ -3,6 +3,7 @@ import { StyleSheet, View, ViewStyle, Image, StyleProp } from 'react-native'; import { withTheme } from '../../core/theming'; import { grey200 } from '../../styles/themes/v2/colors'; import type { Theme } from '../../types'; +import { getCardCoverStyle } from './helpers'; type Props = React.ComponentPropsWithRef & { /** @@ -46,26 +47,7 @@ type Props = React.ComponentPropsWithRef & { * @extends Image props https://reactnative.dev/docs/image#props */ const CardCover = ({ index, total, style, theme, ...rest }: Props) => { - const { roundness } = theme; - - let coverStyle; - - if (index === 0) { - if (total === 1) { - coverStyle = { - borderRadius: roundness, - }; - } else { - coverStyle = { - borderTopLeftRadius: roundness, - borderTopRightRadius: roundness, - }; - } - } else if (typeof total === 'number' && index === total - 1) { - coverStyle = { - borderBottomLeftRadius: roundness, - }; - } + const coverStyle = getCardCoverStyle({ theme, index, total }); return ( diff --git a/src/components/Card/CardTitle.tsx b/src/components/Card/CardTitle.tsx index 251803258b..8f7652f10b 100644 --- a/src/components/Card/CardTitle.tsx +++ b/src/components/Card/CardTitle.tsx @@ -8,9 +8,10 @@ import { } from 'react-native'; import { withTheme } from '../../core/theming'; -import type { Theme } from '../../types'; +import type { MD3TypescaleKey, Theme } from '../../types'; import Caption from '../Typography/v2/Caption'; import Title from '../Typography/v2/Title'; +import Text from '../Typography/Text'; type Props = React.ComponentPropsWithRef & { /** @@ -25,6 +26,23 @@ type Props = React.ComponentPropsWithRef & { * Number of lines for the title. */ titleNumberOfLines?: number; + /** + * @supported Available in v3.x with theme version 3 + * + * Title text variant defines appropriate text styles for type role and its size. + * Available variants: + * + * Display: `displayLarge`, `displayMedium`, `displaySmall` + * + * Headline: `headlineLarge`, `headlineMedium`, `headlineSmall` + * + * Title: `titleLarge`, `titleMedium`, `titleSmall` + * + * Label: `labelLarge`, `labelMedium`, `labelSmall` + * + * Body: `bodyLarge`, `bodyMedium`, `bodySmall` + */ + titleVariant?: keyof typeof MD3TypescaleKey; /** * Text for the subtitle. Note that this will only accept a string or ``-based node. */ @@ -37,6 +55,23 @@ type Props = React.ComponentPropsWithRef & { * Number of lines for the subtitle. */ subtitleNumberOfLines?: number; + /** + * @supported Available in v3.x with theme version 3 + * + * Subtitle text variant defines appropriate text styles for type role and its size. + * Available variants: + * + * Display: `displayLarge`, `displayMedium`, `displaySmall` + * + * Headline: `headlineLarge`, `headlineMedium`, `headlineSmall` + * + * Title: `titleLarge`, `titleMedium`, `titleSmall` + * + * Label: `labelLarge`, `labelMedium`, `labelSmall` + * + * Body: `bodyLarge`, `bodyMedium`, `bodySmall` + */ + subtitleVariant?: keyof typeof MD3TypescaleKey; /** * Callback which returns a React element to display on the left side. */ @@ -98,15 +133,27 @@ const CardTitle = ({ title, titleStyle, titleNumberOfLines = 1, + titleVariant = 'bodyLarge', subtitle, subtitleStyle, subtitleNumberOfLines = 1, + subtitleVariant = 'bodyMedium', left, leftStyle, right, rightStyle, style, + theme, }: Props) => { + const titleComponent = (props: any) => + theme.isV3 ? : ; + + const subtitleComponent = (props: any) => + theme.isV3 ? <Text {...props} /> : <Caption {...props} />; + + const TextComponent = React.memo(({ component, ...rest }: any) => + React.createElement(component, rest) + ); return ( <View style={[ @@ -124,29 +171,31 @@ const CardTitle = ({ ) : null} <View style={[styles.titles]}> - {title ? ( - <Title + {title && ( + <TextComponent + component={titleComponent} style={[ styles.title, { marginBottom: subtitle ? 0 : 2 }, titleStyle, ]} numberOfLines={titleNumberOfLines} + variant={titleVariant} > {title} - - ) : null} - - {subtitle ? ( - + )} + {subtitle && ( + {subtitle} - - ) : null} + + )} - {right ? right({ size: 24 }) : null} ); diff --git a/src/components/Card/helpers.tsx b/src/components/Card/helpers.tsx new file mode 100644 index 0000000000..f6d768ce06 --- /dev/null +++ b/src/components/Card/helpers.tsx @@ -0,0 +1,105 @@ +import type { Animated } from 'react-native'; +import color from 'color'; +import { black, white } from '../../styles/themes/v2/colors'; +import type { Theme } from '../../types'; +import overlay from '../../styles/overlay'; + +type CardMode = 'elevated' | 'outlined' | 'filled'; +type Elevation = 0 | 1 | 2 | 3 | 4 | 5 | Animated.Value; + +export const getCardCoverStyle = ({ + theme, + index, + total, +}: { + theme: Theme; + index?: number; + total?: number; +}) => { + const { isV3, roundness } = theme; + + if (index === 0) { + if (total === 1) { + return { + borderRadius: roundness, + }; + } + + if (isV3) { + return { + borderRadius: roundness, + }; + } + + return { + borderTopLeftRadius: roundness, + borderTopRightRadius: roundness, + }; + } + + if (typeof total === 'number' && index === total - 1) { + return { + borderBottomLeftRadius: roundness, + }; + } + + return undefined; +}; + +const getBorderColor = ({ theme }: { theme: Theme }) => { + if (theme.isV3) { + return theme.colors.outline; + } + + if (theme.dark) { + return color(white).alpha(0.12).rgb().string(); + } + return color(black).alpha(0.12).rgb().string(); +}; + +const getBackgroundColor = ({ + theme, + isMode, + isAdaptiveMode, + elevation, +}: { + theme: Theme; + isMode: (mode: CardMode) => boolean; + isAdaptiveMode?: boolean; + elevation: Elevation; +}) => { + if (theme.isV3 && isMode('filled')) { + return theme.colors.surfaceVariant; + } + + if (theme.dark && isAdaptiveMode) { + return overlay(elevation, theme.colors.surface); + } + return theme.colors.surface; +}; + +export const getCardColors = ({ + theme, + mode, + isAdaptiveMode, + elevation, +}: { + theme: Theme; + mode: CardMode; + isAdaptiveMode?: boolean; + elevation: Elevation; +}) => { + const isMode = (modeToCompare: CardMode) => { + return mode === modeToCompare; + }; + + return { + backgroundColor: getBackgroundColor({ + theme, + isMode, + isAdaptiveMode, + elevation, + }), + borderColor: getBorderColor({ theme }), + }; +}; diff --git a/src/components/__tests__/Card/__snapshots__/Card.test.js.snap b/src/components/__tests__/Card/__snapshots__/Card.test.js.snap index 999596f3fb..73993c39dc 100644 --- a/src/components/__tests__/Card/__snapshots__/Card.test.js.snap +++ b/src/components/__tests__/Card/__snapshots__/Card.test.js.snap @@ -5,13 +5,29 @@ exports[`Card renders an outlined card 1`] = ` style={ Object { "backgroundColor": "#ffffff", - "borderColor": "rgba(0, 0, 0, 0.12)", "borderRadius": 4, - "borderWidth": 1, "elevation": 0, } } > +