diff --git a/docs/6.x/docs/guides/migration.md b/docs/6.x/docs/guides/migration.md index 35a8f74830..76205d0633 100644 --- a/docs/6.x/docs/guides/migration.md +++ b/docs/6.x/docs/guides/migration.md @@ -188,6 +188,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 forces `compact` and `uppercase` on its buttons. Set them on the buttons if you want the old look. + +```tsx +// Before (v5) + + + + +// After (v6) + + + +``` + ### TextInput The Paper 6.x `TextInput` is a complete rewrite with a new API. Import the component the same way, but note that the props and behavior have changed significantly. @@ -308,3 +326,59 @@ const theme = { style={{ fontSize: 16, color: '#1C1B1F' }} /> ``` + +### Card + +#### `Card.Actions` + +`Card.Actions` no longer styles its buttons for you. It used to force `mode="outlined"` on the first button, `mode="contained"` on the rest, and `compact` on all of them. Set what you need on each button. + +```tsx +// Before (v5) + + + + + +// After (v6) + + + + +``` + +#### `Card.Content` + +`Card.Content` now has 16dp of padding on every side. It used to drop its top or bottom padding when it sat next to a `Card.Cover` or `Card.Title`, so cards that mix those sections grow a little taller. Pass `style` if you want the tighter spacing back. + +### 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.Row` + +`ToggleButton.Row` no longer reaches into its children to style them, so a `ToggleButton` you wrap in a component of your own now picks up the segmented look. + +The row draws the dividers between buttons itself. A `borderWidth` or `borderColor` set on an individual `ToggleButton` no longer builds the segmented outline. diff --git a/example/src/Examples/CardExample.tsx b/example/src/Examples/CardExample.tsx index 016bef3e21..30af3c06fb 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..1ae50f5c55 100644 --- a/src/components/Card/Card.tsx +++ b/src/components/Card/Card.tsx @@ -119,8 +119,8 @@ export type Props = Omit & { * * * - * - * + * + * * * * ); @@ -182,15 +182,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, @@ -204,16 +195,8 @@ const Card = ({ const borderRadius = theme.shapes.corner.medium; const content = ( - - {React.Children.map(children, (child, index) => - React.isValidElement(child) - ? React.cloneElement(child as React.ReactElement, { - index, - total, - siblings, - }) - : child - )} + + {children} ); @@ -276,6 +259,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 d541c691bc..8b848a8b61 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 '../../theme/types'; @@ -26,8 +25,8 @@ export type Props = ViewProps & { * const MyComponent = () => ( * * - * - * + * + * * * * ); @@ -46,23 +45,7 @@ const CardActions = ({ theme, style, children, ...rest }: Props) => { 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,11 +56,9 @@ const styles = StyleSheet.create({ container: { flexDirection: 'row', alignItems: 'center', + columnGap: 8, padding: 8, }, - button: { - marginLeft: 8, - }, }); export default CardActions; diff --git a/src/components/Card/CardContent.tsx b/src/components/Card/CardContent.tsx index bde25ddb25..e0bbb172cb 100644 --- a/src/components/Card/CardContent.tsx +++ b/src/components/Card/CardContent.tsx @@ -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,58 +30,15 @@ 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, + padding: 16, }, }); diff --git a/src/components/Card/CardCover.tsx b/src/components/Card/CardCover.tsx index 4542aa7c99..4ffef1fe6c 100644 --- a/src/components/Card/CardCover.tsx +++ b/src/components/Card/CardCover.tsx @@ -8,14 +8,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,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 15477d3033..2855c6428c 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 fc0faa945a..75cbc29b99 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 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 97c930c52e..73e8a20564 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 = ({ { const theme = useInternalTheme(themeOverrides); const borderRadius = theme.shapes.corner.extraSmall; + const rowContext = React.useContext(ToggleButtonRowContext); + const isSegmentedRow = !!rowContext?.segmented; return ( @@ -136,6 +139,7 @@ const ToggleButton = ({ borderRadius, borderColor, }, + isSegmentedRow && styles.segmentedContent, style, ]} ref={ref} @@ -154,6 +158,9 @@ const styles = StyleSheet.create({ height: 42, margin: 0, }, + segmentedContent: { + borderRadius: 0, + }, }); export default ToggleButton; diff --git a/src/components/ToggleButton/ToggleButtonRow.tsx b/src/components/ToggleButton/ToggleButtonRow.tsx index 46e6aee48f..789cde3b62 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 '../../theme/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,34 @@ 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} + + + ); }; @@ -78,27 +87,12 @@ const ToggleButtonRow = ({ value, onValueChange, children, style }: Props) => { ToggleButtonRow.displayName = 'ToggleButton.Row'; const styles = StyleSheet.create({ - row: { + track: { 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, + gap: StyleSheet.hairlineWidth, }, }); 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 7d84f0e44f..6aa3bf916d 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 { Platform, StyleSheet, Text } from 'react-native'; import { afterEach, describe, expect, it, jest } from '@jest/globals'; @@ -88,6 +89,19 @@ describe('Card', () => { 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,19 @@ describe('getCardCoverStyle - border radius', () => { ).toMatchObject({ borderRadius: LightTheme.shapes.corner.medium }); }); }); + +describe('CardContent', () => { + it('keeps its padding when it follows a cover and a title', async () => { + await render( + + + + + Card content + + + ); + + expect(screen.getByTestId('card-content')).toHaveStyle({ padding: 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..44a7d08c4a 100644 --- a/src/components/__tests__/Card/__snapshots__/Card.test.tsx.snap +++ b/src/components/__tests__/Card/__snapshots__/Card.test.tsx.snap @@ -114,6 +114,10 @@ exports[`Card renders an outlined card 1`] = ` [ { "flexShrink": 1, + "overflow": "hidden", + }, + { + "borderRadius": 12, }, undefined, ] @@ -237,6 +241,10 @@ exports[`Card renders an outlined card with a custom outline color 1`] = ` [ { "flexShrink": 1, + "overflow": "hidden", + }, + { + "borderRadius": 12, }, undefined, ] @@ -362,6 +370,10 @@ exports[`Card renders an outlined card with custom border color 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 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__/ToggleButton.test.tsx b/src/components/__tests__/ToggleButton.test.tsx index 006ae179c5..18cfe4231a 100644 --- a/src/components/__tests__/ToggleButton.test.tsx +++ b/src/components/__tests__/ToggleButton.test.tsx @@ -1,6 +1,8 @@ +import { View } from 'react-native'; + import { describe, expect, it } from '@jest/globals'; -import { render } from '../../test-utils'; +import { render, screen } from '../../test-utils'; import { DarkTheme, LightTheme } from '../../theme/schemes'; import ToggleButton from '../ToggleButton'; import { getToggleButtonColor } from '../ToggleButton/utils'; @@ -33,6 +35,69 @@ 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').parent).toHaveStyle({ + borderRadius: 0, + }); + expect(screen.getByTestId('direct-toggle').parent).toHaveStyle({ + borderRadius: 0, + }); +}); + +it('keeps custom row spacing outside the segmented track', async () => { + const tree = ( + await render( + {}} + style={{ paddingHorizontal: 16 }} + > + + + + ) + ).toJSON(); + + expect(tree).toMatchSnapshot(); +}); + +it('applies the same selection color in a row as standalone (no row-specific override)', async () => { + await render( + {}}> + + + + ); + + expect(screen.getByTestId('selected').parent).toHaveStyle({ + backgroundColor: LightTheme.colors.surfaceContainerHighest, + }); + expect(screen.getByTestId('unselected').parent).toHaveStyle({ + backgroundColor: LightTheme.colors.surfaceContainer, + }); +}); + describe('getToggleButtonColor', () => { it('should return correct color when checked and theme version 3', () => { expect(getToggleButtonColor({ theme: LightTheme, checked: true })).toBe( 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)); diff --git a/src/components/__tests__/__snapshots__/ToggleButton.test.tsx.snap b/src/components/__tests__/__snapshots__/ToggleButton.test.tsx.snap index 62fedc3a85..b4b886878e 100644 --- a/src/components/__tests__/__snapshots__/ToggleButton.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/ToggleButton.test.tsx.snap @@ -1,5 +1,290 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`keeps custom row spacing outside the segmented track 1`] = ` + + + + + + + format-align-left + + + + + + + + + format-align-right + + + + + + +`; + exports[`renders disabled toggle button 1`] = `