diff --git a/docs/6.x/docs/guides/migration.md b/docs/6.x/docs/guides/migration.md index 1fcd32bd25..88ecc6fafe 100644 --- a/docs/6.x/docs/guides/migration.md +++ b/docs/6.x/docs/guides/migration.md @@ -126,3 +126,71 @@ const theme = { style={{ fontSize: 16, color: '#1C1B1F' }} /> ``` + +### Card + +#### `Card.Actions` + +`Card.Actions` no longer assigns `mode` to its buttons, previously `outlined` for the first one and `contained` for the rest, and no longer injects `compact`. Set both on the buttons. + +```tsx +// Before (v5) + + + + + +// After (v6) + + + + +``` + +#### `Card.Content` + +`Card.Content` uses the same vertical padding everywhere. Before it changed depending on the sections next to it. + +### Dialog + +#### `Dialog.Actions` + +`Dialog.Actions` no longer injects `compact` and `uppercase` into the action buttons. Set them yourself if you want the old look. + +```tsx +// Before (v5) + + + + +// After (v6) + + + +``` + +### List + +#### `List.Accordion` + +When `List.Accordion` has a `left` element, it used to indent every child that rendered no `left` or `right` of its own, whatever the child was. The indent now comes from context and only `List.Item` reads it, so a custom child keeps its own padding. Indent it yourself if you need the old alignment. + +```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. diff --git a/example/src/Examples/CardExample.tsx b/example/src/Examples/CardExample.tsx index 61c7ebe822..42ce70d91a 100644 --- a/example/src/Examples/CardExample.tsx +++ b/example/src/Examples/CardExample.tsx @@ -75,8 +75,12 @@ const CardExample = () => { - - + + @@ -104,10 +108,10 @@ const CardExample = () => { /> - - 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 f0fdc89407..8cec1489eb 100644 --- a/src/components/Card/Card.tsx +++ b/src/components/Card/Card.tsx @@ -95,6 +95,8 @@ export type Props = $Omit, 'mode'> & { /** * A card is a sheet of material that serves as an entry point to more detailed information. + * Card clips its inner content to the card shape and renders children directly; + * section spacing is owned by the section components themselves. * * ## Usage * ```js @@ -112,8 +114,8 @@ export type Props = $Omit, 'mode'> & { * * * - * - * + * + * * * * ); @@ -185,15 +187,6 @@ const Card = ({ runElevationAnimation('out'); }); - 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, @@ -215,17 +208,11 @@ const Card = ({ }; const content = ( - - {React.Children.map(children, (child, index) => - React.isValidElement(child) - ? React.cloneElement(child as React.ReactElement, { - index, - total, - siblings, - borderRadiusStyles, - }) - : child - )} + + {children} ); @@ -291,6 +278,7 @@ Card.Title = CardTitle; const styles = StyleSheet.create({ innerContainer: { flexShrink: 1, + overflow: 'hidden', }, outline: { borderWidth: 1, diff --git a/src/components/Card/CardActions.tsx b/src/components/Card/CardActions.tsx index 529db00f52..05d136ee3b 100644 --- a/src/components/Card/CardActions.tsx +++ b/src/components/Card/CardActions.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 { CardActionChildProps } from './utils'; import { useInternalTheme } from '../../core/theming'; import type { ThemeProp } from '../../types'; @@ -17,6 +16,8 @@ export type Props = ViewProps & { /** * A component to show a list of actions inside a Card. + * Actions are rendered directly, so set button `mode`, `compact`, and custom + * spacing props explicitly on each action when needed. * * ## Usage * ```js @@ -26,8 +27,8 @@ export type Props = ViewProps & { * const MyComponent = () => ( * * - * - * + * + * * * * ); @@ -43,26 +44,20 @@ const CardActions = ({ theme, style, children, ...rest }: Props) => { { justifyContent: 'flex-end' } satisfies ViewStyle, style, ]; + const items = React.Children.toArray(children); 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, - }); - })} + {items.map((child, index) => ( + + {index > 0 && } + {child} + + ))} ); }; @@ -75,8 +70,8 @@ const styles = StyleSheet.create({ alignItems: 'center', padding: 8, }, - button: { - marginLeft: 8, + spacer: { + width: 8, }, }); diff --git a/src/components/Card/CardContent.tsx b/src/components/Card/CardContent.tsx index f53ff034f5..5784273d1b 100644 --- a/src/components/Card/CardContent.tsx +++ b/src/components/Card/CardContent.tsx @@ -7,23 +7,13 @@ export type Props = ViewProps & { * Items inside the `Card.Content`. */ children: React.ReactNode; - /** - * @internal - */ - index?: number; - /** - * @internal - */ - total?: number; - /** - * @internal - */ - siblings?: Array; style?: StyleProp; }; /** * A component to show content inside a Card. + * Content uses uniform vertical padding and does not depend on neighboring + * card sections. * * ## Usage * ```js @@ -42,59 +32,18 @@ export type Props = ViewProps & { * export default MyComponent; * ``` */ -const CardContent = ({ index, total, siblings, style, ...rest }: Props) => { - const cover = 'withInternalTheme(CardCover)'; - const title = 'withInternalTheme(CardTitle)'; - - 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/CardCover.tsx b/src/components/Card/CardCover.tsx index dc348dbdd8..a84aa1e814 100644 --- a/src/components/Card/CardCover.tsx +++ b/src/components/Card/CardCover.tsx @@ -8,14 +8,6 @@ import type { ThemeProp } from '../../types'; import { splitStyles } from '../../utils/splitStyles'; export type Props = ImageProps & { - /** - * @internal - */ - index?: number; - /** - * @internal - */ - total?: number; style?: StyleProp; /** * @optional @@ -42,13 +34,7 @@ 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 flattenedStyles = StyleSheet.flatten(style) || {}; @@ -59,8 +45,6 @@ const CardCover = ({ const coverStyle = getCardCoverStyle({ theme, - index, - total, borderRadiusStyles, }); diff --git a/src/components/Card/CardTitle.tsx b/src/components/Card/CardTitle.tsx index 2737fe6430..cf8beb20ef 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. */ diff --git a/src/components/Card/utils.tsx b/src/components/Card/utils.tsx index 644c8f4c27..1703249401 100644 --- a/src/components/Card/utils.tsx +++ b/src/components/Card/utils.tsx @@ -17,14 +17,10 @@ export type CardActionChildProps = { export const getCardCoverStyle = ({ theme, - index: _index, - total: _total, borderRadiusStyles, }: { theme: InternalTheme; borderRadiusStyles: BorderRadiusStyles; - index?: number; - total?: number; }) => { if (Object.keys(borderRadiusStyles).length > 0) { return { diff --git a/src/components/Dialog/Dialog.tsx b/src/components/Dialog/Dialog.tsx index a7b082fe89..16b14a371e 100644 --- a/src/components/Dialog/Dialog.tsx +++ b/src/components/Dialog/Dialog.tsx @@ -12,7 +12,6 @@ import DialogTitle from './DialogTitle'; import { useInternalTheme } from '../../core/theming'; import type { ThemeProp } from '../../types'; import Modal from '../Modal'; -import type { DialogChildProps } from './utils'; export type Props = { /** @@ -51,6 +50,8 @@ const DIALOG_ELEVATION: number = 24; /** * Dialogs inform users about a specific task and may contain critical information, require decisions, or involve multiple tasks. * To render the `Dialog` above other components, you'll need to wrap it with the [`Portal`](../Portal) component. + * Dialog owns the top content inset, so first-slot components render without + * adding their own top offset. * * ## Usage * ```js @@ -122,17 +123,7 @@ const Dialog = ({ theme={theme} testID={testID} > - {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} ); }; @@ -160,6 +151,7 @@ const styles = StyleSheet.create({ marginVertical: Platform.OS === 'android' ? 44 : 0, elevation: DIALOG_ELEVATION, justifyContent: 'flex-start', + paddingTop: 24, }, }); diff --git a/src/components/Dialog/DialogActions.tsx b/src/components/Dialog/DialogActions.tsx index 7e3799451e..3cc9f73714 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 '../../types'; @@ -20,6 +19,8 @@ export type Props = ViewProps & { /** * A component to show a list of actions in a Dialog. + * Actions are rendered directly, so configure each action button's props + * explicitly when you need non-default behavior. * * ## Usage * ```js @@ -46,26 +47,28 @@ 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); + + const items = React.Children.toArray(children); 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 - )} + + {items.map((child, index) => ( + + {index > 0 && } + {child} + + ))} ); }; @@ -81,6 +84,9 @@ const styles = StyleSheet.create({ paddingBottom: 24, paddingHorizontal: 24, }, + spacer: { + width: 8, + }, }); export default DialogActions; diff --git a/src/components/Dialog/DialogIcon.tsx b/src/components/Dialog/DialogIcon.tsx index 3ced9e12b0..e8ebc5b751 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 ae553daee2..d997cadc32 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 97730d58e6..9a3dcf0fde 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'; @@ -324,23 +325,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 +363,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..306d9788bc --- /dev/null +++ b/src/components/List/ListAccordionContext.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; + +export type ListAccordionContextType = { + /** + * Whether descendant items that don't render their own `left`/`right` + * element should be indented to align under the accordion's content + * (past the leading icon). + */ + leftIndent: boolean; +}; + +export const ListAccordionContext = + React.createContext({ leftIndent: false }); + +ListAccordionContext.displayName = 'ListAccordionContext'; diff --git a/src/components/List/ListItem.tsx b/src/components/List/ListItem.tsx index a6f0181f02..2c0d3452cd 100644 --- a/src/components/List/ListItem.tsx +++ b/src/components/List/ListItem.tsx @@ -10,6 +10,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'; @@ -161,6 +162,8 @@ const ListItem = ({ ...rest }: Props) => { const theme = useInternalTheme(themeOverrides); + const { leftIndent } = React.useContext(ListAccordionContext); + const shouldIndent = leftIndent && !left && !right; const [alignToTop, setAlignToTop] = React.useState(false); const onDescriptionTextLayout = ( @@ -228,7 +231,11 @@ const ListItem = ({ { const theme = useInternalTheme(themeOverrides); const borderRadius = theme.shapes.corner.extraSmall; + const rowContext = React.useContext(ToggleButtonRowContext); + const isSegmentedRow = !!rowContext?.segmented; return ( @@ -134,6 +137,7 @@ const ToggleButton = ({ borderRadius, borderColor, }, + isSegmentedRow && styles.segmentedContent, style, ]} ref={ref} @@ -152,6 +156,10 @@ const styles = StyleSheet.create({ height: 42, margin: 0, }, + segmentedContent: { + borderRadius: 0, + marginLeft: StyleSheet.hairlineWidth, + }, }); export default ToggleButton; diff --git a/src/components/ToggleButton/ToggleButtonRow.tsx b/src/components/ToggleButton/ToggleButtonRow.tsx index 46e6aee48f..f4895f6fde 100644 --- a/src/components/ToggleButton/ToggleButtonRow.tsx +++ b/src/components/ToggleButton/ToggleButtonRow.tsx @@ -2,8 +2,10 @@ import * as React from 'react'; import { StyleSheet, View } from 'react-native'; import type { StyleProp, ViewStyle } from 'react-native'; -import ToggleButton from './ToggleButton'; import ToggleButtonGroup from './ToggleButtonGroup'; +import { ToggleButtonRowContext } from './ToggleButtonRowContext'; +import { useInternalTheme } from '../../core/theming'; +import type { ThemeProp } from '../../types'; export type Props = { /** @@ -19,8 +21,14 @@ export type Props = { */ children: React.ReactNode; style?: StyleProp; + /** + * @optional + */ + theme?: ThemeProp; }; +const SEGMENTED_ROW_CONTEXT = { segmented: true }; + /** * Toggle button row renders a group of toggle buttons in a row. * @@ -44,33 +52,33 @@ export type Props = { * *``` */ -const ToggleButtonRow = ({ value, onValueChange, children, style }: Props) => { - const count = React.Children.count(children); +const ToggleButtonRow = ({ + value, + onValueChange, + children, + style, + theme: themeOverrides, +}: Props) => { + const theme = useInternalTheme(themeOverrides); + const borderRadius = theme.shapes.corner.extraSmall; + const outlineColor = theme.colors.outline; return ( - - {React.Children.map(children, (child, i) => { - // @ts-expect-error: TypeScript complains about child.type but it doesn't matter - if (child && child.type === ToggleButton) { - // @ts-expect-error: We're sure that child is a React Element - return React.cloneElement(child, { - style: [ - styles.button, - i === 0 - ? styles.first - : i === count - 1 - ? styles.last - : styles.middle, - // @ts-expect-error: We're sure that child is a React Element - child.props.style, - ], - }); - } - - return child; - })} - + + + {children} + + ); }; @@ -80,25 +88,10 @@ ToggleButtonRow.displayName = 'ToggleButton.Row'; const styles = StyleSheet.create({ row: { flexDirection: 'row', - }, - button: { - borderWidth: StyleSheet.hairlineWidth, - }, - - first: { - borderTopRightRadius: 0, - borderBottomRightRadius: 0, - }, - - middle: { - borderRadius: 0, - borderLeftWidth: 0, - }, - - last: { - borderLeftWidth: 0, - borderTopLeftRadius: 0, - borderBottomLeftRadius: 0, + alignSelf: 'flex-start', + overflow: 'hidden', + padding: StyleSheet.hairlineWidth, + paddingLeft: 0, }, }); diff --git a/src/components/ToggleButton/ToggleButtonRowContext.tsx b/src/components/ToggleButton/ToggleButtonRowContext.tsx new file mode 100644 index 0000000000..ff37016102 --- /dev/null +++ b/src/components/ToggleButton/ToggleButtonRowContext.tsx @@ -0,0 +1,8 @@ +import * as React from 'react'; + +type ToggleButtonRowContextType = { + segmented: boolean; +}; + +export const ToggleButtonRowContext = + React.createContext(null); diff --git a/src/components/__tests__/Card/Card.test.tsx b/src/components/__tests__/Card/Card.test.tsx index d9e91d9d73..c1384d2cd0 100644 --- a/src/components/__tests__/Card/Card.test.tsx +++ b/src/components/__tests__/Card/Card.test.tsx @@ -1,3 +1,4 @@ +import type { ComponentProps } from 'react'; import { Animated, StyleSheet, Text } from 'react-native'; import { describe, expect, it, jest } from '@jest/globals'; @@ -91,6 +92,19 @@ describe('Card', () => { expect(screen.getByTestId('card')).toHaveStyle(styles.contentStyle); }); + it('clips inner content to the card shape', async () => { + await render( + + + + ); + + expect(screen.getByTestId('card')).toHaveStyle({ + borderRadius: getTheme().shapes.corner.medium, + overflow: 'hidden', + }); + }); + it('does not render a disabled accessibility state', async () => { await render({null}); @@ -127,18 +141,66 @@ 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', + }); }); it('renders button with custom styles', async () => { @@ -225,6 +287,25 @@ describe('getCardCoverStyle - border radius', () => { }); }); +describe('CardContent', () => { + it('renders uniform vertical padding regardless of neighboring sections', async () => { + await render( + + + + + Card content + + + ); + + expect(screen.getByTestId('card-content')).toHaveStyle({ + paddingTop: 16, + paddingBottom: 16, + }); + }); +}); + it('animated value changes correctly', async () => { const value = new Animated.Value(1); await render( diff --git a/src/components/__tests__/Card/__snapshots__/Card.test.tsx.snap b/src/components/__tests__/Card/__snapshots__/Card.test.tsx.snap index d1492cc475..46c1359b6d 100644 --- a/src/components/__tests__/Card/__snapshots__/Card.test.tsx.snap +++ b/src/components/__tests__/Card/__snapshots__/Card.test.tsx.snap @@ -62,6 +62,10 @@ exports[`Card renders an outlined card 1`] = ` [ { "flexShrink": 1, + "overflow": "hidden", + }, + { + "borderRadius": 12, }, undefined, ] diff --git a/src/components/__tests__/Dialog.test.tsx b/src/components/__tests__/Dialog.test.tsx index 32477e5fe1..6fd8b12615 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,71 @@ describe('Dialog', () => { expect(onDismiss).toHaveBeenCalledTimes(1); }); - it('should apply top margin to the first child if the dialog is V3', async () => { + it('should apply top spacing to the dialog surface for a title-first dialog', async () => { await render( - - + + Test Dialog Content ); + expect(screen.getByTestId('dialog-surface')).toHaveStyle({ + paddingTop: 24, + }); + expect(screen.getByTestId('dialog-title')).toHaveStyle({ + marginTop: 0, + }); + }); + + it('should apply top spacing to the dialog surface for a content-first dialog', async () => { + await render( + + + Test Dialog Content + + + ); + + expect(screen.getByTestId('dialog-surface')).toHaveStyle({ + paddingTop: 24, + }); expect(screen.getByTestId('dialog-content')).toHaveStyle({ - marginTop: 24, + paddingBottom: 24, + }); + }); + + it('should apply top spacing to the dialog surface for an icon-first dialog', async () => { + await render( + + + + ); + + expect(screen.getByTestId('dialog-surface')).toHaveStyle({ + paddingTop: 24, + }); + 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 +180,67 @@ 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, }); - expect(dialogActionButtons[0]).toHaveStyle({ marginRight: 8 }); - expect(dialogActionButtons[1]).toHaveStyle({ marginRight: 0 }); + expect(screen.getByTestId('button-cancel-container')).not.toHaveStyle({ + marginRight: 8, + }); + expect(screen.getByTestId('button-ok-container')).not.toHaveStyle({ + marginRight: 0, + }); + }); + + it('should not inject button props into actions', async () => { + const buttonProps = jest.fn(); + const ProbeButton = (props: ComponentProps) => { + buttonProps(props); + + return - + + ); - const dialogActionsContainer = screen.getByTestId('dialog-actions'); - const dialogActionButtons = dialogActionsContainer.children; - - expect(dialogActionButtons[0]).toHaveStyle({ margin: 10 }); - expect(dialogActionButtons[1]).toHaveStyle({ margin: 0 }); + expect(screen.getByTestId('button-cancel-container')).toHaveStyle({ + margin: 10, + }); + expect(screen.getByTestId('button-ok-container')).toHaveStyle({ + margin: 0, + }); }); }); diff --git a/src/components/__tests__/ListAccordion.test.tsx b/src/components/__tests__/ListAccordion.test.tsx index ad1783ec52..dddcb21de1 100644 --- a/src/components/__tests__/ListAccordion.test.tsx +++ b/src/components/__tests__/ListAccordion.test.tsx @@ -3,7 +3,7 @@ import { StyleSheet, View } from 'react-native'; import { describe, expect, it } from '@jest/globals'; import { getTheme } from '../../core/theming'; -import { render } from '../../test-utils'; +import { render, screen } from '../../test-utils'; import { red500 } from '../../theme/colors'; import ListAccordion from '../List/ListAccordion'; import ListAccordionGroup from '../List/ListAccordionGroup'; @@ -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__/ToggleButton.test.tsx b/src/components/__tests__/ToggleButton.test.tsx index 1ea9e20bae..0d8b6c1719 100644 --- a/src/components/__tests__/ToggleButton.test.tsx +++ b/src/components/__tests__/ToggleButton.test.tsx @@ -1,4 +1,4 @@ -import { Animated } from 'react-native'; +import { Animated, View } from 'react-native'; import { describe, expect, it, jest } from '@jest/globals'; import { act } from '@testing-library/react-native'; @@ -36,6 +36,52 @@ it('renders unchecked toggle button', async () => { expect(tree).toMatchSnapshot(); }); +it('renders row buttons with segmented styling through context', async () => { + await render( + {}}> + + + + + + ); + + expect(screen.getByTestId('wrapped-toggle-container')).toHaveStyle({ + borderRadius: 0, + }); + expect(screen.getByTestId('direct-toggle-container')).toHaveStyle({ + borderRadius: 0, + }); +}); + +it('applies the same selection color in a row as standalone (no row-specific override)', async () => { + await render( + {}}> + + + + ); + + expect(screen.getByTestId('selected-container')).toHaveStyle({ + backgroundColor: getTheme().colors.surfaceContainerHighest, + }); + expect(screen.getByTestId('unselected-container')).toHaveStyle({ + backgroundColor: getTheme().colors.surfaceContainer, + }); +}); + describe('getToggleButtonColor', () => { it('should return correct color when checked and theme version 3', () => { expect(getToggleButtonColor({ theme: getTheme(), checked: true })).toBe( diff --git a/src/components/__tests__/Tooltip.test.tsx b/src/components/__tests__/Tooltip.test.tsx index 75b4a18cf7..43904e5bd5 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));