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 = () => { - + + @@ -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 + + ); - 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( - - + + Test Dialog Content ); + expect(screen.getByTestId('dialog-title')).toHaveStyle({ + marginTop: 0, + }); + }); + + it('should keep the bottom padding on a content-first dialog', async () => { + await render( + + + Test Dialog Content + + + ); + expect(screen.getByTestId('dialog-content')).toHaveStyle({ - marginTop: 24, + paddingBottom: 24, + }); + }); + + it('should not add a top padding to an icon-first dialog', async () => { + await render( + + + + ); + + expect(screen.getByTestId('dialog-icon')).toHaveStyle({ + paddingTop: 0, + }); + }); + + it('should preserve the icon-to-title spacing for an icon dialog', async () => { + await render( + + + + Test Dialog Content + + + ); + + expect(screen.getByTestId('dialog-icon')).toHaveStyle({ + marginBottom: 16, + paddingTop: 0, + }); + expect(screen.getByTestId('dialog-title')).toHaveStyle({ + marginTop: 0, }); }); }); @@ -125,35 +171,64 @@ describe('DialogActions', () => { it('should apply default styles', async () => { await render( - - + + ); const dialogActionsContainer = screen.getByTestId('dialog-actions'); - const dialogActionButtons = dialogActionsContainer.children; expect(dialogActionsContainer).toHaveStyle({ paddingBottom: 24, paddingHorizontal: 24, + columnGap: 8, }); - expect(dialogActionButtons[0]).toHaveStyle({ marginRight: 8 }); - expect(dialogActionButtons[1]).toHaveStyle({ marginRight: 0 }); }); - it('should apply custom styles', async () => { + it('should not inject button props into actions', async () => { + const buttonProps = jest.fn(); + const ProbeButton = (props: ComponentProps) => { + buttonProps(props); + + return - + Cancel + Ok ); - const dialogActionsContainer = screen.getByTestId('dialog-actions'); - const dialogActionButtons = dialogActionsContainer.children; + const [cancelButtonProps] = buttonProps.mock.calls[0]; + const [okButtonProps] = buttonProps.mock.calls[1]; - expect(dialogActionButtons[0]).toHaveStyle({ margin: 10 }); - expect(dialogActionButtons[1]).toHaveStyle({ margin: 0 }); + expect(cancelButtonProps).toHaveProperty('style', styles.spacing); + expect(okButtonProps).toHaveProperty('style', styles.noSpacing); }); }); diff --git a/src/components/__tests__/ListAccordion.test.tsx b/src/components/__tests__/ListAccordion.test.tsx index 59d221f9dc..ce387e343a 100644 --- a/src/components/__tests__/ListAccordion.test.tsx +++ b/src/components/__tests__/ListAccordion.test.tsx @@ -2,7 +2,7 @@ import { StyleSheet, View } from 'react-native'; import { describe, expect, it } from '@jest/globals'; -import { render } from '../../test-utils'; +import { render, screen } from '../../test-utils'; import { red500 } from '../../theme/colors'; import { LightTheme } from '../../theme/schemes'; import ListAccordion from '../List/ListAccordion'; @@ -94,6 +94,34 @@ it('renders list accordion with custom title and description styles', async () = expect(tree).toMatchSnapshot(); }); +it('indents expanded accordion children without their own left/right when the accordion has a left icon', async () => { + await render( + } + title="Accordion with indented children" + expanded + > + + + ); + + expect(screen.getByTestId('accordion-child')).toHaveStyle({ + paddingLeft: 40, + }); +}); + +it('does not indent expanded accordion children when the accordion has no left icon', async () => { + await render( + + + + ); + + expect(screen.getByTestId('accordion-child')).not.toHaveStyle({ + paddingLeft: 40, + }); +}); + describe('ListAccordion', () => { it('should not throw an error when id={0}', async () => { const ListAccordionTest = () => ( diff --git a/src/components/__tests__/Tooltip.test.tsx b/src/components/__tests__/Tooltip.test.tsx index d6b037e10c..2888803613 100644 --- a/src/components/__tests__/Tooltip.test.tsx +++ b/src/components/__tests__/Tooltip.test.tsx @@ -163,7 +163,7 @@ describe('Tooltip', () => { it('hides the tooltip when the user stop pressing the component', async () => { const { wrapper: { queryByText, getByText, findByText }, - } = await setup({ enterTouchDelay: 50, leaveTouchDelay: 0 }); + } = await setup({ enterTouchDelay: 50, leaveTouchDelay: 100 }); await userEvent.longPress(getTrigger(getByText));