diff --git a/example/src/Examples/ListItemExample.tsx b/example/src/Examples/ListItemExample.tsx
index 4d5a69b400..1cf1252071 100644
--- a/example/src/Examples/ListItemExample.tsx
+++ b/example/src/Examples/ListItemExample.tsx
@@ -1,15 +1,7 @@
-import { View, StyleSheet } from 'react-native';
-
import { List, Divider, Checkbox, Avatar, Switch } from 'react-native-paper';
import ScreenWrapper from '../ScreenWrapper';
-const CenteredCheckbox = () => (
-
-
-
-);
-
const ListItemExample = () => {
return (
@@ -21,16 +13,19 @@ const ListItemExample = () => {
description="Supporting text that is long enough to fill up multiple lines in the item"
/>
- } />
+ }
+ />
}
+ right={(props) => }
/>
}
+ right={(props) => }
/>
@@ -54,19 +49,19 @@ const ListItemExample = () => {
}
- right={() => }
+ right={(props) => }
/>
}
- right={() => }
+ right={(props) => }
/>
}
- right={() => }
+ right={(props) => }
/>
@@ -98,7 +93,7 @@ const ListItemExample = () => {
left={(props) => (
)}
- right={() => }
+ right={(props) => }
/>
{
left={(props) => (
)}
- right={() => }
+ right={(props) => }
/>
{
left={(props) => (
)}
- right={() => }
+ right={(props) => }
/>
@@ -158,7 +153,7 @@ const ListItemExample = () => {
source={require('../../../example/assets/images/strawberries.jpg')}
/>
)}
- right={() => }
+ right={(props) => }
/>
{
source={require('../../../example/assets/images/strawberries.jpg')}
/>
)}
- right={() => }
+ right={(props) => }
/>
{
source={require('../../../example/assets/images/strawberries.jpg')}
/>
)}
- right={() => }
+ right={(props) => }
/>
@@ -228,7 +223,7 @@ const ListItemExample = () => {
source={require('../../../example/assets/images/strawberries.jpg')}
/>
)}
- right={() => }
+ right={(props) => }
/>
{
source={require('../../../example/assets/images/strawberries.jpg')}
/>
)}
- right={() => }
+ right={(props) => }
/>
{
source={require('../../../example/assets/images/strawberries.jpg')}
/>
)}
- right={() => }
+ right={(props) => }
/>
@@ -260,35 +255,35 @@ const ListItemExample = () => {
}
+ right={(props) => }
/>
}
+ right={(props) => }
/>
}
+ right={(props) => }
/>
}
- right={() => }
+ right={(props) => }
/>
}
- right={() => }
+ right={(props) => }
/>
}
- right={() => }
+ right={(props) => }
/>
@@ -296,12 +291,6 @@ const ListItemExample = () => {
);
};
-const styles = StyleSheet.create({
- centered: {
- alignSelf: 'center',
- },
-});
-
ListItemExample.title = 'List.Item';
export default ListItemExample;
diff --git a/src/components/List/ListAccordion.tsx b/src/components/List/ListAccordion.tsx
index 97730d58e6..f5a4dc7e51 100644
--- a/src/components/List/ListAccordion.tsx
+++ b/src/components/List/ListAccordion.tsx
@@ -3,18 +3,18 @@ import { StyleSheet, View } from 'react-native';
import type {
ColorValue,
GestureResponderEvent,
- NativeSyntheticEvent,
PressableAndroidRippleConfig,
StyleProp,
- TextLayoutEventData,
TextStyle,
ViewProps,
ViewStyle,
} from 'react-native';
import { ListAccordionGroupContext } from './ListAccordionGroup';
+import { ListTokens } from './tokens';
+import { useMultilineDescription } from './useMultilineDescription';
import type { ListChildProps, Style } from './utils';
-import { getAccordionColors, getLeftStyles } from './utils';
+import { ListRowContext, getAccordionColors, getLeftStyles } from './utils';
import { useLocale } from '../../core/locale';
import { useInternalTheme } from '../../core/theming';
import type { ThemeProp } from '../../types';
@@ -201,14 +201,11 @@ const ListAccordion = ({
const [expanded, setExpanded] = React.useState(
expandedProp || false
);
- const [alignToTop, setAlignToTop] = React.useState(false);
-
- const onDescriptionTextLayout = (
- event: NativeSyntheticEvent
- ) => {
- const { nativeEvent } = event;
- setAlignToTop(nativeEvent.lines.length >= 2);
- };
+ const {
+ isMultiline: isDescriptionMultiline,
+ contentRef,
+ descriptionProps,
+ } = useMultilineDescription(Boolean(description));
const handlePressAction = (e: GestureResponderEvent) => {
onPress?.(e);
@@ -232,96 +229,112 @@ const ListAccordion = ({
? groupContext.expandedId === id
: expandedInternal;
- const { descriptionColor, titleTextColor } = getAccordionColors({
- theme,
- isExpanded,
- });
+ const { descriptionColor, titleTextColor } = getAccordionColors({ theme });
const handlePress =
groupContext && id !== undefined
? () => groupContext.onAccordionPress(id)
: handlePressAction;
+
+ const rowContext = React.useMemo(
+ () => ({
+ verticalPadding: isDescriptionMultiline
+ ? ListTokens.threeLineVerticalPadding
+ : ListTokens.verticalPadding,
+ }),
+ [isDescriptionMultiline]
+ );
+
return (
-
-
-
+
+
- {left
- ? left({
- color: isExpanded ? theme.colors?.primary : descriptionColor,
- style: getLeftStyles(alignToTop, description),
- })
- : null}
-
-
+ {left
+ ? left({
+ color: theme.colors[ListTokens.leadingIconColor],
+ style: getLeftStyles(isDescriptionMultiline, description),
+ })
+ : null}
+
- {title}
-
- {description ? (
- {description}
+ {title}
- ) : null}
-
-
- {right ? (
- right({
- isExpanded: isExpanded,
- })
- ) : (
-
- )}
+ {description ? (
+
+ {description}
+
+ ) : null}
+
+
+ {right ? (
+ right({
+ isExpanded: isExpanded,
+ })
+ ) : (
+
+ )}
+
-
-
+
+
{isExpanded
@@ -349,30 +362,28 @@ ListAccordion.displayName = 'List.Accordion';
const styles = StyleSheet.create({
container: {
- paddingVertical: 8,
- paddingRight: 24,
+ paddingVertical: ListTokens.verticalPadding,
+ paddingRight: ListTokens.trailingSpace,
+ justifyContent: 'center',
},
- row: {
- flexDirection: 'row',
- marginVertical: 6,
+ containerOneLine: {
+ minHeight: ListTokens.oneLineContainerHeight,
},
- multiline: {
- height: 40,
- alignItems: 'center',
- justifyContent: 'center',
+ containerTwoLine: {
+ minHeight: ListTokens.twoLineContainerHeight,
},
- title: {
- fontSize: 16,
+ containerThreeLine: {
+ paddingVertical: ListTokens.threeLineVerticalPadding,
},
- description: {
- fontSize: 14,
+ row: {
+ flexDirection: 'row',
},
contentItem: {
- paddingLeft: 16,
+ paddingLeft: ListTokens.leadingSpace,
},
trailingItem: {
- marginVertical: 6,
- paddingLeft: 8,
+ alignSelf: 'center',
+ paddingLeft: ListTokens.leadingSpace,
},
child: {
paddingLeft: 40,
diff --git a/src/components/List/ListImage.tsx b/src/components/List/ListImage.tsx
index 907c1b63b2..6c2650720d 100644
--- a/src/components/List/ListImage.tsx
+++ b/src/components/List/ListImage.tsx
@@ -1,6 +1,9 @@
+import * as React from 'react';
import { StyleSheet, Image } from 'react-native';
import type { StyleProp, ImageSourcePropType, ImageStyle } from 'react-native';
+import { ListTokens } from './tokens';
+import { ListRowContext } from './utils';
import type { ThemeProp } from '../../types';
export type Props = {
@@ -37,9 +40,17 @@ const ListImage = ({
variant = 'image',
theme: _theme,
}: Props) => {
+ const { verticalPadding } = React.useContext(ListRowContext);
+
const getStyles = () => {
if (variant === 'video') {
- return [style, styles.video];
+ return [
+ style,
+ styles.video,
+ {
+ marginVertical: ListTokens.threeLineVerticalPadding - verticalPadding,
+ },
+ ];
}
return [style, styles.image];
diff --git a/src/components/List/ListItem.tsx b/src/components/List/ListItem.tsx
index a6f0181f02..3b8f5c3ec3 100644
--- a/src/components/List/ListItem.tsx
+++ b/src/components/List/ListItem.tsx
@@ -3,14 +3,14 @@ import { StyleSheet, View } from 'react-native';
import type {
ColorValue,
GestureResponderEvent,
- NativeSyntheticEvent,
StyleProp,
- TextLayoutEventData,
TextStyle,
ViewStyle,
} from 'react-native';
-import { getLeftStyles, getRightStyles } from './utils';
+import { ListTokens } from './tokens';
+import { useMultilineDescription } from './useMultilineDescription';
+import { ListRowContext, getLeftStyles, getRightStyles } from './utils';
import type { Style } from './utils';
import { useInternalTheme } from '../../core/theming';
import type { $RemoveChildren, EllipsizeProp, ThemeProp } from '../../types';
@@ -161,14 +161,11 @@ const ListItem = ({
...rest
}: Props) => {
const theme = useInternalTheme(themeOverrides);
- const [alignToTop, setAlignToTop] = React.useState(false);
-
- const onDescriptionTextLayout = (
- event: NativeSyntheticEvent
- ) => {
- const { nativeEvent } = event;
- setAlignToTop(nativeEvent.lines.length >= 2);
- };
+ const {
+ isMultiline: isDescriptionMultiline,
+ contentRef,
+ descriptionProps,
+ } = useMultilineDescription(Boolean(description));
const renderDescription = (
descriptionColor: ColorValue,
@@ -179,19 +176,17 @@ const ListItem = ({
selectable: false,
ellipsizeMode: descriptionEllipsizeMode,
color: descriptionColor,
- fontSize: styles.description.fontSize,
+ fontSize: theme.fonts.bodyMedium.fontSize,
})
) : (
{description}
@@ -200,21 +195,23 @@ const ListItem = ({
};
const renderTitle = () => {
- const titleColor = theme.colors.onSurface;
+ const titleColor = theme.colors[ListTokens.headlineColor];
return typeof title === 'function' ? (
title({
selectable: false,
ellipsizeMode: titleEllipsizeMode,
color: titleColor,
- fontSize: styles.title.fontSize,
+ fontSize: theme.fonts.bodyLarge.fontSize,
})
) : (
{title}
@@ -222,42 +219,59 @@ const ListItem = ({
);
};
- const descriptionColor = theme.colors.onSurfaceVariant;
+ const descriptionColor = theme.colors[ListTokens.supportingTextColor];
+
+ const rowContext = React.useMemo(
+ () => ({
+ verticalPadding: isDescriptionMultiline
+ ? ListTokens.threeLineVerticalPadding
+ : ListTokens.verticalPadding,
+ }),
+ [isDescriptionMultiline]
+ );
return (
-
-
- {left
- ? left({
- color: descriptionColor,
- style: getLeftStyles(alignToTop, description),
- })
- : null}
-
- {renderTitle()}
+
+
+
+ {left
+ ? left({
+ color: theme.colors[ListTokens.leadingIconColor],
+ style: getLeftStyles(isDescriptionMultiline, description),
+ })
+ : null}
+
+ {renderTitle()}
- {description
- ? renderDescription(descriptionColor, description)
+ {description
+ ? renderDescription(descriptionColor, description)
+ : null}
+
+ {right
+ ? right({
+ color: theme.colors[ListTokens.trailingIconColor],
+ style: getRightStyles(isDescriptionMultiline, description),
+ })
: null}
- {right
- ? right({
- color: descriptionColor,
- style: getRightStyles(alignToTop, description),
- })
- : null}
-
-
+
+
);
};
@@ -265,22 +279,25 @@ ListItem.displayName = 'List.Item';
const styles = StyleSheet.create({
container: {
- paddingVertical: 8,
- paddingRight: 24,
+ paddingVertical: ListTokens.verticalPadding,
+ paddingRight: ListTokens.trailingSpace,
+ justifyContent: 'center',
+ },
+ containerOneLine: {
+ minHeight: ListTokens.oneLineContainerHeight,
+ },
+ containerTwoLine: {
+ minHeight: ListTokens.twoLineContainerHeight,
+ },
+ containerThreeLine: {
+ paddingVertical: ListTokens.threeLineVerticalPadding,
},
row: {
width: '100%',
flexDirection: 'row',
- marginVertical: 6,
- },
- title: {
- fontSize: 16,
- },
- description: {
- fontSize: 14,
},
item: {
- paddingLeft: 16,
+ paddingLeft: ListTokens.leadingSpace,
},
content: {
flexShrink: 1,
diff --git a/src/components/List/tokens.ts b/src/components/List/tokens.ts
new file mode 100644
index 0000000000..5df93882ed
--- /dev/null
+++ b/src/components/List/tokens.ts
@@ -0,0 +1,21 @@
+import type { ColorRole } from '../../theme/types';
+
+const sizes = {
+ verticalPadding: 8,
+ threeLineVerticalPadding: 12,
+ oneLineContainerHeight: 56,
+ twoLineContainerHeight: 72,
+ leadingSpace: 16,
+ trailingSpace: 16,
+} as const;
+
+const colors = {
+ containerColor: 'surface',
+ headlineColor: 'onSurface',
+ supportingTextColor: 'onSurfaceVariant',
+ leadingIconColor: 'onSurfaceVariant',
+ trailingIconColor: 'onSurfaceVariant',
+ expandTrailingIconColor: 'onSurface',
+} as const satisfies Record;
+
+export const ListTokens = { ...sizes, ...colors };
diff --git a/src/components/List/useMultilineDescription.native.tsx b/src/components/List/useMultilineDescription.native.tsx
new file mode 100644
index 0000000000..85d40d5856
--- /dev/null
+++ b/src/components/List/useMultilineDescription.native.tsx
@@ -0,0 +1,33 @@
+import * as React from 'react';
+import type {
+ NativeSyntheticEvent,
+ TextLayoutEventData,
+ TextProps,
+ View,
+} from 'react-native';
+
+type MultilineDescription = {
+ isMultiline: boolean;
+ contentRef: React.RefObject;
+ descriptionProps: Pick;
+};
+
+export const useMultilineDescription = (
+ hasDescription: boolean
+): MultilineDescription => {
+ const contentRef = React.useRef(null);
+ const [isMultiline, setIsMultiline] = React.useState(false);
+
+ const onTextLayout = React.useCallback(
+ (event: NativeSyntheticEvent) => {
+ setIsMultiline(event.nativeEvent.lines.length >= 2);
+ },
+ []
+ );
+
+ return {
+ isMultiline: hasDescription && isMultiline,
+ contentRef,
+ descriptionProps: { onTextLayout },
+ };
+};
diff --git a/src/components/List/useMultilineDescription.tsx b/src/components/List/useMultilineDescription.tsx
new file mode 100644
index 0000000000..4280b15bac
--- /dev/null
+++ b/src/components/List/useMultilineDescription.tsx
@@ -0,0 +1,65 @@
+import * as React from 'react';
+import type { TextProps, View } from 'react-native';
+
+type MultilineDescription = {
+ isMultiline: boolean;
+ contentRef: React.RefObject;
+ descriptionProps: Pick;
+};
+
+const LINE_TOLERANCE_PX = 1;
+
+/**
+ * Web has no `onTextLayout`, so the description is measured from the DOM.
+ * `useLayoutEffect` runs before paint, so the row is laid out at its final
+ * height in the frame it first appears. `ResizeObserver` re-measures on
+ * reflow, for example when a web font swaps in or the column is resized.
+ */
+export const useMultilineDescription = (
+ hasDescription: boolean
+): MultilineDescription => {
+ const contentRef = React.useRef(null);
+ const [isMultiline, setIsMultiline] = React.useState(false);
+
+ React.useLayoutEffect(() => {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
+ const content = contentRef.current as unknown as HTMLElement | null;
+
+ if (!hasDescription || !content) {
+ setIsMultiline(false);
+ return undefined;
+ }
+
+ // The description is rendered after the title, as the last node of the column.
+ const description = content.lastElementChild;
+
+ if (!description) {
+ return undefined;
+ }
+
+ const measure = () => {
+ const { height } = description.getBoundingClientRect();
+
+ if (height === 0) {
+ return;
+ }
+
+ const computed = window.getComputedStyle(description);
+ const lineHeight = Number.parseFloat(computed.lineHeight);
+ const resolvedLineHeight = Number.isNaN(lineHeight)
+ ? Number.parseFloat(computed.fontSize) * 1.2 // `line-height: normal` fallback
+ : lineHeight;
+
+ setIsMultiline(height > resolvedLineHeight + LINE_TOLERANCE_PX);
+ };
+
+ measure();
+
+ const observer = new ResizeObserver(measure);
+ observer.observe(description);
+
+ return () => observer.disconnect();
+ }, [hasDescription]);
+
+ return { isMultiline, contentRef, descriptionProps: {} };
+};
diff --git a/src/components/List/utils.ts b/src/components/List/utils.ts
index 7b7a868de0..d6c0c91caa 100644
--- a/src/components/List/utils.ts
+++ b/src/components/List/utils.ts
@@ -1,6 +1,7 @@
-import { StyleSheet } from 'react-native';
+import * as React from 'react';
import type { StyleProp, ViewStyle } from 'react-native';
+import { ListTokens } from './tokens';
import type { EllipsizeProp, InternalTheme, ThemeProp } from '../../types';
type Description =
@@ -26,82 +27,35 @@ export type Style = {
alignSelf?: 'flex-start' | 'center';
};
-const stylesV3Left = {
- marginRight: 0,
- marginLeft: 16,
-};
-
-const stylesV3Right = {
- marginLeft: 16,
-};
-
-export const getLeftStyles = (
+const getAccessoryStyles = (
alignToTop: boolean,
description: Description
-) => {
- const stylesV3: Style = {
- ...stylesV3Left,
+): Style => {
+ const style: Style = {
+ marginLeft: ListTokens.leadingSpace,
+ marginRight: 0,
alignSelf: alignToTop ? 'flex-start' : 'center',
};
- if (!description) {
- return {
- ...styles.iconMarginLeft,
- ...styles.marginVerticalNone,
- ...stylesV3,
- };
- }
-
- return {
- ...styles.iconMarginLeft,
- ...stylesV3,
- };
+ return description ? style : { ...style, marginVertical: 0 };
};
-export const getRightStyles = (
- alignToTop: boolean,
- description: Description
-) => {
- const stylesV3: Style = {
- ...stylesV3Right,
- alignSelf: alignToTop ? 'flex-start' : 'center',
- };
+export const getLeftStyles = (alignToTop: boolean, description: Description) =>
+ getAccessoryStyles(alignToTop, description);
- if (!description) {
- return {
- ...styles.iconMarginRight,
- ...styles.marginVerticalNone,
- ...stylesV3,
- };
- }
+export const getRightStyles = (alignToTop: boolean, description: Description) =>
+ getAccessoryStyles(alignToTop, description);
- return {
- ...styles.iconMarginRight,
- ...stylesV3,
- };
-};
-
-const styles = StyleSheet.create({
- marginVerticalNone: { marginVertical: 0 },
- iconMarginLeft: { marginLeft: 0, marginRight: 16 },
- iconMarginRight: { marginRight: 0 },
+/**
+ * Rows change their vertical padding with the number of lines, so a leading
+ * element taller than the default slot needs to know how much of the MD3
+ * height the row already covers.
+ */
+export const ListRowContext = React.createContext<{ verticalPadding: number }>({
+ verticalPadding: ListTokens.verticalPadding,
});
-export const getAccordionColors = ({
- theme,
- isExpanded,
-}: {
- theme: InternalTheme;
- isExpanded?: boolean;
-}) => {
- const titleColor = theme.colors.onSurface;
-
- const descriptionColor = theme.colors.onSurfaceVariant;
-
- const titleTextColor = isExpanded ? theme.colors?.primary : titleColor;
-
- return {
- descriptionColor,
- titleTextColor,
- };
-};
+export const getAccordionColors = ({ theme }: { theme: InternalTheme }) => ({
+ titleTextColor: theme.colors[ListTokens.headlineColor],
+ descriptionColor: theme.colors[ListTokens.supportingTextColor],
+});
diff --git a/src/components/__tests__/ListAccordion.test.tsx b/src/components/__tests__/ListAccordion.test.tsx
index ad1783ec52..db0cdd74f2 100644
--- a/src/components/__tests__/ListAccordion.test.tsx
+++ b/src/components/__tests__/ListAccordion.test.tsx
@@ -3,13 +3,12 @@ import { StyleSheet, View } from 'react-native';
import { describe, expect, it } from '@jest/globals';
import { getTheme } from '../../core/theming';
-import { render } from '../../test-utils';
+import { fireEvent, render, screen } from '../../test-utils';
import { red500 } from '../../theme/colors';
import ListAccordion from '../List/ListAccordion';
import ListAccordionGroup from '../List/ListAccordionGroup';
import ListIcon from '../List/ListIcon';
import ListItem from '../List/ListItem';
-import { getAccordionColors } from '../List/utils';
const styles = StyleSheet.create({
coloring: {
@@ -120,39 +119,102 @@ describe('ListAccordion', () => {
'List.Accordion is used inside a List.AccordionGroup without specifying an id prop.'
);
});
-});
-describe('getAccordionColors - description color', () => {
- it('should return theme color, for theme version 3', () => {
- expect(
- getAccordionColors({
- theme: getTheme(),
- })
- ).toMatchObject({
- descriptionColor: getTheme().colors.onSurfaceVariant,
+ it('keeps the title on onSurface when collapsed', async () => {
+ await render(
+
+
+
+ );
+
+ expect(screen.getByText('Accordion item 1')).toHaveStyle({
+ color: getTheme().colors.onSurface,
});
});
-});
-describe('getAccordionColors - title text color', () => {
- it('should return theme color, for theme version 3', () => {
- expect(
- getAccordionColors({
- theme: getTheme(),
- })
- ).toMatchObject({
- titleTextColor: getTheme().colors.onSurface,
+ it('keeps the title on onSurface when expanded', async () => {
+ await render(
+
+
+
+ );
+
+ expect(screen.getByText('Accordion item 1')).toHaveStyle({
+ color: getTheme().colors.onSurface,
+ });
+ });
+
+ it('hits the container heights without measuring the description', async () => {
+ await render(
+
+
+
+ );
+
+ expect(screen.getByTestId('list-accordion')).toHaveStyle({
+ minHeight: 56,
+ paddingVertical: 8,
+ });
+ });
+
+ it('keeps the two line container height once a description is present', async () => {
+ await render(
+
+
+
+ );
+
+ expect(screen.getByTestId('list-accordion')).toHaveStyle({
+ minHeight: 72,
+ paddingVertical: 8,
+ });
+
+ await fireEvent(
+ screen.getByText('Describes the expandable list item'),
+ 'textLayout',
+ { nativeEvent: { lines: [{}, {}] } }
+ );
+
+ expect(screen.getByTestId('list-accordion')).toHaveStyle({
+ minHeight: 72,
+ paddingVertical: 12,
});
});
- it('should return primary color if it is expanded', () => {
+ it('uses the expand token for the chevron', async () => {
+ await render(
+
+
+
+ );
+
expect(
- getAccordionColors({
- theme: getTheme(),
- isExpanded: true,
- })
- ).toMatchObject({
- titleTextColor: getTheme().colors?.primary,
+ screen.getByText('chevron-down', { includeHiddenElements: true })
+ ).toHaveStyle({
+ color: getTheme().colors.onSurface,
+ });
+ });
+
+ it('applies the theme override to title and description typography', async () => {
+ await render(
+
+
+
+ );
+
+ expect(screen.getByText('Accordion item 1')).toHaveStyle({ fontSize: 99 });
+ expect(screen.getByText('Describes the expandable list item')).toHaveStyle({
+ fontSize: 77,
});
});
});
diff --git a/src/components/__tests__/ListImage.test.tsx b/src/components/__tests__/ListImage.test.tsx
index 8aa7e9b54d..3001324a99 100644
--- a/src/components/__tests__/ListImage.test.tsx
+++ b/src/components/__tests__/ListImage.test.tsx
@@ -14,6 +14,7 @@ const styles = StyleSheet.create({
width: 114,
height: 64,
marginLeft: 0,
+ marginVertical: 4,
},
container: {
width: 30,
diff --git a/src/components/__tests__/ListItem.test.tsx b/src/components/__tests__/ListItem.test.tsx
index b50f4e7d3f..3702df27cd 100644
--- a/src/components/__tests__/ListItem.test.tsx
+++ b/src/components/__tests__/ListItem.test.tsx
@@ -5,11 +5,12 @@ import { Text, View } from 'react-native';
import { expect, it, jest } from '@jest/globals';
import { userEvent } from '@testing-library/react-native';
-import { render, screen } from '../../test-utils';
+import { fireEvent, render, screen } from '../../test-utils';
import { red500 } from '../../theme/colors';
import Chip from '../Chip/Chip';
import IconButton from '../IconButton/IconButton';
import ListIcon from '../List/ListIcon';
+import ListImage from '../List/ListImage';
import ListItem from '../List/ListItem';
const styles = StyleSheet.create({
@@ -22,6 +23,14 @@ const styles = StyleSheet.create({
content: {
paddingLeft: 0,
},
+ avatar: {
+ width: 40,
+ height: 40,
+ },
+ image: {
+ width: 56,
+ height: 56,
+ },
});
const testID = 'list-item';
@@ -175,3 +184,164 @@ it('renders list item with custom content style', async () => {
expect(screen.getByTestId('list-item-content')).toHaveStyle(styles.content);
});
+
+it('hits the one line container height without measuring the description', async () => {
+ await render();
+
+ expect(screen.getByTestId(testID)).toHaveStyle({
+ minHeight: 56,
+ paddingVertical: 8,
+ });
+});
+
+it('hits the two and three line container heights without measuring', async () => {
+ await render(
+
+ );
+
+ expect(screen.getByTestId(testID)).toHaveStyle({
+ minHeight: 72,
+ paddingVertical: 8,
+ });
+
+ await fireEvent(screen.getByText('Item description'), 'textLayout', {
+ nativeEvent: { lines: [{}, {}] },
+ });
+
+ expect(screen.getByTestId(testID)).toHaveStyle({
+ minHeight: 72,
+ paddingVertical: 12,
+ });
+});
+
+it('leaves a 40dp leading element on the one line container height', async () => {
+ await render(
+ (
+
+ )}
+ testID={testID}
+ />
+ );
+
+ expect(screen.getByTestId(testID)).toHaveStyle({
+ minHeight: 56,
+ paddingVertical: 8,
+ });
+ expect(screen.getByTestId('left-accessory')).toHaveStyle({ height: 40 });
+});
+
+it('leaves a 56dp leading image on the two line container height', async () => {
+ await render(
+ (
+
+ )}
+ testID={testID}
+ />
+ );
+
+ expect(screen.getByTestId(testID)).toHaveStyle({
+ minHeight: 72,
+ paddingVertical: 8,
+ });
+ expect(screen.getByTestId('left-accessory')).toHaveStyle({ height: 56 });
+});
+
+it('pads a 64dp leading video to the three line container height', async () => {
+ await render(
+ (
+
+ )}
+ testID={testID}
+ />
+ );
+
+ expect(screen.getByTestId(testID)).toHaveStyle({
+ minHeight: 56,
+ paddingVertical: 8,
+ });
+ expect(screen.getByTestId('list-image')).toHaveStyle({
+ height: 64,
+ marginVertical: 4,
+ });
+});
+
+it('keeps a 64dp leading video on the same height once the description wraps', async () => {
+ await render(
+ (
+
+ )}
+ testID={testID}
+ />
+ );
+
+ await fireEvent(screen.getByText('Item description'), 'textLayout', {
+ nativeEvent: { lines: [{}, {}] },
+ });
+
+ expect(screen.getByTestId(testID)).toHaveStyle({ paddingVertical: 12 });
+ expect(screen.getByTestId('list-image')).toHaveStyle({
+ height: 64,
+ marginVertical: 0,
+ });
+});
+
+it('top aligns the accessories once the description wraps', async () => {
+ await render(
+ }
+ testID={testID}
+ />
+ );
+
+ expect(screen.getByTestId('left-accessory')).toHaveStyle({
+ alignSelf: 'center',
+ });
+
+ await fireEvent(screen.getByText('Item description'), 'textLayout', {
+ nativeEvent: { lines: [{}, {}] },
+ });
+
+ expect(screen.getByTestId('left-accessory')).toHaveStyle({
+ alignSelf: 'flex-start',
+ });
+});
+
+it('applies the theme override to title and description typography', async () => {
+ await render(
+
+ );
+
+ expect(screen.getByText('First Item')).toHaveStyle({ fontSize: 99 });
+ expect(screen.getByText('Item description')).toHaveStyle({ fontSize: 77 });
+});
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));
diff --git a/src/components/__tests__/__snapshots__/ListAccordion.test.tsx.snap b/src/components/__tests__/__snapshots__/ListAccordion.test.tsx.snap
index 66fa6d11e8..b54b4a1aa6 100644
--- a/src/components/__tests__/__snapshots__/ListAccordion.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/ListAccordion.test.tsx.snap
@@ -47,9 +47,14 @@ exports[`renders expanded accordion 1`] = `
},
[
{
- "paddingRight": 24,
+ "justifyContent": "center",
+ "paddingRight": 16,
"paddingVertical": 8,
},
+ {
+ "minHeight": 56,
+ },
+ false,
undefined,
],
]
@@ -61,7 +66,6 @@ exports[`renders expanded accordion 1`] = `
[
{
"flexDirection": "row",
- "marginVertical": 6,
},
undefined,
]
@@ -91,21 +95,22 @@ exports[`renders expanded accordion 1`] = `
},
{
"color": "rgba(29, 27, 32, 1)",
- "fontFamily": "System",
- "fontWeight": "400",
- "letterSpacing": 0,
- },
- {
"writingDirection": "ltr",
},
[
{
+ "fontFamily": "System",
"fontSize": 16,
+ "fontWeight": "400",
+ "letterSpacing": 0.5,
+ "lineHeight": 24,
},
- {
- "color": "rgba(103, 80, 164, 1)",
- },
- undefined,
+ [
+ {
+ "color": "rgba(29, 27, 32, 1)",
+ },
+ undefined,
+ ],
],
]
}
@@ -115,13 +120,10 @@ exports[`renders expanded accordion 1`] = `