diff --git a/docs/6.x/docs/guides/migration.md b/docs/6.x/docs/guides/migration.md
index e51de006aa..f014e035cf 100644
--- a/docs/6.x/docs/guides/migration.md
+++ b/docs/6.x/docs/guides/migration.md
@@ -187,6 +187,24 @@ e.g.:
- The default elevation changed from level `1` to level `3`.
- The `style` prop no longer configures the background color or border radius. You can override `theme.colors.surfaceContainerHigh` and `theme.shapes.corner.extraLarge` using the `theme` prop instead.
+#### `Dialog.Actions`
+
+`Dialog.Actions` no longer injects `compact` on its buttons. To keep the previous behavior, you need to add the `compact` prop to each button:
+
+```tsx
+// Before (v5)
+
+
+
+
+// After (v6)
+
+
+
+```
+
### Searchbar
The misspelled `traileringIcon` props have been renamed:
@@ -330,6 +348,124 @@ const theme = {
/>
```
+### Card
+
+#### `Card` layout
+
+`Card` and its related components have been reworked. Instead of injecting padding to its children, the `Card` component now adds a padding (`16dp`) around its content as well as a gap (`16dp`) between its children.
+
+For the related components:
+
+- `Card.Cover` and `Card.Actions` now apply negative margins to extend into the Card's edges:
+ - `Card.Cover` applies top, left and right margins for vertical cards and left, top, and bottom margins for horizontal cards.
+ - `Card.Actions` applies bottom, left, and right margins for vertical cards and left, bottom, and right margins for horizontal cards.
+- `Card.Title` and `Card.Content` no longer apply padding around them.
+
+This means, in a typical card layout, the Card will automatically apply the necessary spacing between these sections:
+
+```tsx
+
+
+
+
+ Card content
+
+
+
+
+
+
+```
+
+So your existing layouts with the following structure will continue to work as before:
+
+- `Card` without `Card.Cover` or `Card.Actions`.
+- `Card` with `Card.Cover` at the top and/or `Card.Actions` at the bottom.
+
+If you have `Card.Cover` or `Card.Actions` in positions other than the top and bottom respectively, you will need to adjust their margins. For example, for a `Card.Cover` in the middle of the Card, you would need to set its top and bottom margins to zero:
+
+```tsx
+
+
+
+
+ Card content
+
+
+```
+
+We have also added a `direction` prop to the `Card` component, which lets you arrange its items horizontally instead of the default vertical layout. So if you have custom `flexDirection` styles on the Card, you should replace them with the `direction` prop.
+
+```tsx
+// Before (v5)
+
+
+
+
+ Card content
+
+
+
+
+
+
+
+// After (v6)
+
+
+
+
+ Card content
+
+
+
+
+
+
+```
+
+#### `Card.Actions`
+
+`Card.Actions` no longer injects `mode` on the buttons. To keep the previous behavior, you need to set `mode="outlined"` on the first button and `mode="contained"` on the rest:
+
+```tsx
+// Before (v5)
+
+
+
+
+
+// After (v6)
+
+
+
+
+```
+
+### List
+
+#### `List.Accordion`
+
+An accordion with a `left` element now indents only its `List.Item` children. Anything else you put inside keeps its own padding, so indent it yourself to line it up with the items.
+
+```tsx
+// Before (v5)
+ }>
+
+ Custom row
+
+
+
+// After (v6)
+ }>
+
+ Custom row
+
+
+```
+
+`theme` set on `List.Accordion` no longer reaches its children either. Pass it to the child that needs the override.
+
### ToggleButton
`ToggleButton`, `ToggleButton.Group` and `ToggleButton.Row` were removed. For an
diff --git a/example/src/Examples/CardExample.tsx b/example/src/Examples/CardExample.tsx
index 016bef3e21..9c4e04f8e2 100644
--- a/example/src/Examples/CardExample.tsx
+++ b/example/src/Examples/CardExample.tsx
@@ -74,8 +74,12 @@ const CardExample = () => {
-
-
+
+
@@ -104,12 +108,14 @@ const CardExample = () => {
@@ -233,6 +239,10 @@ const styles = StyleSheet.create({
borderTopRightRadius: 0,
borderBottomRightRadius: 24,
},
+ bottomCover: {
+ marginTop: 0,
+ marginBottom: -16,
+ },
});
export default CardExample;
diff --git a/example/src/Examples/SegmentedButtons/SegmentedButtonMultiselectRealCase.tsx b/example/src/Examples/SegmentedButtons/SegmentedButtonMultiselectRealCase.tsx
index 28475a32da..2ba4a69d30 100644
--- a/example/src/Examples/SegmentedButtons/SegmentedButtonMultiselectRealCase.tsx
+++ b/example/src/Examples/SegmentedButtons/SegmentedButtonMultiselectRealCase.tsx
@@ -59,17 +59,15 @@ const SegmentedButtonMultiselectRealCase = () => {
contentContainerStyle={styles.contentContainer}
renderItem={({ item }) => {
return (
-
-
-
- }
- />
-
+
+
+ }
+ />
);
}}
@@ -90,12 +88,6 @@ const styles = StyleSheet.create({
marginHorizontal: 16,
marginTop: 16,
},
- content: {
- flexDirection: 'row',
- alignItems: 'center',
- paddingHorizontal: 0,
- paddingVertical: 0,
- },
cover: {
width: 72,
height: 72,
diff --git a/example/src/Examples/SegmentedButtons/SegmentedButtonRealCase.tsx b/example/src/Examples/SegmentedButtons/SegmentedButtonRealCase.tsx
index 172c80a5f6..87c0b74ba9 100644
--- a/example/src/Examples/SegmentedButtons/SegmentedButtonRealCase.tsx
+++ b/example/src/Examples/SegmentedButtons/SegmentedButtonRealCase.tsx
@@ -37,17 +37,15 @@ const SegmentedButtonRealCase = () => {
contentContainerStyle={styles.contentContainer}
renderItem={({ item }) => {
return (
-
-
-
- }
- />
-
+
+
+ }
+ />
);
}}
@@ -68,15 +66,9 @@ const styles = StyleSheet.create({
marginHorizontal: 16,
marginTop: 16,
},
- content: {
- flexDirection: 'row',
- alignItems: 'center',
- paddingHorizontal: 0,
- paddingVertical: 0,
- },
cover: {
- width: 72,
- height: 72,
+ width: 84,
+ height: 84,
},
title: {
flexShrink: 1,
diff --git a/example/src/Examples/TeamDetails.tsx b/example/src/Examples/TeamDetails.tsx
index 5970274f31..463e5148d8 100644
--- a/example/src/Examples/TeamDetails.tsx
+++ b/example/src/Examples/TeamDetails.tsx
@@ -93,8 +93,12 @@ const News = () => {
-
-
+
+
@@ -110,8 +114,12 @@ const News = () => {
-
-
+
+
diff --git a/src/components/Card/Card.tsx b/src/components/Card/Card.tsx
index 712110883b..75a3941bab 100644
--- a/src/components/Card/Card.tsx
+++ b/src/components/Card/Card.tsx
@@ -11,6 +11,7 @@ import useLatestCallback from 'use-latest-callback';
import CardActions from './CardActions';
import CardContent from './CardContent';
+import { CardContext } from './CardContext';
import CardCover from './CardCover';
import CardTitle from './CardTitle';
import { getCardColors } from './utils';
@@ -36,6 +37,7 @@ type ContainedCardProps = {
};
type Mode = 'elevated' | 'outlined' | 'contained';
+type Direction = 'vertical' | 'horizontal';
export type Props = Omit & {
/**
@@ -45,6 +47,12 @@ export type Props = Omit & {
* - `outlined` - Card with an outline.
*/
mode?: Mode;
+ /**
+ * Direction of the Card's content.
+ * - `vertical`
+ * - `horizontal`
+ */
+ direction?: Direction;
/**
* Content of the `Card`.
*/
@@ -78,7 +86,7 @@ export type Props = Omit & {
*/
elevation?: Elevation;
/**
- * Style of card's inner content.
+ * Style of card's content.
*/
contentStyle?: StyleProp;
style?: StyleProp;
@@ -100,6 +108,9 @@ export type Props = Omit & {
ref?: React.Ref;
};
+const DEFAULT_CARD_GAP = 16;
+const DEFAULT_CARD_PADDING = 16;
+
/**
* A card is a sheet of material that serves as an entry point to more detailed information.
*
@@ -112,15 +123,15 @@ export type Props = Omit & {
*
* const MyComponent = () => (
*
+ *
*
*
* Card title
* Card content
*
- *
*
- *
- *
+ *
+ *
*
*
* );
@@ -128,7 +139,6 @@ export type Props = Omit & {
* export default MyComponent;
* ```
*/
-
const Card = ({
elevation: cardElevation = 1,
delayLongPress,
@@ -137,6 +147,7 @@ const Card = ({
onPressOut,
onPressIn,
mode: cardMode = 'elevated',
+ direction: cardDirection = 'vertical',
children,
style,
contentStyle,
@@ -182,15 +193,6 @@ const Card = ({
}
});
- const total = React.Children.count(children);
- const siblings = React.Children.map(children, (child) =>
- React.isValidElement(child) && child.type
- ? typeof child.type !== 'string' && 'displayName' in child.type
- ? child.type.displayName
- : null
- : null
- );
-
const { backgroundColor, borderColor: themedBorderColor } = getCardColors({
theme,
mode: cardMode,
@@ -203,18 +205,28 @@ const Card = ({
const borderRadius = theme.shapes.corner.medium;
+ const cardContext = React.useMemo(
+ () => ({ padding: DEFAULT_CARD_PADDING, direction: cardDirection }),
+ [cardDirection]
+ );
+
const content = (
-
- {React.Children.map(children, (child, index) =>
- React.isValidElement(child)
- ? React.cloneElement(child as React.ReactElement, {
- index,
- total,
- siblings,
- })
- : child
- )}
-
+
+
+ {children}
+
+
);
return (
@@ -274,8 +286,15 @@ Card.Cover = CardCover;
Card.Title = CardTitle;
const styles = StyleSheet.create({
- innerContainer: {
+ content: {
flexShrink: 1,
+ overflow: 'hidden',
+ },
+ horizontal: {
+ flexDirection: 'row',
+ },
+ vertical: {
+ flexDirection: 'column',
},
outline: {
borderWidth: 1,
diff --git a/src/components/Card/CardActions.tsx b/src/components/Card/CardActions.tsx
index d541c691bc..8246a7ec65 100644
--- a/src/components/Card/CardActions.tsx
+++ b/src/components/Card/CardActions.tsx
@@ -2,7 +2,7 @@ import * as React from 'react';
import { StyleSheet, View } from 'react-native';
import type { StyleProp, ViewProps, ViewStyle } from 'react-native';
-import type { CardActionChildProps } from './utils';
+import { CardContext } from './CardContext';
import { useInternalTheme } from '../../core/theming';
import type { ThemeProp } from '../../theme/types';
@@ -26,8 +26,8 @@ export type Props = ViewProps & {
* const MyComponent = () => (
*
*
- *
- *
+ *
+ *
*
*
* );
@@ -37,32 +37,32 @@ export type Props = ViewProps & {
*/
const CardActions = ({ theme, style, children, ...rest }: Props) => {
useInternalTheme(theme);
+ const cardContext = React.useContext(CardContext);
+
+ const cardMarginStyle = cardContext
+ ? cardContext.direction === 'horizontal'
+ ? {
+ marginTop: -cardContext.padding,
+ marginRight: -cardContext.padding,
+ marginBottom: -cardContext.padding,
+ }
+ : {
+ marginLeft: -cardContext.padding,
+ marginRight: -cardContext.padding,
+ marginBottom: -cardContext.padding,
+ }
+ : null;
const containerStyle = [
styles.container,
+ cardMarginStyle,
{ justifyContent: 'flex-end' } satisfies ViewStyle,
style,
];
return (
- {React.Children.map(children, (child, index) => {
- if (!React.isValidElement(child)) {
- return child;
- }
-
- const compact = child.props.compact;
- const mode =
- child.props.mode ?? (index === 0 ? 'outlined' : 'contained');
- const childStyle = [styles.button, child.props.style];
-
- return React.cloneElement(child, {
- ...child.props,
- compact,
- mode,
- style: childStyle,
- });
- })}
+ {children}
);
};
@@ -73,10 +73,9 @@ const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
- padding: 8,
- },
- button: {
- marginLeft: 8,
+ columnGap: 8,
+ paddingHorizontal: 8,
+ paddingBottom: 8,
},
});
diff --git a/src/components/Card/CardContent.tsx b/src/components/Card/CardContent.tsx
index bde25ddb25..fea1e519da 100644
--- a/src/components/Card/CardContent.tsx
+++ b/src/components/Card/CardContent.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import { StyleSheet, View } from 'react-native';
+import { View } from 'react-native';
import type { StyleProp, ViewProps, ViewStyle } from 'react-native';
export type Props = ViewProps & {
@@ -7,18 +7,6 @@ export type Props = ViewProps & {
* Items inside the `Card.Content`.
*/
children: React.ReactNode;
- /**
- * @internal
- */
- index?: number;
- /**
- * @internal
- */
- total?: number;
- /**
- * @internal
- */
- siblings?: Array;
style?: StyleProp;
};
@@ -42,59 +30,10 @@ export type Props = ViewProps & {
* export default MyComponent;
* ```
*/
-const CardContent = ({ index, total, siblings, style, ...rest }: Props) => {
- const cover = 'Card.Cover';
- const title = 'Card.Title';
-
- let contentStyle, prev, next;
-
- if (typeof index === 'number' && siblings) {
- prev = siblings[index - 1];
- next = siblings[index + 1];
- }
-
- if (
- (prev === cover && next === cover) ||
- (prev === title && next === title) ||
- total === 1
- ) {
- contentStyle = styles.only;
- } else if (index === 0) {
- if (next === cover || next === title) {
- contentStyle = styles.only;
- } else {
- contentStyle = styles.first;
- }
- } else if (typeof total === 'number' && index === total - 1) {
- if (prev === cover || prev === title) {
- contentStyle = styles.only;
- } else {
- contentStyle = styles.last;
- }
- } else if (prev === cover || prev === title) {
- contentStyle = styles.first;
- } else if (next === cover || next === title) {
- contentStyle = styles.last;
- }
-
- return ;
-};
+const CardContent = ({ style, ...rest }: Props) => (
+
+);
CardContent.displayName = 'Card.Content';
-const styles = StyleSheet.create({
- container: {
- paddingHorizontal: 16,
- },
- first: {
- paddingTop: 16,
- },
- last: {
- paddingBottom: 16,
- },
- only: {
- paddingVertical: 16,
- },
-});
-
export default CardContent;
diff --git a/src/components/Card/CardContext.tsx b/src/components/Card/CardContext.tsx
new file mode 100644
index 0000000000..1cf9bf0173
--- /dev/null
+++ b/src/components/Card/CardContext.tsx
@@ -0,0 +1,16 @@
+import * as React from 'react';
+
+type CardContextType = {
+ /**
+ * Padding applied to the Card content.
+ */
+ padding: number;
+ /**
+ * Direction of the Card layout.
+ */
+ direction: 'vertical' | 'horizontal';
+};
+
+export const CardContext = React.createContext(null);
+
+CardContext.displayName = 'CardContext';
diff --git a/src/components/Card/CardCover.tsx b/src/components/Card/CardCover.tsx
index 4542aa7c99..266ad045ea 100644
--- a/src/components/Card/CardCover.tsx
+++ b/src/components/Card/CardCover.tsx
@@ -1,6 +1,8 @@
+import * as React from 'react';
import { Image, StyleSheet, View } from 'react-native';
import type { ImageProps, StyleProp, ViewStyle } from 'react-native';
+import { CardContext } from './CardContext';
import { getCardCoverStyle } from './utils';
import { useInternalTheme } from '../../core/theming';
import { grey200 } from '../../theme/colors';
@@ -8,14 +10,6 @@ import type { ThemeProp } from '../../theme/types';
import { splitStyles } from '../../utils/splitStyles';
export type Props = ImageProps & {
- /**
- * @internal
- */
- index?: number;
- /**
- * @internal
- */
- total?: number;
style?: StyleProp;
/**
* @optional
@@ -42,14 +36,9 @@ export type Props = ImageProps & {
*
* @extends Image props https://reactnative.dev/docs/image#props
*/
-const CardCover = ({
- index,
- total,
- style,
- theme: themeOverrides,
- ...rest
-}: Props) => {
+const CardCover = ({ style, theme: themeOverrides, ...rest }: Props) => {
const theme = useInternalTheme(themeOverrides);
+ const cardContext = React.useContext(CardContext);
const flattenedStyles = StyleSheet.flatten(style) || {};
const [, borderRadiusStyles] = splitStyles(
@@ -59,33 +48,47 @@ const CardCover = ({
const coverStyle = getCardCoverStyle({
theme,
- index,
- total,
borderRadiusStyles,
});
+ const cardMarginStyle = cardContext
+ ? cardContext.direction === 'horizontal'
+ ? {
+ marginTop: -cardContext.padding,
+ marginLeft: -cardContext.padding,
+ marginBottom: -cardContext.padding,
+ }
+ : {
+ marginTop: -cardContext.padding,
+ marginLeft: -cardContext.padding,
+ marginRight: -cardContext.padding,
+ }
+ : null;
+
return (
-
+
);
};
CardCover.displayName = 'Card.Cover';
+
const styles = StyleSheet.create({
container: {
- height: 195,
+ height: 194,
backgroundColor: grey200,
overflow: 'hidden',
},
image: {
flex: 1,
- height: undefined,
- width: undefined,
+ height: 'auto',
+ width: 'auto',
justifyContent: 'flex-end',
},
});
diff --git a/src/components/Card/CardTitle.tsx b/src/components/Card/CardTitle.tsx
index 15477d3033..ed8289cde5 100644
--- a/src/components/Card/CardTitle.tsx
+++ b/src/components/Card/CardTitle.tsx
@@ -81,14 +81,6 @@ export type Props = ViewProps & {
* Style for the right element wrapper.
*/
rightStyle?: StyleProp;
- /**
- * @internal
- */
- index?: number;
- /**
- * @internal
- */
- total?: number;
/**
* Specifies the largest possible scale a title font can reach.
*/
@@ -151,11 +143,8 @@ const CardTitle = ({
}: Props) => {
useInternalTheme(themeOverrides);
- const minHeight = subtitle || left || right ? 72 : 50;
- const marginBottom = subtitle ? 0 : 2;
-
return (
-
+
{left ? (
{left({
@@ -167,7 +156,7 @@ const CardTitle = ({
{title && (
{
if (Object.keys(borderRadiusStyles).length > 0) {
return {
diff --git a/src/components/Dialog/Dialog.tsx b/src/components/Dialog/Dialog.tsx
index 61af39bb31..92550a9331 100644
--- a/src/components/Dialog/Dialog.tsx
+++ b/src/components/Dialog/Dialog.tsx
@@ -13,7 +13,6 @@ import { useInternalTheme } from '../../core/theming';
import type { Elevation, ThemeProp } from '../../theme/types';
import Modal from '../Modal';
import type { SurfaceStyle } from '../Surface';
-import type { DialogChildProps } from './utils';
export type Props = {
/**
@@ -131,17 +130,7 @@ const Dialog = ({
testID={testID}
overlayTestID={overlayTestID}
>
- {React.Children.toArray(children)
- .filter((child) => child != null && typeof child !== 'boolean')
- .map((child, i) => {
- if (i === 0 && React.isValidElement(child)) {
- return React.cloneElement(child, {
- style: [{ marginTop: 24 }, child.props.style],
- });
- }
-
- return child;
- })}
+ {children}
);
};
@@ -168,6 +157,7 @@ const styles = StyleSheet.create({
*/
marginVertical: Platform.OS === 'android' ? 44 : 0,
justifyContent: 'flex-start',
+ paddingTop: 24,
},
});
diff --git a/src/components/Dialog/DialogActions.tsx b/src/components/Dialog/DialogActions.tsx
index 0a11970077..8b56611de4 100644
--- a/src/components/Dialog/DialogActions.tsx
+++ b/src/components/Dialog/DialogActions.tsx
@@ -2,7 +2,6 @@ import * as React from 'react';
import { StyleSheet, View } from 'react-native';
import type { StyleProp, ViewProps, ViewStyle } from 'react-native';
-import type { DialogActionChildProps } from './utils';
import { useInternalTheme } from '../../core/theming';
import type { ThemeProp } from '../../theme/types';
@@ -46,26 +45,17 @@ export type Props = ViewProps & {
* export default MyComponent;
* ```
*/
-const DialogActions = (props: Props) => {
- useInternalTheme(props.theme);
- const actionsLength = React.Children.toArray(props.children).length;
+const DialogActions = ({
+ theme: themeOverrides,
+ style,
+ children,
+ ...rest
+}: Props) => {
+ useInternalTheme(themeOverrides);
return (
-
- {React.Children.map(props.children, (child, i) =>
- React.isValidElement(child)
- ? React.cloneElement(child, {
- compact: true,
- uppercase: false,
- style: [
- {
- marginRight: i + 1 === actionsLength ? 0 : 8,
- },
- child.props.style,
- ],
- })
- : child
- )}
+
+ {children}
);
};
@@ -78,6 +68,7 @@ const styles = StyleSheet.create({
flexGrow: 1,
alignItems: 'center',
justifyContent: 'flex-end',
+ columnGap: 8,
paddingBottom: 24,
paddingHorizontal: 24,
},
diff --git a/src/components/Dialog/DialogIcon.tsx b/src/components/Dialog/DialogIcon.tsx
index 791544aefe..95acf8abda 100644
--- a/src/components/Dialog/DialogIcon.tsx
+++ b/src/components/Dialog/DialogIcon.tsx
@@ -23,6 +23,10 @@ export type Props = {
* @optional
*/
theme?: ThemeProp;
+ /**
+ * testID to be used on tests.
+ */
+ testID?: string;
};
/**
@@ -67,6 +71,7 @@ const DialogIcon = ({
color,
icon,
theme: themeOverrides,
+ testID,
}: Props) => {
const theme = useInternalTheme(themeOverrides);
const { colors } = theme;
@@ -75,7 +80,7 @@ const DialogIcon = ({
const iconColor = color || colors.secondary;
return (
-
+
);
@@ -87,7 +92,8 @@ const styles = StyleSheet.create({
wrapper: {
alignItems: 'center',
justifyContent: 'center',
- paddingTop: 24,
+ marginBottom: 16,
+ paddingTop: 0,
},
});
diff --git a/src/components/Dialog/DialogTitle.tsx b/src/components/Dialog/DialogTitle.tsx
index beff3be7d9..d0695e6b5b 100644
--- a/src/components/Dialog/DialogTitle.tsx
+++ b/src/components/Dialog/DialogTitle.tsx
@@ -81,7 +81,7 @@ const styles = StyleSheet.create({
marginHorizontal: 24,
},
v3Text: {
- marginTop: 16,
+ marginTop: 0,
marginBottom: 16,
},
});
diff --git a/src/components/List/ListAccordion.tsx b/src/components/List/ListAccordion.tsx
index 7c552fce36..ff26a075a4 100644
--- a/src/components/List/ListAccordion.tsx
+++ b/src/components/List/ListAccordion.tsx
@@ -12,8 +12,9 @@ import type {
ViewStyle,
} from 'react-native';
+import { ListAccordionContext } from './ListAccordionContext';
import { ListAccordionGroupContext } from './ListAccordionGroup';
-import type { ListChildProps, Style } from './utils';
+import type { Style } from './utils';
import { getAccordionColors, getLeftStyles } from './utils';
import { useLocale } from '../../core/locale';
import { useInternalTheme } from '../../core/theming';
@@ -241,6 +242,10 @@ const ListAccordion = ({
groupContext && id !== undefined
? () => groupContext.onAccordionPress(id)
: handlePressAction;
+
+ const hasLeft = left != null;
+ const accordionContext = React.useMemo(() => ({ hasLeft }), [hasLeft]);
+
return (
@@ -324,23 +329,11 @@ const ListAccordion = ({
- {isExpanded
- ? React.Children.map(children, (child) => {
- if (
- left &&
- React.isValidElement(child) &&
- !child.props.left &&
- !child.props.right
- ) {
- return React.cloneElement(child, {
- style: [styles.child, child.props.style],
- theme,
- });
- }
-
- return child;
- })
- : null}
+ {isExpanded ? (
+
+ {children}
+
+ ) : null}
);
};
@@ -374,9 +367,6 @@ const styles = StyleSheet.create({
marginVertical: 6,
paddingLeft: 8,
},
- child: {
- paddingLeft: 40,
- },
content: {
flex: 1,
justifyContent: 'center',
diff --git a/src/components/List/ListAccordionContext.tsx b/src/components/List/ListAccordionContext.tsx
new file mode 100644
index 0000000000..142c25ff65
--- /dev/null
+++ b/src/components/List/ListAccordionContext.tsx
@@ -0,0 +1,13 @@
+import * as React from 'react';
+
+export type ListAccordionContextType = {
+ /**
+ * Whether the accordion renders a `left` element.
+ */
+ hasLeft: boolean;
+};
+
+export const ListAccordionContext =
+ React.createContext({ hasLeft: false });
+
+ListAccordionContext.displayName = 'ListAccordionContext';
diff --git a/src/components/List/ListItem.tsx b/src/components/List/ListItem.tsx
index feb0f19f12..038ef86a58 100644
--- a/src/components/List/ListItem.tsx
+++ b/src/components/List/ListItem.tsx
@@ -11,6 +11,7 @@ import type {
ViewStyle,
} from 'react-native';
+import { ListAccordionContext } from './ListAccordionContext';
import { getLeftStyles, getRightStyles } from './utils';
import type { Style } from './utils';
import { useInternalTheme } from '../../core/theming';
@@ -166,6 +167,8 @@ const ListItem = ({
...rest
}: Props) => {
const theme = useInternalTheme(themeOverrides);
+ const { hasLeft } = React.useContext(ListAccordionContext);
+ const shouldIndent = hasLeft && !left && !right;
const [alignToTop, setAlignToTop] = React.useState(false);
const onDescriptionTextLayout = (
@@ -233,7 +236,11 @@ const ListItem = ({
{
expect(screen.getByText('Content').parent).toHaveStyle(styles.contentStyle);
});
+ it('clips inner content to the card shape', async () => {
+ await render(
+
+ Content
+
+ );
+
+ expect(screen.getByText('Content').parent).toHaveStyle({
+ borderRadius: LightTheme.shapes.corner.medium,
+ overflow: 'hidden',
+ });
+ });
+
it('does not render a disabled accessibility state', async () => {
await render({null});
@@ -124,18 +138,67 @@ describe('CardCover', () => {
describe('CardActions', () => {
it('renders button with passed mode', async () => {
+ const buttonProps = jest.fn();
+ const ProbeButton = (props: ComponentProps) => {
+ buttonProps(props);
+
+ return ;
+ };
+
+ await render(
+
+
+ Agree
+
+
+ );
+
+ expect(buttonProps).toHaveBeenCalledWith(
+ expect.objectContaining({ mode: 'contained' })
+ );
+ });
+
+ it('does not inject default button props', async () => {
+ const buttonProps = jest.fn();
+ const ProbeButton = (props: ComponentProps) => {
+ buttonProps(props);
+
+ return ;
+ };
+
+ await render(
+
+
+ Cancel
+ Agree
+
+
+ );
+
+ const [cancelButtonProps] = buttonProps.mock.calls[0];
+ const [agreeButtonProps] = buttonProps.mock.calls[1];
+
+ expect(cancelButtonProps).not.toHaveProperty('mode');
+ expect(cancelButtonProps).not.toHaveProperty('compact');
+ expect(agreeButtonProps).not.toHaveProperty('mode');
+ expect(agreeButtonProps).not.toHaveProperty('compact');
+ });
+
+ it('renders actions in a styled row', async () => {
await render(
-
+
+
);
- expect(
- // eslint-disable-next-line no-restricted-syntax -- TODO: replace TestInstance props access with a user-visible assertion.
- screen.getByTestId('card-actions').props.children[0].props.mode
- ).toBe('contained');
+ expect(screen.getByTestId('card-actions')).toHaveStyle({
+ flexDirection: 'row',
+ justifyContent: 'flex-end',
+ columnGap: 8,
+ });
});
});
@@ -201,3 +264,22 @@ describe('getCardCoverStyle - border radius', () => {
).toMatchObject({ borderRadius: LightTheme.shapes.corner.medium });
});
});
+
+describe('CardContent', () => {
+ it('uses the Card padding when it follows a cover and a title', async () => {
+ await render(
+
+
+
+
+ Card content
+
+
+ );
+
+ expect(screen.getByTestId('card-content').parent).toHaveStyle({
+ padding: 16,
+ gap: 16,
+ });
+ });
+});
diff --git a/src/components/__tests__/Card/__snapshots__/Card.test.tsx.snap b/src/components/__tests__/Card/__snapshots__/Card.test.tsx.snap
index 216e5b5d08..2f7cee90ba 100644
--- a/src/components/__tests__/Card/__snapshots__/Card.test.tsx.snap
+++ b/src/components/__tests__/Card/__snapshots__/Card.test.tsx.snap
@@ -114,8 +114,19 @@ exports[`Card renders an outlined card 1`] = `
[
{
"flexShrink": 1,
+ "overflow": "hidden",
+ },
+ {
+ "borderRadius": 12,
+ },
+ {
+ "flexDirection": "column",
},
undefined,
+ {
+ "gap": 16,
+ "padding": 16,
+ },
]
}
/>
@@ -237,8 +248,19 @@ exports[`Card renders an outlined card with a custom outline color 1`] = `
[
{
"flexShrink": 1,
+ "overflow": "hidden",
+ },
+ {
+ "borderRadius": 12,
+ },
+ {
+ "flexDirection": "column",
},
undefined,
+ {
+ "gap": 16,
+ "padding": 16,
+ },
]
}
/>
@@ -362,8 +384,19 @@ exports[`Card renders an outlined card with custom border color 1`] = `
[
{
"flexShrink": 1,
+ "overflow": "hidden",
+ },
+ {
+ "borderRadius": 12,
+ },
+ {
+ "flexDirection": "column",
},
undefined,
+ {
+ "gap": 16,
+ "padding": 16,
+ },
]
}
/>
diff --git a/src/components/__tests__/Dialog.test.tsx b/src/components/__tests__/Dialog.test.tsx
index 48d22a587b..7e593cb5f1 100644
--- a/src/components/__tests__/Dialog.test.tsx
+++ b/src/components/__tests__/Dialog.test.tsx
@@ -1,3 +1,4 @@
+import type { ComponentProps } from 'react';
import {
Text,
StyleSheet,
@@ -94,17 +95,62 @@ describe('Dialog', () => {
expect(onDismiss).toHaveBeenCalledTimes(1);
});
- it('should apply top margin to the first child if the dialog is V3', async () => {
+ it('should not add a top margin to a title-first dialog', async () => {
await render(
-