diff --git a/docs/6.x/docs/components/Chip/Chip.mdx b/docs/6.x/docs/components/Chip/Chip.mdx
index f712768036..ffd3e27f30 100644
--- a/docs/6.x/docs/components/Chip/Chip.mdx
+++ b/docs/6.x/docs/components/Chip/Chip.mdx
@@ -100,14 +100,6 @@ export default MyComponent;
-### showSelectedOverlay Available in v5.x with theme version 3
-
-
-
-
-
-
-
### showSelectedCheck
@@ -196,15 +188,7 @@ export default MyComponent;
-### compact Available in v5.x with theme version 3
-
-
-
-
-
-
-
-### elevated Available in v5.x with theme version 3
+### elevated
diff --git a/docs/src/data/componentDocs6x.json b/docs/src/data/componentDocs6x.json
index 56b356912b..5d6a8a88d4 100644
--- a/docs/src/data/componentDocs6x.json
+++ b/docs/src/data/componentDocs6x.json
@@ -4336,9 +4336,9 @@
}
]
},
- "description": "Mode of the chip.\n- `flat` - flat chip without outline.\n- `outlined` - chip with an outline.",
+ "description": "Mode of the chip.\n- `flat` - chip with a filled container.\n- `outlined` - chip with an outline when unselected.",
"defaultValue": {
- "value": "'flat'",
+ "value": "'outlined'",
"computed": false
}
},
@@ -4355,7 +4355,7 @@
"tsType": {
"name": "IconSource"
},
- "description": "Icon to display for the `Chip`. Both icon and avatar cannot be specified."
+ "description": "Leading icon to display for the `Chip`. Takes precedence over `avatar` when both are specified."
},
"avatar": {
"required": false,
@@ -4363,7 +4363,7 @@
"name": "ReactReactNode",
"raw": "React.ReactNode"
},
- "description": "Avatar to display for the `Chip`. Both icon and avatar cannot be specified."
+ "description": "Leading avatar to display for the `Chip`. Ignored when `icon` is also specified."
},
"closeIcon": {
"required": false,
@@ -4388,14 +4388,7 @@
"tsType": {
"name": "ColorValue"
},
- "description": "Whether to style the chip color as selected.\nNote: With theme version 3 `selectedColor` doesn't apply to the `icon`.\n If you want specify custom color for the `icon`, render your own `Icon` component."
- },
- "showSelectedOverlay": {
- "required": false,
- "tsType": {
- "name": "boolean"
- },
- "description": "@supported Available in v5.x with theme version 3\nWhether to display overlay on selected chip"
+ "description": "Custom color to use for the label, leading icon, trailing icon, and outlined border,\noverriding the default selected/unselected theme colors."
},
"showSelectedCheck": {
"required": false,
@@ -4424,7 +4417,7 @@
"tsType": {
"name": "PressableAndroidRippleConfig"
},
- "description": "Type of background drawabale to display the feedback (Android).\nhttps://reactnative.dev/docs/pressable#rippleconfig"
+ "description": "Type of background drawable to display the feedback (Android).\nhttps://reactnative.dev/docs/pressable#rippleconfig"
},
"aria-label": {
"required": false,
@@ -4471,9 +4464,16 @@
"tsType": {
"name": "signature",
"type": "function",
- "raw": "() => void",
+ "raw": "(e: GestureResponderEvent) => void",
"signature": {
- "arguments": [],
+ "arguments": [
+ {
+ "name": "e",
+ "type": {
+ "name": "GestureResponderEvent"
+ }
+ }
+ ],
"return": {
"name": "void"
}
@@ -4547,19 +4547,12 @@
},
"description": "The number of milliseconds a user must touch the element before executing `onLongPress`."
},
- "compact": {
- "required": false,
- "tsType": {
- "name": "boolean"
- },
- "description": "@supported Available in v5.x with theme version 3\nSets smaller horizontal paddings `12dp` around label, when there is only label."
- },
"elevated": {
"required": false,
"tsType": {
"name": "boolean"
},
- "description": "@supported Available in v5.x with theme version 3\nWhether chip should have the elevation.",
+ "description": "Whether the chip should have elevation.",
"defaultValue": {
"value": "false",
"computed": false
@@ -4576,7 +4569,7 @@
],
"raw": "StyleProp"
},
- "description": "Style of chip's text"
+ "description": "Style of chip's text."
},
"style": {
"required": false,
@@ -4628,7 +4621,7 @@
"tsType": {
"name": "EllipsizeProp"
},
- "description": "Ellipsize Mode for the children text"
+ "description": "Ellipsize Mode for the label text."
},
"maxFontSizeMultiplier": {
"required": false,
diff --git a/example/src/Examples/ChipExample.tsx b/example/src/Examples/ChipExample.tsx
index 8ee757fa0e..5483f35258 100644
--- a/example/src/Examples/ChipExample.tsx
+++ b/example/src/Examples/ChipExample.tsx
@@ -2,11 +2,14 @@ import * as React from 'react';
import { Image, StyleSheet, View } from 'react-native';
import color from 'color';
-import { Chip, List, Palette, Snackbar, Text } from 'react-native-paper';
+import { Chip, List, Palette, Snackbar } from 'react-native-paper';
import ScreenWrapper from '../ScreenWrapper';
+const filters = ['All', 'Unread', 'Starred'];
+
const ChipExample = () => {
+ const [selectedFilter, setSelectedFilter] = React.useState(filters[0]);
const [snackbarProperties, setSnackbarProperties] = React.useState({
visible: false,
text: '',
@@ -16,51 +19,62 @@ const ChipExample = () => {
return (
<>
-
+
- {}} style={styles.chip}>
- Simple
+ {filters.map((filter) => (
+ setSelectedFilter(filter)}
+ style={styles.chip}
+ >
+ {filter}
+
+ ))}
+ {}} style={styles.chip}>
+ With icon
{}}
style={styles.chip}
>
- With selected overlay
+ No check
- {}} style={styles.chip}>
- Elevated
+
+ Disabled
- {}}>
- Compact chip
+
+
+
+
+
+ {}} style={styles.chip}>
+ Outlined
{}}
- onClose={() =>
- setSnackbarProperties({
- visible: true,
- text: 'Close button pressed',
- })
- }
style={styles.chip}
- closeIconAccessibilityLabel="Close icon accessibility label"
>
- Close button
+ Flat
{}}
- onClose={() =>
- setSnackbarProperties({
- visible: true,
- text: 'Heart icon close button pressed',
- })
- }
style={styles.chip}
>
- Icon
+ Elevated
+
+
+
+
+
{
/>
}
onPress={() => {}}
- onClose={() =>
- setSnackbarProperties({
- visible: true,
- text: 'Avatar close button pressed',
- })
- }
style={styles.chip}
>
Avatar
@@ -90,67 +98,10 @@ const ChipExample = () => {
onPress={() => {}}
style={styles.chip}
>
- Avatar (selected)
-
-
- setSnackbarProperties({
- visible: true,
- text: 'Disabled heart icon close button pressed',
- })
- }
- style={styles.chip}
- >
- Icon (disabled)
+ Selected avatar
- }
- style={styles.chip}
- >
- Avatar (disabled)
-
-
-
-
-
- {}} style={styles.chip}>
- Simple
-
- {}}
- style={styles.chip}
- >
- With selected overlay
-
- {}}
- style={styles.chip}
- >
- Elevated
-
- {}}
- style={styles.chip}
- >
- Compact chip
-
- {}}
onClose={() =>
setSnackbarProperties({
@@ -160,208 +111,52 @@ const ChipExample = () => {
}
style={styles.chip}
>
- Close button
+ Removable
{}}
onClose={() =>
setSnackbarProperties({
visible: true,
- text: 'Heart icon close button pressed',
+ text: 'Custom close button pressed',
})
}
style={styles.chip}
+ closeIconAccessibilityLabel="Custom close icon accessibility label"
>
- Icon
-
-
- }
- onPress={() => {}}
- style={styles.chip}
- >
- Avatar
-
-
- }
- onPress={() => {}}
- style={styles.chip}
- >
- Avatar (selected)
-
-
- setSnackbarProperties({
- visible: true,
- text: 'Disabled close button pressed',
- })
- }
- style={styles.chip}
- >
- Icon (disabled)
-
-
- }
- style={styles.chip}
- >
- Avatar (disabled)
+ Custom close
-
+
+
- {}}
- compact
- avatar={
-
- }
- style={[styles.chip, styles.customBorderRadius]}
- >
- Compact with custom border radius
-
{}}
- compact
- avatar={
-
- }
- style={[styles.chip, styles.customBorderRadius]}
- >
- Compact with custom border radius
-
- {}}
- onLongPress={() =>
- setSnackbarProperties({ visible: true, text: '' })
- }
- style={styles.chip}
- >
- With onLongPress
-
- {}}
- style={[
- styles.chip,
- {
- backgroundColor: color(customColor).alpha(0.2).rgb().string(),
- },
- ]}
- selectedColor={customColor}
- >
- Flat selected chip with custom color
-
- {}}
- style={styles.chip}
selectedColor={customColor}
- >
- Flat unselected chip with custom color
-
- {}}
style={[
styles.chip,
{
backgroundColor: color(customColor).alpha(0.2).rgb().string(),
},
]}
- selectedColor={customColor}
- >
- Outlined selected chip with custom color
-
- {}}
- style={styles.chip}
- selectedColor={customColor}
- >
- Outlined unselected chip with custom color
-
- {}}
- style={styles.chip}
- textStyle={styles.tiny}
- >
- With custom size
-
- {}}
- onClose={() =>
- setSnackbarProperties({
- visible: true,
- text: 'Close button pressed',
- })
- }
- style={styles.bigTextFlex}
- textStyle={styles.bigTextStyle}
- ellipsizeMode="middle"
>
- With a very big text: React Native Paper is a high-quality,
- standard-compliant Material Design library that has you covered in
- all major use-cases.
+ Custom color
{}}
- onClose={() =>
- setSnackbarProperties({
- visible: true,
- text: 'Custom icon close button pressed',
- })
- }
- closeIcon="arrow-down"
- style={styles.chip}
- closeIconAccessibilityLabel="Custom Close icon accessibility label"
+ style={[styles.chip, styles.customBorderRadius]}
>
- With custom close icon
+ Rounded
- {}}
- style={styles.chip}
- textStyle={styles.tiny}
- >
- With custom text
+ {}} style={styles.fullWidthChip}>
+ Full width chip
- {}} style={styles.fullWidthChip}>
- Full width chip
-
({
onChange(option)}
>
{option}
diff --git a/example/src/Examples/TeamDetails.tsx b/example/src/Examples/TeamDetails.tsx
index 5970274f31..59c5d2c63e 100644
--- a/example/src/Examples/TeamDetails.tsx
+++ b/example/src/Examples/TeamDetails.tsx
@@ -57,12 +57,7 @@ const News = () => {
style={styles.chipsContainer}
contentContainerStyle={styles.chipsContent}
>
- {}}
- style={styles.chip}
- showSelectedOverlay
- >
+ {}} style={styles.chip}>
Latest
{}} style={styles.chip}>
diff --git a/src/components/Chip/Chip.tsx b/src/components/Chip/Chip.tsx
index 1b09c327a8..e36295cf76 100644
--- a/src/components/Chip/Chip.tsx
+++ b/src/components/Chip/Chip.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import { Animated, Platform, StyleSheet, Pressable, View } from 'react-native';
+import { Animated, Platform, StyleSheet, View } from 'react-native';
import type {
ColorValue,
GestureResponderEvent,
@@ -9,27 +9,24 @@ import type {
ViewStyle,
} from 'react-native';
-import useLatestCallback from 'use-latest-callback';
-
-import { getChipColors } from './helpers';
-import type { ChipAvatarProps } from './helpers';
+import { ChipTokens } from './tokens';
+import { getChipColors } from './utils';
+import type { ChipAvatarProps } from './utils';
import { useInternalTheme } from '../../core/theming';
-import { white } from '../../theme/colors';
-import type { $Omit, EllipsizeProp, Theme, ThemeProp } from '../../types';
+import type { EllipsizeProp, ThemeProp } from '../../types';
import hasTouchHandler from '../../utils/hasTouchHandler';
import type { IconSource } from '../Icon';
import Icon from '../Icon';
-import MaterialCommunityIcon from '../MaterialCommunityIcon';
import Surface from '../Surface';
import TouchableRipple from '../TouchableRipple/TouchableRipple';
import type { Props as TouchableRippleProps } from '../TouchableRipple/TouchableRipple';
import Text from '../Typography/Text';
-export type Props = $Omit, 'mode'> & {
+export type Props = Omit, 'mode'> & {
/**
* Mode of the chip.
- * - `flat` - flat chip without outline.
- * - `outlined` - chip with an outline.
+ * - `flat` - chip with a filled container.
+ * - `outlined` - chip with an outline when unselected.
*/
mode?: 'flat' | 'outlined';
/**
@@ -37,11 +34,11 @@ export type Props = $Omit, 'mode'> & {
*/
children: React.ReactNode;
/**
- * Icon to display for the `Chip`. Both icon and avatar cannot be specified.
+ * Leading icon to display for the `Chip`. Takes precedence over `avatar` when both are specified.
*/
icon?: IconSource;
/**
- * Avatar to display for the `Chip`. Both icon and avatar cannot be specified.
+ * Leading avatar to display for the `Chip`. Ignored when `icon` is also specified.
*/
avatar?: React.ReactNode;
/**
@@ -53,16 +50,10 @@ export type Props = $Omit, 'mode'> & {
*/
selected?: boolean;
/**
- * Whether to style the chip color as selected.
- * Note: With theme version 3 `selectedColor` doesn't apply to the `icon`.
- * If you want specify custom color for the `icon`, render your own `Icon` component.
+ * Custom color to use for the label, leading icon, trailing icon, and outlined border,
+ * overriding the default selected/unselected theme colors.
*/
selectedColor?: ColorValue;
- /**
- * @supported Available in v5.x with theme version 3
- * Whether to display overlay on selected chip
- */
- showSelectedOverlay?: boolean;
/**
* Whether to display default check icon on selected chip.
* Note: Check will not be shown if `icon` is specified. If specified, `icon` will be shown regardless of `selected`.
@@ -73,7 +64,7 @@ export type Props = $Omit, 'mode'> & {
*/
disabled?: boolean;
/**
- * Type of background drawabale to display the feedback (Android).
+ * Type of background drawable to display the feedback (Android).
* https://reactnative.dev/docs/pressable#rippleconfig
*/
background?: PressableAndroidRippleConfig;
@@ -92,7 +83,7 @@ export type Props = $Omit, 'mode'> & {
/**
* Function to execute on long press.
*/
- onLongPress?: () => void;
+ onLongPress?: (e: GestureResponderEvent) => void;
/**
* Function to execute as soon as the touchable element is pressed and invoked even before onPress.
*/
@@ -110,17 +101,11 @@ export type Props = $Omit, 'mode'> & {
*/
delayLongPress?: number;
/**
- * @supported Available in v5.x with theme version 3
- * Sets smaller horizontal paddings `12dp` around label, when there is only label.
- */
- compact?: boolean;
- /**
- * @supported Available in v5.x with theme version 3
- * Whether chip should have the elevation.
+ * Whether the chip should have elevation.
*/
elevated?: boolean;
/**
- * Style of chip's text
+ * Style of chip's text.
*/
textStyle?: StyleProp;
style?: Animated.WithAnimatedValue>;
@@ -137,7 +122,7 @@ export type Props = $Omit, 'mode'> & {
*/
testID?: string;
/**
- * Ellipsize Mode for the children text
+ * Ellipsize Mode for the label text.
*/
ellipsizeMode?: EllipsizeProp;
/**
@@ -170,7 +155,7 @@ export type Props = $Omit, 'mode'> & {
* ```
*/
const Chip = ({
- mode = 'flat',
+ mode = 'outlined',
children,
icon,
avatar,
@@ -194,18 +179,13 @@ const Chip = ({
selectedColor,
showSelectedCheck = true,
ellipsizeMode,
- compact,
elevated = false,
maxFontSizeMultiplier,
hitSlop,
...rest
}: Props) => {
const theme = useInternalTheme(themeOverrides);
- const isWeb = Platform.OS === 'web';
-
- const { current: elevation } = React.useRef(
- new Animated.Value(elevated ? 1 : 0)
- );
+ const isOutlined = mode === 'outlined';
const hasPassedTouchHandler = hasTouchHandler({
onPress,
@@ -213,35 +193,9 @@ const Chip = ({
onPressIn,
onPressOut,
});
+ const isTouchableDisabled = disabled || !hasPassedTouchHandler;
- const isOutlined = mode === 'outlined';
-
- const handlePressIn = useLatestCallback((e: GestureResponderEvent) => {
- const { scale } = theme.animation;
- onPressIn?.(e);
- Animated.timing(elevation, {
- toValue: elevated ? 2 : 0,
- duration: 200 * scale,
- useNativeDriver:
- isWeb || Platform.constants.reactNativeVersion.minor <= 72,
- }).start();
- });
-
- const handlePressOut = useLatestCallback((e: GestureResponderEvent) => {
- const { scale } = theme.animation;
- onPressOut?.(e);
- Animated.timing(elevation, {
- toValue: elevated ? 1 : 0,
- duration: 150 * scale,
- useNativeDriver:
- isWeb || Platform.constants.reactNativeVersion.minor <= 72,
- }).start();
- });
-
- const opacity = 0.38;
const defaultBorderRadius = theme.shapes.corner.small;
- const iconSize = 18;
-
const {
backgroundColor: customBackgroundColor,
borderRadius = defaultBorderRadius,
@@ -251,38 +205,55 @@ const Chip = ({
borderColor,
textColor,
iconColor,
+ closeIconColor,
contentOpacity,
selectedBackgroundColor,
backgroundColor,
+ rippleColor,
+ avatarOverlayColor,
} = getChipColors({
isOutlined,
+ selected,
+ elevated,
theme,
selectedColor,
customBackgroundColor,
disabled,
});
- const elevationStyle = elevation;
- const multiplier = compact ? 1.5 : 2;
- const labelSpacings = {
- marginRight: onClose ? 0 : 8 * multiplier,
- marginLeft:
- avatar || icon || (selected && showSelectedCheck)
- ? 4 * multiplier
- : 8 * multiplier,
- };
- const contentSpacings = {
- paddingRight: onClose ? 34 : 0,
- };
- const labelTextStyle = {
- color: textColor,
- ...(theme as Theme).fonts.labelLarge,
+ const hasAvatar = !!avatar && !icon;
+ const showSelectedIcon = selected && showSelectedCheck && !icon;
+ const showLeadingIcon = !!icon || showSelectedIcon;
+ const hasLeading = hasAvatar || showLeadingIcon;
+ const hasClose = !!onClose;
+
+ const leftPadding = hasAvatar
+ ? ChipTokens.avatarLeadingPadding
+ : hasLeading
+ ? ChipTokens.iconLeadingPadding
+ : ChipTokens.leadingPadding;
+ const rightPadding = hasClose
+ ? ChipTokens.closeTrailingPadding
+ : ChipTokens.trailingPadding;
+ const touchTargetInset =
+ (ChipTokens.minimumTouchTarget - ChipTokens.containerHeight) / 2;
+ const touchTargetHitSlop = {
+ top: touchTargetInset,
+ bottom: touchTargetInset,
};
+ const closeAndroidRipple =
+ Platform.OS === 'android'
+ ? {
+ color: rippleColor,
+ borderless: true,
+ radius: ChipTokens.trailingIconTouchTarget / 2,
+ }
+ : undefined;
+
return (
-
- {avatar && !icon ? (
-
- {React.isValidElement(avatar)
- ? React.cloneElement(avatar, {
- style: [styles.avatar, avatar.props.style],
- })
- : avatar}
-
- ) : null}
- {icon || (selected && showSelectedCheck) ? (
-
- {icon ? (
+
+
+
+ {hasAvatar ? (
+
+ {React.isValidElement(avatar)
+ ? React.cloneElement(avatar, {
+ style: [styles.avatar, avatar.props.style],
+ })
+ : avatar}
+ {showSelectedIcon ? (
+
- ) : (
-
- )}
-
- ) : null}
-
- {children}
-
-
-
- {onClose ? (
-
-
-
- {closeIcon ? (
-
- ) : (
-
- )}
-
-
-
+
+ ) : null}
+
+ ) : null}
+ {showLeadingIcon && !hasAvatar ? (
+
+
+
+ ) : null}
+
+ {children}
+
+
+ {hasClose ? (
+
+
+
) : null}
);
@@ -420,72 +383,71 @@ const Chip = ({
const styles = StyleSheet.create({
container: {
- borderWidth: StyleSheet.hairlineWidth,
+ height: ChipTokens.containerHeight,
+ borderWidth: ChipTokens.outlineWidth,
borderStyle: 'solid',
- flexDirection: Platform.select({ default: 'column', web: 'row' }),
- },
- md3Container: {
- borderWidth: 1,
- },
- content: {
flexDirection: 'row',
alignItems: 'center',
- paddingLeft: 4,
- position: 'relative',
+ alignSelf: 'flex-start',
},
- md3Content: {
- paddingLeft: 0,
- },
- icon: {
- padding: 4,
- alignSelf: 'center',
+ touchable: {
+ height: '100%',
+ flexGrow: 1,
+ flexShrink: 1,
},
- md3Icon: {
- paddingLeft: 8,
- paddingRight: 0,
+ rippleLayer: {
+ ...StyleSheet.absoluteFill,
},
- closeIcon: {
- marginRight: 4,
+ rippleContent: {
+ flex: 1,
},
- md3CloseIcon: {
- marginRight: 8,
- padding: 0,
+ content: {
+ height: '100%',
+ flexGrow: 1,
+ flexShrink: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ position: 'relative',
+ overflow: 'hidden',
},
- md3LabelText: {
- textAlignVertical: 'center',
- marginVertical: 6,
+ avatarWrapper: {
+ width: ChipTokens.avatarSize,
+ height: ChipTokens.avatarSize,
+ borderRadius: ChipTokens.avatarSize / 2,
+ marginRight: ChipTokens.leadingLabelGap,
+ overflow: 'hidden',
},
avatar: {
- width: 24,
- height: 24,
- borderRadius: 12,
+ width: ChipTokens.avatarSize,
+ height: ChipTokens.avatarSize,
+ borderRadius: ChipTokens.avatarSize / 2,
},
- avatarWrapper: {
- marginRight: 4,
- },
- md3AvatarWrapper: {
- marginLeft: 4,
- marginRight: 0,
+ avatarSelectedOverlay: {
+ ...StyleSheet.absoluteFill,
+ alignItems: 'center',
+ justifyContent: 'center',
},
- md3SelectedIcon: {
- paddingLeft: 4,
+ leadingIcon: {
+ width: ChipTokens.leadingIconSize,
+ height: ChipTokens.leadingIconSize,
+ marginRight: ChipTokens.leadingLabelGap,
+ alignItems: 'center',
+ justifyContent: 'center',
},
- // eslint-disable-next-line react-native/no-color-literals
- avatarSelected: {
- position: 'absolute',
- top: 4,
- left: 4,
- backgroundColor: 'rgba(0, 0, 0, .29)',
+ labelText: {
+ textAlignVertical: 'center',
+ includeFontPadding: false,
},
- closeButtonStyle: {
- position: 'absolute',
- right: 0,
+ closeButton: {
+ width: ChipTokens.trailingIconTouchTarget,
height: '100%',
- justifyContent: 'center',
+ borderRadius: ChipTokens.trailingIconTouchTarget / 2,
+ overflow: 'hidden',
alignItems: 'center',
+ justifyContent: 'center',
},
- touchable: {
- width: '100%',
+ disabled: {
+ opacity: ChipTokens.disabledContentOpacity,
},
});
diff --git a/src/components/Chip/helpers.tsx b/src/components/Chip/helpers.tsx
deleted file mode 100644
index 4b0fdf9e06..0000000000
--- a/src/components/Chip/helpers.tsx
+++ /dev/null
@@ -1,197 +0,0 @@
-import type { ColorValue, StyleProp, ViewStyle } from 'react-native';
-
-import color from 'color';
-
-import { tokens } from '../../theme/tokens';
-import type { InternalTheme, Theme } from '../../types';
-
-const md3 = (theme: InternalTheme) => theme as Theme;
-
-const stateOpacity = tokens.md.sys.state.opacity;
-
-export type ChipAvatarProps = {
- style?: StyleProp;
-};
-
-type BaseProps = {
- theme: InternalTheme;
- isOutlined: boolean;
- disabled?: boolean;
-};
-
-const getBorderColor = ({
- theme,
- isOutlined,
- disabled,
- selectedColor,
-}: BaseProps & { backgroundColor: ColorValue; selectedColor?: ColorValue }) => {
- const isSelectedColor = selectedColor !== undefined;
- const { colors } = md3(theme);
-
- if (!isOutlined) {
- // If the Chip mode is "flat", set border color to transparent
- return 'transparent';
- }
-
- if (disabled) {
- return colors.surfaceContainer;
- }
-
- if (isSelectedColor) {
- if (typeof selectedColor === 'string') {
- return color(selectedColor).alpha(0.29).rgb().string();
- }
- // PlatformColor / OpaqueColorValue: skip the alpha pass and render opaque.
- return selectedColor;
- }
-
- return colors.outlineVariant;
-};
-
-const getTextColor = ({
- theme,
- isOutlined,
- disabled,
- selectedColor,
-}: BaseProps & {
- selectedColor?: ColorValue;
-}) => {
- const isSelectedColor = selectedColor !== undefined;
- const { colors } = md3(theme);
- if (disabled) {
- return colors.onSurface;
- }
-
- if (isSelectedColor) {
- return selectedColor;
- }
-
- if (isOutlined) {
- return colors.onSurfaceVariant;
- }
-
- return colors.onSecondaryContainer;
-};
-
-const getDefaultBackgroundColor = ({
- theme,
- isOutlined,
-}: Omit) => {
- const { colors } = md3(theme);
- if (isOutlined) {
- return colors.surface;
- }
-
- return colors.secondaryContainer;
-};
-
-const getBackgroundColor = ({
- theme,
- isOutlined,
- disabled,
- customBackgroundColor,
-}: BaseProps & {
- customBackgroundColor?: ColorValue;
-}) => {
- const { colors } = md3(theme);
- if (typeof customBackgroundColor === 'string') {
- return customBackgroundColor;
- }
-
- if (disabled) {
- if (isOutlined) {
- return 'transparent';
- }
- return colors.surfaceContainerLow;
- }
-
- return getDefaultBackgroundColor({ theme, isOutlined });
-};
-
-const getSelectedBackgroundColor = ({
- theme,
- isOutlined,
- disabled,
- customBackgroundColor,
-}: BaseProps & {
- customBackgroundColor?: ColorValue;
-}) => {
- return getBackgroundColor({
- theme,
- disabled,
- isOutlined,
- customBackgroundColor,
- });
-};
-
-const getIconColor = ({
- theme,
- isOutlined,
- disabled,
- selectedColor,
-}: BaseProps & {
- selectedColor?: ColorValue;
-}) => {
- const isSelectedColor = selectedColor !== undefined;
- const { colors } = md3(theme);
- if (disabled) {
- return colors.onSurface;
- }
-
- if (isSelectedColor) {
- return selectedColor;
- }
-
- if (isOutlined) {
- return colors.onSurfaceVariant;
- }
-
- return colors.onSecondaryContainer;
-};
-
-export const getChipColors = ({
- isOutlined,
- theme,
- selectedColor,
- customBackgroundColor,
- disabled,
-}: BaseProps & {
- customBackgroundColor?: ColorValue;
- disabled?: boolean;
- selectedColor?: ColorValue;
-}) => {
- const baseChipColorProps = { theme, isOutlined, disabled };
-
- const backgroundColor = getBackgroundColor({
- ...baseChipColorProps,
- customBackgroundColor,
- });
-
- const selectedBackgroundColor = getSelectedBackgroundColor({
- ...baseChipColorProps,
- customBackgroundColor,
- });
-
- const contentOpacity = disabled
- ? stateOpacity.disabled
- : stateOpacity.enabled;
-
- return {
- borderColor: getBorderColor({
- ...baseChipColorProps,
- selectedColor,
- backgroundColor,
- }),
- textColor: getTextColor({
- ...baseChipColorProps,
- selectedColor,
- }),
- iconColor: getIconColor({
- ...baseChipColorProps,
- selectedColor,
- }),
- contentOpacity,
- backgroundColor,
- selectedBackgroundColor,
- };
-};
diff --git a/src/components/Chip/tokens.ts b/src/components/Chip/tokens.ts
new file mode 100644
index 0000000000..aa87a2d3c2
--- /dev/null
+++ b/src/components/Chip/tokens.ts
@@ -0,0 +1,45 @@
+import type { ColorRole, TypescaleKey } from '../../theme/types';
+
+/**
+ * MD3 Chip component tokens.
+ * @see https://m3.material.io/components/chips/specs
+ */
+const sizes = {
+ containerHeight: 32,
+ minimumTouchTarget: 48,
+ outlineWidth: 1,
+ leadingIconSize: 18,
+ trailingIconSize: 18,
+ avatarSize: 24,
+ selectedIconSize: 18,
+ leadingPadding: 16,
+ trailingPadding: 16,
+ iconLeadingPadding: 8,
+ avatarLeadingPadding: 4,
+ closeTrailingPadding: 8,
+ leadingLabelGap: 8,
+ trailingIconTouchTarget: 32,
+ labelTypescale: 'labelLarge' as TypescaleKey,
+
+ disabledContentOpacity: 0.38,
+
+ flatElevation: 0,
+ elevatedElevation: 1,
+} as const;
+
+const colors = {
+ elevatedContainerColor: 'surfaceContainerLow',
+ flatContainerColor: 'surfaceContainerLow',
+ selectedContainerColor: 'secondaryContainer',
+ outlinedContainerColor: 'surface',
+ labelColor: 'onSurfaceVariant',
+ selectedLabelColor: 'onSecondaryContainer',
+ leadingIconColor: 'primary',
+ selectedIconColor: 'onSecondaryContainer',
+ trailingIconColor: 'onSurfaceVariant',
+ selectedTrailingIconColor: 'onSecondaryContainer',
+ outlineColor: 'outline',
+ disabledColor: 'onSurface',
+} as const satisfies Record;
+
+export const ChipTokens = { ...sizes, ...colors };
diff --git a/src/components/Chip/utils.ts b/src/components/Chip/utils.ts
new file mode 100644
index 0000000000..6f84ac7387
--- /dev/null
+++ b/src/components/Chip/utils.ts
@@ -0,0 +1,195 @@
+import type { ColorValue, StyleProp, ViewStyle } from 'react-native';
+
+import { ChipTokens } from './tokens';
+import type { InternalTheme } from '../../types';
+
+export type ChipAvatarProps = {
+ style?: StyleProp;
+};
+
+type BaseProps = {
+ theme: InternalTheme;
+ isOutlined: boolean;
+ selected?: boolean;
+ disabled?: boolean;
+ elevated?: boolean;
+};
+
+const getContainerColor = ({
+ theme,
+ isOutlined,
+ selected,
+ disabled,
+ elevated,
+ customBackgroundColor,
+}: BaseProps & {
+ customBackgroundColor?: ColorValue;
+}) => {
+ if (disabled) {
+ return isOutlined ? 'transparent' : theme.colors.stateLayerPressed;
+ }
+
+ if (customBackgroundColor !== undefined) {
+ return customBackgroundColor;
+ }
+
+ if (selected) {
+ return theme.colors[ChipTokens.selectedContainerColor];
+ }
+
+ if (isOutlined) {
+ return theme.colors[ChipTokens.outlinedContainerColor];
+ }
+
+ return elevated
+ ? theme.colors[ChipTokens.elevatedContainerColor]
+ : theme.colors[ChipTokens.flatContainerColor];
+};
+
+const getBorderColor = ({
+ theme,
+ isOutlined,
+ selected,
+ disabled,
+ selectedColor,
+}: BaseProps & {
+ selectedColor?: ColorValue;
+}) => {
+ if (!isOutlined || selected) {
+ return 'transparent';
+ }
+
+ if (disabled) {
+ return theme.colors.outlineVariant;
+ }
+
+ if (selectedColor !== undefined) {
+ return selectedColor;
+ }
+
+ return theme.colors[ChipTokens.outlineColor];
+};
+
+const getLabelColor = ({
+ theme,
+ selected,
+ disabled,
+ selectedColor,
+}: BaseProps & {
+ selectedColor?: ColorValue;
+}) => {
+ if (disabled) {
+ return theme.colors[ChipTokens.disabledColor];
+ }
+
+ if (selectedColor !== undefined) {
+ return selectedColor;
+ }
+
+ if (selected) {
+ return theme.colors[ChipTokens.selectedLabelColor];
+ }
+
+ return theme.colors[ChipTokens.labelColor];
+};
+
+const getLeadingIconColor = ({
+ theme,
+ selected,
+ disabled,
+ selectedColor,
+}: BaseProps & {
+ selectedColor?: ColorValue;
+}) => {
+ if (disabled) {
+ return theme.colors[ChipTokens.disabledColor];
+ }
+
+ if (selectedColor !== undefined) {
+ return selectedColor;
+ }
+
+ if (selected) {
+ return theme.colors[ChipTokens.selectedIconColor];
+ }
+
+ return theme.colors[ChipTokens.leadingIconColor];
+};
+
+const getTrailingIconColor = ({
+ theme,
+ selected,
+ disabled,
+ selectedColor,
+}: BaseProps & {
+ selectedColor?: ColorValue;
+}) => {
+ if (disabled) {
+ return theme.colors[ChipTokens.disabledColor];
+ }
+
+ if (selectedColor !== undefined) {
+ return selectedColor;
+ }
+
+ if (selected) {
+ return theme.colors[ChipTokens.selectedTrailingIconColor];
+ }
+
+ return theme.colors[ChipTokens.trailingIconColor];
+};
+
+export const getChipColors = ({
+ isOutlined,
+ theme,
+ selected,
+ selectedColor,
+ customBackgroundColor,
+ disabled,
+ elevated,
+}: BaseProps & {
+ customBackgroundColor?: ColorValue;
+ disabled?: boolean;
+ selectedColor?: ColorValue;
+}) => {
+ const baseChipColorProps = {
+ theme,
+ isOutlined,
+ selected,
+ disabled,
+ elevated,
+ };
+
+ const contentOpacity = disabled ? ChipTokens.disabledContentOpacity : 1;
+
+ return {
+ borderColor: getBorderColor({
+ ...baseChipColorProps,
+ selectedColor,
+ }),
+ textColor: getLabelColor({
+ ...baseChipColorProps,
+ selectedColor,
+ }),
+ iconColor: getLeadingIconColor({
+ ...baseChipColorProps,
+ selectedColor,
+ }),
+ closeIconColor: getTrailingIconColor({
+ ...baseChipColorProps,
+ selectedColor,
+ }),
+ contentOpacity,
+ backgroundColor: getContainerColor({
+ ...baseChipColorProps,
+ customBackgroundColor,
+ }),
+ selectedBackgroundColor: getContainerColor({
+ ...baseChipColorProps,
+ selected: true,
+ customBackgroundColor,
+ }),
+ rippleColor: theme.colors.stateLayerPressed,
+ avatarOverlayColor: theme.colors.stateLayerPressed,
+ };
+};
diff --git a/src/components/__tests__/Chip.test.tsx b/src/components/__tests__/Chip.test.tsx
index 644906ae33..ec24db14d1 100644
--- a/src/components/__tests__/Chip.test.tsx
+++ b/src/components/__tests__/Chip.test.tsx
@@ -2,15 +2,12 @@ import { Animated } from 'react-native';
import { describe, expect, it, jest } from '@jest/globals';
import { act } from '@testing-library/react-native';
-import color from 'color';
import { getTheme } from '../../core/theming';
import { render, screen } from '../../test-utils';
-import { tokens } from '../../theme/tokens';
+import * as Avatar from '../Avatar/Avatar';
import Chip from '../Chip/Chip';
-import { getChipColors } from '../Chip/helpers';
-
-const stateOpacity = tokens.md.sys.state.opacity;
+import { getChipColors } from '../Chip/utils';
it('renders chip with onPress', async () => {
const tree = (
@@ -70,6 +67,40 @@ it('renders selected chip', async () => {
expect(tree).toMatchSnapshot();
});
+it('renders chip with avatar', async () => {
+ const tree = (
+ await render(
+ }>Example Chip
+ )
+ ).toJSON();
+
+ expect(tree).toMatchSnapshot();
+});
+
+it('renders selected chip with avatar and a check overlay', async () => {
+ const tree = (
+ await render(
+ }>
+ Example Chip
+
+ )
+ ).toJSON();
+
+ expect(tree).toMatchSnapshot();
+});
+
+it('prefers icon over avatar when both are specified', async () => {
+ const tree = (
+ await render(
+ }>
+ Example Chip
+
+ )
+ ).toJSON();
+
+ expect(tree).toMatchSnapshot();
+});
+
it('renders disabled chip if there is no touch handler passed', async () => {
await render(Disabled chip);
@@ -86,6 +117,60 @@ it('renders active chip if only onLongPress handler is passed', async () => {
expect(screen.getByTestId('active-chip')).toBeEnabled();
});
+it('applies disabled opacity to the close button', async () => {
+ await render(
+ {}} testID="disabled-chip">
+ Disabled chip
+
+ );
+
+ expect(screen.getByTestId('disabled-chip-close')).toHaveStyle({
+ opacity: 0.38,
+ });
+});
+
+it('spans the chip ripple behind the close button', async () => {
+ await render(
+ {}} onClose={() => {}} testID="chip">
+ Removable chip
+
+ );
+
+ expect(screen.getByTestId('chip')).toHaveStyle({
+ bottom: 0,
+ left: 0,
+ position: 'absolute',
+ right: 0,
+ top: 0,
+ });
+});
+
+it('clips the ripple to custom chip border radius', async () => {
+ await render(
+ {}} testID="rounded-chip" style={{ borderRadius: 16 }}>
+ Rounded chip
+
+ );
+
+ expect(screen.getByTestId('rounded-chip')).toHaveStyle({
+ borderRadius: 16,
+ overflow: 'hidden',
+ });
+});
+
+it('renders close button with a circular state layer', async () => {
+ await render(
+ {}} testID="chip">
+ Removable chip
+
+ );
+
+ expect(screen.getByTestId('chip-close')).toHaveStyle({
+ borderRadius: 16,
+ overflow: 'hidden',
+ });
+});
+
it('renders chip with zero border radius', async () => {
await render(
@@ -108,7 +193,7 @@ describe('getChipColors - text color', () => {
})
).toMatchObject({
textColor: getTheme().colors.onSurface,
- contentOpacity: stateOpacity.disabled,
+ contentOpacity: 0.38,
});
});
@@ -119,7 +204,7 @@ describe('getChipColors - text color', () => {
isOutlined: false,
})
).toMatchObject({
- textColor: getTheme().colors.onSecondaryContainer,
+ textColor: getTheme().colors.onSurfaceVariant,
});
});
@@ -157,7 +242,7 @@ describe('getChipColors - icon color', () => {
})
).toMatchObject({
iconColor: getTheme().colors.onSurface,
- contentOpacity: stateOpacity.disabled,
+ contentOpacity: 0.38,
});
});
@@ -168,7 +253,7 @@ describe('getChipColors - icon color', () => {
isOutlined: false,
})
).toMatchObject({
- iconColor: getTheme().colors.onSecondaryContainer,
+ iconColor: getTheme().colors.primary,
});
});
@@ -179,7 +264,7 @@ describe('getChipColors - icon color', () => {
isOutlined: true,
})
).toMatchObject({
- iconColor: getTheme().colors.onSurfaceVariant,
+ iconColor: getTheme().colors.primary,
});
});
@@ -226,6 +311,7 @@ describe('getChipColor - selected background color', () => {
getChipColors({
theme: getTheme(),
isOutlined: false,
+ selected: true,
})
).toMatchObject({
selectedBackgroundColor: getTheme().colors.secondaryContainer,
@@ -264,7 +350,43 @@ describe('getChipColor - background color', () => {
isOutlined: false,
})
).toMatchObject({
- backgroundColor: getTheme().colors.secondaryContainer,
+ backgroundColor: getTheme().colors.surfaceContainerLow,
+ });
+ });
+
+ it('uses the precomputed state layer color for disabled filled chips', () => {
+ const theme = getTheme();
+
+ expect(
+ getChipColors({
+ theme,
+ disabled: true,
+ isOutlined: false,
+ })
+ ).toMatchObject({
+ backgroundColor: theme.colors.stateLayerPressed,
+ });
+ });
+});
+
+describe('getChipColor - ripple color', () => {
+ it('uses the precomputed state layer color', () => {
+ const theme = {
+ ...getTheme(),
+ colors: {
+ ...getTheme().colors,
+ stateLayerPressed: 'rgba(29, 27, 32, 0.1)',
+ },
+ };
+
+ expect(
+ getChipColors({
+ theme,
+ isOutlined: true,
+ })
+ ).toMatchObject({
+ rippleColor: 'rgba(29, 27, 32, 0.1)',
+ avatarOverlayColor: 'rgba(29, 27, 32, 0.1)',
});
});
});
@@ -313,7 +435,21 @@ describe('getChipColor - border color', () => {
isOutlined: true,
})
).toMatchObject({
- borderColor: color('purple').alpha(0.29).rgb().string(),
+ borderColor: 'purple',
+ });
+ });
+
+ it('uses the tokenized outline color for disabled outlined chips', () => {
+ const theme = getTheme();
+
+ expect(
+ getChipColors({
+ theme,
+ disabled: true,
+ isOutlined: true,
+ })
+ ).toMatchObject({
+ borderColor: theme.colors.outlineVariant,
});
});
@@ -336,7 +472,7 @@ describe('getChipColor - border color', () => {
isOutlined: true,
})
).toMatchObject({
- borderColor: getTheme(false).colors.outlineVariant,
+ borderColor: getTheme(false).colors.outline,
});
});
@@ -347,7 +483,7 @@ describe('getChipColor - border color', () => {
isOutlined: true,
})
).toMatchObject({
- borderColor: getTheme(true).colors.outlineVariant,
+ borderColor: getTheme(true).colors.outline,
});
});
diff --git a/src/components/__tests__/ListItem.test.tsx b/src/components/__tests__/ListItem.test.tsx
index b50f4e7d3f..b635b9f2f8 100644
--- a/src/components/__tests__/ListItem.test.tsx
+++ b/src/components/__tests__/ListItem.test.tsx
@@ -149,17 +149,18 @@ it('renders with a description with typeof number', async () => {
it('calling onPress on ListItem right component', async () => {
Platform.OS = 'web';
const onPress = jest.fn<(event: GestureResponderEvent) => void>();
+ const user = userEvent.setup();
await render(
}
/>
);
- await userEvent.press(screen.getByTestId('icon-button'));
+ await user.press(screen.getByTestId('icon-button'));
expect(onPress).toHaveBeenCalledTimes(1);
});
diff --git a/src/components/__tests__/__snapshots__/Chip.test.tsx.snap b/src/components/__tests__/__snapshots__/Chip.test.tsx.snap
index 7bf18dde0e..71f1efba55 100644
--- a/src/components/__tests__/__snapshots__/Chip.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/Chip.test.tsx.snap
@@ -1,12 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`renders chip with close button 1`] = `
+exports[`prefers icon over avatar when both are specified 1`] = `
-
+
+
+
-
- information
-
-
+ }
+ >
- Example Chip
+ information
-
-
-
-
-
- close
-
-
-
+ {
+ "color": "rgba(73, 69, 79, 1)",
+ },
+ undefined,
+ ],
+ ],
+ ]
+ }
+ >
+ Example Chip
+
`;
-exports[`renders chip with custom close button 1`] = `
+exports[`renders chip with avatar 1`] = `
+
+
+
@@ -411,198 +370,937 @@ exports[`renders chip with custom close button 1`] = `
style={
[
{
- "alignSelf": "center",
- "padding": 4,
+ "backgroundColor": "rgba(103, 80, 164, 1)",
+ "borderRadius": 12,
+ "height": 24,
+ "width": 24,
},
{
- "paddingLeft": 8,
- "paddingRight": 0,
+ "alignItems": "center",
+ "justifyContent": "center",
+ },
+ {
+ "borderRadius": 12,
+ "height": 24,
+ "width": 24,
},
- null,
]
}
>
- information
+ XD
-
+
- Example Chip
-
-
-
-
-
+ Example Chip
+
+
+
+
+`;
+
+exports[`renders chip with close button 1`] = `
+
+
+
+
+
+
+
+
+ information
+
+
+
+ Example Chip
+
+
+
+
+ close
+
+
+
+
+`;
+
+exports[`renders chip with custom close button 1`] = `
+
+
+
+
+
+
+
+
+ information
+
+
+
+ Example Chip
+
+
+
+
+ arrow-down
+
+
+
+
+`;
+
+exports[`renders chip with icon 1`] = `
+
+
+
+
+
+
+
-
-
+
+
- arrow-down
-
-
-
+ {
+ "color": "rgba(73, 69, 79, 1)",
+ },
+ undefined,
+ ],
+ ],
+ ]
+ }
+ >
+ Example Chip
+
`;
-exports[`renders chip with icon 1`] = `
+exports[`renders chip with onPress 1`] = `
+
+
+
-
-
- information
-
-
-
- Example Chip
-
-
+ ],
+ ]
+ }
+ >
+ Example Chip
+
`;
-exports[`renders chip with onPress 1`] = `
+exports[`renders outlined disabled chip 1`] = `
+
+
+
-
- Example Chip
-
-
+ ],
+ ]
+ }
+ >
+ Example Chip
+
`;
-exports[`renders outlined disabled chip 1`] = `
+exports[`renders selected chip 1`] = `
+
+
+
- Example Chip
+ check
+
+ Example Chip
+
`;
-exports[`renders selected chip 1`] = `
+exports[`renders selected chip with avatar and a check overlay 1`] = `
+
+
+
@@ -1234,14 +2014,72 @@ exports[`renders selected chip 1`] = `
style={
[
{
- "alignSelf": "center",
- "padding": 4,
+ "backgroundColor": "rgba(103, 80, 164, 1)",
+ "borderRadius": 12,
+ "height": 24,
+ "width": 24,
+ },
+ {
+ "alignItems": "center",
+ "justifyContent": "center",
+ },
+ {
+ "borderRadius": 12,
+ "height": 24,
+ "width": 24,
+ },
+ ]
+ }
+ >
+
+ XD
+
+
+
@@ -1253,7 +2091,7 @@ exports[`renders selected chip 1`] = `
style={
[
{
- "color": "rgba(29, 25, 43, 1)",
+ "color": "rgba(254, 247, 255, 1)",
"fontSize": 18,
},
[
@@ -1275,52 +2113,43 @@ exports[`renders selected chip 1`] = `
check
-
+
- Example Chip
-
-
+ ],
+ ]
+ }
+ >
+ Example Chip
+
diff --git a/src/components/__tests__/__snapshots__/ListItem.test.tsx.snap b/src/components/__tests__/__snapshots__/ListItem.test.tsx.snap
index f06b87045f..c83f9df476 100644
--- a/src/components/__tests__/__snapshots__/ListItem.test.tsx.snap
+++ b/src/components/__tests__/__snapshots__/ListItem.test.tsx.snap
@@ -121,8 +121,10 @@ exports[`renders list item with custom description 1`] = `
collapsable={false}
style={
{
- "backgroundColor": "rgba(232, 222, 248, 1)",
+ "alignSelf": "flex-start",
+ "backgroundColor": "rgba(254, 247, 255, 1)",
"borderRadius": 8,
+ "height": 32,
"shadowColor": "rgba(0, 0, 0, 1)",
"shadowOffset": {
"height": 0,
@@ -138,13 +140,14 @@ exports[`renders list item with custom description 1`] = `
collapsable={false}
style={
{
- "backgroundColor": "rgba(232, 222, 248, 1)",
- "borderColor": "transparent",
+ "alignItems": "center",
+ "backgroundColor": "rgba(254, 247, 255, 1)",
+ "borderColor": "rgba(121, 116, 126, 1)",
"borderRadius": 8,
"borderStyle": "solid",
"borderWidth": 1,
- "flex": undefined,
- "flexDirection": "column",
+ "flex": 1,
+ "flexDirection": "row",
"shadowColor": "rgba(0, 0, 0, 1)",
"shadowOffset": {
"height": 0,
@@ -177,6 +180,12 @@ exports[`renders list item with custom description 1`] = `
accessible={true}
collapsable={false}
focusable={true}
+ hitSlop={
+ {
+ "bottom": 8,
+ "top": 8,
+ }
+ }
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
@@ -194,10 +203,19 @@ exports[`renders list item with custom description 1`] = `
},
[
{
- "borderRadius": 8,
+ "flexGrow": 1,
+ "flexShrink": 1,
+ "height": "100%",
},
{
- "width": "100%",
+ "bottom": 0,
+ "left": 0,
+ "position": "absolute",
+ "right": 0,
+ "top": 0,
+ },
+ {
+ "borderRadius": 8,
},
],
]
@@ -206,116 +224,110 @@ exports[`renders list item with custom description 1`] = `
>
-
+
+
+
-
- file-pdf-box
-
-
+ }
+ >
- DOCS.pdf
+ file-pdf-box
+
+ DOCS.pdf
+